diff --git a/packages/bundle-source/NEWS.md b/packages/bundle-source/NEWS.md index 1212a2cc36..a110edc50f 100644 --- a/packages/bundle-source/NEWS.md +++ b/packages/bundle-source/NEWS.md @@ -11,6 +11,8 @@ - Adds a `-f,--format` command flag to specify other module formats. - Adds a new `endoScript` module format. - Adds a no-cache, bundle-to-stdout mode. +- Adds a `-t,--tag` command flag to specify export/import conditions like + `"browser"`. # v3.2.1 (2024-03-20) diff --git a/packages/bundle-source/cache.js b/packages/bundle-source/cache.js index 073b194f43..6506dbfc7d 100644 --- a/packages/bundle-source/cache.js +++ b/packages/bundle-source/cache.js @@ -20,6 +20,7 @@ const { Fail, quote: q } = assert; * @property {number} bundleSize * @property {boolean} noTransforms * @property {ModuleFormat} format + * @property {string[]} tags * @property {{ relative: string, absolute: string }} moduleSource * @property {Array<{ relativePath: string, mtime: string, size: number }>} contents */ @@ -51,12 +52,19 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { * @param {Logger} [log] * @param {object} [options] * @param {boolean} [options.noTransforms] + * @param {string[]} [options.tags] * @param {ModuleFormat} [options.format] */ const add = async (rootPath, targetName, log = defaultLog, options = {}) => { const srcRd = cwd.neighbor(rootPath); - const { noTransforms = false, format = 'endoZipBase64' } = options; + const { + noTransforms = false, + format = 'endoZipBase64', + tags = [], + } = options; + + tags.sort(); const statsByPath = new Map(); @@ -115,6 +123,7 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { ), noTransforms, format, + tags, }; await metaWr.atomicWriteText(JSON.stringify(meta, null, 2)); @@ -144,6 +153,7 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { * @param {object} [options] * @param {boolean} [options.noTransforms] * @param {ModuleFormat} [options.format] + * @param {string[]} [options.tags] * @returns {Promise} */ const validate = async ( @@ -154,8 +164,12 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { options = {}, ) => { await null; - const { noTransforms: expectedNoTransforms, format: expectedFormat } = - options; + const { + noTransforms: expectedNoTransforms, + format: expectedFormat, + tags: expectedTags = [], + } = options; + expectedTags.sort(); if (!meta) { const metaJson = await loadMetaText(targetName, log); if (metaJson) { @@ -180,10 +194,16 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { moduleSource: { absolute: moduleSource }, format = 'endoZipBase64', noTransforms = false, + tags = [], } = meta; + tags.sort(); assert.equal(bundleFileName, toBundleName(targetName)); assert.equal(format, expectedFormat); assert.equal(noTransforms, expectedNoTransforms); + assert.equal(tags.length, expectedTags.length); + tags.forEach((tag, index) => { + assert.equal(tag, expectedTags[index]); + }); if (rootOpt) { moduleSource === cwd.neighbor(rootOpt).absolute() || Fail`bundle ${targetName} was for ${moduleSource}, not ${rootOpt}`; @@ -230,6 +250,7 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { * @param {object} [options] * @param {boolean} [options.noTransforms] * @param {ModuleFormat} [options.format] + * @param {string[]} [options.tags] * @returns {Promise} */ const validateOrAdd = async ( @@ -248,6 +269,7 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { meta = await validate(targetName, rootPath, log, meta, { format: options.format, noTransforms: options.noTransforms, + tags: options.tags, }); const { bundleTime, @@ -255,6 +277,7 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { contents, noTransforms, format = 'endoZipBase64', + tags = [], } = meta; log( `${wr}`, @@ -268,6 +291,8 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { noTransforms ? 'w/o transforms' : 'with transforms', 'with format', format, + 'and tags', + JSON.stringify(tags), ); } catch (invalid) { meta = undefined; @@ -284,6 +309,7 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { contents, noTransforms, format = 'endoZipBase64', + tags = [], } = meta; log( `${wr}`, @@ -296,6 +322,8 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { noTransforms ? 'w/o transforms' : 'with transforms', 'with format', format, + 'and tags', + JSON.stringify(tags), ); } @@ -310,6 +338,7 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => { * @param {object} [options] * @param {boolean} [options.noTransforms] * @param {ModuleFormat} [options.format] + * @param {string[]} [options.tags] */ const load = async ( rootPath, diff --git a/packages/bundle-source/demo/node_modules/conditional-exports/a.js b/packages/bundle-source/demo/node_modules/conditional-exports/a.js new file mode 100644 index 0000000000..0ed5c30080 --- /dev/null +++ b/packages/bundle-source/demo/node_modules/conditional-exports/a.js @@ -0,0 +1 @@ +export default 'a'; diff --git a/packages/bundle-source/demo/node_modules/conditional-exports/b.js b/packages/bundle-source/demo/node_modules/conditional-exports/b.js new file mode 100644 index 0000000000..a68ac2819d --- /dev/null +++ b/packages/bundle-source/demo/node_modules/conditional-exports/b.js @@ -0,0 +1 @@ +export default 'b'; diff --git a/packages/bundle-source/demo/node_modules/conditional-exports/package.json b/packages/bundle-source/demo/node_modules/conditional-exports/package.json new file mode 100644 index 0000000000..8e9974e421 --- /dev/null +++ b/packages/bundle-source/demo/node_modules/conditional-exports/package.json @@ -0,0 +1,9 @@ +{ + "name": "conditional-exports", + "version": "0.0.0", + "type": "module", + "exports": { + "a": "./a.js", + "b": "./b.js" + } +} diff --git a/packages/bundle-source/demo/node_modules/conditional-reexports/entry.js b/packages/bundle-source/demo/node_modules/conditional-reexports/entry.js new file mode 100644 index 0000000000..65a337fe86 --- /dev/null +++ b/packages/bundle-source/demo/node_modules/conditional-reexports/entry.js @@ -0,0 +1,2 @@ +import value from 'conditional-exports'; +export default value; diff --git a/packages/bundle-source/demo/node_modules/conditional-reexports/package.json b/packages/bundle-source/demo/node_modules/conditional-reexports/package.json new file mode 100644 index 0000000000..d43ce8f30a --- /dev/null +++ b/packages/bundle-source/demo/node_modules/conditional-reexports/package.json @@ -0,0 +1,8 @@ +{ + "name": "conditional-reexports", + "version": "0.0.0", + "type": "module", + "dependencies": { + "conditional-exports": "*" + } +} diff --git a/packages/bundle-source/src/main.js b/packages/bundle-source/src/main.js index 136c42ae2f..4f31983c19 100644 --- a/packages/bundle-source/src/main.js +++ b/packages/bundle-source/src/main.js @@ -8,9 +8,10 @@ import { jsOpts, jsonOpts, makeNodeBundleCache } from '../cache.js'; /** @import {ModuleFormat} from './types.js' */ const USAGE = `\ -bundle-source [-Tf] -bundle-source [-Tf] --cache-js|--cache-json ( )* +bundle-source [-Tft] +bundle-source [-Tft] --cache-js|--cache-json ( )* -f,--format endoZipBase64*|nestedEvaluate|getExport + -t,--tag (browser, node, &c) -T,--no-transforms`; const options = /** @type {const} */ ({ @@ -32,6 +33,11 @@ const options = /** @type {const} */ ({ short: 'f', multiple: false, }, + tag: { + type: 'string', + short: 't', + multiple: true, + }, // deprecated to: { type: 'string', @@ -52,6 +58,7 @@ export const main = async (args, { loadModule, pid, log }) => { const { values: { format: moduleFormat = 'endoZipBase64', + tag: tags = [], 'no-transforms': noTransforms, 'cache-json': cacheJson, 'cache-js': cacheJs, @@ -84,7 +91,11 @@ export const main = async (args, { loadModule, pid, log }) => { throw new Error(USAGE); } const [entryPath] = positionals; - const bundle = await bundleSource(entryPath, { noTransforms, format }); + const bundle = await bundleSource(entryPath, { + noTransforms, + format, + tags, + }); process.stdout.write(JSON.stringify(bundle)); process.stdout.write('\n'); return; @@ -114,6 +125,7 @@ export const main = async (args, { loadModule, pid, log }) => { await cache.validateOrAdd(bundleRoot, bundleName, undefined, { noTransforms, format, + tags, }); } }; diff --git a/packages/bundle-source/src/script.js b/packages/bundle-source/src/script.js index f2761b3ba0..2e430927b9 100644 --- a/packages/bundle-source/src/script.js +++ b/packages/bundle-source/src/script.js @@ -37,6 +37,7 @@ export async function bundleScript( dev = false, cacheSourceMaps = false, noTransforms = false, + tags = [], commonDependencies, } = options; const powers = { ...readPowers, ...grantedPowers }; @@ -69,6 +70,7 @@ export async function bundleScript( const source = await makeBundle(powers, entry, { dev, + tags, commonDependencies, parserForLanguage, moduleTransforms, diff --git a/packages/bundle-source/src/types.js b/packages/bundle-source/src/types.js index b527bea652..836f1b4326 100644 --- a/packages/bundle-source/src/types.js +++ b/packages/bundle-source/src/types.js @@ -70,6 +70,8 @@ export {}; * @property {boolean} [noTransforms] - when true, generates a bundle with the * original sources instead of SES-shim specific ESM and CJS. This may become * default in a future major version. + * @property {string[]} [tags] - conditions for package.json conditional + * exports and imports. */ /** diff --git a/packages/bundle-source/src/zip-base64.js b/packages/bundle-source/src/zip-base64.js index 7eb9f130bb..e7bd1e29d8 100644 --- a/packages/bundle-source/src/zip-base64.js +++ b/packages/bundle-source/src/zip-base64.js @@ -22,6 +22,7 @@ const readPowers = makeReadPowers({ fs, url, crypto }); * @param {boolean} [options.dev] * @param {boolean} [options.cacheSourceMaps] * @param {boolean} [options.noTransforms] + * @param {string[]} [options.tags] * @param {Record} [options.commonDependencies] * @param {object} [grantedPowers] * @param {(bytes: string | Uint8Array) => string} [grantedPowers.computeSha512] @@ -39,6 +40,7 @@ export async function bundleZipBase64( dev = false, cacheSourceMaps = false, noTransforms = false, + tags = [], commonDependencies, } = options; const powers = { ...readPowers, ...grantedPowers }; @@ -71,6 +73,7 @@ export async function bundleZipBase64( const compartmentMap = await mapNodeModules(powers, entry, { dev, + tags, commonDependencies, }); diff --git a/packages/bundle-source/test/tags-command.test.js b/packages/bundle-source/test/tags-command.test.js new file mode 100644 index 0000000000..ead4ccd88b --- /dev/null +++ b/packages/bundle-source/test/tags-command.test.js @@ -0,0 +1,67 @@ +/* global Buffer */ +import test from '@endo/ses-ava/prepare-endo.js'; + +import { spawn } from 'child_process'; +import url from 'url'; + +const textDecoder = new TextDecoder(); + +const cwd = url.fileURLToPath(new URL('..', import.meta.url)); + +const bundleSource = async (...args) => { + const bundleBytes = await new Promise((resolve, reject) => { + const errorChunks = []; + const outputChunks = []; + const child = spawn('node', ['bin/bundle-source', ...args], { + cwd, + stdio: ['inherit', 'pipe', 'pipe'], + }); + child.on('close', code => { + if (code !== 0) { + reject( + new Error( + `Exit code: ${code}\nError output: ${new TextDecoder().decode( + Buffer.concat(errorChunks), + )}`, + ), + ); + } else { + resolve(Buffer.concat(outputChunks)); + } + }); + child.stdout.on('data', chunk => { + outputChunks.push(chunk); + }); + child.stderr.on('data', chunk => { + errorChunks.push(chunk); + }); + }); + const bundleText = textDecoder.decode(bundleBytes); + return JSON.parse(bundleText); +}; + +test('bundle-source with --format and --tag', async t => { + const compartment = new Compartment(); + { + const bundle = await bundleSource( + '--tag', + 'b', + '--format', + 'endoScript', + 'demo/node_modules/conditional-reexports/entry.js', + ); + const namespace = compartment.evaluate(bundle.source); + t.is(namespace.default, 'b'); + } + { + const bundle = await bundleSource( + '--tag', + 'a', + '--format', + 'endoScript', + 'demo/node_modules/conditional-reexports/entry.js', + ); + const namespace = compartment.evaluate(bundle.source); + t.is(namespace.default, 'a'); + } +});