Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bundle closing #3883

Merged
merged 21 commits into from Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/run/build.ts
Expand Up @@ -62,6 +62,7 @@ export default async function build(
}

await Promise.all(outputOptions.map(bundle.write));
await bundle.close();
if (!silent) {
warnings.flush();
stderr(green(`created ${bold(files.join(', '))} in ${bold(ms(Date.now() - start))}`));
Expand Down
5 changes: 5 additions & 0 deletions docs/02-javascript-api.md
Expand Up @@ -10,6 +10,8 @@ The `rollup.rollup` function receives an input options object as parameter and r

On a `bundle` object, you can call `bundle.generate` multiple times with different output options objects to generate different bundles in-memory. If you directly want to write them to disk, use `bundle.write` instead.

Once you're finished with the `bundle` object, you can call `bundle.close`, which will let plugins clean up their external processes or services.
intrnl marked this conversation as resolved.
Show resolved Hide resolved

```javascript
const rollup = require('rollup');

Expand Down Expand Up @@ -69,6 +71,9 @@ async function build() {

// or write the bundle to disk
await bundle.write(outputOptions);

// closes the bundle
await bundle.close();
}

build();
Expand Down
7 changes: 7 additions & 0 deletions docs/05-plugin-development.md
Expand Up @@ -251,6 +251,13 @@ Previous/Next Hook: This hook can be triggered at any time both during the build

Notifies a plugin whenever rollup has detected a change to a monitored file in `--watch` mode. This hook cannot be used by output plugins. Second argument contains additional details of change event.

#### `closeBundle`
Type: `closeBundle: () => Promise<void> | void`<br>
Kind: `async, parallel`<br>
Previous/Next Hook: This hook can be triggered at any time, during build errors or when `bundle.close()` is called, in which case this would be the last hook to be triggered.

Can be used to clean up any external service that may be running, this hook is never run during watch mode.

### Output Generation Hooks

Output generation hooks can provide information about a generated bundle and modify a build once complete. They work the same way and have the same types as [Build Hooks](guide/en/#build-hooks) but are called separately for each call to `bundle.generate(outputOptions)` or `bundle.write(outputOptions)`. Plugins that only use output generation hooks can also be passed in via the output options and therefore run only for certain outputs.
Expand Down
15 changes: 14 additions & 1 deletion src/rollup/rollup.ts
Expand Up @@ -2,7 +2,7 @@ import { version as rollupVersion } from 'package.json';
import Bundle from '../Bundle';
import Graph from '../Graph';
import { ensureArray } from '../utils/ensureArray';
import { errCannotEmitFromOptionsHook, error } from '../utils/error';
import { errAlreadyClosed, errCannotEmitFromOptionsHook, error } from '../utils/error';
import { writeFile } from '../utils/fs';
import { normalizeInputOptions } from '../utils/options/normalizeInputOptions';
import { normalizeOutputOptions } from '../utils/options/normalizeOutputOptions';
Expand Down Expand Up @@ -56,6 +56,7 @@ export async function rollupInternal(
err.watchFiles = watchFiles;
}
await graph.pluginDriver.hookParallel('buildEnd', [err]);
if (!watcher) await graph.pluginDriver.hookParallel('closeBundle', []);
intrnl marked this conversation as resolved.
Show resolved Hide resolved
throw err;
}

Expand All @@ -65,7 +66,17 @@ export async function rollupInternal(

const result: RollupBuild = {
cache: useCache ? graph.getCache() : undefined,
closed: false,
async close() {
if (result.closed) return;

result.closed = true;

await graph.pluginDriver.hookParallel('closeBundle', []);
},
async generate(rawOutputOptions: OutputOptions) {
if (result.closed) return error(errAlreadyClosed());

return handleGenerateWrite(
false,
inputOptions,
Expand All @@ -76,6 +87,8 @@ export async function rollupInternal(
},
watchFiles: Object.keys(graph.watchFiles),
async write(rawOutputOptions: OutputOptions) {
if (result.closed) return error(errAlreadyClosed());

return handleGenerateWrite(
true,
inputOptions,
Expand Down
21 changes: 15 additions & 6 deletions src/rollup/types.d.ts
Expand Up @@ -320,8 +320,12 @@ export type ResolveFileUrlHook = (
export type AddonHookFunction = (this: PluginContext) => string | Promise<string>;
export type AddonHook = string | AddonHookFunction;

export type ChangeEvent = 'create' | 'update' | 'delete'
export type WatchChangeHook = (this: PluginContext, id: string, change: {event: ChangeEvent}) => void
export type ChangeEvent = 'create' | 'update' | 'delete';
export type WatchChangeHook = (
this: PluginContext,
id: string,
change: { event: ChangeEvent }
) => void;

/**
* use this type for plugin annotation
Expand Down Expand Up @@ -350,6 +354,7 @@ export interface OutputBundleWithPlaceholders {
export interface PluginHooks extends OutputPluginHooks {
buildEnd: (this: PluginContext, err?: Error) => Promise<void> | void;
buildStart: (this: PluginContext, options: NormalizedInputOptions) => Promise<void> | void;
closeBundle: (this: PluginContext) => Promise<void> | void;
closeWatcher: (this: PluginContext) => void;
load: LoadHook;
moduleParsed: ModuleParsedHook;
Expand Down Expand Up @@ -412,7 +417,8 @@ export type AsyncPluginHooks =
| 'resolveDynamicImport'
| 'resolveId'
| 'transform'
| 'writeBundle';
| 'writeBundle'
| 'closeBundle';

export type PluginValueHooks = 'banner' | 'footer' | 'intro' | 'outro';

Expand Down Expand Up @@ -447,7 +453,8 @@ export type ParallelPluginHooks =
| 'outro'
| 'renderError'
| 'renderStart'
| 'writeBundle';
| 'writeBundle'
| 'closeBundle';

interface OutputPluginValueHooks {
banner: AddonHook;
Expand Down Expand Up @@ -740,6 +747,8 @@ export interface RollupOutput {

export interface RollupBuild {
cache: RollupCache | undefined;
close: () => Promise<void>;
closed: boolean;
generate: (outputOptions: OutputOptions) => Promise<RollupOutput>;
getTimings?: () => SerializedTimings;
watchFiles: string[];
Expand Down Expand Up @@ -794,7 +803,7 @@ export interface RollupWatchOptions extends InputOptions {
watch?: WatcherOptions | false;
}

interface TypedEventEmitter<T extends {[event: string]: (...args: any) => any}> {
interface TypedEventEmitter<T extends { [event: string]: (...args: any) => any }> {
addListener<K extends keyof T>(event: K, listener: T[K]): this;
emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): boolean;
eventNames(): Array<keyof T>;
Expand Down Expand Up @@ -827,7 +836,7 @@ export type RollupWatcherEvent =

export interface RollupWatcher
extends TypedEventEmitter<{
change: (id: string, change: {event: ChangeEvent}) => void;
change: (id: string, change: { event: ChangeEvent }) => void;
close: () => void;
event: (event: RollupWatcherEvent) => void;
restart: () => void;
Expand Down
1 change: 1 addition & 0 deletions src/utils/PluginDriver.ts
Expand Up @@ -47,6 +47,7 @@ const inputHookNames: {
} = {
buildEnd: 1,
buildStart: 1,
closeBundle: 1,
closeWatcher: 1,
load: 1,
moduleParsed: 1,
Expand Down
10 changes: 9 additions & 1 deletion src/utils/error.ts
Expand Up @@ -65,7 +65,8 @@ export enum Errors {
UNRESOLVED_IMPORT = 'UNRESOLVED_IMPORT',
VALIDATION_ERROR = 'VALIDATION_ERROR',
EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS',
SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT = 'SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT'
SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT = 'SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT',
ALREADY_CLOSED = 'ALREADY_CLOSED'
intrnl marked this conversation as resolved.
Show resolved Hide resolved
}

export function errAssetNotFinalisedForFileName(name: string) {
Expand Down Expand Up @@ -377,6 +378,13 @@ export function errFailedValidation(message: string) {
};
}

export function errAlreadyClosed() {
return {
code: Errors.ALREADY_CLOSED,
message: "Bundle is already closed, no more calls to 'generate' or 'write' is allowed."
};
}

export function warnDeprecation(
deprecation: string | RollupWarning,
activeDeprecation: boolean,
Expand Down
42 changes: 42 additions & 0 deletions test/hooks/index.js
Expand Up @@ -1034,6 +1034,48 @@ describe('hooks', () => {
})
.then(bundle => bundle.generate({ format: 'es' })));

it('supports closeBundle hook', () => {
let closeBundleCalls = 0;
return rollup
.rollup({
input: 'input',
plugins: [
loader({ input: `alert('hello')` }),
{
closeBundle() {
closeBundleCalls++;
}
}
]
})
.then(bundle => bundle.close())
.then(() => {
assert.strictEqual(closeBundleCalls, 1);
});
});

it('calls closeBundle hook on build error', () => {
let closeBundleCalls = 0;
return rollup
.rollup({
input: 'input',
plugins: [
loader({ input: `alert('hello')` }),
{
buildStart() {
this.error('build start error');
},
closeBundle() {
closeBundleCalls++;
}
}
]
})
.catch(() => {
assert.strictEqual(closeBundleCalls, 1);
});
});

describe('deprecated', () => {
it('caches chunk emission in transform hook', () => {
let cache;
Expand Down