Skip to content

Commit

Permalink
Extend test utils to execute ESM bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
mischnic committed Dec 30, 2020
1 parent 67aefd8 commit 4b66387
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 22 deletions.
2 changes: 1 addition & 1 deletion packages/core/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 1 addition & 4 deletions packages/core/integration-tests/test/output-formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
134 changes: 117 additions & 17 deletions packages/core/test-utils/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -201,6 +202,23 @@ export function getNextBuild(b: Parcel): Promise<BuildEvent> {
});
}

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(
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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<string>,
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);
}

0 comments on commit 4b66387

Please sign in to comment.