test(carriers): pin exact import resolution, portable separators, and regex after a condition

Review on aec97150 found four ways the closure gate passes a carrier that
fails at run time. Each is reproduced here against the current head:

- import('./helper') with only helper.js planned is accepted, but Node's
  ESM resolver does no extension search.
- import('./data') with only data/index.json (or index.js) planned is
  accepted, but Node rejects directory imports even from CommonJS.
- require('./lib\helper.js') with lib/helper.js planned is accepted
  because backslashes were normalized, but Linux and macOS read the
  backslash as part of the filename.
- In `if (ok) /['"]/.test(text); require('./missing')` the regex after
  the condition's closing paren is read as division, its quote opens a
  fake string, and the require disappears.

Controls that must keep passing: an exact import() of a planned file, an
extensionless require through the CommonJS search, and division after a
method named `if` or a call nested inside a condition. extractReferences
gains an `imports` list so the resolver can tell the two forms apart.
This commit is contained in:
montjeffrey
2026-10-10 18:00:50 -04:00
parent bf729d56ba
commit df82e75a4c
+52 -1
View File
@@ -87,6 +87,46 @@ test('a directory require resolves through index.json', () => withFixture(root =
assert.equal(result.status, 'success', JSON.stringify(result.failures));
}));
// Node resolves import() and static imports by exact path: no extension
// search and no directory index, even when the calling file is CommonJS.
for (const [label, planned, source] of [
['an extensionless import()', 'helper.js', "module.exports = import('./helper');\n"],
['a directory import() through index.json', 'data/index.json', "module.exports = import('./data');\n"],
['a directory import() through index.js', 'data/index.js', "module.exports = import('./data');\n"],
['an extensionless static import', 'helper.js', "import helper from './helper';\nexport default helper;\n"],
]) {
test(`${label} fails even when the require form would resolve`, () => withFixture(root => {
write(root, `skills/ecc-guide/scripts/${planned}`, planned.endsWith('.json') ? '{}\n' : 'module.exports = 1;\n');
plantSkillScript(root, source);
const result = validator().validate(root);
assert.equal(result.status, 'failure');
assert.ok(result.failures.every(failure => failure.reason === 'unplanned-target'));
}));
}
test('an import() naming the planned file exactly resolves', () => withFixture(root => {
write(root, 'skills/ecc-guide/scripts/helper.js', 'module.exports = 1;\n');
plantSkillScript(root, "module.exports = import('./helper.js');\n");
const result = validator().validate(root);
assert.equal(result.status, 'success', JSON.stringify(result.failures));
}));
test('an extensionless require still resolves through the CommonJS extension search', () => withFixture(root => {
write(root, 'skills/ecc-guide/scripts/helper.js', 'module.exports = 1;\n');
plantSkillScript(root, "module.exports = require('./helper');\n");
const result = validator().validate(root);
assert.equal(result.status, 'success', JSON.stringify(result.failures));
}));
test('a backslash-separated relative require fails: Linux and macOS read it as one filename', () => withFixture(root => {
write(root, 'skills/ecc-guide/scripts/lib/helper.js', 'module.exports = 1;\n');
plantSkillScript(root, "module.exports = require('./lib\\\\helper.js');\n");
const result = validator().validate(root);
assert.equal(result.status, 'failure');
assert.ok(specifiersOf(result.failures).includes('./lib\\helper.js'));
assert.ok(result.failures.every(failure => failure.reason === 'non-portable-separator'));
}));
test('a package import warns because carriers ship no node_modules; builtins do not', () => withFixture(root => {
plantSkillScript(root, "'use strict';\n\nconst yaml = require('js-yaml');\nconst { x } = require('@scope/pkg/deep');\n"
+ "const fs = require('node:fs');\nconst path = require('path');\nmodule.exports = { yaml, x, fs, path };\n");
@@ -144,6 +184,10 @@ const SCANNER_CASES = [
['unicode escape in a specifier', "require('\\u002e./escaped');", ['../escaped']],
['hex and braced escapes in a specifier', "require('\\x2e/a'); require(`\\u{2e}/b`);", ['./a', './b']],
['escaped quote and backslash', "require('./it\\'s'); require('.\\\\win');", ["./it's", '.\\win']],
['regex after an if condition', "if (ok) /['\"]/.test(text); require('./missing');", ['./missing']],
['regex after a while condition', "while (more) /\"/.test(s); require('./after-while');", ['./after-while']],
['division after a method named if', "const v = a.if(x) / 2, q = \"/\"; require('./after-method');", ['./after-method']],
['division after a call inside a condition', "if (f(a) / 2 > 1) { q = \"/\"; } require('./after-nested');", ['./after-nested']],
];
for (const [label, source, expected] of SCANNER_CASES) {
@@ -154,7 +198,14 @@ for (const [label, source, expected] of SCANNER_CASES) {
test('scanner: an identifier named from before a template chunk is not an import', () => {
assert.deepEqual(validator().extractReferences('const s = `${from} is shipped as ${to}`;'),
{ specifiers: [], packages: [], dynamic: [] });
{ specifiers: [], imports: [], packages: [], dynamic: [] });
});
test('scanner: import() and static imports are reported apart from require', () => {
const { specifiers, imports } = validator().extractReferences(
"require('./a'); import('./b'); import c from './c'; import './d'; require(path.join(__dirname, 'e.js'));");
assert.deepEqual(specifiers.sort(), ['./a', './b', './c', './d', './e.js']);
assert.deepEqual(imports.sort(), ['./b', './c', './d']);
});
test('unknown flags are rejected', () => {