Skip to content

Commit d29a457

Browse files
authored
feat(5858): make closeBundle hook receive the last error (#5867)
* feat(5858): make closeBundle hook receive the last error * keep the error chain * dry * Revert "dry" This reverts commit 6a3d70f4fe7baf75507911ef6de6dfcf374b4f9b. * watch files (without tests for now) * compound error * oops, watchFiles * docs: almost forgot * Update docs/plugin-development/index.md * Update docs/plugin-development/index.md * indeed it works
1 parent b346ce9 commit d29a457

File tree

4 files changed

+116
-5
lines changed

4 files changed

+116
-5
lines changed

docs/plugin-development/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,14 +877,16 @@ Cf. [`output.banner/output.footer`](../configuration-options/index.md#output-ban
877877

878878
| | |
879879
| --: | :-- |
880-
| Type: | `closeBundle: () => Promise<void> \| void` |
880+
| Type: | `closeBundle: (error?: Error) => Promise<void> \| void` |
881881
| Kind: | async, parallel |
882882
| Previous: | [`buildEnd`](#buildend) if there was a build error, otherwise when [`bundle.close()`](../javascript-api/index.md#rollup-rollup) is called, in which case this would be the last hook to be triggered |
883883

884884
Can be used to clean up any external service that may be running. Rollup's CLI will make sure this hook is called after each run, but it is the responsibility of users of the JavaScript API to manually call `bundle.close()` once they are done generating bundles. For that reason, any plugin relying on this feature should carefully mention this in its documentation.
885885

886886
If a plugin wants to retain resources across builds in watch mode, they can check for [`this.meta.watchMode`](#this-meta) in this hook and perform the necessary cleanup for watch mode in [`closeWatcher`](#closewatcher).
887887

888+
If an error occurs during build or the `buildEnd` hook, it is passed to this hook as first argument.
889+
888890
### footer
889891

890892
| | |

src/rollup/rollup.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { LOGLEVEL_DEBUG, LOGLEVEL_INFO, LOGLEVEL_WARN } from '../utils/logging';
99
import { getLogHandler } from '../utils/logHandler';
1010
import {
1111
error,
12+
getRollupError,
1213
logAlreadyClosed,
1314
logCannotEmitFromOptionsHook,
1415
logMissingFileOrDirOption,
@@ -79,11 +80,26 @@ export async function rollupInternal(
7980
if (watchFiles.length > 0) {
8081
error_.watchFiles = watchFiles;
8182
}
82-
await graph.pluginDriver.hookParallel('buildEnd', [error_]);
83-
await graph.pluginDriver.hookParallel('closeBundle', []);
83+
try {
84+
await graph.pluginDriver.hookParallel('buildEnd', [error_]);
85+
} catch (buildEndError: any) {
86+
// Create a compound error object to include both errors, based on the original error
87+
const compoundError = getRollupError({
88+
...error_,
89+
message: `There was an error during the build:\n ${error_.message}\nAdditionally, handling the error in the 'buildEnd' hook caused the following error:\n ${buildEndError.message}`
90+
});
91+
await graph.pluginDriver.hookParallel('closeBundle', [compoundError]);
92+
throw compoundError;
93+
}
94+
await graph.pluginDriver.hookParallel('closeBundle', [error_]);
8495
throw error_;
8596
}
86-
await graph.pluginDriver.hookParallel('buildEnd', []);
97+
try {
98+
await graph.pluginDriver.hookParallel('buildEnd', []);
99+
} catch (buildEndError: any) {
100+
await graph.pluginDriver.hookParallel('closeBundle', [buildEndError]);
101+
throw buildEndError;
102+
}
87103
});
88104

89105
timeEnd('BUILD', 1);

src/rollup/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ export interface FunctionPluginHooks {
404404
augmentChunkHash: (this: PluginContext, chunk: RenderedChunk) => string | void;
405405
buildEnd: (this: PluginContext, error?: Error) => void;
406406
buildStart: (this: PluginContext, options: NormalizedInputOptions) => void;
407-
closeBundle: (this: PluginContext) => void;
407+
closeBundle: (this: PluginContext, error?: Error) => void;
408408
closeWatcher: (this: PluginContext) => void;
409409
generateBundle: (
410410
this: PluginContext,

test/hooks/index.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,99 @@ describe('hooks', () => {
10321032
});
10331033
});
10341034

1035+
it('passes build error to the closeBundle hook', () => {
1036+
let buildError;
1037+
return rollup
1038+
.rollup({
1039+
input: 'input',
1040+
plugins: [
1041+
loader({ input: 'some broken code' }),
1042+
{
1043+
closeBundle(error) {
1044+
buildError = error;
1045+
}
1046+
}
1047+
]
1048+
})
1049+
.then(bundle => bundle.close())
1050+
.catch(error => {
1051+
assert.strictEqual(
1052+
error.message,
1053+
`input (1:5): Expected ';', '}' or <eof> (Note that you need plugins to import files that are not JavaScript)`
1054+
);
1055+
})
1056+
.then(() => {
1057+
assert.ok(buildError);
1058+
});
1059+
});
1060+
1061+
it('passes buildEnd error to the closeBundle hook', () => {
1062+
let buildEndError;
1063+
return rollup
1064+
.rollup({
1065+
input: 'input',
1066+
plugins: [
1067+
loader({ input: 'console.log(42);' }),
1068+
{
1069+
buildEnd() {
1070+
this.error('build end error');
1071+
},
1072+
closeBundle(error) {
1073+
buildEndError = error;
1074+
}
1075+
}
1076+
]
1077+
})
1078+
.then(bundle => bundle.close())
1079+
.catch(error => {
1080+
assert.strictEqual(error.message, '[plugin at position 2] build end error');
1081+
})
1082+
.then(() => {
1083+
assert.ok(buildEndError);
1084+
});
1085+
});
1086+
1087+
it('merges build and buildEnd errors', () => {
1088+
let buildEndError;
1089+
return rollup
1090+
.rollup({
1091+
input: 'input',
1092+
plugins: [
1093+
loader({ input: 'some broken code' }),
1094+
{
1095+
buildEnd() {
1096+
this.error('build end error');
1097+
},
1098+
closeBundle(error) {
1099+
buildEndError = error;
1100+
}
1101+
}
1102+
]
1103+
})
1104+
.then(bundle => bundle.close())
1105+
.catch(error => {
1106+
// Ensure error correctly copies the original build error
1107+
assert.strictEqual(error.cause.message, "Expected ';', '}' or <eof>");
1108+
assert.strictEqual(error.code, 'PARSE_ERROR');
1109+
assert.strictEqual(error.frame, '1: some broken code\n ^');
1110+
assert.strictEqual(error.id, 'input');
1111+
assert.deepStrictEqual(error.loc, { file: 'input', line: 1, column: 5 });
1112+
assert.strictEqual(
1113+
error.message,
1114+
'There was an error during the build:\n' +
1115+
" input (1:5): Expected ';', '}' or <eof> (Note that you need plugins to import files that are not JavaScript)\n" +
1116+
"Additionally, handling the error in the 'buildEnd' hook caused the following error:\n" +
1117+
' [plugin at position 2] build end error'
1118+
);
1119+
assert.strictEqual(error.name, 'RollupError');
1120+
assert.strictEqual(error.pos, 5);
1121+
assert.ok(error.stack.startsWith(`${error.name}: ${error.message}\n`));
1122+
})
1123+
.then(() => {
1124+
assert.ok(buildEndError);
1125+
});
1126+
});
1127+
10351128
it('supports disabling sanitization for in-memory / in-browser / non-fs builds', () =>
10361129
rollup
10371130
.rollup({

0 commit comments

Comments
 (0)