From 4b6638761085f85d866b78d7a97673a1d40ed3f1 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Wed, 30 Dec 2020 22:06:28 +0100 Subject: [PATCH] Extend test utils to execute ESM bundles --- packages/core/integration-tests/package.json | 2 +- .../integration-tests/test/output-formats.js | 5 +- packages/core/test-utils/src/utils.js | 134 +++++++++++++++--- 3 files changed, 119 insertions(+), 22 deletions(-) diff --git a/packages/core/integration-tests/package.json b/packages/core/integration-tests/package.json index 6c7dc4a5aef6..59906e77b18b 100644 --- a/packages/core/integration-tests/package.json +++ b/packages/core/integration-tests/package.json @@ -8,7 +8,7 @@ "url": "https://github.com/parcel-bundler/parcel.git" }, "scripts": { - "test": "cross-env NODE_ENV=test PARCEL_BUILD_ENV=test mocha", + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test PARCEL_BUILD_ENV=test mocha", "test-ci": "yarn test --reporter mocha-multi-reporters --reporter-options configFile=./test/mochareporters.json" }, "devDependencies": { diff --git a/packages/core/integration-tests/test/output-formats.js b/packages/core/integration-tests/test/output-formats.js index a9c4f9fa2038..882e40166f65 100644 --- a/packages/core/integration-tests/test/output-formats.js +++ b/packages/core/integration-tests/test/output-formats.js @@ -516,10 +516,7 @@ describe('output formats', function() { path.join(__dirname, '/integration/formats/esm/named.js'), ); - let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); - assert(!dist.includes('function')); // no iife - assert(dist.includes('export const foo = 2')); - assert(/export const bar = .+ \+ 3/.test(dist)); + assert.deepEqual({...(await run(b))}, {bar: 5, foo: 2}); }); it('should support esmodule output (default identifier)', async function() { diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index d4ca35066e93..97ba1e9ef318 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -9,6 +9,7 @@ import type { InitialParcelOptions, NamedBundle, } from '@parcel/types'; +import type {FileSystem} from '@parcel/fs'; import type WorkerFarm from '@parcel/workers'; import invariant from 'assert'; @@ -201,6 +202,23 @@ export function getNextBuild(b: Parcel): Promise { }); } +export function shallowEqual( + a: $Shape<{|+[string]: mixed|}>, + b: $Shape<{|+[string]: mixed|}>, +): boolean { + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + + for (let [key, value] of Object.entries(a)) { + if (!b.hasOwnProperty(key) || b[key] !== value) { + return false; + } + } + + return true; +} + type RunOpts = {require?: boolean, ...}; export async function runBundles( @@ -216,7 +234,8 @@ export async function runBundles( .filter(Boolean)[0], ); let env = entryAsset.env; - let target = entryAsset.env.context; + let target = env.context; + let outputFormat = env.outputFormat; let ctx, promises; switch (target) { @@ -244,19 +263,29 @@ export async function runBundles( } vm.createContext(ctx); - for (let b of bundles) { - new vm.Script(await overlayFS.readFile(nullthrows(b.filePath), 'utf8'), { - filename: b.name, - }).runInContext(ctx); + let esmOutput; + if (outputFormat === 'esmodule') { + invariant(bundles.length === 1, 'currently there can only be one bundle'); + [esmOutput] = await runESM( + [nullthrows(bundles[0].filePath)], + ctx, + overlayFS, + ); + } else { + for (let b of bundles) { + // require, parcelRequire was set up in prepare*Context + new vm.Script(await overlayFS.readFile(nullthrows(b.filePath), 'utf8'), { + filename: b.name, + }).runInContext(ctx); + } } - if (promises) { // await any ongoing dynamic imports during the run await Promise.all(promises); } if (opts.require !== false) { - switch (env.outputFormat) { + switch (outputFormat) { case 'global': if (env.scopeHoist) { return typeof ctx.output !== 'undefined' ? ctx.output : undefined; @@ -272,6 +301,8 @@ export async function runBundles( case 'commonjs': invariant(typeof ctx.module === 'object' && ctx.module != null); return ctx.module.exports; + case 'esmodule': + return esmOutput; default: throw new Error( 'Unable to run bundle with outputFormat ' + env.outputFormat, @@ -610,19 +641,88 @@ function prepareNodeContext(filePath, globals) { return ctx; } -export function shallowEqual( - a: $Shape<{|+[string]: mixed|}>, - b: $Shape<{|+[string]: mixed|}>, -): boolean { - if (Object.keys(a).length !== Object.keys(b).length) { - return false; +async function runESM( + entries: Array, + context: vm$Context, + fs: FileSystem, + externalModules = {}, +) { + let cache = new Map(); + function load(specifier, referrer) { + if (path.isAbsolute(specifier) || specifier.startsWith('.')) { + // if (!path.extname(specifier)) { + // specifier = specifier + '.js'; + // } + + let filename = path.resolve(path.dirname(referrer.identifier), specifier); + + let m = cache.get(filename); + if (m) { + return m; + } + + let source = fs.readFileSync(filename, 'utf8'); + // $FlowFixMe Experimental + m = new vm.SourceTextModule(source, { + identifier: filename, + importModuleDynamically: entry, + context, + }); + cache.set(filename, m); + return m; + } else { + if (!(specifier in externalModules)) { + console.error( + `Couldn't resolve ${specifier} from ${referrer.identifier}`, + ); + throw new Error( + `Couldn't resolve ${specifier} from ${referrer.identifier}`, + ); + } + + let m = cache.get(specifier); + if (m) { + return m; + } + + let ns = externalModules[specifier](context); + + // $FlowFixMe Experimental + m = new vm.SyntheticModule( + Object.keys(ns), + function() { + for (let [k, v] of Object.entries(ns)) { + this.setExport(k, v); + } + }, + {identifier: specifier, context}, + ); + cache.set(specifier, m); + return m; + } } - for (let [key, value] of Object.entries(a)) { - if (!b.hasOwnProperty(key) || b[key] !== value) { - return false; + async function entry(specifier, referrer) { + let m = load(specifier, referrer); + if (m.status === 'unlinked') { + await m.link(load); + } + if (m.status === 'linked') { + await m.evaluate(); } + return m; } - return true; + let modules = []; + for (let f of entries) { + modules.push(await entry(f, {identifier: ''})); + } + + for (let m of modules) { + if (m.status === 'errored') { + throw m.error; + } + } + + return modules.map(m => m.namespace); }