From 0d3c85bd543baf25a5519cf74ebd7eada09982df Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 16:48:33 +0000 Subject: [PATCH] fix(carriers): match spaced calls, cook escapes, treat Windows and file URLs as absolute --- scripts/ci/validate-carrier-closure.js | 39 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js index 8738db360..13f898427 100644 --- a/scripts/ci/validate-carrier-closure.js +++ b/scripts/ci/validate-carrier-closure.js @@ -26,16 +26,37 @@ const BUILTINS = new Set(builtinModules); const SCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']); const MARK = '\u0000'; const SLOT = `${MARK}(\\d+)${MARK}`; -const CALL_PATTERN = new RegExp(`\\b(?:require|import)\\(\\s*${SLOT}\\s*\\)`, 'g'); +const CALL_PATTERN = new RegExp(`\\b(?:require|import)\\s*\\(\\s*${SLOT}\\s*\\)`, 'g'); const FROM_PATTERN = new RegExp(`\\bfrom\\s*${SLOT}`, 'g'); const SIDE_EFFECT_PATTERN = new RegExp(`\\bimport\\s*${SLOT}`, 'g'); const DIRNAME_JOIN_PATTERN = new RegExp( - `\\brequire\\(\\s*path\\.join\\(\\s*__dirname\\s*((?:,\\s*${SLOT}\\s*)+)\\)\\s*\\)`, 'g'); -const DYNAMIC_PATTERN = /\b(?:require|import)\(\s*((?:[^()]|\([^()]*\))+)\)/g; -const STATIC_JOIN_PATTERN = new RegExp(`^path\\.join\\(\\s*__dirname\\s*(?:,\\s*${SLOT}\\s*)+\\)$`); + `\\brequire\\s*\\(\\s*path\\.join\\s*\\(\\s*__dirname\\s*((?:,\\s*${SLOT}\\s*)+)\\)\\s*\\)`, 'g'); +const DYNAMIC_PATTERN = /\b(?:require|import)\s*\(\s*((?:[^()]|\([^()]*\))+)\)/g; +const STATIC_JOIN_PATTERN = new RegExp(`^path\\.join\\s*\\(\\s*__dirname\\s*(?:,\\s*${SLOT}\\s*)+\\)$`); const SLOT_PATTERN = new RegExp(SLOT, 'g'); +const ESCAPE_PATTERN = /\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\s\S]))/g; +const SIMPLE_ESCAPES = Object.freeze({ n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0' }); +const LINE_CONTINUATIONS = new Set(['\n', '\r', '\r\n', '\u2028', '\u2029']); const REGEX_PRECEDES = /(?:[([{,;:=!&|?+\-*/%~^<>]|\b(?:return|typeof|case|in|of|new|delete|void|instanceof|do|else|yield|await))$/; +/** Cook a raw string or template chunk the way the JavaScript parser would. */ +function decodeEscapes(raw) { + return raw.replace(ESCAPE_PATTERN, (whole, braced, unicode, hex, other) => { + if (braced !== undefined) { + const codePoint = parseInt(braced, 16); + return codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : whole; + } + if (unicode !== undefined) return String.fromCharCode(parseInt(unicode, 16)); + if (hex !== undefined) return String.fromCharCode(parseInt(hex, 16)); + if (LINE_CONTINUATIONS.has(other)) return ''; + return SIMPLE_ESCAPES[other] ?? other; + }); +} + +function isAbsoluteSpecifier(value) { + return value.startsWith('/') || value.startsWith('\\\\') || /^[A-Za-z]:[\\/]/.test(value) || /^file:/i.test(value); +} + function readRegexLiteral(source, start) { let index = start + 1; let inClass = false; @@ -63,7 +84,7 @@ function scanSource(rawSource) { const stack = []; let code = ''; let index = 0; - const slot = value => { code += `${MARK}${literals.push(value) - 1}${MARK}`; }; + const slot = value => { code += `${MARK}${literals.push(decodeEscapes(value)) - 1}${MARK}`; }; const frame = () => (stack.length ? stack[stack.length - 1] : null); while (index < source.length) { @@ -154,7 +175,7 @@ function extractReferences(rawSource) { for (const pattern of [CALL_PATTERN, FROM_PATTERN, SIDE_EFFECT_PATTERN]) { collect(pattern, code, match => { const value = literalAt(match); - if (value.startsWith('.') || value.startsWith('/')) specifiers.add(value); + if (value.startsWith('.') || isAbsoluteSpecifier(value)) specifiers.add(value); else if (!value.startsWith('node:') && !BUILTINS.has(value.split('/')[0])) { packages.add(value.startsWith('@') ? value.split('/').slice(0, 2).join('/') : value.split('/')[0]); } @@ -174,12 +195,12 @@ function extractReferences(rawSource) { } function candidateDestinations(destinationPath, specifier) { - if (specifier.startsWith('/')) return { base: specifier, candidates: null }; - const base = path.posix.join(path.posix.dirname(destinationPath), specifier); + if (isAbsoluteSpecifier(specifier)) return { base: specifier, candidates: null }; + const base = path.posix.join(path.posix.dirname(destinationPath), specifier.replace(/\\/g, '/')); if (base === '..' || base.startsWith('../')) return { base, candidates: null }; const candidates = destinationPath.endsWith('.mjs') ? [base] - : [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`]; + : [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`, `${base}/index.json`]; return { base, candidates }; }