feat(compartment-mapper): Use package.json files in nested folders of a package when determining module type
MODIFY
packages/compartment-mapper/src/node-modules.js
packages/compartment-mapper/src/node-modules.js
*/
import { inferExports } from './infer-exports.js';
+import { searchDescriptor } from './search.js';
import { parseLocatedJson } from './json.js';
import { unpackReadPowers } from './powers.js';
import { pathCompare } from './compartment-map.js';
@@ -277,6 +278,13 @@ const graphPackage = async (
/** @type {Record<string, Language>} */
const types = {};
+ const readDescriptorUpwards = async path => {
+ const location = resolveLocation(path, packageLocation);
+ // readDescriptor coming from above is memoized, so this is not awfully slow
+ const { data } = await searchDescriptor(location, readDescriptor);
+ return data;
+ };
+
Object.assign(result, {
name,
path: undefined,
@@ -288,6 +296,15 @@ const graphPackage = async (
parsers: inferParsers(packageDescriptor, packageLocation),
});
+ await Promise.all(
+ values(result.exports).map(async item => {
+ const descriptor = await readDescriptorUpwards(item);
+ if (descriptor && descriptor.type === 'module') {
+ types[item] = 'mjs';
+ }
+ }),
+ );
+
await Promise.all(children);
return undefined;
};

MODIFY
packages/compartment-mapper/src/search.js
packages/compartment-mapper/src/search.js
@@ -17,6 +17,63 @@ const decoder = new TextDecoder();
*/
const resolveLocation = (rel, abs) => new URL(rel, abs).toString();
+/**
+ * Searches for the first ancestor directory of given location that contains a
+ * package.json.
+ * Probes by calling readDescriptor.
+ * Returns the result of readDescriptor as data whenever a value is returned.
+ *
+ * @template T
+ * @param {string} location
+ * @param {(location:string)=>Promise<T>} readDescriptor
+ * @returns {Promise<{data:T, directory: string, location:string, packageDescriptorLocation: string}>}
+ */
+export const searchDescriptor = async (location, readDescriptor) => {
+ let directory = resolveLocation('./', location);
+ for (;;) {
+ const packageDescriptorLocation = resolveLocation(
+ 'package.json',
+ directory,
+ );
+ // eslint-disable-next-line no-await-in-loop
+ const data = await readDescriptor(packageDescriptorLocation).catch(
+ () => undefined,
+ );
+ if (data !== undefined) {
+ return {
+ data,
+ directory,
+ location,
+ packageDescriptorLocation,
+ };
+ }
+ const parentDirectory = resolveLocation('../', directory);
+ if (parentDirectory === directory) {
+ throw new Error(`Cannot find package.json along path to ${q(location)}`);
+ }
+ directory = parentDirectory;
+ }
+};
+
+/**
+ * @param {ReadFn} read
+ * @param {string} packageDescriptorLocation
+ * @returns {Promise<string | undefined>}
+ */
+const readDescriptorDefault = async (
+ read,
+ packageDescriptorLocation,
+ // eslint-disable-next-line consistent-return
+) => {
+ const packageDescriptorBytes = await read(packageDescriptorLocation).catch(
+ () => undefined,
+ );
+ if (packageDescriptorBytes !== undefined) {
+ const packageDescriptorText = decoder.decode(packageDescriptorBytes);
+ return packageDescriptorText;
+ }
+};
+
/**
* Searches for the first ancestor directory of a module file that contains a
* package.json.
@@ -34,31 +91,25 @@ const resolveLocation = (rel, abs) => new URL(rel, abs).toString();
* }>}
*/
export const search = async (read, moduleLocation) => {
- let directory = resolveLocation('./', moduleLocation);
- for (;;) {
- const packageDescriptorLocation = resolveLocation(
- 'package.json',
+ const {
+ data,
directory,
+ location,
+ packageDescriptorLocation,
+ } = await searchDescriptor(moduleLocation, loc =>
+ readDescriptorDefault(read, loc),
);
- // eslint-disable-next-line no-await-in-loop
- const packageDescriptorBytes = await read(packageDescriptorLocation).catch(
- () => undefined,
+
+ if (!data) {
+ throw new Error(
+ `Cannot find package.json along path to module ${q(moduleLocation)}`,
);
- if (packageDescriptorBytes !== undefined) {
- const packageDescriptorText = decoder.decode(packageDescriptorBytes);
+ }
+
return {
packageLocation: directory,
- packageDescriptorText,
+ packageDescriptorText: data,
packageDescriptorLocation,
- moduleSpecifier: relativize(relative(directory, moduleLocation)),
+ moduleSpecifier: relativize(relative(directory, location)),
};
- }
- const parentDirectory = resolveLocation('../', directory);
- if (parentDirectory === directory) {
- throw new Error(
- `Cannot find package.json along path to module ${q(moduleLocation)}`,
- );
- }
- directory = parentDirectory;
- }
};

ADD
packages/compartment-mapper/test/fixtures-nested-pkg/node_modules/apackage/afolder/anotherfolder/file2.js
+export const isOk = 1;
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-nested-pkg/node_modules/apackage/afolder/file.js
+export const isOk = 1;
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-nested-pkg/node_modules/apackage/afolder/package.json
+{
+ "type": "module"
+}
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-nested-pkg/node_modules/apackage/package.json
+{
+ "name": "apackage",
+ "exports": {
+ "./file": "./afolder/file.js",
+ "./file2": "./afolder/anotherfolder/file2.js"
+ }
+}
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-nested-pkg/node_modules/app/index.js
+import { isOk as file1 } from "apackage/file";
+import { isOk as file2 } from "apackage/file2";
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-nested-pkg/node_modules/app/package.json
+{
+ "name": "app",
+ "version": "1.0.0",
+ "main": "./index.js",
+ "type": "module",
+ "dependencies": {
+ "apackage": "^1.0.0"
+ }
+}

ADD
packages/compartment-mapper/test/snapshots/test-infer-exports.js.md
+# Snapshot report for `test/test-infer-exports.js`
+
+The actual snapshot is saved in `test-infer-exports.js.snap`.
+
+Generated by [AVA](https://avajs.dev).
+
+## infer-exports for @floating-ui/dom
+
+> Snapshot 1
+
+ {
+ exports: {
+ '@floating-ui/dom': './dist/floating-ui.dom.esm.js',
+ '@floating-ui/dom/package.json': './package.json',
+ },
+ types: {
+ './dist/floating-ui.dom.esm.js': 'mjs',
+ },
+ }
+
+## infer-exports for @noble/hashes
+
+> Snapshot 1
+
+ {
+ exports: {
+ '@noble/hashes/_sha2': './_sha2.js',
+ '@noble/hashes/blake2b': './blake2b.js',
+ '@noble/hashes/blake2b.d.ts': './blake2b.d.ts',
+ '@noble/hashes/blake2s': './blake2s.js',
+ '@noble/hashes/blake2s.d.ts': './blake2s.d.ts',
+ '@noble/hashes/blake3': './blake3.js',
+ '@noble/hashes/blake3.d.ts': './blake3.d.ts',
+ '@noble/hashes/crypto': './crypto.js',
+ '@noble/hashes/eskdf': './eskdf.js',
+ '@noble/hashes/eskdf.d.ts': './eskdf.d.ts',
+ '@noble/hashes/hkdf': './hkdf.js',
+ '@noble/hashes/hkdf.d.ts': './hkdf.d.ts',
+ '@noble/hashes/hmac': './hmac.js',
+ '@noble/hashes/hmac.d.ts': './hmac.d.ts',
+ '@noble/hashes/index': './index.js',
+ '@noble/hashes/pbkdf2': './pbkdf2.js',
+ '@noble/hashes/pbkdf2.d.ts': './pbkdf2.d.ts',
+ '@noble/hashes/ripemd160': './ripemd160.js',
+ '@noble/hashes/ripemd160.d.ts': './ripemd160.d.ts',
+ '@noble/hashes/scrypt': './scrypt.js',
+ '@noble/hashes/scrypt.d.ts': './scrypt.d.ts',
+ '@noble/hashes/sha256': './sha256.js',
+ '@noble/hashes/sha256.d.ts': './sha256.d.ts',
+ '@noble/hashes/sha3': './sha3.js',
+ '@noble/hashes/sha3-addons': './sha3-addons.js',
+ '@noble/hashes/sha3-addons.d.ts': './sha3-addons.d.ts',
+ '@noble/hashes/sha3.d.ts': './sha3.d.ts',
+ '@noble/hashes/sha512': './sha512.js',
+ '@noble/hashes/sha512.d.ts': './sha512.d.ts',
+ '@noble/hashes/utils': './utils.js',
+ '@noble/hashes/utils.d.ts': './utils.d.ts',
+ },
+ types: {},
+ }
+
+## infer-exports for babel-plugin-polyfill-es-shims
+
+> Snapshot 1
+
+ {
+ exports: {
+ 'babel-plugin-polyfill-es-shims': './lib/index.js',
+ 'babel-plugin-polyfill-es-shims/package.json': './package.json',
+ },
+ types: {},
+ }
+
+## infer-exports for color2k
+
+> Snapshot 1
+
+ {
+ exports: {
+ color2k: './dist/index.exports.require.cjs.js',
+ 'color2k/package.json': './package.json',
+ },
+ types: {
+ './dist/index.module.es.js': 'mjs',
+ },
+ }
+
+## infer-exports for socket.io-client
+
+> Snapshot 1
+
+ {
+ exports: {
+ 'socket.io-client': './build/esm/index.js',
+ 'socket.io-client/dist/socket.io.js': './dist/socket.io.js',
+ 'socket.io-client/dist/socket.io.js.map': './dist/socket.io.js.map',
+ 'socket.io-client/package.json': './package.json',
+ },
+ types: {
+ './build/esm/index.js': 'mjs',
+ },
+ }
+
+## infer-exports for underscore
+
+> Snapshot 1
+
+ {
+ exports: {
+ underscore: './underscore-umd.js',
+ 'underscore/amd/*': './amd/*',
+ 'underscore/cjs/*': './cjs/*',
+ 'underscore/modules/*': './modules/*',
+ 'underscore/package.json': './package.json',
+ 'underscore/underscore*': './underscore*',
+ },
+ types: {
+ './modules/index-all.js': 'mjs',
+ },
+ }
+
+## infer-exports for vue
+
+> Snapshot 1
+
+ {
+ exports: {
+ vue: './dist/vue.runtime.esm-bundler.js',
+ 'vue/compiler-sfc': './compiler-sfc/index.mjs',
+ 'vue/dist/*': './dist/*',
+ 'vue/macros': './macros.d.ts',
+ 'vue/macros-global': './macros-global.d.ts',
+ 'vue/package.json': './package.json',
+ 'vue/ref-macros': './ref-macros.d.ts',
+ 'vue/server-renderer': './server-renderer/index.mjs',
+ },
+ types: {
+ './dist/vue.runtime.esm-bundler.js': 'mjs',
+ },
+ }

ADD
packages/compartment-mapper/test/snapshots/test-infer-exports.js.snap
packages/compartment-mapper/test/snapshots/test-infer-exports.js.snap
Binary files /dev/null and b/packages/compartment-mapper/test/snapshots/test-infer-exports.js.snap differ

ADD
packages/compartment-mapper/test/test-infer-exports.js
+import test from 'ava';
+import { inferExports } from '../src/infer-exports.js';
+
+function scaffold(cases) {
+ const tags = new Set(['node', 'import', 'default']);
+ cases.forEach(pkg => {
+ test(`infer-exports for ${pkg.name}`, t => {
+ const types = {};
+ const exports = inferExports(pkg, tags, types);
+ t.snapshot({ types, exports });
+ });
+ });
+}
+
+const cases = [
+ {
+ name: '@floating-ui/dom',
+ exports: {
+ '.': {
+ import: {
+ development: './dist/floating-ui.dom.esm.development.js',
+ production: './dist/floating-ui.dom.esm.min.js',
+ default: './dist/floating-ui.dom.esm.js',
+ },
+ require: './dist/floating-ui.dom.cjs',
+ },
+ './package.json': './package.json',
+ },
+ type: 'module',
+ main: 'dist/floating-ui.dom.js',
+ module: 'dist/floating-ui.dom.esm.js',
+ },
+ {
+ name: 'underscore',
+ main: 'underscore-umd.js',
+ module: 'modules/index-all.js',
+ type: 'commonjs',
+ exports: {
+ '.': {
+ import: {
+ module: './modules/index-all.js',
+ browser: {
+ production: './underscore-esm-min.js',
+ default: './underscore-esm.js',
+ },
+ node: './underscore-node.mjs',
+ default: './underscore-esm.js',
+ },
+ require: {
+ browser: {
+ production: './underscore-umd-min.js',
+ default: './underscore-umd.js',
+ },
+ node: './underscore-node.cjs',
+ default: './underscore-umd.js',
+ },
+ default: './underscore-umd.js',
+ },
+ './underscore*': './underscore*',
+ './modules/*': {
+ require: './cjs/*',
+ default: './modules/*',
+ },
+ './amd/*': './amd/*',
+ './cjs/*': './cjs/*',
+ './package.json': './package.json',
+ },
+ },
+ {
+ name: 'vue',
+ exports: {
+ '.': {
+ import: {
+ node: './index.mjs',
+ default: './dist/vue.runtime.esm-bundler.js',
+ },
+ require: './index.js',
+ types: './dist/vue.d.ts',
+ },
+ './server-renderer': {
+ import: './server-renderer/index.mjs',
+ require: './server-renderer/index.js',
+ },
+ './compiler-sfc': {
+ import: './compiler-sfc/index.mjs',
+ require: './compiler-sfc/index.js',
+ },
+ './dist/*': './dist/*',
+ './package.json': './package.json',
+ './macros': './macros.d.ts',
+ './macros-global': './macros-global.d.ts',
+ './ref-macros': './ref-macros.d.ts',
+ },
+ main: 'index.js',
+ module: 'dist/vue.runtime.esm-bundler.js',
+ },
+ {
+ name: 'socket.io-client',
+ exports: {
+ './package.json': './package.json',
+ './dist/socket.io.js': './dist/socket.io.js',
+ './dist/socket.io.js.map': './dist/socket.io.js.map',
+ '.': {
+ import: {
+ node: './build/esm-debug/index.js',
+ default: './build/esm/index.js',
+ },
+ require: './build/cjs/index.js',
+ types: './build/esm/index.d.ts',
+ },
+ },
+ type: 'commonjs',
+ main: './build/cjs/index.js',
+ module: './build/esm/index.js',
+ browser: { './test/node.ts': false },
+ },
+ {
+ name: 'babel-plugin-polyfill-es-shims',
+ exports: {
+ '.': [
+ { import: './esm/index.mjs', default: './lib/index.js' },
+ './lib/index.js',
+ ],
+ './package.json': './package.json',
+ },
+ main: 'lib/index.js',
+ },
+ {
+ name: 'color2k',
+ exports: {
+ '.': [
+ {
+ import: './dist/index.exports.import.es.mjs',
+ default: './dist/index.exports.require.cjs.js',
+ },
+ './dist/index.exports.require.cjs.js',
+ ],
+ './package.json': './package.json',
+ },
+ main: './dist/index.main.cjs.js',
+ module: './dist/index.module.es.js',
+ },
+ {
+ name: '@noble/hashes',
+ browser: { crypto: false, './crypto': './cryptoBrowser.js' },
+ exports: {
+ './index': { import: './esm/index.js', default: './index.js' },
+ './crypto': {
+ browser: {
+ import: './esm/cryptoBrowser.js',
+ default: './cryptoBrowser.js',
+ },
+ import: './esm/crypto.js',
+ default: './crypto.js',
+ },
+ './_sha2': { import: './esm/_sha2.js', default: './_sha2.js' },
+ './blake2b': { import: './esm/blake2b.js', default: './blake2b.js' },
+ './blake2b.d.ts': 'blake2b.d.ts',
+ './blake2s': { import: './esm/blake2s.js', default: './blake2s.js' },
+ './blake2s.d.ts': 'blake2s.d.ts',
+ './blake3': { import: './esm/blake3.js', default: './blake3.js' },
+ './blake3.d.ts': 'blake3.d.ts',
+ './eskdf': { import: './esm/eskdf.js', default: './eskdf.js' },
+ './eskdf.d.ts': 'eskdf.d.ts',
+ './hkdf': { import: './esm/hkdf.js', default: './hkdf.js' },
+ './hkdf.d.ts': 'hkdf.d.ts',
+ './hmac': { import: './esm/hmac.js', default: './hmac.js' },
+ './hmac.d.ts': 'hmac.d.ts',
+ './pbkdf2': { import: './esm/pbkdf2.js', default: './pbkdf2.js' },
+ './pbkdf2.d.ts': 'pbkdf2.d.ts',
+ './ripemd160': {
+ import: './esm/ripemd160.js',
+ default: './ripemd160.js',
+ },
+ './ripemd160.d.ts': 'ripemd160.d.ts',
+ './scrypt': { import: './esm/scrypt.js', default: './scrypt.js' },
+ './scrypt.d.ts': 'scrypt.d.ts',
+ './sha3-addons': {
+ import: './esm/sha3-addons.js',
+ default: './sha3-addons.js',
+ },
+ './sha3-addons.d.ts': 'sha3-addons.d.ts',
+ './sha3': { import: './esm/sha3.js', default: './sha3.js' },
+ './sha3.d.ts': 'sha3.d.ts',
+ './sha256': { import: './esm/sha256.js', default: './sha256.js' },
+ './sha256.d.ts': 'sha256.d.ts',
+ './sha512': { import: './esm/sha512.js', default: './sha512.js' },
+ './sha512.d.ts': 'sha512.d.ts',
+ './utils': { import: './esm/utils.js', default: './utils.js' },
+ './utils.d.ts': 'utils.d.ts',
+ },
+ },
+];
+
+scaffold(cases);

ADD
packages/compartment-mapper/test/test-nested-pkg.js
+// import "./ses-lockdown.js";
+import 'ses';
+import fs from 'fs';
+import url from 'url';
+import test from 'ava';
+import { loadLocation } from '../src/import.js';
+import { makeReadPowers } from '../src/node-powers.js';
+
+const readPowers = makeReadPowers({ fs, url });
+const { read } = readPowers;
+
+test('nested package.json changing type', async t => {
+ t.plan(1);
+
+ const fixture = new URL(
+ 'fixtures-nested-pkg/node_modules/app/index.js',
+ import.meta.url,
+ ).toString();
+
+ const application = await loadLocation(read, fixture);
+ await application.import({});
+ t.pass();
+});

feat(compartment-mapper): Use package.json files in nested folders of a package when determining module type