feat(compartment-mapper): support various types of optional deps
MODIFY
packages/compartment-mapper/src/node-modules.js
packages/compartment-mapper/src/node-modules.js
@@ -252,16 +252,37 @@ const graphPackage = async (
graph[packageLocation] = /** @type {Node} */ (result);
/** @type {Record<string, string>} */
- const dependencies = {};
+ const dependencyLocations = {};
const children = [];
- const predicates = packageDescriptor.dependencies || {};
- assign(predicates, packageDescriptor.peerDependencies || {});
- assign(predicates, packageDescriptor.bundleDependencies || {});
+ const optionals = new Set();
+ const {
+ dependencies = {},
+ peerDependencies = {},
+ peerDependenciesMeta = {},
+ bundleDependencies = {},
+ optionalDependencies = {},
+ devDependencies = {},
+ } = packageDescriptor;
+ const allDependencies = {};
+ assign(allDependencies, dependencies);
+ assign(allDependencies, peerDependencies);
+ for (const [name, { optional }] of Object.entries(peerDependenciesMeta)) {
+ if (optional) {
+ optionals.add(name);
+ }
+ }
+ assign(allDependencies, bundleDependencies);
+ assign(allDependencies, optionalDependencies);
+ Object.keys(optionalDependencies).forEach(name => {
+ optionals.add(name);
+ });
if (dev) {
- assign(predicates, packageDescriptor.devDependencies || {});
+ assign(allDependencies, devDependencies);
}
- for (const name of keys(predicates).sort()) {
+
+ for (const name of keys(allDependencies).sort()) {
+ const optional = optionals.has(name);
children.push(
// Mutual recursion ahead:
// eslint-disable-next-line no-use-before-define
@@ -269,10 +290,11 @@ const graphPackage = async (
readDescriptor,
canonical,
graph,
- dependencies,
+ dependencyLocations,
packageLocation,
name,
tags,
+ optional,
),
);
}
@@ -294,7 +316,7 @@ const graphPackage = async (
label: `${name}${version ? `-v${version}` : ''}`,
explicit: exports !== undefined,
exports: inferExports(packageDescriptor, tags, types),
- dependencies,
+ dependencies: dependencyLocations,
types,
parsers: inferParsers(packageDescriptor, packageLocation),
});
@@ -320,6 +342,7 @@ const graphPackage = async (
* @param {string} packageLocation - location of the package of interest.
* @param {string} name - name of the package of interest.
* @param {Set<string>} tags
+ * @param {boolean} optional - whether the dependency is optional
*/
const gatherDependency = async (
readDescriptor,
@@ -329,6 +352,7 @@ const gatherDependency = async (
packageLocation,
name,
tags,
+ optional = false,
) => {
const dependency = await findPackage(
readDescriptor,
@@ -337,6 +361,10 @@ const gatherDependency = async (
name,
);
if (dependency === undefined) {
+ // allow the dependency to be missing if optional
+ if (optional) {
+ return;
+ }
throw new Error(`Cannot find dependency ${name} for ${packageLocation}`);
}
dependencies[name] = dependency.packageLocation;

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/alpha/index.js
+export default 'alpha'
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/alpha/package.json
+{
+ "name": "alpha",
+ "version": "1.0.0",
+ "description": "dummy package",
+ "type": "module",
+ "main": "./index.js",
+ "scripts": {
+ "preinstall": "echo DO NOT INSTALL TEST FIXTURES; exit -1"
+ }
+}

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/beta/index.js
+export default 'beta'
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/beta/package.json
+{
+ "name": "beta",
+ "version": "1.0.0",
+ "description": "dummy package",
+ "type": "module",
+ "main": "./index.js",
+ "scripts": {
+ "preinstall": "echo DO NOT INSTALL TEST FIXTURES; exit -1"
+ }
+}

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/optional-cjs/index.js
+module.exports.tryOptionalDeps = () => {
+ const results = { exports: {}, errors: {} };
+ try {
+ results.exports.alpha = require('alpha');
+ } catch (err) {
+ results.errors.alpha = err;
+ }
+ try {
+ results.exports.beta = require('beta');
+ } catch (err) {
+ results.errors.beta = err;
+ }
+ try {
+ results.exports.one = require('missing-one');
+ } catch (err) {
+ results.errors.one = err;
+ }
+ try {
+ results.exports.two = require('missing-two');
+ } catch (err) {
+ results.errors.two = err;
+ }
+ return results
+}

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/optional-cjs/package.json
+{
+ "name": "optional-deps",
+ "version": "1.0.0",
+ "description": "optional-deps uses `optionalDependencies` to test missing and present optional deps",
+ "type": "commonjs",
+ "main": "./index.js",
+ "optionalDependencies": {
+ "alpha": "^1.0.0",
+ "missing-one": "^1.0.0"
+ },
+ "peerDependencies": {
+ "beta": "^1.0.0",
+ "missing-two": "^1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "beta": {
+ "optional": true
+ },
+ "missing-two": {
+ "optional": true
+ }
+ },
+ "scripts": {
+ "preinstall": "echo DO NOT INSTALL TEST FIXTURES; exit -1"
+ }
+}

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/optional-esm/index.js
+export const tryOptionalDeps = async () => {
+ const results = { exports: {}, errors: {} };
+ try {
+ results.exports.alpha = await import('alpha');
+ } catch (err) {
+ results.errors.alpha = err;
+ }
+ try {
+ results.exports.beta = await import('beta');
+ } catch (err) {
+ results.errors.beta = err;
+ }
+ try {
+ results.exports.one = await import('missing-one');
+ } catch (err) {
+ results.errors.one = err;
+ }
+ try {
+ results.exports.two = await import('missing-two');
+ } catch (err) {
+ results.errors.two = err;
+ }
+ return results
+}

ADD
packages/compartment-mapper/test/fixtures-optional/node_modules/optional-esm/package.json
+{
+ "name": "optional-deps",
+ "version": "1.0.0",
+ "description": "optional-deps uses `optionalDependencies` to test missing and present optional deps",
+ "type": "module",
+ "main": "./index.js",
+ "optionalDependencies": {
+ "alpha": "^1.0.0",
+ "missing-one": "^1.0.0"
+ },
+ "peerDependencies": {
+ "beta": "^1.0.0",
+ "missing-two": "^1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "beta": {
+ "optional": true
+ },
+ "missing-two": {
+ "optional": true
+ }
+ },
+ "scripts": {
+ "preinstall": "echo DO NOT INSTALL TEST FIXTURES; exit -1"
+ }
+}

MODIFY
packages/compartment-mapper/test/scaffold.js
packages/compartment-mapper/test/scaffold.js
@@ -53,10 +53,18 @@ export function scaffold(
fixture,
assertFixture,
fixtureAssertionCount,
- { onError, shouldFailBeforeArchiveOperations = false } = {},
+ {
+ onError,
+ shouldFailBeforeArchiveOperations = false,
+ knownFailure = false,
+ } = {},
) {
// wrapping each time allows for convenient use of test.only
const wrap = (testFunc, testCategoryHint) => (title, implementation) => {
+ // mark as known failure if available (but fallback to support test.only)
+ if (knownFailure) {
+ testFunc = testFunc.failing || testFunc;
+ }
return testFunc(title, async t => {
let namespace;
try {

ADD
packages/compartment-mapper/test/test-optional.js
+// import "./ses-lockdown.js";
+import 'ses';
+import test from 'ava';
+import { scaffold } from './scaffold.js';
+
+const fixtureOptionalDepsEsm = new URL(
+ 'fixtures-optional/node_modules/optional-esm/index.js',
+ import.meta.url,
+).toString();
+
+const fixtureOptionalDepsCjs = new URL(
+ 'fixtures-optional/node_modules/optional-cjs/index.js',
+ import.meta.url,
+).toString();
+
+scaffold(
+ 'optionalDependencies/esm',
+ // this test fails because it relies on dynamic import
+ test,
+ fixtureOptionalDepsEsm,
+ async (t, { namespace }) => {
+ const { tryOptionalDeps } = namespace;
+ const result = await tryOptionalDeps();
+ t.deepEqual(
+ Reflect.ownKeys(result.exports),
+ ['alpha', 'beta'],
+ 'expected exports',
+ );
+ t.deepEqual(
+ Reflect.ownKeys(result.errors),
+ ['one', 'two'],
+ 'expected errors',
+ );
+ t.true(
+ result.errors.one.message.startsWith(
+ `Cannot find external module "missing-one" in package`,
+ ),
+ );
+ t.true(
+ result.errors.two.message.startsWith(
+ `Cannot find external module "missing-two" in package`,
+ ),
+ );
+ },
+ 4,
+ { knownFailure: true },
+);
+
+scaffold(
+ 'optionalDependencies/cjs',
+ test,
+ fixtureOptionalDepsCjs,
+ (t, { namespace }) => {
+ const { tryOptionalDeps } = namespace;
+ const result = tryOptionalDeps();
+ t.deepEqual(
+ Reflect.ownKeys(result.exports),
+ ['alpha', 'beta'],
+ 'expected exports',
+ );
+ t.deepEqual(
+ Reflect.ownKeys(result.errors),
+ ['one', 'two'],
+ 'expected errors',
+ );
+ t.true(
+ result.errors.one.message.startsWith(
+ `Cannot find external module "missing-one" in package`,
+ ),
+ );
+ t.true(
+ result.errors.two.message.startsWith(
+ `Cannot find external module "missing-two" in package`,
+ ),
+ );
+ },
+ 4,
+);

feat(compartment-mapper): support various types of optional deps