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

Directly restart Rollup when config file change is detected #4344

Merged
merged 10 commits into from Jan 14, 2022
2 changes: 1 addition & 1 deletion cli/logging.ts
Expand Up @@ -3,7 +3,7 @@ import { bold, cyan, dim, red } from '../src/utils/colors';
import relativeId from '../src/utils/relativeId';

// log to stderr to keep `rollup main.js > bundle.js` from breaking
export const stderr = console.error.bind(console);
export const stderr = (...args: unknown[]) => process.stderr.write(`${args.join('')}\n`);

export function handleError(err: RollupError, recover = false): void {
let description = err.message || err;
Expand Down
45 changes: 18 additions & 27 deletions cli/run/watch-cli.ts
Expand Up @@ -19,10 +19,9 @@ export async function watch(command: Record<string, any>): Promise<void> {
process.env.ROLLUP_WATCH = 'true';
const isTTY = process.stderr.isTTY;
const silent = command.silent;
let configs: MergedRollupOptions[];
let warnings: BatchWarnings;
let watcher: RollupWatcher;
let configWatcher: FSWatcher;
let resetScreen: (heading: string) => void;
const configFile = command.config ? getConfigPath(command.config) : null;

onExit(close);
Expand All @@ -33,11 +32,10 @@ export async function watch(command: Record<string, any>): Promise<void> {
}

async function loadConfigFromFileAndTrack(configFile: string): Promise<void> {
let reloadingConfig = false;
let aborted = false;
let configFileData: string | null = null;
let configFileRevision = 0;

configWatcher = chokidar.watch(configFile).on('change', () => reloadConfigFile());
configWatcher = chokidar.watch(configFile).on('change', reloadConfigFile);
await reloadConfigFile();

async function reloadConfigFile() {
Expand All @@ -46,29 +44,21 @@ export async function watch(command: Record<string, any>): Promise<void> {
if (newConfigFileData === configFileData) {
return;
}
if (reloadingConfig) {
aborted = true;
return;
}
configFileRevision++;
const currentConfigFileRevision = configFileRevision;
if (configFileData) {
stderr(`\nReloading updated config...`);
}
configFileData = newConfigFileData;
reloadingConfig = true;
({ options: configs, warnings } = await loadAndParseConfigFile(configFile, command));
reloadingConfig = false;
if (aborted) {
aborted = false;
reloadConfigFile();
} else {
if (watcher) {
watcher.close();
}
start(configs);
const { options, warnings } = await loadAndParseConfigFile(configFile, command);
if (currentConfigFileRevision !== configFileRevision) {
return;
}
if (watcher) {
watcher.close();
}
start(options, warnings);
} catch (err: any) {
configs = [];
reloadingConfig = false;
handleError(err, true);
}
}
Expand All @@ -77,13 +67,11 @@ export async function watch(command: Record<string, any>): Promise<void> {
if (configFile) {
await loadConfigFromFileAndTrack(configFile);
} else {
({ options: configs, warnings } = await loadConfigFromCommand(command));
start(configs);
const { options, warnings } = await loadConfigFromCommand(command);
start(options, warnings);
}

const resetScreen = getResetScreen(configs!, isTTY);

function start(configs: MergedRollupOptions[]): void {
function start(configs: MergedRollupOptions[], warnings: BatchWarnings): void {
try {
watcher = rollup.watch(configs as any);
} catch (err: any) {
Expand All @@ -99,6 +87,9 @@ export async function watch(command: Record<string, any>): Promise<void> {

case 'START':
if (!silent) {
if (!resetScreen) {
resetScreen = getResetScreen(configs, isTTY);
}
resetScreen(underline(`rollup v${rollup.VERSION}`));
}
break;
Expand Down
173 changes: 88 additions & 85 deletions test/cli/index.js
Expand Up @@ -35,113 +35,116 @@ function runTest(dir, config, pass) {
if (pass > 0) {
getFileNamesAndRemoveOutput(dir);
}
if (config.before) config.before();

const command = config.command.replace(
/(^| )rollup($| )/g,
`node ${path.resolve(__dirname, '../../dist/bin')}${path.sep}rollup `
);

const childProcess = exec(
command,
{
timeout: 40000,
env: { ...process.env, FORCE_COLOR: '0', ...config.env }
},
(err, code, stderr) => {
if (config.after) config.after(err, code, stderr);
if (err && !err.killed) {
if (config.error) {
const shouldContinue = config.error(err);
Promise.resolve(config.before && config.before()).then(() => {
const childProcess = exec(
command,
{
timeout: 40000,
env: { ...process.env, FORCE_COLOR: '0', ...config.env }
},
(err, code, stderr) => {
if (config.after) config.after(err, code, stderr);
if (err && !err.killed) {
if (config.error) {
const shouldContinue = config.error(err);
if (!shouldContinue) return done();
} else {
throw err;
}
}

if ('stderr' in config) {
const shouldContinue = config.stderr(stderr);
if (!shouldContinue) return done();
} else {
throw err;
} else if (stderr) {
console.error(stderr);
}
}

if ('stderr' in config) {
const shouldContinue = config.stderr(stderr);
if (!shouldContinue) return done();
} else if (stderr) {
console.error(stderr);
}
let unintendedError;

let unintendedError;
if (config.execute) {
try {
const fn = new Function('require', 'module', 'exports', 'assert', code);
const module = {
exports: {}
};
fn(require, module, module.exports, assert);

if (config.execute) {
try {
const fn = new Function('require', 'module', 'exports', 'assert', code);
const module = {
exports: {}
};
fn(require, module, module.exports, assert);
if (config.error) {
unintendedError = new Error('Expected an error while executing output');
}

if (config.error) {
unintendedError = new Error('Expected an error while executing output');
if (config.exports) {
config.exports(module.exports);
}
} catch (err) {
if (config.error) {
config.error(err);
} else {
unintendedError = err;
}
}

if (config.exports) {
config.exports(module.exports);
if (config.show || unintendedError) {
console.log(code + '\n\n\n');
}
} catch (err) {
if (config.error) {
config.error(err);
} else {
unintendedError = err;
}
}

if (config.show || unintendedError) {
console.log(code + '\n\n\n');
}
if (config.solo) console.groupEnd();

if (config.solo) console.groupEnd();

unintendedError ? done(unintendedError) : done();
} else if (config.result) {
try {
config.result(code);
done();
} catch (err) {
done(err);
}
} else if (config.test) {
try {
config.test();
done();
} catch (err) {
done(err);
}
} else if (sander.existsSync('_expected') && sander.statSync('_expected').isDirectory()) {
try {
assertDirectoriesAreEqual('_actual', '_expected');
done();
} catch (err) {
done(err);
}
} else {
const expected = sander.readFileSync('_expected.js').toString();
try {
assert.equal(normaliseOutput(code), normaliseOutput(expected));
done();
} catch (err) {
done(err);
unintendedError ? done(unintendedError) : done();
} else if (config.result) {
try {
config.result(code);
done();
} catch (err) {
done(err);
}
} else if (config.test) {
try {
config.test();
done();
} catch (err) {
done(err);
}
} else if (
sander.existsSync('_expected') &&
sander.statSync('_expected').isDirectory()
) {
try {
assertDirectoriesAreEqual('_actual', '_expected');
done();
} catch (err) {
done(err);
}
} else {
const expected = sander.readFileSync('_expected.js').toString();
try {
assert.equal(normaliseOutput(code), normaliseOutput(expected));
done();
} catch (err) {
done(err);
}
}
}
}
);
);

childProcess.stderr.on('data', async data => {
if (config.abortOnStderr) {
try {
if (await config.abortOnStderr(data)) {
childProcess.stderr.on('data', async data => {
if (config.abortOnStderr) {
try {
if (await config.abortOnStderr(data)) {
childProcess.kill('SIGTERM');
}
} catch (err) {
childProcess.kill('SIGTERM');
done(err);
}
} catch (err) {
childProcess.kill('SIGTERM');
done(err);
}
}
});
});
}
).timeout(50000);
Expand Down
2 changes: 2 additions & 0 deletions test/cli/samples/wait-for-bundle-input/_config.js
Expand Up @@ -18,5 +18,7 @@ module.exports = {
// wait longer than one polling interval
setTimeout(() => atomicWriteFileSync(mainFile, 'export default 42;'), 600);
}
// We wait for a regular abort as we do not watch
return false;
}
};
4 changes: 3 additions & 1 deletion test/cli/samples/watch/no-config-file/_config.js
@@ -1,8 +1,10 @@
const path = require('path');

module.exports = {
description: 'watches without a config file',
command: 'rollup main.js --watch --format es --file _actual/main.js',
abortOnStderr(data) {
if (data.includes('created _actual/main.js')) {
if (data.includes(`created _actual${path.sep}main.js`)) {
return true;
}
}
Expand Down
4 changes: 3 additions & 1 deletion test/cli/samples/watch/node-config-file/_config.js
@@ -1,8 +1,10 @@
const path = require('path');

module.exports = {
description: 'watches using a node_modules config files',
command: 'rollup --watch --config node:custom',
abortOnStderr(data) {
if (data.includes('created _actual/main.js')) {
if (data.includes(`created _actual${path.sep}main.js`)) {
return true;
}
}
Expand Down