diff --git a/packages/core/integration-tests/test/integration/formats/esm-async-chained-reexport2/index.js b/packages/core/integration-tests/test/integration/formats/esm-async-chained-reexport2/index.js index 2d54c27c10a..3825ac58074 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-async-chained-reexport2/index.js +++ b/packages/core/integration-tests/test/integration/formats/esm-async-chained-reexport2/index.js @@ -1,4 +1,4 @@ import { createAndFireEvent } from "./library/b.js"; var createAndFireEventOnAtlaskit = createAndFireEvent("index"); -output = import("./library/a.js") - .then((m) => [createAndFireEventOnAtlaskit(), m.default(), m.a]); + +export default import("./library/a.js").then((m) => [createAndFireEventOnAtlaskit(), m.default(), m.a]); diff --git a/packages/core/integration-tests/test/integration/formats/esm-async/index.js b/packages/core/integration-tests/test/integration/formats/esm-async/index.js index b3c8e2afe9a..3315231f8f4 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-async/index.js +++ b/packages/core/integration-tests/test/integration/formats/esm-async/index.js @@ -1 +1 @@ -output = import('./async').then(a => a.foo + 2); +export default import('./async').then(a => a.foo + 2); diff --git a/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/index.js b/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/index.js index 4536f82a81d..806f1ee3be2 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/index.js +++ b/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/index.js @@ -1,8 +1,5 @@ -import {T} from "./i18n"; +import {T} from './i18n/index.js'; -const version = import("./version.js"); +const version = import('./version.js'); -console.log( - T("index"), - version.then(v => "Diagram" + v.default()) -); +export default version.then((v) => [T('index'), 'Diagram' + v.default()]); diff --git a/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/version.js b/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/version.js index 5dd681f6841..2f47e0a6e08 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/version.js +++ b/packages/core/integration-tests/test/integration/formats/esm-bundle-import-reexport/version.js @@ -1,4 +1,4 @@ -import {T} from "./i18n"; +import {T} from "./i18n/index.js"; export default function() { return "Version: " + T("some name"); diff --git a/packages/core/integration-tests/test/integration/formats/esm-split/async1.js b/packages/core/integration-tests/test/integration/formats/esm-split/async1.js index 399c8fb2afb..1e21b020006 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-split/async1.js +++ b/packages/core/integration-tests/test/integration/formats/esm-split/async1.js @@ -1,6 +1,4 @@ import _ from 'lodash'; import react from 'react'; -console.log('async1'); -console.log(_); -console.log(react); +export default ['async1', _, react]; diff --git a/packages/core/integration-tests/test/integration/formats/esm-split/async2.js b/packages/core/integration-tests/test/integration/formats/esm-split/async2.js index 9b260f23b55..c917cac7dfd 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-split/async2.js +++ b/packages/core/integration-tests/test/integration/formats/esm-split/async2.js @@ -1,6 +1,4 @@ import _ from 'lodash'; import react from 'react'; -console.log('async2'); -console.log(_); -console.log(react); +export default ['async2', _, react]; diff --git a/packages/core/integration-tests/test/integration/formats/esm-split/index.js b/packages/core/integration-tests/test/integration/formats/esm-split/index.js index bf8e09a26c6..906b021906a 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-split/index.js +++ b/packages/core/integration-tests/test/integration/formats/esm-split/index.js @@ -1,2 +1,4 @@ -import('./async1'); -import('./async2'); +export default Promise.all([import('./async1'), import('./async2')]).then( + ([{default: [a, b, c]}, {default: [x, y, z]}]) => + a === 'async1' && x === 'async2' && b === y && c === z, +); diff --git a/packages/core/integration-tests/test/integration/formats/esm-wrap-codesplit/a.js b/packages/core/integration-tests/test/integration/formats/esm-wrap-codesplit/a.js index 641fe3b2ca4..e00a4010a73 100644 --- a/packages/core/integration-tests/test/integration/formats/esm-wrap-codesplit/a.js +++ b/packages/core/integration-tests/test/integration/formats/esm-wrap-codesplit/a.js @@ -2,4 +2,4 @@ if (Date.now() < 0) { let x = require("./c.js"); } -output = import("./b.js"); +export default import("./b.js"); diff --git a/packages/core/integration-tests/test/output-formats.js b/packages/core/integration-tests/test/output-formats.js index 01d8fad9473..74d3bc63b5d 100644 --- a/packages/core/integration-tests/test/output-formats.js +++ b/packages/core/integration-tests/test/output-formats.js @@ -3,14 +3,17 @@ import path from 'path'; import nullthrows from 'nullthrows'; import { assertBundles, + assertESMExports, bundle as _bundle, outputFS, run, runBundle, } from '@parcel/test-utils'; +import * as react from 'react'; +import * as lodash from 'lodash'; +import * as lodashFP from 'lodash/fp'; -const bundle = (name, opts = {}) => - _bundle(name, Object.assign({scopeHoist: true}, opts)); +const bundle = (name, opts) => _bundle(name, {scopeHoist: true, ...opts}); describe('output formats', function() { describe('commonjs', function() { @@ -526,7 +529,7 @@ describe('output formats', function() { path.join(__dirname, '/integration/formats/esm/named.js'), ); - assert.deepEqual({...(await run(b))}, {bar: 5, foo: 2}); + await assertESMExports(b, {bar: 5, foo: 2}); }); it('should support esmodule output (default identifier)', async function() { @@ -538,6 +541,7 @@ describe('output formats', function() { assert(!dist.includes('function')); // no iife assert(dist.includes('var _default = $')); assert(dist.includes('export default _default')); + await assertESMExports(b, {default: 4}); }); it('should support esmodule output (default function)', async function() { @@ -547,6 +551,7 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export default function')); + assert.strictEqual((await run(b)).default(), 2); }); it('should support esmodule output (multiple)', async function() { @@ -557,6 +562,7 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export {a, c}')); assert(dist.includes('export default')); + await assertESMExports(b, {a: 2, c: 5, default: 3}); }); it('should support esmodule output (exporting symbol multiple times)', async function() { @@ -567,6 +573,7 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export {test, test as other, foo};')); assert(dist.includes('export default test;')); + await assertESMExports(b, {default: 1, foo: 2, other: 1, test: 1}); }); it('should support esmodule output (re-export)', async function() { @@ -577,6 +584,7 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export {a, c}')); assert(!dist.includes('export default')); + await assertESMExports(b, {a: 2, c: 5}); }); it.skip('should support esmodule output (re-export namespace as)', async function() { @@ -589,6 +597,7 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export var ns')); + await assertESMExports(b, {ns: {a: 2, c: 5}}); }); it('should support esmodule output (renaming re-export)', async function() { @@ -599,6 +608,7 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export var foo')); assert(!dist.includes('export default')); + await assertESMExports(b, {foo: 4}); }); it('should support esmodule output with external modules (named import)', async function() { @@ -609,6 +619,11 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export const bar')); assert(dist.includes('import {add} from "lodash"')); + await assertESMExports( + b, + {bar: 3}, + {lodash: () => ({add: (a, b) => a + b})}, + ); }); it('should support esmodule output with external modules (named import with same name)', async function() { @@ -621,6 +636,14 @@ describe('output formats', function() { assert(dist.includes('import {assign} from "lodash/fp"')); assert(dist.includes('import {assign as _assign} from "lodash"')); assert(dist.includes('assign !== _assign')); + await assertESMExports( + b, + {bar: true}, + { + lodash: () => lodash, + 'lodash/fp': () => lodashFP, + }, + ); }); it('should support esmodule output with external modules (namespace import)', async function() { @@ -641,6 +664,13 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('export const bar')); assert(dist.includes('import _lodash from "lodash"')); + await assertESMExports( + b, + {bar: 3}, + { + lodash: () => lodash, + }, + ); }); it('should support esmodule output with external modules (multiple specifiers)', async function() { @@ -652,6 +682,13 @@ describe('output formats', function() { assert(dist.includes('export const bar')); assert(dist.includes('import _lodash, * as _lodash2 from "lodash"')); assert(dist.includes('import {add} from "lodash"')); + await assertESMExports( + b, + {bar: 6}, + { + lodash: () => lodash, + }, + ); }); it('should support esmodule output with external modules (export)', async function() { @@ -662,6 +699,14 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('import {add} from "lodash"')); assert(dist.includes('export {add}')); + await assertESMExports( + b, + 3, + { + lodash: () => lodash, + }, + ns => ns.add(1, 2), + ); }); it('should support esmodule output with external modules (re-export)', async function() { @@ -672,6 +717,14 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('import {add} from "lodash"')); assert(dist.includes('export {add}')); + await assertESMExports( + b, + 3, + { + lodash: () => lodash, + }, + ns => ns.add(1, 2), + ); }); it('should support importing sibling bundles in library mode', async function() { @@ -726,6 +779,8 @@ describe('output formats', function() { 'utf8', ); assert(dist.includes('$parcel$interopDefault')); + let ns = await run(b); + assert.deepEqual(await ns.default, [123, 123]); }); it('should rename imports that conflict with exports', async function() { @@ -736,6 +791,7 @@ describe('output formats', function() { let dist = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); assert(dist.includes('import {foo as _foo} from "foo";')); assert(dist.includes('export const foo = _foo + 3;')); + await assertESMExports(b, {foo: 13}, {foo: () => ({foo: 10})}); }); it('should support async imports', async function() { @@ -754,6 +810,7 @@ describe('output formats', function() { 'utf8', ); assert(async.includes('export const foo')); + await assertESMExports(b, 4, {}, ns => ns.default); }); // This is currently not possible, it would have to do something like this: @@ -786,6 +843,7 @@ describe('output formats', function() { 'utf8', ); assert(!async.includes('$import$')); + await assertESMExports(b, ['index', 'a', 1], {}, ns => ns.default); }); it('should throw an error on missing export with esmodule output and sideEffects: false', async function() { @@ -866,6 +924,13 @@ describe('output formats', function() { async2, ), ); + + await assertESMExports( + b, + true, + {lodash: () => lodash, react: () => react}, + ns => ns.default, + ); }); it('should call init for wrapped modules when codesplitting to esmodules', async function() { @@ -891,6 +956,8 @@ describe('output formats', function() { childBundleContents, ), ); + let ns = await run(b); + assert.deepStrictEqual({...(await ns.default)}, {default: 2}); }); it('should support async split bundles for workers', async function() { @@ -1124,6 +1191,13 @@ describe('output formats', function() { dist2.match(/import {([a-z0-9$]+)} from "\.\/index\.js";/)[1], exportName, ); + + await assertESMExports( + b, + ['!!!index!!!', 'DiagramVersion: !!!some name!!!'], + {}, + ns => ns.default, + ); }); it('should support generating ESM from CommonJS', async function() { @@ -1138,6 +1212,9 @@ describe('output formats', function() { assert(dist.includes('import {add} from "lodash"')); assert(dist.includes('add(a, b)')); assert(dist.includes('export default')); + + let ns = await run(b, {}, {}, {lodash: () => lodash}); + assert.strictEqual(ns.default(1, 2), 3); }); it('should support re-assigning to module.exports', async function() { @@ -1156,6 +1233,9 @@ describe('output formats', function() { lines[lines.length - 3].startsWith('export default'), ); assert.equal(dist.match(/export default/g).length, 1); + + let ns = await run(b); + assert.deepStrictEqual({...ns}, {default: 'xyz'}); }); it("doesn't support require.resolve calls for excluded assets without commonjs", async function() { @@ -1207,6 +1287,9 @@ describe('output formats', function() { assert(contents.includes('var exports = {}')); assert(contents.includes('export default exports')); assert(contents.includes('exports.default =')); + + let ns = await run(b); + assert.deepEqual({...ns}, {default: {default: 'default'}}); }); }); diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index 2cbcbeff8a6..13a1523b622 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -65,6 +65,10 @@ console.warn = (...args) => { }; /* eslint-enable no-console */ +type ExternalModules = {| + [name: string]: (vm$Context) => {[string]: mixed}, +|}; + export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -98,7 +102,7 @@ If you don't know how, check here: https://bit.ly/2UmWsbD export function bundler( entries: FilePath | Array, - opts?: InitialParcelOptions, + opts?: $Shape, ): Parcel { return new Parcel({ entries, @@ -227,6 +231,7 @@ export async function runBundles( bundles: Array, globals: mixed, opts: RunOpts = {}, + externalModules?: ExternalModules, ): Promise { let entryAsset = nullthrows( bundles @@ -247,13 +252,19 @@ export async function runBundles( } case 'node': case 'electron-main': - ctx = prepareNodeContext(parent.filePath, globals); + ctx = prepareNodeContext( + outputFormat === 'commonjs' && parent.filePath, + globals, + ); break; case 'electron-renderer': { let browser = prepareBrowserContext(parent.filePath, globals); ctx = { ...browser.ctx, - ...prepareNodeContext(parent.filePath, globals), + ...prepareNodeContext( + outputFormat === 'commonjs' && parent.filePath, + globals, + ), }; promises = browser.promises; break; @@ -270,8 +281,14 @@ export async function runBundles( [nullthrows(bundles[0].filePath)], ctx, overlayFS, + externalModules, + true, ); } else { + invariant( + externalModules == null, + 'externalModules are only supported with ESM', + ); for (let b of bundles) { // require, parcelRequire was set up in prepare*Context new vm.Script(await overlayFS.readFile(nullthrows(b.filePath), 'utf8'), { @@ -318,6 +335,7 @@ export async function runBundle( bundle: NamedBundle, globals: mixed, opts: RunOpts = {}, + externalModules?: ExternalModules, ): Promise { if (bundle.type === 'html') { let code = await overlayFS.readFile(nullthrows(bundle.filePath)); @@ -343,9 +361,17 @@ export async function runBundle( scripts.map(p => nullthrows(bundles.find(b => b.filePath === p))), globals, opts, + externalModules, ); } else { - return runBundles(bundleGraph, bundle, [bundle], globals, opts); + return runBundles( + bundleGraph, + bundle, + [bundle], + globals, + opts, + externalModules, + ); } } @@ -353,12 +379,13 @@ export function run( bundleGraph: BundleGraph, globals: mixed, opts: RunOpts = {}, + externalModules?: ExternalModules, // $FlowFixMe[unclear-type] ): Promise { let bundle = nullthrows( bundleGraph.getBundles().find(b => b.type === 'js' || b.type === 'html'), ); - return runBundle(bundleGraph, bundle, globals, opts); + return runBundle(bundleGraph, bundle, globals, opts, externalModules); } export function assertBundles( @@ -560,101 +587,115 @@ function prepareBrowserContext( } const nodeCache = {}; +// no filepath = ESM function prepareNodeContext(filePath, globals) { let exports = {}; - let req = specifier => { - // $FlowFixMe[prop-missing] - let res = resolve.sync(specifier, { - basedir: path.dirname(filePath), - preserveSymlinks: true, - extensions: ['.js', '.json'], - readFileSync: (...args) => { - return overlayFS.readFileSync(...args); - }, - isFile: file => { - try { - var stat = overlayFS.statSync(file); - } catch (err) { - return false; - } - return stat.isFile(); - }, - isDirectory: file => { - try { - var stat = overlayFS.statSync(file); - } catch (err) { - return false; - } - return stat.isDirectory(); - }, - }); - - // Shim FS module using overlayFS - if (res === 'fs') { - return { - readFile: async (file, encoding, cb) => { - let res = await overlayFS.readFile(file, encoding); - cb(null, res); + let req = + filePath && + (specifier => { + // $FlowFixMe[prop-missing] + let res = resolve.sync(specifier, { + basedir: path.dirname(filePath), + preserveSymlinks: true, + extensions: ['.js', '.json'], + readFileSync: (...args) => { + return overlayFS.readFileSync(...args); }, - readFileSync: (file, encoding) => { - return overlayFS.readFileSync(file, encoding); + isFile: file => { + try { + var stat = overlayFS.statSync(file); + } catch (err) { + return false; + } + return stat.isFile(); }, - }; - } + isDirectory: file => { + try { + var stat = overlayFS.statSync(file); + } catch (err) { + return false; + } + return stat.isDirectory(); + }, + }); - if (res === specifier) { - // $FlowFixMe[unsupported-syntax] - return require(specifier); - } + // Shim FS module using overlayFS + if (res === 'fs') { + return { + readFile: async (file, encoding, cb) => { + let res = await overlayFS.readFile(file, encoding); + cb(null, res); + }, + readFileSync: (file, encoding) => { + return overlayFS.readFileSync(file, encoding); + }, + }; + } - if (nodeCache[res]) { - return nodeCache[res].module.exports; - } + if (res === specifier) { + // $FlowFixMe[unsupported-syntax] + return require(specifier); + } - let ctx = prepareNodeContext(res, globals); - nodeCache[res] = ctx; + if (nodeCache[res]) { + return nodeCache[res].module.exports; + } - vm.createContext(ctx); - vm.runInContext( - '"use strict";\n' + overlayFS.readFileSync(res, 'utf8'), - ctx, - ); - return ctx.module.exports; - }; + let ctx = prepareNodeContext(res, globals); + nodeCache[res] = ctx; - var ctx = Object.assign( - { + vm.createContext(ctx); + vm.runInContext( + '"use strict";\n' + overlayFS.readFileSync(res, 'utf8'), + ctx, + ); + return ctx.module.exports; + }); + + // $FlowFixMe any! + var ctx: any = { + ...(filePath && { module: {exports, require: req}, exports, __filename: filePath, __dirname: path.dirname(filePath), require: req, - console, - process: process, - setTimeout: setTimeout, - setImmediate: setImmediate, - }, - globals, - ); + }), + console, + process: process, + setTimeout: setTimeout, + setImmediate: setImmediate, + global: null, + ...globals, + }; ctx.global = ctx; return ctx; } -async function runESM( +export async function runESM( entries: Array, context: vm$Context, fs: FileSystem, - externalModules = {}, -) { + externalModules: ExternalModules = {}, + requireExtensions: boolean = false, +): Promise> { 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 extname = path.extname(specifier); + if (extname && extname !== '.js' && extname !== '.mjs') { + throw new Error( + 'Unknown file extension in ' + + specifier + + ' from ' + + referrer.identifier, + ); + } + let filename = path.resolve( + path.dirname(referrer.identifier), + !extname && !requireExtensions ? specifier + '.js' : specifier, + ); let m = cache.get(filename); if (m) { @@ -723,3 +764,41 @@ async function runESM( return modules.map(m => m.namespace); } + +export async function assertESMExports( + b: BundleGraph, + expected: mixed, + externalModules?: ExternalModules, + // $FlowFixMe[unclear-type] + evaluate: ?({|[string]: any|}) => mixed, +) { + let parcelResult = await run(b, undefined, undefined, externalModules); + + let entry = nullthrows( + b + .getBundles() + .find(b => b.type === 'js') + ?.getMainEntry(), + ); + let [nodeResult] = await runESM( + [entry.filePath], + vm.createContext(prepareNodeContext(false, {})), + inputFS, + externalModules, + ); + + if (evaluate) { + parcelResult = await evaluate(parcelResult); + nodeResult = await evaluate(nodeResult); + } + assert.deepEqual( + parcelResult, + nodeResult, + "Bundle exports don't match Node's native behaviour", + ); + + if (!evaluate) { + parcelResult = {...parcelResult}; + } + assert.deepEqual(parcelResult, expected); +}