diff --git a/packages/bundlers/default/src/DefaultBundler.js b/packages/bundlers/default/src/DefaultBundler.js index c8a2ca41773..e94568c147d 100644 --- a/packages/bundlers/default/src/DefaultBundler.js +++ b/packages/bundlers/default/src/DefaultBundler.js @@ -32,9 +32,16 @@ export default new Bundler({ // Start a new bundle if this is an async dependency, or entry point. if (dep.isAsync || dep.isEntry) { let isIsolated = dep.isEntry || dep.env.isIsolated(); + let resolved = assetGraph.getDependencyResolution(dep); + if (!resolved) { + // TODO: is this right? + return; + } + let bundleGroup: BundleGroup = { dependency: dep, - target: dep.target || (context && context.bundleGroup.target) + target: dep.target || (context && context.bundleGroup.target), + entryAssetId: resolved.id }; bundleGraph.addBundleGroup( diff --git a/packages/configs/default/index.json b/packages/configs/default/index.json index 7914849d488..17d3406e454 100644 --- a/packages/configs/default/index.json +++ b/packages/configs/default/index.json @@ -9,6 +9,10 @@ "*.css": ["@parcel/transformer-css"] }, "namers": ["@parcel/namer-default"], + "runtimes": { + "browser": ["@parcel/runtime-js"], + "node": ["@parcel/runtime-js"] + }, "packagers": { "*.js": "@parcel/packager-js" }, diff --git a/packages/core/core/src/Asset.js b/packages/core/core/src/Asset.js index 68b234b6032..a07c49e5d71 100644 --- a/packages/core/core/src/Asset.js +++ b/packages/core/core/src/Asset.js @@ -28,6 +28,7 @@ type AssetOptions = { dependencies?: Array, connectedFiles?: Array, output?: AssetOutput, + outputSize?: number, outputHash?: string, env: Environment, meta?: JSONObject @@ -64,7 +65,7 @@ export default class Asset implements IAsset { ? options.connectedFiles.slice() : []; this.output = options.output || {code: this.code}; - this.outputSize = this.output.code.length; + this.outputSize = options.outputSize || this.output.code.length; this.outputHash = options.outputHash || ''; this.env = options.env; this.meta = options.meta || {}; diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js index 196e8fc3ee2..34c10c89ae3 100644 --- a/packages/core/core/src/AssetGraph.js +++ b/packages/core/core/src/AssetGraph.js @@ -11,8 +11,7 @@ import type { Target, Environment, Bundle, - GraphTraversalCallback, - DependencyResolution + GraphTraversalCallback } from '@parcel/types'; import md5 from '@parcel/utils/md5'; import Dependency from './Dependency'; @@ -75,8 +74,9 @@ type FileUpdates = { }; type AssetGraphOpts = { - entries: Array, - targets: Array, + entries?: Array, + targets?: Array, + transformerRequest?: TransformerRequest, rootDir: string }; @@ -98,28 +98,42 @@ export default class AssetGraph extends Graph { this.invalidNodes = new Map(); } - initializeGraph({entries, targets, rootDir}: AssetGraphOpts) { + initializeGraph({ + entries, + targets, + transformerRequest, + rootDir + }: AssetGraphOpts) { let rootNode = nodeFromRootDir(rootDir); this.setRootNode(rootNode); - let depNodes = []; - for (let entry of entries) { - for (let target of targets) { - let node = nodeFromDep( - new Dependency({ - moduleSpecifier: entry, - target: target, - env: target.env, - isEntry: true - }) - ); + let nodes = []; + if (entries) { + if (!targets) { + throw new Error('Targets are required when entries are specified'); + } - depNodes.push(node); + for (let entry of entries) { + for (let target of targets) { + let node = nodeFromDep( + new Dependency({ + moduleSpecifier: entry, + target: target, + env: target.env, + isEntry: true + }) + ); + + nodes.push(node); + } } + } else if (transformerRequest) { + let node = nodeFromTransformerRequest(transformerRequest); + nodes.push(node); } - this.replaceNodesConnectedTo(rootNode, depNodes); - for (let depNode of depNodes) { + this.replaceNodesConnectedTo(rootNode, nodes); + for (let depNode of nodes) { this.incompleteNodes.set(depNode.id, depNode); } } @@ -230,30 +244,21 @@ export default class AssetGraph extends Graph { return this.getNodesConnectedFrom(node).map(node => node.value); } - getDependencyResolution(dep: IDependency): DependencyResolution { + getDependencyResolution(dep: IDependency): ?Asset { let depNode = this.getNode(dep.id); if (!depNode) { - return {}; - } - - let node = this.getNodesConnectedFrom(depNode)[0]; - if (!node) { - return {}; + return null; } - if (node.type === 'transformer_request') { - let assetNode = this.getNodesConnectedFrom(node).find( - node => node.type === 'asset' || node.type === 'asset_reference' - ); - if (assetNode) { - return {asset: assetNode.value}; + let res = null; + this.traverse((node, ctx, traversal) => { + if (node.type === 'asset' || node.type === 'asset_reference') { + res = (node.value: Asset); + traversal.stop(); } - } else if (node.type === 'bundle_group') { - let bundles = this.getNodesConnectedFrom(node).map(node => node.value); - return {bundles}; - } + }, depNode); - return {}; + return res; } traverseAssets(visit: GraphTraversalCallback, startNode: ?Node) { @@ -281,7 +286,8 @@ export default class AssetGraph extends Graph { return { id: 'bundle:' + asset.id, type: asset.type, - assetGraph: graph + assetGraph: graph, + env: asset.env }; } @@ -296,12 +302,13 @@ export default class AssetGraph extends Graph { } getEntryAssets(): Array { - let root = this.getRootNode(); - if (!root) { - return []; - } + let entries = []; + this.traverseAssets((asset, ctx, traversal) => { + entries.push(asset); + traversal.skipChildren(); + }); - return this.getNodesConnectedFrom(root).map(node => node.value); + return entries; } removeAsset(asset: Asset) { diff --git a/packages/core/core/src/AssetGraphBuilder.js b/packages/core/core/src/AssetGraphBuilder.js new file mode 100644 index 00000000000..f228c4ff10c --- /dev/null +++ b/packages/core/core/src/AssetGraphBuilder.js @@ -0,0 +1,176 @@ +// @flow +import type { + CLIOptions, + Dependency, + FilePath, + Target, + TransformerRequest +} from '@parcel/types'; +import type {Node} from './Graph'; +import type Config from './Config'; +import EventEmitter from 'events'; +import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill'; +import Watcher from '@parcel/watcher'; +import PromiseQueue from './PromiseQueue'; +import AssetGraph from './AssetGraph'; +import ResolverRunner from './ResolverRunner'; +import WorkerFarm from '@parcel/workers'; + +const abortError = new Error('Build aborted'); + +type Signal = { + aborted: boolean, + addEventListener?: Function +}; + +type BuildOpts = { + signal: Signal, + shallow?: boolean +}; + +type Opts = {| + cliOpts: CLIOptions, + config: Config, + entries?: Array, + targets?: Array, + transformerRequest?: TransformerRequest, + rootDir: FilePath +|}; + +export default class AssetGraphBuilder extends EventEmitter { + graph: AssetGraph; + watcher: Watcher; + queue: PromiseQueue; + resolverRunner: ResolverRunner; + controller: AbortController; + farm: WorkerFarm; + runTransform: (file: TransformerRequest) => Promise; + + constructor(opts: Opts) { + super(); + + this.queue = new PromiseQueue(); + this.resolverRunner = new ResolverRunner({ + config: opts.config, + cliOpts: opts.cliOpts, + rootDir: opts.rootDir + }); + + this.graph = new AssetGraph(); + this.graph.initializeGraph(opts); + + this.controller = new AbortController(); + if (opts.cliOpts.watch) { + this.watcher = new Watcher(); + this.watcher.on('change', async filePath => { + if (this.graph.hasNode(filePath)) { + this.controller.abort(); + this.graph.invalidateFile(filePath); + + this.emit('invalidate', filePath); + } + }); + } + } + + async initFarm() { + // This expects the worker farm to already be initialized by Parcel prior to calling + // AssetGraphBuilder, which avoids needing to pass the options through here. + this.farm = await WorkerFarm.getShared(); + this.runTransform = this.farm.mkhandle('runTransform'); + } + + async build() { + if (!this.farm) { + await this.initFarm(); + } + + this.controller = new AbortController(); + let signal = this.controller.signal; + + await this.updateGraph({signal}); + await this.completeGraph({signal}); + return this.graph; + } + + async updateGraph({signal}: BuildOpts) { + for (let [, node] of this.graph.invalidNodes) { + this.queue.add(() => this.processNode(node, {signal, shallow: true})); + } + await this.queue.run(); + } + + async completeGraph({signal}: BuildOpts) { + for (let [, node] of this.graph.incompleteNodes) { + this.queue.add(() => this.processNode(node, {signal})); + } + + await this.queue.run(); + } + + processNode(node: Node, {signal}: BuildOpts) { + switch (node.type) { + case 'dependency': + return this.resolve(node.value, {signal}); + case 'transformer_request': + return this.transform(node.value, {signal}); + default: + throw new Error( + `Cannot process graph node with type ${node.type || 'undefined'}` + ); + } + } + + async resolve(dep: Dependency, {signal}: BuildOpts) { + let resolvedPath; + try { + resolvedPath = await this.resolverRunner.resolve(dep); + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND' && dep.isOptional) { + return; + } + + throw err; + } + + if (signal.aborted) { + throw abortError; + } + + let req = {filePath: resolvedPath, env: dep.env}; + let {newRequest} = this.graph.resolveDependency(dep, req); + + if (newRequest) { + this.queue.add(() => this.transform(newRequest, {signal})); + if (this.watcher) this.watcher.watch(newRequest.filePath); + } + } + + async transform(req: TransformerRequest, {signal, shallow}: BuildOpts) { + let cacheEntry = await this.runTransform(req); + + if (signal.aborted) throw abortError; + let { + addedFiles, + removedFiles, + newDeps + } = this.graph.resolveTransformerRequest(req, cacheEntry); + + if (this.watcher) { + for (let file of addedFiles) { + this.watcher.watch(file.filePath); + } + + for (let file of removedFiles) { + this.watcher.unwatch(file.filePath); + } + } + + // The shallow option is used during the update phase + if (!shallow) { + for (let dep of newDeps) { + this.queue.add(() => this.resolve(dep, {signal})); + } + } + } +} diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index 07a8ab48b06..f8604956d19 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -8,7 +8,7 @@ import type { import AssetGraph from './AssetGraph'; const getBundleGroupId = (bundleGroup: BundleGroup) => - 'bundle_group:' + bundleGroup.dependency.id; + 'bundle_group:' + bundleGroup.entryAssetId; export default class BundleGraph extends AssetGraph { constructor() { @@ -65,8 +65,23 @@ export default class BundleGraph extends AssetGraph { to: bundleNode.id }); - // Add a connection from the bundle group to the bundle in all bundles this.traverse(node => { + // Replace dependencies in this bundle with bundle group references for + // already created bundles in the bundle graph. This can happen when two + // bundles point to the same dependency, which has an async import. + if (node.type === 'bundle_group') { + // TODO: fix the AssetGraph interface so we don't need to do this + let assetGraph: AssetGraph = (bundle.assetGraph: any); + let bundleGroup: BundleGroup = node.value; + let depNode = assetGraph.getNode(bundleGroup.dependency.id); + if (depNode && !assetGraph.hasNode(node.id)) { + assetGraph.merge(this.getSubGraph(node)); + assetGraph.replaceNodesConnectedTo(depNode, [node]); + this.addEdge({from: bundle.id, to: node.id}); + } + } + + // Add a connection from the bundle group to the bundle in all bundles if ( node.type === 'bundle' && node.value.assetGraph.hasNode(bundleGroupId) diff --git a/packages/core/core/src/BundlerRunner.js b/packages/core/core/src/BundlerRunner.js index 7551cae136a..d183db552de 100644 --- a/packages/core/core/src/BundlerRunner.js +++ b/packages/core/core/src/BundlerRunner.js @@ -1,13 +1,15 @@ // @flow +import type AssetGraph from './AssetGraph'; import type { - AssetGraph, Namer, Bundle, FilePath, - CLIOptions + CLIOptions, + TransformerRequest } from '@parcel/types'; import type Config from './Config'; import BundleGraph from './BundleGraph'; +import AssetGraphBuilder from './AssetGraphBuilder'; type Opts = { cliOpts: CLIOptions, @@ -32,6 +34,7 @@ export default class BundlerRunner { let bundleGraph = new BundleGraph(); await bundler.bundle(graph, bundleGraph, this.cliOpts); await this.nameBundles(bundleGraph); + await this.applyRuntimes(bundleGraph); return bundleGraph; } @@ -60,4 +63,61 @@ export default class BundlerRunner { throw new Error('Unable to name bundle'); } + + async applyRuntimes(bundleGraph: BundleGraph) { + let bundles = []; + bundleGraph.traverseBundles(bundle => { + bundles.push(bundle); + }); + + for (let bundle of bundles) { + await this.applyRuntimesToBundle(bundleGraph, bundle); + } + } + + async applyRuntimesToBundle(bundleGraph: BundleGraph, bundle: Bundle) { + // HACK. TODO: move this into some sort of asset graph proxy + // $FlowFixMe + bundle.assetGraph.addRuntimeAsset = this.addRuntimeAsset.bind( + this, + bundleGraph, + bundle + ); + + let runtimes = await this.config.getRuntimes(bundle.env.context); + for (let runtime of runtimes) { + await runtime.apply(bundle, this.cliOpts); + } + } + + async addRuntimeAsset( + bundleGraph: BundleGraph, + bundle: Bundle, + node: {id: string}, + transformerRequest: TransformerRequest + ) { + let builder = new AssetGraphBuilder({ + cliOpts: this.cliOpts, + config: this.config, + rootDir: this.rootDir, + transformerRequest + }); + + let graph: AssetGraph = await builder.build(); + let entry = graph.getEntryAssets()[0]; + // $FlowFixMe - node will always exist + let subGraph = graph.getSubGraph(graph.getNode(entry.id)); + + // Exclude modules that are already included in an ancestor bundle + subGraph.traverseAssets(asset => { + if (bundleGraph.isAssetInAncestorBundle(bundle, asset)) { + subGraph.removeAsset(asset); + } + }); + + bundle.assetGraph.merge(subGraph); + // $FlowFixMe + bundle.assetGraph.addEdge({from: node.id, to: entry.id}); + return entry; + } } diff --git a/packages/core/core/src/Config.js b/packages/core/core/src/Config.js index d449573e2fe..569b02f97e3 100644 --- a/packages/core/core/src/Config.js +++ b/packages/core/core/src/Config.js @@ -7,6 +7,8 @@ import type { Resolver, Bundler, Namer, + Runtime, + EnvironmentContext, PackageName, Packager, Optimizer @@ -59,6 +61,15 @@ export default class Config { return this.loadPlugins(this.config.namers); } + async getRuntimes(context: EnvironmentContext): Promise> { + let runtimes = this.config.runtimes[context]; + if (!runtimes) { + return []; + } + + return await this.loadPlugins(runtimes); + } + async getPackager(filePath: FilePath): Promise { let packagerName: PackageName | null = this.matchGlobMap( filePath, diff --git a/packages/core/core/src/Graph.js b/packages/core/core/src/Graph.js index d61e5f1e3ff..0aef8e7ed3b 100644 --- a/packages/core/core/src/Graph.js +++ b/packages/core/core/src/Graph.js @@ -169,7 +169,8 @@ export default class Graph implements IGraph { for (let edge of this.edges) { if (edge.to === fromNode.id) { - edge.to = toNode.id; + this.addEdge({from: edge.from, to: toNode.id}); + this.edges.delete(edge); } } diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js index a6edc67142e..0779e0cd8a0 100644 --- a/packages/core/core/src/Parcel.js +++ b/packages/core/core/src/Parcel.js @@ -1,19 +1,7 @@ // @flow 'use strict'; -import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -import Watcher from '@parcel/watcher'; -import PromiseQueue from './PromiseQueue'; import AssetGraph from './AssetGraph'; -import {Node} from './Graph'; -import type { - Bundle, - BundleGraph, - CLIOptions, - Dependency, - Target, - TransformerRequest -} from '@parcel/types'; -import ResolverRunner from './ResolverRunner'; +import type {Bundle, BundleGraph, CLIOptions} from '@parcel/types'; import BundlerRunner from './BundlerRunner'; import Config from './Config'; import WorkerFarm from '@parcel/workers'; @@ -22,6 +10,7 @@ import getRootDir from '@parcel/utils/getRootDir'; import loadEnv from './loadEnv'; import path from 'path'; import Cache from '@parcel/cache'; +import AssetGraphBuilder from './AssetGraphBuilder'; // TODO: use custom config if present const defaultConfig = require('@parcel/config-default'); @@ -36,64 +25,23 @@ type ParcelOpts = { env?: {[string]: ?string} }; -type Signal = { - aborted: boolean, - addEventListener?: Function -}; - -type BuildOpts = { - signal: Signal, - shallow?: boolean -}; - export default class Parcel { options: ParcelOpts; entries: Array; rootDir: string; - graph: AssetGraph; - watcher: Watcher; - queue: PromiseQueue; - resolverRunner: ResolverRunner; + assetGraphBuilder: AssetGraphBuilder; bundlerRunner: BundlerRunner; farm: WorkerFarm; - targetResolver: TargetResolver; - targets: Array; - runTransform: (file: TransformerRequest) => Promise; runPackage: (bundle: Bundle) => Promise; constructor(options: ParcelOpts) { - let {entries, cliOpts} = options; + let {entries} = options; this.options = options; this.entries = Array.isArray(entries) ? entries : [entries]; this.rootDir = getRootDir(this.entries); - - this.graph = new AssetGraph(); - this.watcher = cliOpts.watch ? new Watcher() : null; - this.queue = new PromiseQueue(); - - let config = new Config( - defaultConfig, - require.resolve('@parcel/config-default') - ); - this.resolverRunner = new ResolverRunner({ - config, - cliOpts, - rootDir: this.rootDir - }); - this.bundlerRunner = new BundlerRunner({ - config, - cliOpts, - rootDir: this.rootDir - }); - - this.targetResolver = new TargetResolver(); - this.targets = []; } async run() { - let controller = new AbortController(); - let signal = controller.signal; - Cache.createCacheDir(this.options.cliOpts.cacheDir); if (!this.options.env) { @@ -112,45 +60,47 @@ export default class Parcel { } ); - this.runTransform = this.farm.mkhandle('runTransform'); this.runPackage = this.farm.mkhandle('runPackage'); - this.targets = await this.targetResolver.resolve(this.rootDir); - this.graph.initializeGraph({ - entries: this.entries, - targets: this.targets, + // TODO: resolve config from filesystem + let config = new Config( + defaultConfig, + require.resolve('@parcel/config-default') + ); + + this.bundlerRunner = new BundlerRunner({ + config, + cliOpts: this.options.cliOpts, rootDir: this.rootDir }); - let buildPromise = this.build({signal}); + let targetResolver = new TargetResolver(); + let targets = await targetResolver.resolve(this.rootDir); - if (this.watcher) { - this.watcher.on('change', filePath => { - if (this.graph.hasNode(filePath)) { - controller.abort(); - this.graph.invalidateFile(filePath); - - controller = new AbortController(); - signal = controller.signal; + this.assetGraphBuilder = new AssetGraphBuilder({ + cliOpts: this.options.cliOpts, + config, + entries: this.entries, + targets, + rootDir: this.rootDir + }); - this.build({signal}); - } - }); - } + this.assetGraphBuilder.on('invalidate', () => { + this.build(); + }); - return await buildPromise; + return await this.build(); } - async build({signal}: BuildOpts) { + async build() { try { // console.log('Starting build'); // eslint-disable-line no-console - await this.updateGraph({signal}); - await this.completeGraph({signal}); - // await this.graph.dumpGraphViz(); - let bundleGraph = await this.bundle(); + let assetGraph = await this.assetGraphBuilder.build(); + // await graph.dumpGraphViz(); + let bundleGraph = await this.bundle(assetGraph); await this.package(bundleGraph); - if (!this.watcher && this.options.killWorkers !== false) { + if (!this.options.cliOpts.watch && this.options.killWorkers !== false) { await this.farm.end(); } @@ -163,89 +113,8 @@ export default class Parcel { } } - async updateGraph({signal}: BuildOpts) { - for (let [, node] of this.graph.invalidNodes) { - this.queue.add(() => this.processNode(node, {signal, shallow: true})); - } - await this.queue.run(); - } - - async completeGraph({signal}: BuildOpts) { - for (let [, node] of this.graph.incompleteNodes) { - this.queue.add(() => this.processNode(node, {signal})); - } - - await this.queue.run(); - } - - processNode(node: Node, {signal}: BuildOpts) { - switch (node.type) { - case 'dependency': - return this.resolve(node.value, {signal}); - case 'transformer_request': - return this.transform(node.value, {signal}); - default: - throw new Error( - `Cannot process graph node with type ${node.type || 'undefined'}` - ); - } - } - - async resolve(dep: Dependency, {signal}: BuildOpts) { - let resolvedPath; - try { - resolvedPath = await this.resolverRunner.resolve(dep); - } catch (err) { - if (err.code === 'MODULE_NOT_FOUND' && dep.isOptional) { - return; - } - - throw err; - } - - if (signal.aborted) { - throw abortError; - } - - let req = {filePath: resolvedPath, env: dep.env}; - let {newRequest} = this.graph.resolveDependency(dep, req); - - if (newRequest) { - this.queue.add(() => this.transform(newRequest, {signal})); - if (this.watcher) this.watcher.watch(newRequest.filePath); - } - } - - async transform(req: TransformerRequest, {signal, shallow}: BuildOpts) { - let cacheEntry = await this.runTransform(req); - - if (signal.aborted) throw abortError; - let { - addedFiles, - removedFiles, - newDeps - } = this.graph.resolveTransformerRequest(req, cacheEntry); - - if (this.watcher) { - for (let file of addedFiles) { - this.watcher.watch(file.filePath); - } - - for (let file of removedFiles) { - this.watcher.unwatch(file.filePath); - } - } - - // The shallow option is used during the update phase - if (!shallow) { - for (let dep of newDeps) { - this.queue.add(() => this.resolve(dep, {signal})); - } - } - } - - bundle() { - return this.bundlerRunner.bundle(this.graph); + bundle(assetGraph: AssetGraph) { + return this.bundlerRunner.bundle(assetGraph); } package(bundleGraph: BundleGraph) { diff --git a/packages/core/core/src/TransformerRunner.js b/packages/core/core/src/TransformerRunner.js index 839c8f344eb..6a833601316 100644 --- a/packages/core/core/src/TransformerRunner.js +++ b/packages/core/core/src/TransformerRunner.js @@ -33,12 +33,12 @@ class TransformerRunner { } async transform(req: TransformerRequest): Promise { - let code = await fs.readFile(req.filePath, 'utf8'); + let code = req.code || (await fs.readFile(req.filePath, 'utf8')); let hash = md5(code); // If a cache entry matches, no need to transform. let cacheEntry; - if (this.cliOpts.cache !== false) { + if (this.cliOpts.cache !== false && req.code == null) { cacheEntry = await Cache.read(req.filePath, req.env); } @@ -65,6 +65,14 @@ class TransformerRunner { cacheEntry ); + // If the transformer request passed code rather than a filename, + // use a hash as the id to ensure it is unique. + if (req.code) { + for (let asset of assets) { + asset.id = asset.outputHash; + } + } + cacheEntry = { filePath: req.filePath, env: req.env, diff --git a/packages/core/integration-tests/test/integration/dynamic-hoist/a.js b/packages/core/integration-tests/test/integration/dynamic-common-large/a.js similarity index 100% rename from packages/core/integration-tests/test/integration/dynamic-hoist/a.js rename to packages/core/integration-tests/test/integration/dynamic-common-large/a.js diff --git a/packages/core/integration-tests/test/integration/dynamic-hoist/b.js b/packages/core/integration-tests/test/integration/dynamic-common-large/b.js similarity index 100% rename from packages/core/integration-tests/test/integration/dynamic-hoist/b.js rename to packages/core/integration-tests/test/integration/dynamic-common-large/b.js diff --git a/packages/core/integration-tests/test/integration/dynamic-common-large/common.js b/packages/core/integration-tests/test/integration/dynamic-common-large/common.js new file mode 100644 index 00000000000..8d080affc6e --- /dev/null +++ b/packages/core/integration-tests/test/integration/dynamic-common-large/common.js @@ -0,0 +1 @@ +module.exports = require('lodash').add(1, 1); diff --git a/packages/core/integration-tests/test/integration/dynamic-hoist/index.js b/packages/core/integration-tests/test/integration/dynamic-common-large/index.js similarity index 100% rename from packages/core/integration-tests/test/integration/dynamic-hoist/index.js rename to packages/core/integration-tests/test/integration/dynamic-common-large/index.js diff --git a/packages/core/integration-tests/test/integration/dynamic-common-small/a.js b/packages/core/integration-tests/test/integration/dynamic-common-small/a.js new file mode 100644 index 00000000000..e0b45b97bd1 --- /dev/null +++ b/packages/core/integration-tests/test/integration/dynamic-common-small/a.js @@ -0,0 +1,4 @@ +var c = require('./common'); + +exports.a = 1; +exports.b = c; diff --git a/packages/core/integration-tests/test/integration/dynamic-common-small/b.js b/packages/core/integration-tests/test/integration/dynamic-common-small/b.js new file mode 100644 index 00000000000..3b6bb8d152a --- /dev/null +++ b/packages/core/integration-tests/test/integration/dynamic-common-small/b.js @@ -0,0 +1,3 @@ +var c = require('./common'); + +module.exports = c + 2; \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/dynamic-hoist/common-dep.js b/packages/core/integration-tests/test/integration/dynamic-common-small/common-dep.js similarity index 100% rename from packages/core/integration-tests/test/integration/dynamic-hoist/common-dep.js rename to packages/core/integration-tests/test/integration/dynamic-common-small/common-dep.js diff --git a/packages/core/integration-tests/test/integration/dynamic-hoist/common.js b/packages/core/integration-tests/test/integration/dynamic-common-small/common.js similarity index 100% rename from packages/core/integration-tests/test/integration/dynamic-hoist/common.js rename to packages/core/integration-tests/test/integration/dynamic-common-small/common.js diff --git a/packages/core/integration-tests/test/integration/dynamic-common-small/index.js b/packages/core/integration-tests/test/integration/dynamic-common-small/index.js new file mode 100644 index 00000000000..eba1a818eaf --- /dev/null +++ b/packages/core/integration-tests/test/integration/dynamic-common-small/index.js @@ -0,0 +1,8 @@ +var a = import('./a'); +var b = import('./b'); + +module.exports = function () { + return Promise.all([a, b]).then(function ([a, b]) { + return a.a + a.b + b; + }); +}; diff --git a/packages/core/integration-tests/test/integration/dynamic-node/index.js b/packages/core/integration-tests/test/integration/dynamic-node/index.js new file mode 100644 index 00000000000..ce3822c64fa --- /dev/null +++ b/packages/core/integration-tests/test/integration/dynamic-node/index.js @@ -0,0 +1,7 @@ +var local = import('./local'); + +module.exports = function () { + return local.then(function (l) { + return l.a + l.b; + }); +}; diff --git a/packages/core/integration-tests/test/integration/dynamic-node/local.js b/packages/core/integration-tests/test/integration/dynamic-node/local.js new file mode 100644 index 00000000000..59aa6ffd125 --- /dev/null +++ b/packages/core/integration-tests/test/integration/dynamic-node/local.js @@ -0,0 +1,2 @@ +exports.a = 1; +exports.b = 2; diff --git a/packages/core/integration-tests/test/integration/dynamic-node/package.json b/packages/core/integration-tests/test/integration/dynamic-node/package.json new file mode 100644 index 00000000000..b22d1affb6c --- /dev/null +++ b/packages/core/integration-tests/test/integration/dynamic-node/package.json @@ -0,0 +1,7 @@ +{ + "name": "dynamic-node", + "private": true, + "engines": { + "node": "8" + } +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index c659776314b..0eab43a4032 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -164,7 +164,7 @@ describe('javascript', function() { assert.equal(output.default(), 3); }); - it.skip('should split bundles when a dynamic import is used a browser environment', async function() { + it('should split bundles when a dynamic import is used a browser environment', async function() { let b = await bundle(path.join(__dirname, '/integration/dynamic/index.js')); await assertBundles(b, [ @@ -174,18 +174,13 @@ describe('javascript', function() { 'index.js', 'bundle-loader.js', 'bundle-url.js', - 'js-loader.js' + 'js-loader.js', + 'JSRuntime.js' ] }, - // { - // type: 'map' - // }, { assets: ['local.js'] } - // { - // type: 'map' - // } ]); let output = await run(b); @@ -193,31 +188,26 @@ describe('javascript', function() { assert.equal(await output(), 3); }); - it.skip('should split bundles when a dynamic import is used with --target=node', async function() { + it('should split bundles when a dynamic import is used with a node environment', async function() { let b = await bundle( - path.join(__dirname, '/integration/dynamic/index.js'), - { - target: 'node' - } + path.join(__dirname, '/integration/dynamic-node/index.js') ); - await assertBundles(b, { - name: 'index.js', - assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'], - childBundles: [ - { - type: 'map' - }, - { - assets: ['local.js'], - childBundles: [ - { - type: 'map' - } - ] - } - ] - }); + await assertBundles(b, [ + { + name: 'index.js', + assets: [ + 'index.js', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js', + 'JSRuntime.js' + ] + }, + { + assets: ['local.js'] + } + ]); let output = await run(b); assert.equal(typeof output, 'function'); @@ -412,56 +402,87 @@ describe('javascript', function() { assert.equal(await output(), 3); }); - it.skip('should return all exports as an object when using ES modules', async function() { + it('should return all exports as an object when using ES modules', async function() { let b = await bundle( path.join(__dirname, '/integration/dynamic-esm/index.js') ); - await assertBundles(b, { - name: 'index.js', - assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'], - childBundles: [ - { - type: 'map' - }, - { - assets: ['local.js'], - childBundles: [ - { - type: 'map' - } - ] - } - ] - }); + await assertBundles(b, [ + { + name: 'index.js', + assets: [ + 'index.js', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js', + 'JSRuntime.js' + ] + }, + { + assets: ['local.js'] + } + ]); let output = (await run(b)).default; assert.equal(typeof output, 'function'); assert.equal(await output(), 3); }); - it.skip('should hoist common dependencies into a parent bundle', async function() { + it('should duplicate small modules across multiple bundles', async function() { let b = await bundle( - path.join(__dirname, '/integration/dynamic-hoist/index.js') + path.join(__dirname, '/integration/dynamic-common-small/index.js') ); await assertBundles(b, [ + { + assets: ['a.js', 'common.js', 'common-dep.js'] + }, + { + assets: ['b.js', 'common.js', 'common-dep.js'] + }, { name: 'index.js', assets: [ 'index.js', - 'common.js', - 'common-dep.js', 'bundle-loader.js', 'bundle-url.js', - 'js-loader.js' + 'js-loader.js', + 'JSRuntime.js', + 'JSRuntime.js' ] - }, + } + ]); + + let output = await run(b); + assert.equal(typeof output, 'function'); + assert.equal(await output(), 7); + }); + + it('should create a separate bundle for large modules shared between bundles', async function() { + let b = await bundle( + path.join(__dirname, '/integration/dynamic-common-large/index.js') + ); + + await assertBundles(b, [ { assets: ['a.js'] }, { assets: ['b.js'] + }, + { + name: 'index.js', + assets: [ + 'index.js', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js', + 'JSRuntime.js', + 'JSRuntime.js' + ] + }, + { + assets: ['common.js', 'lodash.js'] } ]); @@ -470,73 +491,60 @@ describe('javascript', function() { assert.equal(await output(), 7); }); - it.skip('should not duplicate a module which is already in a parent bundle', async function() { + it('should not duplicate a module which is already in a parent bundle', async function() { let b = await bundle( path.join(__dirname, '/integration/dynamic-hoist-dup/index.js') ); - await assertBundles(b, { - name: 'index.js', - assets: [ - 'index.js', - 'common.js', - 'bundle-loader.js', - 'bundle-url.js', - 'js-loader.js' - ], - childBundles: [ - { - assets: ['a.js'], - childBundles: [ - { - type: 'map' - } - ] - }, - { - type: 'map' - } - ] - }); + await assertBundles(b, [ + { + name: 'index.js', + assets: [ + 'index.js', + 'common.js', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js', + 'JSRuntime.js' + ] + }, + { + assets: ['a.js'] + } + ]); let output = await run(b); assert.equal(typeof output, 'function'); assert.equal(await output(), 5); }); - it.skip('should support hoisting shared modules with async imports up multiple levels', async function() { + it('should support shared modules with async imports', async function() { let b = await bundle( - path.join(__dirname, '/integration/dynamic-hoist-deep/index.js'), - { - sourceMaps: false - } + path.join(__dirname, '/integration/dynamic-hoist-deep/index.js') ); - await assertBundles(b, { - name: 'index.js', - assets: [ - 'index.js', - 'c.js', - 'bundle-loader.js', - 'bundle-url.js', - 'js-loader.js' - ], - childBundles: [ - { - assets: ['a.js'], - childBundles: [ - { - assets: ['1.js'], - childBundles: [] - } - ] - }, - { - assets: ['b.js'], - childBundles: [] - } - ] - }); + await assertBundles(b, [ + { + name: 'index.js', + assets: [ + 'index.js', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js', + 'JSRuntime.js', + 'JSRuntime.js' + ] + }, + { + assets: ['a.js', 'c.js', 'JSRuntime.js'] + }, + { + assets: ['b.js', 'c.js', 'JSRuntime.js'] + }, + { + assets: ['1.js'] + } + ]); let output = await run(b); assert.deepEqual(output, {default: {asdf: 1}}); diff --git a/packages/core/integration-tests/test/utils.js b/packages/core/integration-tests/test/utils.js index 925cff111a8..630dafd33b9 100644 --- a/packages/core/integration-tests/test/utils.js +++ b/packages/core/integration-tests/test/utils.js @@ -229,7 +229,7 @@ async function assertBundles(bundleGraph, bundles) { assets.push(path.basename(asset.filePath)); }); - assets.sort(); + assets.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)); actualBundles.push({ name: path.basename(bundle.filePath), type: bundle.type, @@ -238,7 +238,7 @@ async function assertBundles(bundleGraph, bundles) { }); for (let bundle of bundles) { - bundle.assets.sort(); + bundle.assets.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)); } bundles.sort((a, b) => (a.assets[0] < b.assets[0] ? -1 : 1)); @@ -261,7 +261,7 @@ async function assertBundles(bundleGraph, bundles) { } if (bundle.assets) { - assert.deepEqual(actualBundle.assets, bundle.assets.sort()); + assert.deepEqual(actualBundle.assets, bundle.assets); } // assert(await fs.exists(bundle.filePath), 'expected file does not exist'); diff --git a/packages/core/plugin/src/PluginAPI.js b/packages/core/plugin/src/PluginAPI.js index 4704494ac8c..a7e85efec24 100644 --- a/packages/core/plugin/src/PluginAPI.js +++ b/packages/core/plugin/src/PluginAPI.js @@ -5,6 +5,7 @@ import type { Resolver as ResolverOpts, Bundler as BundlerOpts, Namer as NamerOpts, + Runtime as RuntimeOpts, Packager as PackagerOpts, Optimizer as OptimizerOpts, Reporter as ReporterOpts @@ -40,6 +41,13 @@ export class Namer { } } +export class Runtime { + constructor(opts: RuntimeOpts) { + // $FlowFixMe + this[CONFIG] = opts; + } +} + export class Packager { constructor(opts: PackagerOpts) { // $FlowFixMe diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 438a57b1a2a..4370d300346 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -25,11 +25,11 @@ export type ParcelConfig = { transforms: { [Glob]: Array }, - loaders: { - [Glob]: PackageName - }, bundler: PackageName, namers: Array, + runtimes: { + [EnvironmentContext]: Array + }, packagers: { [Glob]: PackageName }, @@ -150,7 +150,8 @@ export type File = { export type TransformerRequest = { filePath: FilePath, - env: Environment + env: Environment, + code?: string }; export interface Asset { @@ -252,11 +253,6 @@ export interface Graph { merge(graph: Graph): void; } -export type DependencyResolution = { - asset?: Asset, - bundles?: Array -}; - // TODO: what do we want to expose here? export interface AssetGraph extends Graph { traverseAssets(visit: GraphTraversalCallback): any; @@ -265,18 +261,20 @@ export interface AssetGraph extends Graph { getEntryAssets(): Array; removeAsset(asset: Asset): void; getDependencies(asset: Asset): Array; - getDependencyResolution(dependency: Dependency): DependencyResolution; + getDependencyResolution(dependency: Dependency): ?Asset; } export type BundleGroup = { dependency: Dependency, - target: ?Target + target: ?Target, + entryAssetId: string }; export type Bundle = { id: string, type: string, assetGraph: AssetGraph, + env: Environment, isEntry?: boolean, target?: Target, filePath?: FilePath @@ -304,6 +302,10 @@ export type Namer = { name(bundle: Bundle, opts: CLIOptions): Async }; +export type Runtime = { + apply(bundle: Bundle, opts: CLIOptions): Async +}; + export type Packager = { package(bundle: Bundle, opts: CLIOptions): Async }; diff --git a/packages/packagers/js/src/JSPackager.js b/packages/packagers/js/src/JSPackager.js index 6d38028bc3d..9b8cbf6673f 100644 --- a/packages/packagers/js/src/JSPackager.js +++ b/packages/packagers/js/src/JSPackager.js @@ -25,10 +25,8 @@ export default new Packager({ let dependencies = bundle.assetGraph.getDependencies(asset); for (let dep of dependencies) { let resolved = bundle.assetGraph.getDependencyResolution(dep); - if (resolved.bundles) { - deps[dep.moduleSpecifier] = resolved.bundles.map(b => b.id); - } else if (resolved.asset) { - deps[dep.moduleSpecifier] = resolved.asset.id; + if (resolved) { + deps[dep.moduleSpecifier] = resolved.id; } } diff --git a/packages/runtimes/js/package.json b/packages/runtimes/js/package.json new file mode 100644 index 00000000000..7ff68ceb549 --- /dev/null +++ b/packages/runtimes/js/package.json @@ -0,0 +1,16 @@ +{ + "name": "@parcel/runtime-js", + "version": "2.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/parcel-bundler/parcel.git" + }, + "main": "src/JSRuntime", + "dependencies": { + "@parcel/plugin": "^2.0.0" + }, + "devDependencies": { + "@parcel/eslint-config": "1.10.3" + } +} diff --git a/packages/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js new file mode 100644 index 00000000000..ba2411dab8c --- /dev/null +++ b/packages/runtimes/js/src/JSRuntime.js @@ -0,0 +1,80 @@ +// @flow +import {Runtime} from '@parcel/plugin'; +import path from 'path'; + +const LOADERS = { + browser: { + css: './loaders/browser/css-loader', + html: './loaders/browser/html-loader', + js: './loaders/browser/js-loader', + wasm: './loaders/browser/wasm-loader' + }, + node: { + css: './loaders/node/css-loader', + html: './loaders/node/html-loader', + js: './loaders/node/js-loader', + wasm: './loaders/node/wasm-loader' + } +}; + +export default new Runtime({ + async apply(bundle) { + // Dependency ids in code replaced with referenced bundle names + // Loader runtime added for bundle groups that don't have a native loader (e.g. HTML/CSS/Worker - isURL?), + // and which are not loaded by a parent bundle. + // Loaders also added for modules that were moved to a separate bundle because they are a different type + // (e.g. WASM, HTML). These should be preloaded prior to the bundle being executed. Replace the entry asset(s) + // with the preload module. + + if (bundle.type !== 'js') { + return; + } + + // $FlowFixMe - ignore unknown properties? + let loaders = LOADERS[bundle.env.context]; + if (!loaders) { + return; + } + + // $FlowFixMe - define a better asset graph interface + let bundleGroups = Array.from(bundle.assetGraph.nodes.values()).filter( + n => n.type === 'bundle_group' + ); + for (let bundleGroup of bundleGroups) { + // Ignore deps with native loaders, e.g. workers. + if (bundleGroup.value.dependency.isURL) { + continue; + } + + let bundles = bundle.assetGraph + // $FlowFixMe - define a better asset graph interface + .getNodesConnectedFrom(bundleGroup) + .map(node => node.value) + .sort( + bundle => + bundle.assetGraph.hasNode(bundleGroup.value.entryAssetId) ? 1 : -1 + ); + + let loaderModules = bundles.map(b => { + let loader = loaders[b.type]; + if (!loader) { + throw new Error('Could not find a loader for '); + } + + return `[require(${JSON.stringify(loader)}), ${JSON.stringify( + // $FlowFixMe - bundle.filePath already exists here + path.relative(path.dirname(bundle.filePath), b.filePath) + )}]`; + }); + + // $FlowFixMe + await bundle.assetGraph.addRuntimeAsset(bundleGroup, { + filePath: __filename, + env: bundle.env, + code: `module.exports = require('./bundle-loader')([${loaderModules.join( + ', ' + )}, ${JSON.stringify(bundleGroup.value.entryAssetId)}]);` + }); + } + } +}); diff --git a/packages/runtimes/js/src/bundle-loader.js b/packages/runtimes/js/src/bundle-loader.js new file mode 100644 index 00000000000..6e3b6c7a3aa --- /dev/null +++ b/packages/runtimes/js/src/bundle-loader.js @@ -0,0 +1,82 @@ +var getBundleURL = require('./bundle-url').getBundleURL; + +function loadBundlesLazy(bundles) { + if (!Array.isArray(bundles)) { + bundles = [bundles]; + } + + var id = bundles[bundles.length - 1]; + + try { + return Promise.resolve(require(id)); + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND') { + return new LazyPromise(function(resolve, reject) { + loadBundles(bundles.slice(0, -1)) + .then(function() { + return require(id); + }) + .then(resolve, reject); + }); + } + + throw err; + } +} + +function loadBundles(bundles) { + return Promise.all(bundles.map(loadBundle)); +} + +var bundleLoaders = {}; +function registerBundleLoader(type, loader) { + bundleLoaders[type] = loader; +} + +module.exports = exports = loadBundlesLazy; +exports.load = loadBundles; +exports.register = registerBundleLoader; + +var bundles = {}; +function loadBundle([bundleLoader, bundle]) { + var id; + if (Array.isArray(bundle)) { + id = bundle[1]; + bundle = bundle[0]; + } + + if (bundles[bundle]) { + return bundles[bundle]; + } + + if (bundleLoader) { + return (bundles[bundle] = bundleLoader(getBundleURL() + bundle) + .then(function(resolved) { + if (resolved) { + module.bundle.register(id, resolved); + } + + return resolved; + }) + .catch(function(e) { + delete bundles[bundle]; + + throw e; + })); + } +} + +function LazyPromise(executor) { + this.executor = executor; + this.promise = null; +} + +LazyPromise.prototype.then = function(onSuccess, onError) { + if (this.promise === null) this.promise = new Promise(this.executor); + return this.promise.then(onSuccess, onError); +}; + +LazyPromise.prototype.catch = function(onError) { + if (this.promise === null) this.promise = new Promise(this.executor); + return this.promise.catch(onError); +}; diff --git a/packages/runtimes/js/src/bundle-url.js b/packages/runtimes/js/src/bundle-url.js new file mode 100644 index 00000000000..95de7dd8a3c --- /dev/null +++ b/packages/runtimes/js/src/bundle-url.js @@ -0,0 +1,31 @@ +var bundleURL = null; +function getBundleURLCached() { + if (!bundleURL) { + bundleURL = getBundleURL(); + } + + return bundleURL; +} + +function getBundleURL() { + // Attempt to find the URL of the current script and use that as the base URL + try { + throw new Error(); + } catch (err) { + var matches = ('' + err.stack).match(/(https?|file|ftp):\/\/[^)\n]+/g); + if (matches) { + return getBaseURL(matches[0]); + } + } + + return '/'; +} + +function getBaseURL(url) { + return ( + ('' + url).replace(/^((?:https?|file|ftp):\/\/.+)\/[^/]+$/, '$1') + '/' + ); +} + +exports.getBundleURL = getBundleURLCached; +exports.getBaseURL = getBaseURL; diff --git a/packages/runtimes/js/src/loaders/.eslintrc.json b/packages/runtimes/js/src/loaders/.eslintrc.json new file mode 100644 index 00000000000..f76437cf2e0 --- /dev/null +++ b/packages/runtimes/js/src/loaders/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "extends": "@parcel/eslint-config", + "parser": "espree", + "parserOptions": { + "ecmaVersion": 5 + }, + "env": { + "browser": true + }, + "rules": { + "no-global-assign": 1, + "no-unused-vars": 0 + } +} diff --git a/packages/runtimes/js/src/loaders/browser/css-loader.js b/packages/runtimes/js/src/loaders/browser/css-loader.js new file mode 100644 index 00000000000..9eeb43b38e8 --- /dev/null +++ b/packages/runtimes/js/src/loaders/browser/css-loader.js @@ -0,0 +1,18 @@ +module.exports = function loadCSSBundle(bundle) { + return new Promise(function(resolve, reject) { + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = bundle; + link.onerror = function(e) { + link.onerror = link.onload = null; + reject(e); + }; + + link.onload = function() { + link.onerror = link.onload = null; + resolve(); + }; + + document.getElementsByTagName('head')[0].appendChild(link); + }); +}; diff --git a/packages/runtimes/js/src/loaders/browser/html-loader.js b/packages/runtimes/js/src/loaders/browser/html-loader.js new file mode 100644 index 00000000000..21bfdb73083 --- /dev/null +++ b/packages/runtimes/js/src/loaders/browser/html-loader.js @@ -0,0 +1,5 @@ +module.exports = function loadHTMLBundle(bundle) { + return fetch(bundle).then(function(res) { + return res.text(); + }); +}; diff --git a/packages/runtimes/js/src/loaders/browser/js-loader.js b/packages/runtimes/js/src/loaders/browser/js-loader.js new file mode 100644 index 00000000000..f6fd9eab193 --- /dev/null +++ b/packages/runtimes/js/src/loaders/browser/js-loader.js @@ -0,0 +1,20 @@ +module.exports = function loadJSBundle(bundle) { + return new Promise(function(resolve, reject) { + var script = document.createElement('script'); + script.async = true; + script.type = 'text/javascript'; + script.charset = 'utf-8'; + script.src = bundle; + script.onerror = function(e) { + script.onerror = script.onload = null; + reject(e); + }; + + script.onload = function() { + script.onerror = script.onload = null; + resolve(); + }; + + document.getElementsByTagName('head')[0].appendChild(script); + }); +}; diff --git a/packages/runtimes/js/src/loaders/browser/wasm-loader.js b/packages/runtimes/js/src/loaders/browser/wasm-loader.js new file mode 100644 index 00000000000..524d5bd4e3e --- /dev/null +++ b/packages/runtimes/js/src/loaders/browser/wasm-loader.js @@ -0,0 +1,15 @@ +module.exports = function loadWASMBundle(bundle) { + return fetch(bundle) + .then(function(res) { + if (WebAssembly.instantiateStreaming) { + return WebAssembly.instantiateStreaming(res); + } else { + return res.arrayBuffer().then(function(data) { + return WebAssembly.instantiate(data); + }); + } + }) + .then(function(wasmModule) { + return wasmModule.instance.exports; + }); +}; diff --git a/packages/runtimes/js/src/loaders/node/css-loader.js b/packages/runtimes/js/src/loaders/node/css-loader.js new file mode 100644 index 00000000000..06b8382fcb6 --- /dev/null +++ b/packages/runtimes/js/src/loaders/node/css-loader.js @@ -0,0 +1,4 @@ +// loading a CSS style is a no-op in Node.js +module.exports = function loadCSSBundle() { + return Promise.resolve(); +}; diff --git a/packages/runtimes/js/src/loaders/node/html-loader.js b/packages/runtimes/js/src/loaders/node/html-loader.js new file mode 100644 index 00000000000..64f6f958be6 --- /dev/null +++ b/packages/runtimes/js/src/loaders/node/html-loader.js @@ -0,0 +1,17 @@ +var fs = require('fs'); + +module.exports = function loadHTMLBundle(bundle) { + return new Promise(function(resolve, reject) { + fs.readFile(__dirname + bundle, 'utf8', function(err, data) { + if (err) { + reject(err); + } else { + // wait for the next event loop iteration, so we are sure + // the current module is fully loaded + setImmediate(function() { + resolve(data); + }); + } + }); + }); +}; diff --git a/packages/runtimes/js/src/loaders/node/js-loader.js b/packages/runtimes/js/src/loaders/node/js-loader.js new file mode 100644 index 00000000000..888c00bccae --- /dev/null +++ b/packages/runtimes/js/src/loaders/node/js-loader.js @@ -0,0 +1,19 @@ +var fs = require('fs'); + +module.exports = function loadJSBundle(bundle) { + return new Promise(function(resolve, reject) { + fs.readFile(__dirname + bundle, 'utf8', function(err, data) { + if (err) { + reject(err); + } else { + // wait for the next event loop iteration, so we are sure + // the current module is fully loaded + setImmediate(function() { + resolve(data); + }); + } + }); + }).then(function(code) { + new Function('', code)(); + }); +}; diff --git a/packages/runtimes/js/src/loaders/node/wasm-loader.js b/packages/runtimes/js/src/loaders/node/wasm-loader.js new file mode 100644 index 00000000000..9708291e73c --- /dev/null +++ b/packages/runtimes/js/src/loaders/node/wasm-loader.js @@ -0,0 +1,19 @@ +var fs = require('fs'); + +module.exports = function loadWASMBundle(bundle) { + return new Promise(function(resolve, reject) { + fs.readFile(__dirname + bundle, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data.buffer); + } + }); + }) + .then(function(data) { + return WebAssembly.instantiate(data); + }) + .then(function(wasmModule) { + return wasmModule.instance.exports; + }); +}; diff --git a/packages/transformers/babel/src/env.js b/packages/transformers/babel/src/env.js index 7652df9dbae..e75c354cd8f 100644 --- a/packages/transformers/babel/src/env.js +++ b/packages/transformers/babel/src/env.js @@ -58,7 +58,7 @@ async function getEnvPlugins(targets, useBuiltIns = false) { { targets, modules: false, - useBuiltIns: useBuiltIns ? 'usage' : false, + useBuiltIns: useBuiltIns ? 'entry' : false, shippedProposals: true } ).plugins; diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index eb77ffa3a20..3f9db560b93 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -73,7 +73,7 @@ export default new Transformer({ } // Collect dependencies - if (canHaveDependencies(asset.code)) { + if (canHaveDependencies(asset.code) || ast.isDirty) { walk.ancestor(ast.program, collectDependencies, asset); } @@ -141,7 +141,11 @@ export default new Transformer({ if (asset.meta.globals && asset.meta.globals.size > 0) { res.code = - Array.from(asset.meta.globals.values()).join('\n') + '\n' + res.code; + Array.from(asset.meta.globals.values()) + .map(g => (g ? g.code : '')) + .join('\n') + + '\n' + + res.code; } delete asset.meta.globals; diff --git a/packages/transformers/js/src/visitors/dependencies.js b/packages/transformers/js/src/visitors/dependencies.js index 3f7d0e249e1..e7245fb871f 100644 --- a/packages/transformers/js/src/visitors/dependencies.js +++ b/packages/transformers/js/src/visitors/dependencies.js @@ -1,11 +1,9 @@ import * as types from '@babel/types'; -// import template from '@babel/template'; import traverse from '@babel/traverse'; import nodeBuiltins from 'node-libs-browser'; import isURL from '@parcel/utils/is-url'; +import {hasBinding} from './utils'; -// const requireTemplate = template('require("_bundle_loader")'); -// const argTemplate = template('require.resolve(MODULE)'); const serviceWorkerPattern = ['navigator', 'serviceWorker', 'register']; export default { @@ -59,12 +57,10 @@ export default { return; } - // asset.addDependency({moduleSpecifier: '_bundle_loader'}); addDependency(asset, args[0], {isAsync: true}); - // node.callee = requireTemplate().expression; - // node.arguments[0] = argTemplate({MODULE: args[0]}).expression; - // asset.ast.isDirty = true; + node.callee = types.identifier('require'); + asset.ast.isDirty = true; return; } @@ -99,33 +95,6 @@ export default { } }; -function hasBinding(node, name) { - if (Array.isArray(node)) { - return node.some(ancestor => hasBinding(ancestor, name)); - } else if ( - types.isProgram(node) || - types.isBlockStatement(node) || - types.isBlock(node) - ) { - return node.body.some(statement => hasBinding(statement, name)); - } else if ( - types.isFunctionDeclaration(node) || - types.isFunctionExpression(node) || - types.isArrowFunctionExpression(node) - ) { - return ( - (node.id && node.id.name === name) || - node.params.some( - param => types.isIdentifier(param) && param.name === name - ) - ); - } else if (types.isVariableDeclaration(node)) { - return node.declarations.some(declaration => declaration.id.name === name); - } - - return false; -} - function isInFalsyBranch(ancestors) { // Check if any ancestors are if statements return ancestors.some((node, index) => { diff --git a/packages/transformers/js/src/visitors/globals.js b/packages/transformers/js/src/visitors/globals.js index 1c3684a24e6..c9c5057c6e3 100644 --- a/packages/transformers/js/src/visitors/globals.js +++ b/packages/transformers/js/src/visitors/globals.js @@ -1,25 +1,32 @@ import Path from 'path'; import * as types from '@babel/types'; +import {hasBinding} from './utils'; const VARS = { - process: asset => { - asset.addDependency({moduleSpecifier: 'process'}); - return 'var process = require("process");'; - }, - global: () => - `var global = arguments[${/*asset.options.scopeHoist ? 0 : */ 3}];`, - __dirname: asset => - `var __dirname = ${JSON.stringify(Path.dirname(asset.filePath))};`, - __filename: asset => `var __filename = ${JSON.stringify(asset.filePath)};`, - Buffer: asset => { - asset.addDependency({moduleSpecifier: 'buffer'}); - return 'var Buffer = require("buffer").Buffer;'; - }, + process: () => ({ + code: 'var process = require("process");', + deps: ['process'] + }), + global: () => ({ + code: `var global = arguments[${/*asset.options.scopeHoist ? 0 : */ 3}];` + }), + __dirname: asset => ({ + code: `var __dirname = ${JSON.stringify(Path.dirname(asset.filePath))};` + }), + __filename: asset => ({ + code: `var __filename = ${JSON.stringify(asset.filePath)};` + }), + Buffer: () => ({ + code: 'var Buffer = require("buffer").Buffer;', + deps: ['buffer'] + }), // Prevent AMD defines from working when loading UMD bundles. // Ideally the CommonJS check would come before the AMD check, but many // existing modules do the checks the opposite way leading to modules // not exporting anything to Parcel. - define: () => 'var define;' + define: () => ({ + code: 'var define;' + }) }; export default { @@ -28,7 +35,8 @@ export default { if ( VARS.hasOwnProperty(node.name) && !asset.meta.globals.has(node.name) && - types.isReferenced(node, parent) + types.isReferenced(node, parent) && + !hasBinding(ancestors, node.name) ) { asset.meta.globals.set(node.name, VARS[node.name](asset)); } @@ -40,7 +48,21 @@ export default { for (let id in identifiers) { if (VARS.hasOwnProperty(id) && !inScope(ancestors)) { // Don't delete entirely, so we don't add it again when the declaration is referenced - asset.meta.globals.set(id, ''); + asset.meta.globals.set(id, null); + } + } + }, + + Program: { + exit(node, asset) { + // Add dependencies at the end so that items that were deleted later don't leave + // their dependencies around. + for (let g of asset.meta.globals.values()) { + if (g && g.deps) { + for (let dep of g.deps) { + asset.addDependency({moduleSpecifier: dep}); + } + } } } } diff --git a/packages/transformers/js/src/visitors/utils.js b/packages/transformers/js/src/visitors/utils.js new file mode 100644 index 00000000000..568923f9d8c --- /dev/null +++ b/packages/transformers/js/src/visitors/utils.js @@ -0,0 +1,28 @@ +import * as types from '@babel/types'; + +export function hasBinding(node, name) { + if (Array.isArray(node)) { + return node.some(ancestor => hasBinding(ancestor, name)); + } else if ( + types.isProgram(node) || + types.isBlockStatement(node) || + types.isBlock(node) + ) { + return node.body.some(statement => hasBinding(statement, name)); + } else if ( + types.isFunctionDeclaration(node) || + types.isFunctionExpression(node) || + types.isArrowFunctionExpression(node) + ) { + return ( + (node.id && node.id.name === name) || + node.params.some( + param => types.isIdentifier(param) && param.name === name + ) + ); + } else if (types.isVariableDeclaration(node)) { + return node.declarations.some(declaration => declaration.id.name === name); + } + + return false; +}