Warn on file changes while reviewing a diagnostic
MODIFY
packages/@romejs/cli-diagnostics/DiagnosticsPrinter.ts
packages/@romejs/cli-diagnostics/DiagnosticsPrinter.ts
@@ -363,6 +363,24 @@ export default class DiagnosticsPrinter extends Error {
this.reporter.redirectOutToErr(restoreRedirect);
}
+ getOutdatedFiles(diag: Diagnostic): UnknownFilePathSet {
+ let outdatedFiles: UnknownFilePathSet = new UnknownFilePathSet();
+ for (const {
+ path,
+ mtime: expectedMtime,
+ } of this.getDependenciesFromDiagnostics([diag])) {
+ const mtime = this.fileMtimes.get(path);
+ if (
+ mtime !== undefined &&
+ expectedMtime !== undefined &&
+ mtime > expectedMtime
+ ) {
+ outdatedFiles.add(path);
+ }
+ }
+ return outdatedFiles;
+ }
+
displayDiagnostic(diag: Diagnostic) {
const {reporter} = this;
const {start, end, filename} = diag.location;
@@ -395,23 +413,8 @@ export default class DiagnosticsPrinter extends Error {
}
}
- // Check if any files this diagnostic depends on have changed
- let outdatedFiles: UnknownFilePathSet = new UnknownFilePathSet();
- for (const {
- path,
- mtime: expectedMtime,
- } of this.getDependenciesFromDiagnostics([diag])) {
- const mtime = this.fileMtimes.get(path);
- if (
- mtime !== undefined &&
- expectedMtime !== undefined &&
- mtime > expectedMtime
- ) {
- outdatedFiles.add(path);
- }
- }
-
const outdatedAdvice: DiagnosticAdvice = [];
+ const outdatedFiles = this.getOutdatedFiles(diag);
const isOutdated = outdatedFiles.size > 0;
if (isOutdated) {
const outdatedFilesArr = Array.from(outdatedFiles, (path) => path.join());

MODIFY
packages/@romejs/cli-reporter/Reporter.ts
packages/@romejs/cli-reporter/Reporter.ts
@@ -37,6 +37,7 @@ import {
} from '@romejs/string-markup/types';
import select from './select';
import {MarkupLinesAndWidth} from '@romejs/string-markup/format';
+import {onKeypress} from './util';
type ListOptions = {
reverse?: boolean;
@@ -535,6 +536,23 @@ export default class Reporter {
return answer === 'yes';
}
+ async confirm(message: string = 'Press any key to continue'): Promise<void> {
+ this.logAll(`<dim>${message}</dim>`, {newline: false});
+
+ await new Promise((resolve) => {
+ const keypress = onKeypress(
+ this,
+ () => {
+ keypress.finish();
+ resolve();
+ },
+ );
+ });
+
+ // Newline
+ this.logAll('');
+ }
+
async radio<Options extends SelectOptions>(
message: string,
arg: SelectArguments<Options>,

MODIFY
packages/@romejs/cli-reporter/select.ts
packages/@romejs/cli-reporter/select.ts
import {ansiEscapes} from '@romejs/string-markup';
import Reporter from './Reporter';
import {SelectArguments, SelectOption, SelectOptions} from './types';
-import readline = require('readline');
+import {onKeypress, setRawMode} from './util';
function formatShortcut({shortcut}: SelectOption): string {
if (shortcut === undefined) {
@@ -144,51 +144,16 @@ export default async function select<Options extends SelectOptions>(
}
}
- let onkeypress = undefined;
-
const stdin = reporter.getStdin();
render();
- readline.emitKeypressEvents(stdin);
-
- if (stdin.isTTY && stdin.setRawMode !== undefined) {
- stdin.setRawMode(true);
- }
-
- stdin.resume();
+ setRawMode(stdin, true);
await new Promise((resolve) => {
- function finish() {
- cleanup();
-
- // Remove initial help message
- reporter.writeAll(ansiEscapes.cursorUp());
- reporter.writeAll(ansiEscapes.eraseLine);
-
- // Remove initial log message
- reporter.writeAll(ansiEscapes.cursorUp());
- reporter.writeAll(ansiEscapes.eraseLine);
-
- prompt += ': ';
- if (selectedOptions.size > 0) {
- prompt += Array.from(selectedOptions, (key) => options[key]!.label).join(
- ', ',
- );
- } else {
- prompt += '<dim>none</dim>';
- }
- reporter.logAll(prompt);
-
- resolve();
- }
- onkeypress = (
- chunk: Buffer,
- key: {
- name: string;
- ctrl: boolean;
- },
- ) => {
+ const keypress = onKeypress(
+ reporter,
+ (key) => {
// Check if this is an option shortcut
if (!key.ctrl) {
for (const optionName in options) {
@@ -229,22 +194,6 @@ export default async function select<Options extends SelectOptions>(
break;
}
- case 'c': {
- if (key.ctrl) {
- reporter.br(true);
- reporter.warn('Cancelled by user');
- process.exit(1);
- }
- return;
- }
-
- case 'escape': {
- reporter.br(true);
- reporter.warn('Cancelled by user');
- process.exit(1);
- return;
- }
-
case 'return': {
finish();
return;
@@ -257,20 +206,35 @@ export default async function select<Options extends SelectOptions>(
boundActive();
cleanup();
render();
- };
+ },
+ );
- stdin.addListener('keypress', onkeypress);
- });
+ function finish() {
+ cleanup();
- if (onkeypress !== undefined) {
- stdin.removeListener('keypress', onkeypress);
- }
+ // Remove initial help message
+ reporter.writeAll(ansiEscapes.cursorUp());
+ reporter.writeAll(ansiEscapes.eraseLine);
- if (stdin.isTTY && stdin.setRawMode !== undefined) {
- stdin.setRawMode(false);
+ // Remove initial log message
+ reporter.writeAll(ansiEscapes.cursorUp());
+ reporter.writeAll(ansiEscapes.eraseLine);
+
+ prompt += ': ';
+ if (selectedOptions.size > 0) {
+ prompt += Array.from(selectedOptions, (key) => options[key]!.label).join(
+ ', ',
+ );
+ } else {
+ prompt += '<dim>none</dim>';
}
+ reporter.logAll(prompt);
- stdin.pause();
+ // Stop listening for keypress
+ keypress.finish();
+ resolve();
+ }
+ });
return selectedOptions;
}

MODIFY
packages/@romejs/cli-reporter/util.ts
packages/@romejs/cli-reporter/util.ts
*/
import {ReporterProgress} from './types';
+import Reporter from './Reporter';
+import readline = require('readline');
export function mergeProgresses(
progresses: Array<ReporterProgress>,
@@ -72,3 +74,63 @@ export function mergeProgresses(
},
};
}
+
+type Key = {
+ name: string;
+ ctrl: boolean;
+};
+
+export function onKeypress(
+ reporter: Reporter,
+ callback: (key: Key) => void,
+): {
+ finish: () => void;
+} {
+ const stdin = reporter.getStdin();
+
+ setRawMode(stdin, true);
+ readline.emitKeypressEvents(stdin);
+
+ function onkeypress(chunk: Buffer, key: Key) {
+ switch (key.name) {
+ case 'c': {
+ if (key.ctrl) {
+ reporter.br(true);
+ reporter.warn('Cancelled by user');
+ process.exit(1);
+ }
+ return;
+ }
+
+ case 'escape': {
+ reporter.br(true);
+ reporter.warn('Cancelled by user');
+ process.exit(1);
+ return;
+ }
+ }
+
+ callback(key);
+ }
+
+ stdin.addListener('keypress', onkeypress);
+
+ return {
+ finish() {
+ stdin.removeListener('keypress', onkeypress);
+ setRawMode(stdin, false);
+ },
+ };
+}
+
+export function setRawMode(stdin: NodeJS.ReadStream, raw: boolean) {
+ if (stdin.isTTY && stdin.setRawMode !== undefined) {
+ stdin.setRawMode(raw);
+ }
+
+ if (raw) {
+ stdin.resume();
+ } else {
+ stdin.pause();
+ }
+}

MODIFY
packages/@romejs/core/client/review.ts
packages/@romejs/core/client/review.ts
*/
import ClientRequest from './ClientRequest';
-import {printDiagnostics} from '@romejs/cli-diagnostics';
+import {DiagnosticsPrinter, printDiagnostics} from '@romejs/cli-diagnostics';
import {SelectOption} from '@romejs/cli-reporter';
import {
Diagnostic,
@@ -77,7 +77,7 @@ async function ask(
diag: Diagnostic,
req: ClientRequest,
state: State,
- more: boolean,
+ showMoreOptions: boolean,
): Promise<MasterQueryResponse> {
const {client} = req;
const {reporter} = client;
@@ -89,8 +89,13 @@ async function ask(
const actions: Array<DiagnosticAdviceAction> = [];
for (const item of advice) {
if (item.type === 'action') {
- if (item.extra === true && !more) {
+ // Only show extra items and hide all non-extra items when `more === true`
+ if (item.extra === true) {
hasExtraOptions = true;
+ if (!showMoreOptions) {
+ continue;
+ }
+ } else if (showMoreOptions) {
continue;
}
@@ -129,6 +134,7 @@ async function ask(
ignore: SelectOption;
exit: SelectOption;
more?: SelectOption;
+ less?: SelectOption;
} = {
ignore: {
label: 'Do nothing',
@@ -141,21 +147,25 @@ async function ask(
},
};
- if (!more && hasExtraOptions) {
+ if (hasExtraOptions) {
+ if (showMoreOptions) {
+ options.more = {
+ label: 'Less options...',
+ shortcut: 'l',
+ };
+ } else {
options.more = {
label: 'More options...',
shortcut: 'm',
};
}
+ }
- printDiagnostics({
- diagnostics: [diag],
- suppressions: [],
- excludeFooter: true,
- printerOptions: {
+ const printer = new DiagnosticsPrinter({
reporter,
- },
});
+ diag = printer.processor.addDiagnosticAssert(diag);
+ printer.print();
const answer = await reporter.radio(
'How do you want to resolve this?',
@@ -164,6 +174,37 @@ async function ask(
},
);
+ // Check if this diagnostic is now out of date
+ printer.fetchFileSources([diag]);
+ const outdatedFiles = printer.getOutdatedFiles(diag);
+ if (outdatedFiles.size > 0) {
+ const files = Array.from(
+ outdatedFiles,
+ (path) => `<filelink emphasis target="${path.join()}" />`,
+ );
+
+ reporter.br();
+
+ if (files.length === 1) {
+ reporter.warn(
+ `The file ${files[0]} changed while waiting for your response.`,
+ );
+ } else {
+ reporter.warn(
+ 'The following diagnostic dependencies changed while waiting for your response.',
+ );
+ reporter.list(files);
+ }
+
+ await reporter.confirm('Press any key to try again');
+
+ return await check(req, state);
+ }
+
+ if (answer === 'less') {
+ return await ask(diag, req, state, false);
+ }
+
if (answer === 'more') {
return await ask(diag, req, state, true);
}

MODIFY
packages/@romejs/diagnostics/DiagnosticsProcessor.ts
packages/@romejs/diagnostics/DiagnosticsProcessor.ts
@@ -57,6 +57,7 @@ export default class DiagnosticsProcessor {
this.includedKeys = new Set();
this.unique = options.unique === undefined ? DEFAULT_UNIQUE : options.unique;
this.throwAfter = undefined;
+ this.locked = false;
this.origins = options.origins === undefined ? [] : [...options.origins];
this.allowedUnusedSuppressionPrefixes = new Set();
this.usedSuppressions = new Set();
@@ -71,6 +72,7 @@ export default class DiagnosticsProcessor {
this.cachedDiagnostics = undefined;
}
+ locked: boolean;
normalizer: DiagnosticsNormalizer;
sourceMaps: SourceMapConsumerCollection;
unique: UniqueRules;
@@ -97,6 +99,10 @@ export default class DiagnosticsProcessor {
return diagnostics;
}
+ lock() {
+ this.locked = true;
+ }
+
unshiftOrigin(origin: DiagnosticOrigin) {
this.origins.unshift(origin);
}
@@ -241,6 +247,10 @@ export default class DiagnosticsProcessor {
return keys;
}
+ addDiagnosticAssert(diag: Diagnostic, origin?: DiagnosticOrigin): Diagnostic {
+ return this.addDiagnostics([diag], origin, true)[0];
+ }
+
addDiagnostic(
diag: Diagnostic,
origin?: DiagnosticOrigin,
@@ -248,13 +258,23 @@ export default class DiagnosticsProcessor {
return this.addDiagnostics([diag], origin)[0];
}
- addDiagnostics(diags: Diagnostics, origin?: DiagnosticOrigin): Diagnostics {
+ addDiagnostics(
+ diags: Diagnostics,
+ origin?: DiagnosticOrigin,
+ force?: boolean,
+ ): Diagnostics {
if (diags.length === 0) {
return diags;
}
this.cachedDiagnostics = undefined;
+ if (this.locked) {
+ throw new Error(
+ 'DiagnosticsProcessor is locked and cannot accept anymore diagnostics',
+ );
+ }
+
const {max} = this.options;
const added: Diagnostics = [];
@@ -267,29 +287,31 @@ export default class DiagnosticsProcessor {
// Filter diagnostics
diagLoop: for (let diag of diags) {
- if (max !== undefined && this.diagnostics.length > max) {
+ if (!force && max !== undefined && this.diagnostics.length > max) {
break;
}
// Check before normalization
- if (this.doesMatchFilter(diag)) {
+ if (!force && this.doesMatchFilter(diag)) {
continue;
}
diag = this.normalizer.normalizeDiagnostic(diag);
// Check after normalization
- if (this.doesMatchFilter(diag)) {
+ if (!force && this.doesMatchFilter(diag)) {
continue;
}
const keys = this.buildDedupeKeys(diag);
+ if (!force) {
for (const key of keys) {
if (this.includedKeys.has(key)) {
continue diagLoop;
}
}
+ }
this.diagnostics.push(diag);
added.push(diag);

Warn on file changes while reviewing a diagnostic