mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 20:25:26 +02:00
fix(carriers): follow Node's CommonJS candidates, see regex after a block, ignore non-import from
- An extensionless require now tries only the path, .js, .json, and a
directory's index.js/index.json, as Node does; .cjs and .mjs must be
named, so require('./helper') no longer passes on helper.cjs alone.
- Treat `}` as a statement boundary, so a regex after a block is read as
a regex and cannot swallow a later require.
- Decide whether a paren opens a condition from the token before the
keyword, ignoring whitespace, so `obj.\nif(x) / 2` and
`obj?. while(x) / 2` stay method calls followed by division.
- Record which slots came from template literals and skip them for
static `from` and side-effect `import`, which accept only a quoted
string; a tagged template named `from` is no longer an import.
A template passed to import() is still a dependency.
Rescanning all 802 repository JavaScript files against dd14493e changes
no result, and the real registry still reports 35/35 resolved with zero
failures or warnings.
This commit is contained in:
@@ -39,9 +39,9 @@ const CLOSE = `${MARK}${MARK}`;
|
||||
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))$/;
|
||||
// A `(` opening a statement condition; `a.if(` is a method call, not one.
|
||||
const CONTROL_OPENS = /(?:^|[^.\w$])(?:if|while|for|with)$/;
|
||||
// `}` ends a block, so a slash after it starts a statement: a regex.
|
||||
const REGEX_PRECEDES = /(?:[([{},;:=!&|?+\-*/%~^<>]|\b(?:return|typeof|case|in|of|new|delete|void|instanceof|do|else|yield|await))$/;
|
||||
const CONDITION_KEYWORD = /(?:^|[^\w$])(if|while|for|with)$/;
|
||||
|
||||
/** Cook a raw string or template chunk the way the JavaScript parser would. */
|
||||
function decodeEscapes(raw) {
|
||||
@@ -61,6 +61,16 @@ function isAbsoluteSpecifier(value) {
|
||||
return value.startsWith('/') || value.startsWith('\\\\') || /^[A-Za-z]:[\\/]/.test(value) || /^file:/i.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether code ends with a keyword whose `(` opens a statement condition.
|
||||
* `obj.if(`, `obj?. while(` and `obj.` + newline + `if(` are method calls,
|
||||
* so the token before the keyword, ignoring whitespace, must not be a dot.
|
||||
*/
|
||||
function opensCondition(code) {
|
||||
const match = CONDITION_KEYWORD.exec(code);
|
||||
return Boolean(match) && !code.slice(0, code.length - match[1].length).trimEnd().endsWith('.');
|
||||
}
|
||||
|
||||
function readRegexLiteral(source, start) {
|
||||
let index = start + 1;
|
||||
let inClass = false;
|
||||
@@ -92,7 +102,13 @@ function scanSource(rawSource) {
|
||||
let closedCondition = false;
|
||||
let code = '';
|
||||
let index = 0;
|
||||
const slot = value => { code += `${MARK}${literals.push(decodeEscapes(value)) - 1}${MARK}`; };
|
||||
// Slots cut from template literals; `from` and `import` need a quoted string.
|
||||
const templates = new Set();
|
||||
const slot = (value, fromTemplate = false) => {
|
||||
const slotIndex = literals.push(decodeEscapes(value)) - 1;
|
||||
if (fromTemplate) templates.add(slotIndex);
|
||||
code += `${MARK}${slotIndex}${MARK}`;
|
||||
};
|
||||
const frame = () => (stack.length ? stack[stack.length - 1] : null);
|
||||
|
||||
while (index < source.length) {
|
||||
@@ -102,9 +118,9 @@ function scanSource(rawSource) {
|
||||
|
||||
if (open && open.type === 'template') {
|
||||
if (char === '\\') { open.text += source.substr(index, 2); index += 2; continue; }
|
||||
if (char === '`') { stack.pop(); slot(open.text); index += 1; continue; }
|
||||
if (char === '`') { stack.pop(); slot(open.text, true); index += 1; continue; }
|
||||
if (char === '$' && next === '{') {
|
||||
slot(open.text);
|
||||
slot(open.text, true);
|
||||
open.text = '';
|
||||
stack.push({ type: 'interpolation', depth: 0 });
|
||||
index += 2;
|
||||
@@ -148,13 +164,13 @@ function scanSource(rawSource) {
|
||||
open.depth -= 1;
|
||||
}
|
||||
}
|
||||
if (char === '(') parens.push(CONTROL_OPENS.test(code.trimEnd()));
|
||||
if (char === '(') parens.push(opensCondition(code.trimEnd()));
|
||||
else if (char === ')') closedCondition = parens.pop() === true;
|
||||
code += char;
|
||||
index += 1;
|
||||
}
|
||||
if (frame() && frame().type === 'template') slot(frame().text);
|
||||
return { code, literals };
|
||||
if (frame() && frame().type === 'template') slot(frame().text, true);
|
||||
return { code, literals, templates };
|
||||
}
|
||||
|
||||
function collect(pattern, code, handler) {
|
||||
@@ -177,7 +193,7 @@ function renderExpression(text, literals) {
|
||||
* or static `import`/`from`, which Node resolves by exact path.
|
||||
*/
|
||||
function extractReferences(rawSource) {
|
||||
const { code, literals } = scanSource(rawSource);
|
||||
const { code, literals, templates } = scanSource(rawSource);
|
||||
const specifiers = new Set();
|
||||
const imports = new Set();
|
||||
const packages = new Set();
|
||||
@@ -192,8 +208,12 @@ function extractReferences(rawSource) {
|
||||
}
|
||||
};
|
||||
collect(CALL_PATTERN, code, match => record(literals[Number(match[2])], match[1] === 'import'));
|
||||
// Static `from` and side-effect `import` take only a quoted string, so a
|
||||
// template there (a tag named `from`, say) is not an import.
|
||||
for (const pattern of [FROM_PATTERN, SIDE_EFFECT_PATTERN]) {
|
||||
collect(pattern, code, match => record(literals[Number(match[1])], true));
|
||||
collect(pattern, code, match => {
|
||||
if (!templates.has(Number(match[1]))) record(literals[Number(match[1])], true);
|
||||
});
|
||||
}
|
||||
collect(DIRNAME_JOIN_PATTERN, code, match => {
|
||||
const segments = [];
|
||||
@@ -210,8 +230,9 @@ function extractReferences(rawSource) {
|
||||
|
||||
/**
|
||||
* The planned paths that would satisfy a specifier, or the failure reason
|
||||
* when none can. CommonJS `require` searches extensions and directory
|
||||
* indexes; ESM (`import`, or any `.mjs` file) resolves only the exact path.
|
||||
* when none can. CommonJS `require` tries the path, then `.js` and `.json`,
|
||||
* then a directory's `index.js` and `index.json` (`.cjs` and `.mjs` must be
|
||||
* named); ESM (`import`, or any `.mjs` file) resolves only the exact path.
|
||||
* A relative backslash is not a separator on Linux or macOS, so it is
|
||||
* rejected rather than normalized.
|
||||
*/
|
||||
@@ -222,7 +243,7 @@ function candidateDestinations(destinationPath, specifier, exact) {
|
||||
if (base === '..' || base.startsWith('../')) return { base, candidates: null, reason: 'outside-carrier-tree' };
|
||||
const candidates = exact
|
||||
? [base]
|
||||
: [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`, `${base}/index.json`];
|
||||
: [base, `${base}.js`, `${base}.json`, `${base}/index.js`, `${base}/index.json`];
|
||||
return { base, candidates, reason: null };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user