Skip to content

Commit

Permalink
[Fix] display full key path in problem text; properly normalize neste…
Browse files Browse the repository at this point in the history
…d objects
  • Loading branch information
ljharb committed Jan 17, 2024
1 parent f9de2a3 commit 1ee59b3
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 11 deletions.
21 changes: 13 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var fnMsg = 'package `exports` is invalid; how did you get a function in there?'

/** @typedef {null | string | ExportsConditions | ExportsFiles | ExportsArray} Exports */

/** @type {<T = unknown>(obj: T) => { __proto__: null, normalized: undefined | Exports, problems: string[], status: false | StatusString }} */
/** @type {<T = unknown>(obj: T, parentKey?: string) => { __proto__: null, normalized: undefined | Exports, problems: string[], status: false | StatusString }} */
module.exports = function validateExportsObject(obj) {
if (!obj || typeof obj !== 'object') {
var isFn = typeof obj === 'function';
Expand All @@ -43,6 +43,9 @@ module.exports = function validateExportsObject(obj) {

/** @type {Exports} */ var normalized = isArray(obj) ? [] : { __proto__: null };

/** @type {string | null} */ var parentKey = arguments.length > 1 ? arguments[1] : null;
var fullKey = typeof parentKey === 'string' ? parentKey + '` -> `' : '';

for (var i = 0; i < exportKeys.length; i++) {
var key = exportKeys[i];
var start = $charAt(key, 0);
Expand All @@ -56,29 +59,31 @@ module.exports = function validateExportsObject(obj) {
nmSegments[nmSegments.length] = '`' + key + '`: `' + value + '`';
} else if (start === '.') {
if (status === 'conditions') {
problems[problems.length] = 'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `' + key + '`) is invalid; an object in "exports" cannot contain some keys starting with `.` and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only.';
problems[problems.length] = 'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `' + fullKey + key + '`) is invalid; an object in "exports" cannot contain some keys starting with `.` and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only.';
} else {
// @ts-expect-error ts(7053) objects are arbitrarily writable
normalized[key] = value;
status = 'files';
}
} else if (status === 'files') {
problems[problems.length] = 'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `' + key + '`) is invalid; an object in "exports" cannot contain some keys starting with `.` and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only.';
problems[problems.length] = 'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `' + fullKey + key + '`) is invalid; an object in "exports" cannot contain some keys starting with `.` and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only.';
} else {
// @ts-expect-error ts(7053) objects are arbitrarily writable
normalized[key] = value;
status = 'conditions';
}
} else {
var subObjectResult = validateExportsObject(value);
var subObjectResult = validateExportsObject(value, key);

if (subObjectResult.problems.length > 0) {
problems = safeConcat(problems, subObjectResult.problems);
break; // eslint-disable-line no-restricted-syntax
} else if (subObjectResult.status === 'empty') {
problems[problems.length] = 'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `' + fullKey + key + '`) is invalid; sub-object is empty.';
} else if (subObjectResult.status === 'files') {
problems[problems.length] = 'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `' + fullKey + key + '`) is invalid; sub-object has keys that start with `./`.';
}

if (subObjectResult.status === 'empty') {
problems[problems.length] = 'ERR_INVALID_PACKAGE_CONFIG: package `exports` is invalid; sub-object for `' + key + '` is empty.';
} else {
if (subObjectResult.status === 'conditions') {
if (status === 'empty') {
status = start === '.' ? 'files' : 'conditions';
}
Expand Down
63 changes: 60 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ var comboMsg = function (key) {
var nmMsg = function (pairs) {
return 'ERR_INVALID_PACKAGE_TARGET: package `exports` is invalid; values may not contain a `node_modules` path segment (' + pairs.sort().join(', ') + ').';
};
/** @type {(key: string) => string} */
var topLevelMsg = function (key) {
return 'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `' + key + '`) is invalid; sub-object has keys that start with `./`.';
};

var fixturesDir = path.resolve(__dirname, './list-exports/packages/tests/fixtures');

Expand Down Expand Up @@ -235,23 +239,76 @@ test('validateExportsObject', function (t) {
}
},
problems: [
'ERR_INVALID_PACKAGE_CONFIG: package `exports` is invalid; sub-object for `./bar` is empty.'
'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `./bar`) is invalid; sub-object is empty.'
],
status: 'files'
},
inspect(nestedEmpty) + ' is invalid; only a non-nested object can have file path keys'
);

var nestedFiles = {
'./foo': {
'./bar': './foo.mjs',
'default': './foo.js'
},
'./bar': {
'import': './foo.mjs',
'./bar': './foo.mjs',
'default': './foo.js'
},
'./baz': {
'import': {
'./baz': './bar.mjs'
},
'default': './bar/bar.mjs'
}
};
t.deepEqual(
validateExportsObject(nestedFiles),
{
__proto__: null,
normalized: {
__proto__: null,
'./bar': {
__proto__: null,
'import': './foo.mjs',
'default': './foo.js'
},
'./baz': {
__proto__: null,
'default': './bar/bar.mjs'
}
},
problems: [
comboMsg('./bar` -> `./bar'),
comboMsg('./foo` -> `default'),
topLevelMsg('./baz` -> `import')
].sort(),
status: 'files'
},
inspect(nestedFiles) + ' is invalid'
);

/** @type {string[]} */ var fixtures;
try { fixtures = fs.readdirSync(fixturesDir); } catch (e) {}
/** @type {{ [fixture in string]: string[] }} */
var fixtureProblems = {
'ex-node-modules': [
nmMsg(['`./local`: `./node_modules/dep/dep.js`', '`./local-encoded`: `./no%64e_modules/dep/dep.js`'])
],
'single-spa-layout': [
'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `import`) is invalid; sub-object has keys that start with `./`.',
'ERR_INVALID_PACKAGE_CONFIG: package `exports` (key `require`) is invalid; sub-object has keys that start with `./`.'
]
};

// @ts-expect-error ts(2454) TS can't narrow based on tape's `skip`
t.test('fixtures', { skip: !fixtures }, function (st) {
forEach(fixtures, function (fixture) {
var fixtureDir = path.resolve(fixturesDir, fixture);
var pkg = require(path.resolve(fixtureDir, 'project/package.json')); // eslint-disable-line global-require

if ('exports' in pkg) {
var expectedProblems = fixture === 'ex-node-modules' ? [nmMsg(['`./local`: `./node_modules/dep/dep.js`', '`./local-encoded`: `./no%64e_modules/dep/dep.js`'])] : [];
st.test('fixture: ' + fixture, function (s2t) {
var result = validateExportsObject(pkg.exports);

Expand All @@ -260,7 +317,7 @@ test('validateExportsObject', function (t) {
{
__proto__: null,
normalized: result.normalized,
problems: expectedProblems,
problems: fixtureProblems[fixture] || [],
status: result.status // TODO: figure out how to test the status properly
},
'fixture ' + fixture + ' has ' + (result.problems.length > 0 ? 'an invalid' : 'a valid') + ' exports object'
Expand Down

0 comments on commit 1ee59b3

Please sign in to comment.