From 64a0ceefe1bdfe65fc250de21f6e5a9e387c33ce Mon Sep 17 00:00:00 2001 From: Holograph PressWorks Date: Wed, 5 Aug 2026 19:39:52 -0400 Subject: [PATCH] feat: add --continueOnError flag to skip a hung/crashing document - Wrap checkSingle() inside pMap in a try/catch when continueOnError is set; log a warning and yield null for the failing doc instead of aborting. - Filter nulls from the results before consolidation. - Plumb the option through checker.js and ace.js in the same shape as doNotReportMedia (options.continueOnError || env ACE_CONTINUE_ON_ERROR). - Expose the CLI flag as -C / --continueOnError. Default behavior is unchanged: without the flag, pMap's fail-fast still aborts the whole audit on the first per-doc rejection. Closes #465 --- packages/ace-cli-shared/src/index.js | 8 +++++++ .../ace-core/src/checker/checker-chromium.js | 22 +++++++++++++++---- packages/ace-core/src/checker/checker.js | 4 ++-- packages/ace-core/src/core/ace.js | 9 +++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/ace-cli-shared/src/index.js b/packages/ace-cli-shared/src/index.js index 4ec0ff65..16f942d2 100755 --- a/packages/ace-cli-shared/src/index.js +++ b/packages/ace-cli-shared/src/index.js @@ -38,6 +38,9 @@ const meowHelpMessage = ` -M, --doNotReportMedia skips reporting media resources + -C, --continueOnError skip a content document that hangs or crashes the axe runner + (default is to abort the whole audit on the first per-doc error) + -E, --exiterror2 exit process with code 2 when fail (return-2-on-validation-error) Examples $ ace -o out ~/Documents/book.epub @@ -90,6 +93,10 @@ const meowOptions = { doNotReportMedia: { alias: 'M', }, + continueOnError: { + alias: 'C', + type: 'boolean' + }, exiterror2: { // process exit code alias: 'E', type: 'boolean' @@ -168,6 +175,7 @@ ${overrides.map(file => ` - ${file}`).join('\n')} lang: cli.flags.lang, timeout: cli.flags.timeout || undefined, doNotReportMedia: cli.flags.doNotReportMedia || undefined, + continueOnError: cli.flags.continueOnError || undefined, exiterror2: cli.flags.exiterror2 || undefined, }, axeRunner) .then(async (jobData) => { diff --git a/packages/ace-core/src/checker/checker-chromium.js b/packages/ace-core/src/checker/checker-chromium.js index 9725c35e..7a00f2fc 100644 --- a/packages/ace-core/src/checker/checker-chromium.js +++ b/packages/ace-core/src/checker/checker-chromium.js @@ -205,15 +205,29 @@ async function checkSingle(spineItem, epub, lang, doNotReportMedia, axeRunner) { } } -module.exports.check = async (epub, lang, doNotReportMedia, axeRunner) => { +module.exports.check = async (epub, lang, doNotReportMedia, axeRunner, continueOnError) => { await axeRunner.launch(); winston.info('Checking documents...'); - return pMap(epub.contentDocs, (doc) => { - return checkSingle(doc, epub, lang, doNotReportMedia, axeRunner); // not AWAIT'ed! (pMap takes the Promise) + return pMap(epub.contentDocs, async (doc) => { + // When continueOnError is set, a single hung / crashing document + // logs a warning and yields a null result, letting the rest of the + // audit finish. Without the flag we preserve the original behavior: + // pMap's default fail-fast bubbles the first per-doc rejection up + // and aborts the whole run. + if (!continueOnError) { + return checkSingle(doc, epub, lang, doNotReportMedia, axeRunner); + } + try { + return await checkSingle(doc, epub, lang, doNotReportMedia, axeRunner); + } catch (err) { + const which = (doc && (doc.relpath || doc.href)) || '?'; + winston.warn(`Skipping document ${which} (continueOnError): ${err && err.message ? err.message : err}`); + return null; + } }, { concurrency: DISABLE_CONCURRENT_TO_DEBUG_INDIVIDUAL_SPINE_ITEM ? 1 : axeRunner.concurrency }) .then(async (results) => { await axeRunner.close(); - return results; + return continueOnError ? results.filter((r) => r !== null) : results; }).catch(async (err) => { winston.error(`Ace HTML check error: ${err.message ? err.message : err}`); if (err.stack) winston.debug(err.stack); diff --git a/packages/ace-core/src/checker/checker.js b/packages/ace-core/src/checker/checker.js index 9fd8c8ba..dad632b8 100644 --- a/packages/ace-core/src/checker/checker.js +++ b/packages/ace-core/src/checker/checker.js @@ -42,10 +42,10 @@ function consolidate(results, report) { return report; } -module.exports.check = function check(epub, report, lang, doNotReportMedia, axeRunner) { +module.exports.check = function check(epub, report, lang, doNotReportMedia, axeRunner, continueOnError) { return epubChecker.check(epub, report) .then(async (obj) => { - return htmlChecker.check(epub, lang, doNotReportMedia, axeRunner).then((results) => { + return htmlChecker.check(epub, lang, doNotReportMedia, axeRunner, continueOnError).then((results) => { return new Promise((res) => { res({ assertion: obj.assertion, diff --git a/packages/ace-core/src/core/ace.js b/packages/ace-core/src/core/ace.js index a7e80e53..07f7176f 100644 --- a/packages/ace-core/src/core/ace.js +++ b/packages/ace-core/src/core/ace.js @@ -24,6 +24,13 @@ module.exports = function ace(epubPath, options, axeRunner) { // if (!!options.doNotReportMedia) axeRunner.setDoNotReportMedia(); //if (options.doNotReportMedia && process && process.env) process.env.ACE_DO_NOT_REPORT_MEDIA_RESOURCES = "1"; + // When set (via --continue-on-error, options.continueOnError, or + // ACE_CONTINUE_ON_ERROR=1), a single content document that hangs or + // crashes the axe runner is logged as a warning and skipped, and the + // rest of the audit continues. Without the flag the current behavior + // is preserved: pMap's default fail-fast aborts the whole run. + const continueOnError = !!options.continueOnError || process && process.env && process.env.ACE_CONTINUE_ON_ERROR === '1'; + return new Promise((resolve, reject) => { function l10nDoneCallback() { @@ -75,7 +82,7 @@ module.exports = function ace(epubPath, options, axeRunner) { // initialize the report .then(() => new Report(epub, options.outdir, options.lang).init()) // Check each Content Doc - .then(report => checker.check(epub, report, options.lang, doNotReportMedia, axeRunner)) + .then(report => checker.check(epub, report, options.lang, doNotReportMedia, axeRunner, continueOnError)) // Process the Results .then((report) => { if (!options.outdir) {