diff --git a/packages/core/core/src/public/Environment.js b/packages/core/core/src/public/Environment.js index 811c48541d1..d40f277f10b 100644 --- a/packages/core/core/src/public/Environment.js +++ b/packages/core/core/src/public/Environment.js @@ -85,6 +85,18 @@ const supportData = { 'service-worker-module': { // TODO: Safari 14.1?? }, + 'import-meta-url': { + edge: '79', + firefox: '62', + chrome: '64', + safari: '11.1', + opera: '51', + ios: '12', + android: '64', + and_chr: '64', + and_ff: '62', + samsung: '9.2', + }, }; const internalEnvironmentToEnvironment: WeakMap< diff --git a/packages/core/core/src/requests/TargetRequest.js b/packages/core/core/src/requests/TargetRequest.js index f5d1d2ee32b..83971b2fc22 100644 --- a/packages/core/core/src/requests/TargetRequest.js +++ b/packages/core/core/src/requests/TargetRequest.js @@ -232,7 +232,9 @@ export class TargetResolver { env: createEnvironment({ engines: descriptor.engines, context: descriptor.context, - isLibrary: descriptor.isLibrary, + isLibrary: + descriptor.isLibrary ?? + this.options.defaultTargetOptions.isLibrary, includeNodeModules: descriptor.includeNodeModules, outputFormat: descriptor.outputFormat ?? @@ -798,7 +800,9 @@ export class TargetResolver { this.options.defaultTargetOptions.outputFormat ?? inferredOutputFormat ?? undefined, - isLibrary: descriptor.isLibrary, + isLibrary: + descriptor.isLibrary ?? + this.options.defaultTargetOptions.isLibrary, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && descriptor.optimize !== false, @@ -827,6 +831,7 @@ export class TargetResolver { engines: pkgEngines, context, outputFormat: this.options.defaultTargetOptions.outputFormat, + isLibrary: this.options.defaultTargetOptions.isLibrary, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize, shouldScopeHoist: this.options.defaultTargetOptions.shouldScopeHoist, sourceMap: this.options.defaultTargetOptions.sourceMaps diff --git a/packages/core/core/src/resolveOptions.js b/packages/core/core/src/resolveOptions.js index e632a1cbac9..4284a3cd2bb 100644 --- a/packages/core/core/src/resolveOptions.js +++ b/packages/core/core/src/resolveOptions.js @@ -164,6 +164,7 @@ export default async function resolveOptions( : {...null}), engines: initialOptions?.defaultTargetOptions?.engines, outputFormat: initialOptions?.defaultTargetOptions?.outputFormat, + isLibrary: initialOptions?.defaultTargetOptions?.isLibrary, }, }; } diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index 03b37cb4cef..0c0a3274ee7 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -282,6 +282,7 @@ export type ParcelOptions = {| +distDir?: ProjectPath, +engines?: Engines, +outputFormat?: OutputFormat, + +isLibrary?: boolean, |}, |}; diff --git a/packages/core/integration-tests/test/html.js b/packages/core/integration-tests/test/html.js index 74f7b330eec..7c96e7c1d7b 100644 --- a/packages/core/integration-tests/test/html.js +++ b/packages/core/integration-tests/test/html.js @@ -1556,12 +1556,11 @@ describe('html', function() { 'index.js', 'index.js', 'js-loader.js', - 'relative-path.js', ], }, { type: 'js', - assets: ['index.js', 'index.js', 'index.js'], + assets: ['bundle-manifest.js', 'index.js', 'index.js', 'index.js'], }, { name: 'index.html', @@ -1640,7 +1639,7 @@ describe('html', function() { }, { type: 'js', - assets: ['bundle-url.js', 'get-worker-url.js', 'index.js'], + assets: ['bundle-manifest.js', 'get-worker-url.js', 'index.js'], }, { name: 'index.html', @@ -1695,7 +1694,7 @@ describe('html', function() { }, { type: 'js', - assets: ['bundle-url.js', 'get-worker-url.js', 'index.js'], + assets: ['bundle-manifest.js', 'get-worker-url.js', 'index.js'], }, { name: 'index.html', diff --git a/packages/core/integration-tests/test/integration/commonjs-bundle-require/.parcelrc b/packages/core/integration-tests/test/integration/commonjs-bundle-require/.parcelrc deleted file mode 100644 index d6c32156c4f..00000000000 --- a/packages/core/integration-tests/test/integration/commonjs-bundle-require/.parcelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@parcel/config-default", - "bundler": "parcel-bundler-splitable" -} diff --git a/packages/core/integration-tests/test/integration/commonjs-bundle-require/index.js b/packages/core/integration-tests/test/integration/commonjs-bundle-require/index.js deleted file mode 100644 index 74333395349..00000000000 --- a/packages/core/integration-tests/test/integration/commonjs-bundle-require/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const local = require("./local") - -module.exports = { - double(x) { - return local.add(x,x) - } -} diff --git a/packages/core/integration-tests/test/integration/commonjs-bundle-require/local.js b/packages/core/integration-tests/test/integration/commonjs-bundle-require/local.js deleted file mode 100644 index a1bdb1c0e5a..00000000000 --- a/packages/core/integration-tests/test/integration/commonjs-bundle-require/local.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - add(a, b) { - return a + b; - } -} diff --git a/packages/core/integration-tests/test/integration/commonjs-bundle-require/node_modules/parcel-bundler-splitable/index.js b/packages/core/integration-tests/test/integration/commonjs-bundle-require/node_modules/parcel-bundler-splitable/index.js deleted file mode 100644 index a024fce6434..00000000000 --- a/packages/core/integration-tests/test/integration/commonjs-bundle-require/node_modules/parcel-bundler-splitable/index.js +++ /dev/null @@ -1,173 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, '__esModule', { - value: true, -}); -exports.default = void 0; - -var _assert = _interopRequireDefault(require('assert')); - -var _plugin = require('@parcel/plugin'); - -var _utils = require('@parcel/utils'); - -var _nullthrows = _interopRequireDefault(require('nullthrows')); - -function _interopRequireDefault(obj) { - return obj && obj.__esModule ? obj : {default: obj}; -} - -const OPTIONS = { - minBundles: 1, - minBundleSize: 30000, - maxParallelRequests: 5, -}; - -var _default = new _plugin.Bundler({ - // RULES: - // 1. If dep.isAsync or dep.isEntry, start a new bundle group. - // 2. If an asset is a different type than the current bundle, make a parallel bundle in the same bundle group. - // 3. If an asset is already in a parent bundle in the same entry point, exclude from child bundles. - // 4. If an asset is only in separate isolated entry points (e.g. workers, different HTML pages), duplicate it. - // 5. If the sub-graph from an asset is >= 30kb, and the number of parallel requests in the bundle group is < 5, create a new bundle containing the sub-graph. - // 6. If two assets are always seen together, put them in the same extracted bundle - bundle({ - bundleGraph, - options - }) { - let bundleRoots = new Map(); - let bundlesByEntryAsset = new Map(); - let siblingBundlesByAsset = new Map(); // Step 1: create bundles for each of the explicit code split points. - - bundleGraph.traverse({ - enter: (node, context) => { - if (node.type !== 'dependency') { - var _bundlesByEntryAsset$; - - return { - ...context, - bundleGroup: context === null || context === void 0 ? void 0 : context.bundleGroup, - bundleByType: context === null || context === void 0 ? void 0 : context.bundleByType, - bundleGroupDependency: context === null || context === void 0 ? void 0 : context.bundleGroupDependency, - parentNode: node, - parentBundle: (_bundlesByEntryAsset$ = bundlesByEntryAsset.get(node.value)) !== null && _bundlesByEntryAsset$ !== void 0 ? _bundlesByEntryAsset$ : context === null || context === void 0 ? void 0 : context.parentBundle, - }; - } - - let dependency = node.value; - let assets = bundleGraph.getDependencyAssets(dependency); - let resolution = bundleGraph.getDependencyResolution(dependency); - - if (dependency.isEntry && resolution || dependency.isAsync && resolution || (resolution === null || resolution === void 0 ? void 0 : resolution.isIsolated) || (resolution === null || resolution === void 0 ? void 0 : resolution.isInline) || (resolution === null || resolution === void 0 ? void 0 : resolution.filePath.endsWith("local.js"))) { - var _dependency$target, _context$bundleGroup; - - let bundleGroup = bundleGraph.createBundleGroup(dependency, (0, _nullthrows.default)((_dependency$target = dependency.target) !== null && _dependency$target !== void 0 ? _dependency$target : context === null || context === void 0 ? void 0 : (_context$bundleGroup = context.bundleGroup) === null || _context$bundleGroup === void 0 ? void 0 : _context$bundleGroup.target)); - let bundleByType = new Map(); - - for (let asset of assets) { - let bundle = bundleGraph.createBundle({ - entryAsset: asset, - needsStableName: asset.isIsolated ? false : Boolean(dependency.isEntry), - isInline: asset.isInline, - target: bundleGroup.target, - }); - bundleByType.set(bundle.type, bundle); - bundleRoots.set(bundle, [asset]); - bundlesByEntryAsset.set(asset, bundle); - siblingBundlesByAsset.set(asset.id, []); - bundleGraph.addBundleToBundleGroup(bundle, bundleGroup); - } - - return { - bundleGroup, - bundleByType, - bundleGroupDependency: dependency, - parentNode: node, - parentBundle: context === null || context === void 0 ? void 0 : context.parentBundle, - }; - } - - (0, _assert.default)(context != null); - (0, _assert.default)(context.parentNode.type === 'asset'); - (0, _assert.default)(context.parentBundle != null); - let parentAsset = context.parentNode.value; - let parentBundle = context.parentBundle; - let bundleGroup = (0, _nullthrows.default)(context.bundleGroup); - let bundleGroupDependency = (0, _nullthrows.default)(context.bundleGroupDependency); - let bundleByType = (0, _nullthrows.default)(context.bundleByType); - let siblingBundles = (0, _nullthrows.default)(siblingBundlesByAsset.get(parentAsset.id)); - let allSameType = assets.every(a => a.type === parentAsset.type); - - for (let asset of assets) { - let siblings = siblingBundlesByAsset.get(asset.id); - - if (parentAsset.type === asset.type) { - if (allSameType && siblings) { - // If any sibling bundles were created for this asset or its subtree previously, - // add them all to the current bundle group as well. This fixes cases where two entries - // depend on a shared asset which has siblings. Due to DFS, the subtree of the shared - // asset is only processed once, meaning any sibling bundles created due to type changes - // would only be connected to the first bundle group. To work around this, we store a list - // of sibling bundles for each asset in the graph, and when we re-visit a shared asset, we - // connect them all to the current bundle group as well. - for (let bundle of siblings) { - bundleGraph.addBundleToBundleGroup(bundle, bundleGroup); - } - } else if (!siblings) { - // Propagate the same siblings further if there are no bundles being created in this - // asset group, otherwise start a new set of siblings. - siblingBundlesByAsset.set(asset.id, allSameType ? siblingBundles : []); - } - - continue; - } - - let existingBundle = bundleByType.get(asset.type); - - if (existingBundle) { - // If a bundle of this type has already been created in this group, - // merge this subgraph into it. - (0, _nullthrows.default)(bundleRoots.get(existingBundle)).push(asset); - bundleGraph.createAssetReference(dependency, asset); - } else { - let bundle = bundleGraph.createBundle({ - entryAsset: asset, - target: bundleGroup.target, - needsStableName: bundleGroupDependency.isEntry, - isInline: asset.isInline, - }); - bundleByType.set(bundle.type, bundle); - siblingBundles.push(bundle); - bundleRoots.set(bundle, [asset]); - bundlesByEntryAsset.set(asset, bundle); - bundleGraph.createAssetReference(dependency, asset); - bundleGraph.createBundleReference(parentBundle, bundle); - bundleGraph.addBundleToBundleGroup(bundle, bundleGroup); - } - - if (!siblings) { - siblingBundlesByAsset.set(asset.id, []); - } - } - - return { - ...context, - parentNode: node, - }; - }, - }); - - for (let [bundle, rootAssets] of bundleRoots) { - for (let asset of rootAssets) { - bundleGraph.addAssetGraphToBundle(asset, bundle); - } - } - }, - - optimize({ - bundleGraph, - }) {}, - -}); - -exports.default = _default; diff --git a/packages/core/integration-tests/test/integration/commonjs-bundle-require/node_modules/parcel-bundler-splitable/package.json b/packages/core/integration-tests/test/integration/commonjs-bundle-require/node_modules/parcel-bundler-splitable/package.json deleted file mode 100644 index fa370aaac25..00000000000 --- a/packages/core/integration-tests/test/integration/commonjs-bundle-require/node_modules/parcel-bundler-splitable/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parcel-bundler-splitable", - "version": "1.2.3", - "engines": { - "parcel": "^2.0.0-alpha.1" - } -} diff --git a/packages/core/integration-tests/test/integration/commonjs-bundle-require/package.json b/packages/core/integration-tests/test/integration/commonjs-bundle-require/package.json deleted file mode 100644 index 982b4f74f26..00000000000 --- a/packages/core/integration-tests/test/integration/commonjs-bundle-require/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "commonjs-bundle-require", - "version": "1.2.3", - "main": "dist/node/index.js", - "bundleSplits": [ - "local.js" - ], - "engines": { - "node": "8" - } -} diff --git a/packages/core/integration-tests/test/integration/commonjs-bundle-require/yarn.lock b/packages/core/integration-tests/test/integration/commonjs-bundle-require/yarn.lock deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/core/integration-tests/test/integration/import-raw-import-meta-url/cjs.js b/packages/core/integration-tests/test/integration/import-raw-import-meta-url/cjs.js new file mode 100644 index 00000000000..63a33549412 --- /dev/null +++ b/packages/core/integration-tests/test/integration/import-raw-import-meta-url/cjs.js @@ -0,0 +1 @@ +export default new URL("test.txt", 'file:' + __filename); diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index ae6c211cf55..e7afd4d6789 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -35,26 +35,6 @@ describe('javascript', function() { assert.equal(output(), 3); }); - it('should import child bundles using a require call in CommonJS', async function() { - let b = await bundle( - path.join(__dirname, '/integration/commonjs-bundle-require/index.js'), - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js'], - }, - { - assets: ['local.js'], - }, - ]); - - let output = await run(b); - assert.strictEqual(typeof output.double, 'function'); - assert.strictEqual(output.double(3), 6); - }); - it('should support url: imports with CommonJS output', async function() { let b = await bundle( path.join(__dirname, '/integration/commonjs-import-url/index.js'), @@ -63,7 +43,7 @@ describe('javascript', function() { assertBundles(b, [ { name: 'index.js', - assets: ['bundle-url.js', 'index.js', 'esmodule-helpers.js'], + assets: ['index.js', 'esmodule-helpers.js'], }, { type: 'txt', @@ -945,7 +925,7 @@ describe('javascript', function() { }, { name: 'index.js', - assets: ['index.js', 'bundle-url.js', 'get-worker-url.js'], + assets: ['index.js', 'bundle-manifest.js', 'get-worker-url.js'], }, { type: 'js', @@ -1554,7 +1534,6 @@ describe('javascript', function() { 'bundle-url.js', 'get-worker-url.js', 'bundle-manifest.js', - 'relative-path.js', 'esmodule-helpers.js', ], }, @@ -1564,7 +1543,6 @@ describe('javascript', function() { 'bundle-url.js', 'get-worker-url.js', 'bundle-manifest.js', - 'relative-path.js', ], }, { @@ -1708,7 +1686,6 @@ describe('javascript', function() { 'bundle-url.js', 'get-worker-url.js', 'bundle-manifest.js', - 'relative-path.js', ], }, { @@ -1849,7 +1826,6 @@ describe('javascript', function() { 'cacheLoader.js', 'js-loader.js', 'bundle-manifest.js', - 'relative-path.js', ], }, { @@ -2066,6 +2042,33 @@ describe('javascript', function() { assert.equal(stats.size, 9); }); + it('should support referencing a raw asset with static URL and CJS __filename', async function() { + let b = await bundle( + path.join(__dirname, '/integration/import-raw-import-meta-url/cjs.js'), + ); + + assertBundles(b, [ + { + name: 'cjs.js', + assets: ['cjs.js', 'bundle-url.js', 'esmodule-helpers.js'], + }, + { + type: 'txt', + assets: ['test.txt'], + }, + ]); + + let contents = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); + assert(!contents.includes('import.meta.url')); + + let output = await run(b); + assert(/^http:\/\/localhost\/test\.[0-9a-f]+\.txt$/.test(output.default)); + let stats = await outputFS.stat( + path.join(distDir, url.parse(output.default).pathname), + ); + assert.equal(stats.size, 9); + }); + it('should ignore new URL and import.meta.url with local binding', async function() { let b = await bundle( path.join( @@ -3716,7 +3719,6 @@ describe('javascript', function() { 'cacheLoader.js', 'dep.js', 'js-loader.js', - 'relative-path.js', 'same-ancestry.js', 'esmodule-helpers.js', ], @@ -3764,7 +3766,6 @@ describe('javascript', function() { 'cacheLoader.js', 'get-dep.js', 'js-loader.js', - 'relative-path.js', 'esmodule-helpers.js', ], }, @@ -3820,7 +3821,6 @@ describe('javascript', function() { 'cacheLoader.js', 'index.js', 'js-loader.js', - 'relative-path.js', 'esmodule-helpers.js', ], }, @@ -4249,7 +4249,6 @@ describe('javascript', function() { 'cacheLoader.js', 'js-loader.js', 'bundle-manifest.js', - 'relative-path.js', ], }, { @@ -4329,7 +4328,6 @@ describe('javascript', function() { 'esmodule-helpers.js', 'js-loader.js', 'bundle-manifest.js', - 'relative-path.js', ], }, { diff --git a/packages/core/integration-tests/test/output-formats.js b/packages/core/integration-tests/test/output-formats.js index 25e707dae87..0c08d4b98d2 100644 --- a/packages/core/integration-tests/test/output-formats.js +++ b/packages/core/integration-tests/test/output-formats.js @@ -453,6 +453,100 @@ describe('output formats', function() { ); assert.deepEqual(await run(b), {foo: 'foo'}); }); + + it('should compile workers to statically analyzable URL expressions', async function() { + let b = await bundle( + path.join(__dirname, '/integration/workers-module/index.js'), + { + mode: 'production', + defaultTargetOptions: { + outputFormat: 'commonjs', + shouldScopeHoist: true, + shouldOptimize: false, + isLibrary: true, + }, + }, + ); + + let contents = await outputFS.readFile( + b.getBundles()[0].filePath, + 'utf8', + ); + let workerBundle = b + .getBundles() + .find(b => b.name.startsWith('dedicated-worker')); + let sharedWorkerBundle = b + .getBundles() + .find(b => b.name.startsWith('shared-worker')); + assert( + contents.includes( + `new Worker(new URL("${path.basename( + workerBundle.filePath, + )}", "file:" + __filename)`, + ), + ); + assert( + contents.includes( + `new SharedWorker(new URL("${path.basename( + sharedWorkerBundle.filePath, + )}", "file:" + __filename)`, + ), + ); + }); + + it('should compile url: pipeline dependencies to statically analyzable URL expressions for libraries', async function() { + let b = await bundle( + path.join(__dirname, '/integration/worklet/pipeline.js'), + { + mode: 'production', + defaultTargetOptions: { + outputFormat: 'commonjs', + shouldScopeHoist: true, + shouldOptimize: false, + isLibrary: true, + }, + }, + ); + + let contents = await outputFS.readFile( + b.getBundles()[0].filePath, + 'utf8', + ); + assert( + contents.includes( + `new URL("${path.basename( + b.getBundles()[1].filePath, + )}", 'file:' + __filename)`, + ), + ); + }); + + it('should URL dependencies to statically analyzable URL expressions for libraries', async function() { + let b = await bundle( + path.join(__dirname, '/integration/worklet/url.js'), + { + mode: 'production', + defaultTargetOptions: { + outputFormat: 'commonjs', + shouldScopeHoist: true, + shouldOptimize: false, + isLibrary: true, + }, + }, + ); + + let contents = await outputFS.readFile( + b.getBundles()[0].filePath, + 'utf8', + ); + assert( + contents.includes( + `new URL("${path.basename( + b.getBundles()[1].filePath, + )}", "file:" + __filename)`, + ), + ); + }); }); describe('esmodule', function() { @@ -953,9 +1047,9 @@ describe('output formats', function() { ); assert( new RegExp( - 'Promise.all\\(\\[\\n.+?getBundleURL\\(\\) \\+ "' + + 'Promise.all\\(\\[\\n.+?new URL\\("' + path.basename(asyncCssBundle.filePath) + - '"\\),\\n\\s*import\\("\\.\\/' + + '", import.meta.url\\).toString\\(\\)\\),\\n\\s*import\\("\\.\\/' + path.basename(asyncJsBundle.filePath) + '"\\)\\n\\s*\\]\\)', ).test(entry), @@ -1008,9 +1102,7 @@ describe('output formats', function() { // async import both bundles in parallel for performance assert( new RegExp( - `import\\("\\./${path.basename( - sharedBundle.filePath, - )}"\\),\\n\\s*import\\("./${path.basename(bundle.filePath)}"\\)`, + `import\\("\\./" \\+ .+\\.resolve\\("${sharedBundle.publicId}"\\)\\),\\n\\s*import\\("./" \\+ .+\\.resolve\\("${bundle.publicId}"\\)\\)`, ).test(entry), ); } @@ -1169,6 +1261,100 @@ describe('output formats', function() { assert(!output.includes('import ')); assert(output.includes('require(')); }); + + it('should compile workers to statically analyzable URL expressions', async function() { + let b = await bundle( + path.join(__dirname, '/integration/workers-module/index.js'), + { + mode: 'production', + defaultTargetOptions: { + outputFormat: 'esmodule', + shouldScopeHoist: true, + shouldOptimize: false, + isLibrary: true, + }, + }, + ); + + let contents = await outputFS.readFile( + b.getBundles()[0].filePath, + 'utf8', + ); + let workerBundle = b + .getBundles() + .find(b => b.name.startsWith('dedicated-worker')); + let sharedWorkerBundle = b + .getBundles() + .find(b => b.name.startsWith('shared-worker')); + assert( + contents.includes( + `new Worker(new URL("${path.basename( + workerBundle.filePath, + )}", import.meta.url)`, + ), + ); + assert( + contents.includes( + `new SharedWorker(new URL("${path.basename( + sharedWorkerBundle.filePath, + )}", import.meta.url)`, + ), + ); + }); + + it('should compile url: pipeline dependencies to statically analyzable URL expressions for libraries', async function() { + let b = await bundle( + path.join(__dirname, '/integration/worklet/pipeline.js'), + { + mode: 'production', + defaultTargetOptions: { + outputFormat: 'esmodule', + shouldScopeHoist: true, + shouldOptimize: false, + isLibrary: true, + }, + }, + ); + + let contents = await outputFS.readFile( + b.getBundles()[0].filePath, + 'utf8', + ); + assert( + contents.includes( + `new URL("${path.basename( + b.getBundles()[1].filePath, + )}", import.meta.url)`, + ), + ); + }); + + it('should URL dependencies to statically analyzable URL expressions for libraries', async function() { + let b = await bundle( + path.join(__dirname, '/integration/worklet/url.js'), + { + mode: 'production', + defaultTargetOptions: { + outputFormat: 'esmodule', + shouldScopeHoist: true, + shouldOptimize: false, + isLibrary: true, + }, + }, + ); + + let contents = await outputFS.readFile( + b.getBundles()[0].filePath, + 'utf8', + ); + assert( + contents.includes( + `new URL("${path.basename( + b.getBundles()[1].filePath, + )}", import.meta.url)`, + ), + ); + }); }); it('should support generating ESM from universal module wrappers', async function() { @@ -1233,7 +1419,7 @@ describe('output formats', function() { assertBundles(b, [ { type: 'js', - assets: ['bundle-url.js', 'get-worker-url.js', 'index.js'], + assets: ['bundle-manifest.js', 'get-worker-url.js', 'index.js'], }, {type: 'html', assets: ['index.html']}, {type: 'js', assets: ['lodash.js']}, diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js index 887feeae62b..29587ebd65f 100644 --- a/packages/core/integration-tests/test/scope-hoisting.js +++ b/packages/core/integration-tests/test/scope-hoisting.js @@ -5544,7 +5544,6 @@ describe('scope hoisting', function() { 'cacheLoader.js', 'dep.js', 'js-loader.js', - 'relative-path.js', 'same-ancestry-scope-hoisting.js', ], }, @@ -5571,7 +5570,6 @@ describe('scope hoisting', function() { 'bundle-url.js', 'cacheLoader.js', 'js-loader.js', - 'relative-path.js', ], }, {assets: ['dep.js']}, @@ -5612,7 +5610,6 @@ describe('scope hoisting', function() { 'cacheLoader.js', 'get-dep-scope-hoisting.js', 'js-loader.js', - 'relative-path.js', ], }, ]); @@ -5660,7 +5657,6 @@ describe('scope hoisting', function() { 'cacheLoader.js', 'scope-hoisting.js', 'js-loader.js', - 'relative-path.js', ], }, ]); diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index 321555d1058..5af39cbaf62 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -853,6 +853,7 @@ function prepareNodeContext(filePath, globals, ctx: any = {}) { ctx.setTimeout = setTimeout; ctx.setImmediate = setImmediate; ctx.global = ctx; + ctx.URL = URL; Object.assign(ctx, globals); return ctx; } @@ -894,6 +895,9 @@ export async function runESM( identifier: filename + '?id=' + id, importModuleDynamically: entry, context, + initializeImportMeta(meta) { + meta.url = `http://localhost/${path.basename(filename)}`; + }, }); cache.set(filename, m); return m; diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 6b4754d3eb1..325ce6f1c22 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -194,7 +194,8 @@ export type EnvironmentFeature = | 'esmodules' | 'dynamic-import' | 'worker-module' - | 'service-worker-module'; + | 'service-worker-module' + | 'import-meta-url'; /** * Defines the environment in for the output bundle @@ -310,6 +311,7 @@ export type InitialParcelOptions = {| +distDir?: FilePath, +engines?: Engines, +outputFormat?: OutputFormat, + +isLibrary?: boolean, |}, +additionalReporters?: Array<{| diff --git a/packages/core/utils/src/replaceBundleReferences.js b/packages/core/utils/src/replaceBundleReferences.js index 1fcf520df7c..ce9ed43318f 100644 --- a/packages/core/utils/src/replaceBundleReferences.js +++ b/packages/core/utils/src/replaceBundleReferences.js @@ -12,6 +12,7 @@ import type { import {Readable} from 'stream'; import nullthrows from 'nullthrows'; +import invariant from 'assert'; import URL from 'url'; import {bufferStream, relativeBundlePath, urlJoin} from './'; @@ -53,10 +54,13 @@ export function replaceURLReferences({ continue; } + let placeholder = dependency.meta?.placeholder ?? dependency.id; + invariant(typeof placeholder === 'string'); + let resolved = bundleGraph.getReferencedBundle(dependency, bundle); if (resolved == null) { - replacements.set(dependency.id, { - from: dependency.id, + replacements.set(placeholder, { + from: placeholder, to: dependency.specifier, }); continue; @@ -69,7 +73,7 @@ export function replaceURLReferences({ } replacements.set( - dependency.id, + placeholder, getURLReplacement({ dependency, fromBundle: bundle, @@ -172,8 +176,10 @@ export function getURLReplacement({ ); } + let placeholder = dependency.meta?.placeholder ?? dependency.id; + invariant(typeof placeholder === 'string'); return { - from: dependency.id, + from: placeholder, to, }; } diff --git a/packages/packagers/js/src/index.js b/packages/packagers/js/src/index.js index 0d67d76ad67..0e8107c6c90 100644 --- a/packages/packagers/js/src/index.js +++ b/packages/packagers/js/src/index.js @@ -2,7 +2,7 @@ import type {Async} from '@parcel/types'; import type SourceMap from '@parcel/source-map'; import {Packager} from '@parcel/plugin'; -import {replaceInlineReferences} from '@parcel/utils'; +import {replaceInlineReferences, replaceURLReferences} from '@parcel/utils'; import {hashString} from '@parcel/hash'; import path from 'path'; import nullthrows from 'nullthrows'; @@ -64,6 +64,17 @@ export default (new Packager({ contents += '\n' + (await getSourceMapSuffix(getSourceMapReference, map)); + // For library builds, we need to replace URL references with their final resolved paths. + // For non-library builds, this is handled in the JS runtime. + if (bundle.env.isLibrary) { + ({contents, map} = replaceURLReferences({ + bundle, + bundleGraph, + contents, + map, + })); + } + return replaceInlineReferences({ bundle, bundleGraph, diff --git a/packages/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js index 77c20f30cbb..640518cadaf 100644 --- a/packages/runtimes/js/src/JSRuntime.js +++ b/packages/runtimes/js/src/JSRuntime.js @@ -181,17 +181,9 @@ export default (new Runtime({ }), ); - if (bundle.env.outputFormat === 'commonjs' && mainBundle.type === 'js') { - assets.push({ - filePath: __filename, - dependency, - code: `module.exports = __parcel__require__("./" + ${getRelativePathExpr( - bundle, - mainBundle, - options, - )})`, - env: {sourceType: 'module'}, - }); + // Skip URL runtimes for library builds. This is handled in packaging so that + // the url is inlined and statically analyzable. + if (bundle.env.isLibrary && dependency.meta?.placeholder != null) { continue; } @@ -224,7 +216,7 @@ export default (new Runtime({ ); let loaderCode = `require(${JSON.stringify( loader, - )})(require('./bundle-url').getBundleURL() + ${relativePathExpr})`; + )})( ${getAbsoluteUrlExpr(relativePathExpr, bundle)})`; assets.push({ filePath: __filename, code: loaderCode, @@ -332,7 +324,8 @@ function getLoaderRuntime({ } // Determine if we need to add a dynamic import() polyfill, or if all target browsers support it natively. - let needsDynamicImportPolyfill = !bundle.env.supports('dynamic-import', true); + let needsDynamicImportPolyfill = + !bundle.env.isLibrary && !bundle.env.supports('dynamic-import', true); let loaderModules = externalBundles .map(to => { @@ -357,9 +350,10 @@ function getLoaderRuntime({ return `Promise.resolve(__parcel__require__("./" + ${relativePathExpr}))`; } - let code = `require(${JSON.stringify( - loader, - )})(require('./bundle-url').getBundleURL() + ${relativePathExpr})`; + let code = `require(${JSON.stringify(loader)})(${getAbsoluteUrlExpr( + relativePathExpr, + bundle, + )})`; // In development, clear the require cache when an error occurs so the // user can try again (e.g. after fixing a build error). @@ -482,11 +476,10 @@ function getHintLoaders( ); let priority = TYPE_TO_RESOURCE_PRIORITY[bundleToPreload.type]; hintLoaders.push( - `require(${JSON.stringify( - loader, - )})(require('./bundle-url').getBundleURL() + ${relativePathExpr}, ${ - priority ? JSON.stringify(priority) : 'null' - }, ${JSON.stringify( + `require(${JSON.stringify(loader)})(${getAbsoluteUrlExpr( + relativePathExpr, + from, + )}, ${priority ? JSON.stringify(priority) : 'null'}, ${JSON.stringify( bundleToPreload.target.env.outputFormat === 'esmodule', )})`, ); @@ -521,20 +514,32 @@ function getURLRuntime( options: PluginOptions, ): RuntimeAsset { let relativePathExpr = getRelativePathExpr(from, to, options); - if (dependency.meta.webworker === true) { - return { - filePath: __filename, - code: `module.exports = require('./get-worker-url')(${relativePathExpr}, ${String( + let code; + + if (dependency.meta.webworker === true && !from.env.isLibrary) { + code = `let workerURL = require('./get-worker-url');\n`; + if ( + from.env.outputFormat === 'esmodule' && + from.env.supports('import-meta-url') + ) { + code += `let url = new __parcel__URL__(${relativePathExpr}, import.meta.url);\n`; + code += `module.exports = workerURL(url.toString(), url.origin, ${String( + from.env.outputFormat === 'esmodule', + )});`; + } else { + code += `let bundleURL = require('./bundle-url');\n`; + code += `let url = bundleURL.getBundleURL() + ${relativePathExpr};`; + code += `module.exports = workerURL(url, bundleURL.getOrigin(url), ${String( from.env.outputFormat === 'esmodule', - )});`, - dependency, - env: {sourceType: 'module'}, - }; + )});`; + } + } else { + code = `module.exports = ${getAbsoluteUrlExpr(relativePathExpr, from)};`; } return { filePath: __filename, - code: `module.exports = require('./bundle-url').getBundleURL() + ${relativePathExpr}`, + code, dependency, env: {sourceType: 'module'}, }; @@ -550,7 +555,7 @@ function getRegisterCode( return; } - idToName[bundle.publicId] = nullthrows(bundle.name); + idToName[bundle.publicId] = path.basename(nullthrows(bundle.name)); if (bundle !== entryBundle && isNewContext(bundle, bundleGraph)) { // New contexts have their own manifests, so there's no need to continue. @@ -570,13 +575,35 @@ function getRelativePathExpr( to: NamedBundle, options: PluginOptions, ): string { + let relativePath = relativeBundlePath(from, to, {leadingDotSlash: false}); if (shouldUseRuntimeManifest(from, options)) { - return `require('./relative-path')(${JSON.stringify( - from.publicId, - )}, ${JSON.stringify(to.publicId)})`; + // Get the relative part of the path. This part is not in the manifest, only the basename is. + let relativeBase = path.posix.dirname(relativePath); + if (relativeBase === '.') { + relativeBase = ''; + } else { + relativeBase = `${JSON.stringify(relativeBase + '/')} + `; + } + return ( + relativeBase + + `require('./bundle-manifest').resolve(${JSON.stringify(to.publicId)})` + ); } - return JSON.stringify(relativeBundlePath(from, to, {leadingDotSlash: false})); + return JSON.stringify(relativePath); +} + +function getAbsoluteUrlExpr(relativePathExpr: string, bundle: NamedBundle) { + if ( + bundle.env.outputFormat === 'esmodule' && + bundle.env.supports('import-meta-url') + ) { + return `new __parcel__URL__(${relativePathExpr}, import.meta.url).toString()`; + } else if (bundle.env.outputFormat === 'commonjs' || bundle.env.isNode()) { + return `new __parcel__URL__(${relativePathExpr}, 'file:' + __filename).toString()`; + } else { + return `require('./bundle-url').getBundleURL() + ${relativePathExpr}`; + } } function shouldUseRuntimeManifest( @@ -587,7 +614,6 @@ function shouldUseRuntimeManifest( return ( !env.isLibrary && bundle.bundleBehavior !== 'inline' && - env.outputFormat === 'global' && env.isBrowser() && options.mode === 'production' ); diff --git a/packages/runtimes/js/src/get-worker-url.js b/packages/runtimes/js/src/get-worker-url.js index 62c0a0cb8b6..82619262db6 100644 --- a/packages/runtimes/js/src/get-worker-url.js +++ b/packages/runtimes/js/src/get-worker-url.js @@ -1,10 +1,7 @@ /* global self, Blob */ -var bundleUrl = require('./bundle-url'); - -module.exports = function loadWorker(relativePath, isESM) { - var workerUrl = bundleUrl.getBundleURL() + relativePath; - if (bundleUrl.getOrigin(workerUrl) === self.location.origin) { +module.exports = function loadWorker(workerUrl, origin, isESM) { + if (origin === self.location.origin) { // If the worker bundle's url is on the same origin as the document, // use the worker bundle's own url. return workerUrl; diff --git a/packages/runtimes/js/src/relative-path.js b/packages/runtimes/js/src/relative-path.js deleted file mode 100644 index 31e6cae6b64..00000000000 --- a/packages/runtimes/js/src/relative-path.js +++ /dev/null @@ -1,66 +0,0 @@ -var resolve = require('./bundle-manifest').resolve; - -module.exports = function getRelativePath(fromId, toId) { - return relative(dirname(resolve(fromId)), resolve(toId)); -}; - -function dirname(_filePath) { - if (_filePath === '') { - return '.'; - } - - var filePath = - _filePath[_filePath.length - 1] === '/' - ? _filePath.slice(0, _filePath.length - 1) - : _filePath; - - var slashIndex = filePath.lastIndexOf('/'); - return slashIndex === -1 ? '.' : filePath.slice(0, slashIndex); -} - -function relative(from, to) { - if (from === to) { - return ''; - } - - var fromParts = from.split('/'); - if (fromParts[0] === '.') { - fromParts.shift(); - } - - var toParts = to.split('/'); - if (toParts[0] === '.') { - toParts.shift(); - } - - // Find where path segments diverge. - var i; - var divergeIndex; - for ( - i = 0; - (i < toParts.length || i < fromParts.length) && divergeIndex == null; - i++ - ) { - if (fromParts[i] !== toParts[i]) { - divergeIndex = i; - } - } - - // If there are segments from "from" beyond the point of divergence, - // return back up the path to that point using "..". - var parts = []; - for (i = 0; i < fromParts.length - divergeIndex; i++) { - parts.push('..'); - } - - // If there are segments from "to" beyond the point of divergence, - // continue using the remaining segments. - if (toParts.length > divergeIndex) { - parts.push.apply(parts, toParts.slice(divergeIndex)); - } - - return parts.join('/'); -} - -module.exports._dirname = dirname; -module.exports._relative = relative; diff --git a/packages/runtimes/js/test/relative-path.test.js b/packages/runtimes/js/test/relative-path.test.js deleted file mode 100644 index 2bd751a2675..00000000000 --- a/packages/runtimes/js/test/relative-path.test.js +++ /dev/null @@ -1,54 +0,0 @@ -// @flow - -import assert from 'assert'; -import {_dirname as dirname, _relative as relative} from '../src/relative-path'; - -describe('relative-path', () => { - describe('dirname', () => { - it('returns "." for a path without slashes', () => { - assert.equal(dirname(''), '.'); - assert.equal(dirname('foo'), '.'); - }); - - it('returns "." for a path with a single trailing slash', () => { - assert.equal(dirname('foo/'), '.'); - }); - - it('returns the directory for a relative path', () => { - assert.equal(dirname('foo/bar/baz'), 'foo/bar'); - }); - - it('returns the directory for a path with a trailing slash', () => { - assert.equal(dirname('foo/bar/'), 'foo'); - }); - - it('returns the directory for an absolute path', () => { - assert.equal(dirname('/foo/bar/baz'), '/foo/bar'); - }); - }); - - describe('relative', () => { - it('returns "" when to and from are the same', () => { - assert.equal(relative('foo/bar/baz', 'foo/bar/baz'), ''); - }); - - it('returns a relative upward path when to contains from', () => { - assert.equal(relative('foo/bar/baz', 'foo/bar'), '..'); - assert.equal(relative('foo/bar/baz', './foo/bar'), '..'); - }); - - it('returns a relative upward path when they share a common root', () => { - assert.equal(relative('foo/bar/baz', 'foo/bar/foobar'), '../foobar'); - assert.equal( - relative('foo/bar/baz/foobaz', 'foo/bar/foobar'), - '../../foobar', - ); - }); - - it('returns a relative forward path when from contains to', () => { - assert.equal(relative('foo/bar', 'foo/bar/baz'), 'baz'); - assert.equal(relative('foo/bar', 'foo/bar/baz/foobar'), 'baz/foobar'); - assert.equal(relative('.', 'foo/bar'), 'foo/bar'); - }); - }); -}); diff --git a/packages/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index 68e485719e0..4b6ab176e8a 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -9,6 +9,17 @@ use swc_ecmascript::utils::ident::IdentLike; use swc_ecmascript::visit::{Fold, FoldWith}; use crate::utils::*; +use crate::Config; + +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +macro_rules! hash { + ($str:expr) => {{ + let mut hasher = DefaultHasher::new(); + $str.hash(&mut hasher); + hasher.finish() + }}; +} #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum DependencyKind { @@ -39,6 +50,7 @@ pub struct DependencyDescriptor { pub is_optional: bool, pub is_helper: bool, pub source_type: Option, + pub placeholder: Option, } /// This pass collects dependencies in a module and compiles references as needed to work with Parcel's JSRuntime. @@ -47,9 +59,7 @@ pub fn dependency_collector<'a>( items: &'a mut Vec, decls: &'a HashSet<(JsWord, SyntaxContext)>, ignore_mark: swc_common::Mark, - scope_hoist: bool, - source_type: SourceType, - supports_module_workers: bool, + config: &'a Config, diagnostics: &'a mut Vec, ) -> impl Fold + 'a { DependencyCollector { @@ -60,9 +70,7 @@ pub fn dependency_collector<'a>( require_node: None, decls, ignore_mark, - scope_hoist, - source_type, - supports_module_workers, + config, diagnostics, } } @@ -75,9 +83,7 @@ struct DependencyCollector<'a> { require_node: Option, decls: &'a HashSet<(JsWord, SyntaxContext)>, ignore_mark: swc_common::Mark, - scope_hoist: bool, - source_type: SourceType, - supports_module_workers: bool, + config: &'a Config, diagnostics: &'a mut Vec, } @@ -99,7 +105,53 @@ impl<'a> DependencyCollector<'a> { is_optional, is_helper: span.is_dummy(), source_type: Some(source_type), + placeholder: None, + }); + } + + fn add_url_dependency( + &mut self, + specifier: JsWord, + span: swc_common::Span, + kind: DependencyKind, + source_type: SourceType, + ) -> ast::Expr { + // If not a library, replace with a require call pointing to a runtime that will resolve the url dynamically. + if !self.config.is_library { + self.add_dependency(specifier.clone(), span, kind, None, false, source_type); + return ast::Expr::Call(self.create_require(specifier)); + } + + // For library builds, we need to create something that can be statically analyzed by another bundler, + // so rather than replacing with a require call that is resolved by a runtime, replace with a `new URL` + // call with a placeholder for the relative path to be replaced during packaging. + let placeholder = format!( + "{:x}", + hash!(format!( + "parcel_url:{}:{}:{}", + self.config.filename, specifier, kind + )) + ); + self.items.push(DependencyDescriptor { + kind, + loc: SourceLocation::from(self.source_map, span), + specifier, + attributes: None, + is_optional: false, + is_helper: span.is_dummy(), + source_type: Some(source_type), + placeholder: Some(placeholder.clone()), }); + + create_url_constructor( + ast::Expr::Lit(ast::Lit::Str(ast::Str { + span, + value: placeholder.into(), + kind: ast::StrKind::Synthesized, + has_escape: false, + })), + self.config.is_esm_output, + ) } fn create_require(&mut self, specifier: JsWord) -> ast::CallExpr { @@ -107,7 +159,7 @@ impl<'a> DependencyCollector<'a> { // For scripts, we replace with __parcel__require__, which is later replaced // by a real parcelRequire of the resolved asset in the packager. - if self.source_type == SourceType::Script { + if self.config.source_type == SourceType::Script { res.callee = ast::ExprOrSuper::Expr(Box::new(ast::Expr::Ident(ast::Ident::new( "__parcel__require__".into(), DUMMY_SP, @@ -151,7 +203,7 @@ fn rewrite_require_specifier(node: ast::CallExpr) -> ast::CallExpr { impl<'a> Fold for DependencyCollector<'a> { fn fold_module_decl(&mut self, node: ast::ModuleDecl) -> ast::ModuleDecl { // If an import or export is seen within a script, flag it to throw an error from JS. - if self.source_type == SourceType::Script { + if self.config.source_type == SourceType::Script { match node { ast::ModuleDecl::Import(ast::ImportDecl { span, .. }) | ast::ModuleDecl::ExportAll(ast::ExportAll { span, .. }) @@ -180,7 +232,7 @@ impl<'a> Fold for DependencyCollector<'a> { DependencyKind::Import, None, false, - self.source_type, + self.config.source_type, ); return node; @@ -198,7 +250,7 @@ impl<'a> Fold for DependencyCollector<'a> { DependencyKind::Export, None, false, - self.source_type, + self.config.source_type, ); } @@ -212,7 +264,7 @@ impl<'a> Fold for DependencyCollector<'a> { DependencyKind::Export, None, false, - self.source_type, + self.config.source_type, ); return node; @@ -267,7 +319,7 @@ impl<'a> Fold for DependencyCollector<'a> { } } "importScripts" => { - let msg = if self.source_type == SourceType::Script { + let msg = if self.config.source_type == SourceType::Script { "importScripts() is not supported in worker scripts." } else { "importScripts() is not supported in module workers." @@ -281,7 +333,7 @@ impl<'a> Fold for DependencyCollector<'a> { hints: Some(vec![String::from( "Use a static `import`, or dynamic `import()` instead.", )]), - show_environment: self.source_type == SourceType::Script, + show_environment: self.config.source_type == SourceType::Script, }); return node.fold_children_with(self); } @@ -305,13 +357,17 @@ impl<'a> Fold for DependencyCollector<'a> { } } Member(member) => { - if match_member_expr( - member, - vec!["navigator", "serviceWorker", "register"], - self.decls, - ) { + if self.config.is_browser + && match_member_expr( + member, + vec!["navigator", "serviceWorker", "register"], + self.decls, + ) + { DependencyKind::ServiceWorker - } else if match_member_expr(member, vec!["CSS", "paintWorklet", "addModule"], self.decls) { + } else if self.config.is_browser + && match_member_expr(member, vec!["CSS", "paintWorklet", "addModule"], self.decls) + { DependencyKind::Worklet } else { let was_in_promise = self.in_promise; @@ -447,16 +503,9 @@ impl<'a> Fold for DependencyCollector<'a> { return node; }; - self.add_dependency( - specifier.clone(), - span, - kind.clone(), - attributes, - false, - source_type, - ); + node.args[0].expr = + Box::new(self.add_url_dependency(specifier.clone(), span, kind.clone(), source_type)); - node.args[0].expr = Box::new(Call(self.create_require(specifier))); match opts { Some(opts) => { node.args[1] = opts; @@ -471,7 +520,7 @@ impl<'a> Fold for DependencyCollector<'a> { if let Lit(lit) = &*arg.expr { if let ast::Lit::Str(str_) = lit { // require() calls aren't allowed in scripts, flag as an error. - if kind == DependencyKind::Require && self.source_type == SourceType::Script { + if kind == DependencyKind::Require && self.config.source_type == SourceType::Script { self.add_script_error(node.span); return node; } @@ -482,7 +531,7 @@ impl<'a> Fold for DependencyCollector<'a> { kind.clone(), attributes, kind == DependencyKind::Require && self.in_try, - self.source_type, + self.config.source_type, ); } } @@ -491,8 +540,8 @@ impl<'a> Fold for DependencyCollector<'a> { // Replace import() with require() if kind == DependencyKind::DynamicImport { let mut call = node.clone(); - if !self.scope_hoist { - let name = match &self.source_type { + if !self.config.scope_hoist { + let name = match &self.config.source_type { SourceType::Module => "require", SourceType::Script => "__parcel__require__", }; @@ -540,19 +589,29 @@ impl<'a> Fold for DependencyCollector<'a> { let matched = match &*node.callee { Ident(id) => { - match id.sym { - js_word!("Worker") | js_word!("SharedWorker") => { + match &id.sym { + &js_word!("Worker") | &js_word!("SharedWorker") => { // Bail if defined in scope - !self.decls.contains(&id.to_id()) + self.config.is_browser && !self.decls.contains(&id.to_id()) } - js_word!("Promise") => { + &js_word!("Promise") => { // Match requires inside promises (e.g. Rollup compiled dynamic imports) // new Promise(resolve => resolve(require('foo'))) // new Promise(resolve => { resolve(require('foo')) }) // new Promise(function (resolve) { resolve(require('foo')) }) return self.fold_new_promise(node); } - _ => false, + sym => { + if sym.to_string() == "__parcel__URL__" { + let mut call = node.clone().fold_children_with(self); + call.callee = Box::new(ast::Expr::Ident(ast::Ident::new( + "URL".into(), + DUMMY_SP.apply_mark(self.ignore_mark), + ))); + return call; + } + false + } } } _ => false, @@ -597,23 +656,21 @@ impl<'a> Fold for DependencyCollector<'a> { }; let (source_type, opts) = match_worker_type(args.get(1)); - self.add_dependency( + let placeholder = self.add_url_dependency( specifier.clone(), span, DependencyKind::WebWorker, - None, - false, source_type, ); // Replace argument with a require call to resolve the URL at runtime. let mut node = node.clone(); if let Some(mut args) = node.args.clone() { - args[0].expr = Box::new(Call(self.create_require(specifier))); + args[0].expr = Box::new(placeholder); // If module workers aren't supported natively, remove the `type: 'module'` option. // If no other options are passed, remove the argument entirely. - if !self.supports_module_workers { + if !self.config.supports_module_workers { match opts { None => { args.truncate(1); @@ -648,15 +705,12 @@ impl<'a> Fold for DependencyCollector<'a> { use ast::*; if let Some((specifier, span)) = self.match_import_meta_url(&node, self.decls) { - self.add_dependency( + return self.add_url_dependency( specifier.clone(), span, DependencyKind::URL, - None, - false, - self.source_type, + self.config.source_type, ); - return ast::Expr::Call(self.create_require(specifier)); } let is_require = match &node { @@ -863,6 +917,51 @@ fn build_promise_chain(node: ast::CallExpr, require_node: ast::CallExpr) -> ast: return node; } +fn create_url_constructor(url: ast::Expr, use_import_meta: bool) -> ast::Expr { + use ast::*; + + let expr = if use_import_meta { + Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(Expr::MetaProp(MetaPropExpr { + meta: Ident::new(js_word!("import"), DUMMY_SP), + prop: Ident::new(js_word!("meta"), DUMMY_SP), + }))), + prop: Box::new(Expr::Ident(Ident::new(js_word!("url"), DUMMY_SP))), + computed: false, + }) + } else { + // CJS output: "file:" + __filename + Expr::Bin(BinExpr { + span: DUMMY_SP, + left: Box::new(Expr::Lit(Lit::Str(Str { + value: "file:".into(), + kind: StrKind::Synthesized, + span: DUMMY_SP, + has_escape: false, + }))), + op: BinaryOp::Add, + right: Box::new(Expr::Ident(Ident::new("__filename".into(), DUMMY_SP))), + }) + }; + + Expr::New(NewExpr { + span: DUMMY_SP, + callee: Box::new(Expr::Ident(Ident::new(js_word!("URL"), DUMMY_SP))), + args: Some(vec![ + ExprOrSpread { + expr: Box::new(url), + spread: None, + }, + ExprOrSpread { + expr: Box::new(expr), + spread: None, + }, + ]), + type_args: None, + }) +} + struct PromiseTransformer { require_node: Option, } @@ -920,12 +1019,12 @@ impl<'a> DependencyCollector<'a> { expr: &ast::Expr, decls: &HashSet<(JsWord, SyntaxContext)>, ) -> Option<(JsWord, swc_common::Span)> { + use ast::*; + match expr { - ast::Expr::New(new) => { + Expr::New(new) => { let is_url = match &*new.callee { - ast::Expr::Ident(id) => { - id.sym == js_word!("URL") && !decls.contains(&(id.sym.clone(), id.span.ctxt())) - } + Expr::Ident(id) => id.sym == js_word!("URL") && !decls.contains(&id.to_id()), _ => false, }; @@ -936,7 +1035,7 @@ impl<'a> DependencyCollector<'a> { if let Some(args) = &new.args { let specifier = if let Some(arg) = args.get(0) { match &*arg.expr { - ast::Expr::Lit(ast::Lit::Str(s)) => s, + Expr::Lit(Lit::Str(s)) => s, _ => return None, } } else { @@ -945,17 +1044,17 @@ impl<'a> DependencyCollector<'a> { if let Some(arg) = args.get(1) { match &*arg.expr { - ast::Expr::Member(member) => { + Expr::Member(member) => { match &member.obj { - ast::ExprOrSuper::Expr(expr) => match &**expr { - ast::Expr::MetaProp(ast::MetaPropExpr { + ExprOrSuper::Expr(expr) => match &**expr { + ast::Expr::MetaProp(MetaPropExpr { meta: - ast::Ident { + Ident { sym: js_word!("import"), .. }, prop: - ast::Ident { + Ident { sym: js_word!("meta"), .. }, @@ -966,8 +1065,8 @@ impl<'a> DependencyCollector<'a> { } let is_url = match &*member.prop { - ast::Expr::Ident(id) => id.sym == js_word!("url") && !member.computed, - ast::Expr::Lit(ast::Lit::Str(str)) => str.value == js_word!("url"), + Expr::Ident(id) => id.sym == js_word!("url") && !member.computed, + Expr::Lit(Lit::Str(str)) => str.value == js_word!("url"), _ => false, }; @@ -975,7 +1074,7 @@ impl<'a> DependencyCollector<'a> { return None; } - if self.source_type == SourceType::Script { + if self.config.source_type == SourceType::Script { self.diagnostics.push(Diagnostic { message: "`import.meta` is not supported outside a module.".to_string(), code_highlights: Some(vec![CodeHighlight { @@ -989,6 +1088,23 @@ impl<'a> DependencyCollector<'a> { return Some((specifier.value.clone(), specifier.span)); } + Expr::Bin(BinExpr { + op: BinaryOp::Add, + left, + right, + .. + }) => { + // Match "file:" + __filename + match (&**left, &**right) { + ( + Expr::Lit(Lit::Str(Str { value: left, .. })), + Expr::Ident(Ident { sym: right, .. }), + ) if left == "file:" && right == "__filename" => { + return Some((specifier.value.clone(), specifier.span)); + } + _ => return None, + } + } _ => return None, } } diff --git a/packages/transformers/js/core/src/env_replacer.rs b/packages/transformers/js/core/src/env_replacer.rs index fca81b2bcf3..23ddcb0bca5 100644 --- a/packages/transformers/js/core/src/env_replacer.rs +++ b/packages/transformers/js/core/src/env_replacer.rs @@ -10,7 +10,7 @@ use crate::utils::*; pub struct EnvReplacer<'a> { pub replace_env: bool, pub is_browser: bool, - pub env: HashMap, + pub env: &'a HashMap, pub decls: &'a HashSet<(JsWord, SyntaxContext)>, pub used_env: &'a mut HashSet, } diff --git a/packages/transformers/js/core/src/fs.rs b/packages/transformers/js/core/src/fs.rs index 5368c0a8b0c..a4bc8082f09 100644 --- a/packages/transformers/js/core/src/fs.rs +++ b/packages/transformers/js/core/src/fs.rs @@ -21,7 +21,7 @@ pub fn inline_fs<'a>( source_map: swc_common::sync::Lrc, decls: HashSet, global_mark: Mark, - project_root: &'a String, + project_root: &'a str, deps: &'a mut Vec, ) -> impl Fold + 'a { InlineFS { @@ -37,7 +37,7 @@ struct InlineFS<'a> { filename: PathBuf, collect: Collect, global_mark: Mark, - project_root: &'a String, + project_root: &'a str, deps: &'a mut Vec, } @@ -197,6 +197,7 @@ impl<'a> InlineFS<'a> { is_optional: false, is_helper: false, source_type: None, + placeholder: None, }); // If buffer, wrap in Buffer.from(base64String, 'base64') diff --git a/packages/transformers/js/core/src/global_replacer.rs b/packages/transformers/js/core/src/global_replacer.rs index 6f7d73ef30f..81326008237 100644 --- a/packages/transformers/js/core/src/global_replacer.rs +++ b/packages/transformers/js/core/src/global_replacer.rs @@ -73,6 +73,7 @@ impl<'a> Fold for GlobalReplacer<'a> { is_optional: false, is_helper: false, source_type: Some(SourceType::Module), + placeholder: None, }); } "Buffer" => { @@ -99,6 +100,7 @@ impl<'a> Fold for GlobalReplacer<'a> { is_optional: false, is_helper: false, source_type: Some(SourceType::Module), + placeholder: None, }); } "__filename" => { diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 4be06d83e5b..5b60fc5800f 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -78,6 +78,8 @@ pub struct Config { scope_hoist: bool, source_type: SourceType, supports_module_workers: bool, + is_library: bool, + is_esm_output: bool, } #[derive(Serialize, Debug, Deserialize, Default)] @@ -220,11 +222,11 @@ pub fn transform(config: Config) -> Result { let mut react_options = react::Options::default(); if config.is_jsx { react_options.use_spread = true; - if let Some(jsx_pragma) = config.jsx_pragma { - react_options.pragma = jsx_pragma; + if let Some(jsx_pragma) = &config.jsx_pragma { + react_options.pragma = jsx_pragma.clone(); } - if let Some(jsx_pragma_frag) = config.jsx_pragma_frag { - react_options.pragma_frag = jsx_pragma_frag; + if let Some(jsx_pragma_frag) = &config.jsx_pragma_frag { + react_options.pragma_frag = jsx_pragma_frag.clone(); } react_options.development = config.is_development; react_options.refresh = if config.react_refresh { @@ -234,8 +236,8 @@ pub fn transform(config: Config) -> Result { }; react_options.runtime = if config.automatic_jsx_runtime { - if let Some(import_source) = config.jsx_import_source { - react_options.import_source = import_source; + if let Some(import_source) = &config.jsx_import_source { + react_options.import_source = import_source.clone(); } Some(react::Runtime::Automatic) } else { @@ -284,7 +286,7 @@ pub fn transform(config: Config) -> Result { Optional::new( EnvReplacer { replace_env: config.replace_env, - env: config.env, + env: &config.env, is_browser: config.is_browser, decls: &decls, used_env: &mut result.used_env @@ -334,9 +336,7 @@ pub fn transform(config: Config) -> Result { &mut result.dependencies, &decls, ignore_mark, - config.scope_hoist, - config.source_type, - config.supports_module_workers, + &config, &mut diagnostics, ), ); diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index 22f3da193c4..390ef0b21ee 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -396,6 +396,8 @@ export default (new Transformer({ asset.env.shouldScopeHoist && asset.env.sourceType !== 'script', source_type: asset.env.sourceType === 'script' ? 'Script' : 'Module', supports_module_workers: supportsModuleWorkers, + is_library: asset.env.isLibrary, + is_esm_output: asset.env.outputFormat === 'esmodule', }); let convertLoc = loc => { @@ -511,6 +513,7 @@ export default (new Transformer({ }, meta: { webworker: true, + placeholder: dep.placeholder, }, }); } else if (dep.kind === 'ServiceWorker') { @@ -524,6 +527,9 @@ export default (new Transformer({ outputFormat: 'global', // TODO: module service worker support loc, }, + meta: { + placeholder: dep.placeholder, + }, }); } else if (dep.kind === 'Worklet') { let loc = convertLoc(dep.loc); @@ -535,11 +541,17 @@ export default (new Transformer({ outputFormat: 'esmodule', // Worklets require ESM loc, }, + meta: { + placeholder: dep.placeholder, + }, }); } else if (dep.kind === 'URL') { asset.addURLDependency(dep.specifier, { bundleBehavior: 'isolated', loc: convertLoc(dep.loc), + meta: { + placeholder: dep.placeholder, + }, }); } else if (dep.kind === 'File') { asset.invalidateOnFileChange(dep.specifier);