From 4d70ea93957d9245efafcf0f9159d8a809fb5764 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 15 May 2019 15:18:01 -0700 Subject: [PATCH] V2 scope hoisting (#2967) * WIP: v2 scope hoisting * Support sideEffects flag * Don't edit parcel 1 * Fix flow errors * Type safe traversal filtering * Run tests faster * Base child assets on the same id base as parents * Fix flow errors * Only enable scope hoisting in production mode by default * Fix lint * Clean up * Shake exports with pure property assignments (#2909) * Send BundleGraph to packagers * Lint * Fix flow * Clear scope cache before crawling * Fix shake * Define __esModule interop flag when requiring ES module from CommonJS * Replace module.require in scope hoisting * Fix assigning to exports from inside a function in scope hoisting * Format --- packages/bundlers/default/.babelrc | 3 + packages/core/core/src/Asset.js | 32 +- packages/core/core/src/AssetGraph.js | 112 +++- packages/core/core/src/AssetGraphBuilder.js | 6 +- packages/core/core/src/BundleGraph.js | 27 +- packages/core/core/src/Dependency.js | 34 +- packages/core/core/src/Graph.js | 113 +++- packages/core/core/src/PackagerRunner.js | 24 +- packages/core/core/src/Parcel.js | 9 +- packages/core/core/src/ResolverRunner.js | 10 +- packages/core/core/src/TransformerRunner.js | 29 +- packages/core/core/src/public/Asset.js | 11 +- packages/core/core/src/public/Bundle.js | 59 +- packages/core/core/src/public/BundleGraph.js | 4 + .../core/core/src/public/MainAssetGraph.js | 23 +- packages/core/core/src/public/utils.js | 31 +- packages/core/core/src/resolveOptions.js | 4 +- packages/core/core/src/worker.js | 5 +- packages/core/integration-tests/package.json | 4 +- .../commonjs/export-assign-scope/a.js | 4 + .../commonjs/export-assign-scope/b.js | 3 + .../commonjs/interop-require-es-module/a.js | 1 + .../commonjs/interop-require-es-module/b.js | 1 + .../commonjs/module-object/a.js | 1 + .../es6/default-export-class-rename/a.js | 2 + .../es6/default-export-class-rename/b.js | 9 + .../default-export-class-rename/package.json | 5 + .../scope-hoisting/es6/pure-assignment/a.js | 2 +- .../es6/side-effects-false-duplicate/a.js | 3 + .../node_modules/bar/bar.js | 7 + .../node_modules/bar/foo.js | 5 + .../node_modules/bar/index.js | 3 + .../node_modules/bar/package.json | 4 + .../node_modules/bar/shared.js | 1 + .../node_modules/bar/b.js | 2 + .../integration-tests/test/scope-hoisting.js | 158 ++++- packages/core/parcel/src/cli.js | 1 + packages/core/register/src/hook.js | 2 +- packages/core/types/index.js | 74 ++- packages/examples/simple/src/index.js | 12 +- packages/examples/simple/src/message.js | 2 +- packages/namers/default/.babelrc | 3 + packages/packagers/js/.babelrc | 3 + packages/packagers/js/package.json | 3 +- packages/packagers/js/src/JSPackager.js | 12 +- packages/reporters/cli/src/Table.js | 4 + .../resolvers/default/src/DefaultResolver.js | 42 +- packages/runtimes/js/.babelrc | 3 + packages/shared/scope-hoisting/.babelrc | 3 + packages/shared/scope-hoisting/.eslintrc.json | 6 + packages/shared/scope-hoisting/package.json | 32 + packages/shared/scope-hoisting/src/concat.js | 326 +++++++++ .../shared/scope-hoisting/src/generate.js | 17 + packages/shared/scope-hoisting/src/helpers.js | 37 ++ packages/shared/scope-hoisting/src/hoist.js | 619 ++++++++++++++++++ packages/shared/scope-hoisting/src/index.js | 6 + packages/shared/scope-hoisting/src/link.js | 378 +++++++++++ packages/shared/scope-hoisting/src/mangler.js | 70 ++ packages/shared/scope-hoisting/src/prelude.js | 20 + packages/shared/scope-hoisting/src/renamer.js | 36 + packages/shared/scope-hoisting/src/shake.js | 125 ++++ packages/shared/scope-hoisting/src/utils.js | 24 + packages/transformers/babel/src/babelrc.js | 10 +- packages/transformers/babel/src/config.js | 4 +- packages/transformers/babel/src/env.js | 4 +- packages/transformers/babel/src/flow.js | 4 +- .../babel/src/getTargetEngines.js | 4 +- packages/transformers/babel/src/jsx.js | 4 +- packages/transformers/css/.babelrc | 3 + packages/transformers/js/package.json | 1 + packages/transformers/js/src/JSTransformer.js | 18 +- .../terser/src/TerserTransformer.js | 4 +- yarn.lock | 109 ++- 73 files changed, 2598 insertions(+), 178 deletions(-) create mode 100644 packages/bundlers/default/.babelrc create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/a.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/b.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/a.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/b.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/a.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/b.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/package.json create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/a.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/bar.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/foo.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/index.js create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/package.json create mode 100644 packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/shared.js create mode 100644 packages/namers/default/.babelrc create mode 100644 packages/packagers/js/.babelrc create mode 100644 packages/runtimes/js/.babelrc create mode 100644 packages/shared/scope-hoisting/.babelrc create mode 100644 packages/shared/scope-hoisting/.eslintrc.json create mode 100644 packages/shared/scope-hoisting/package.json create mode 100644 packages/shared/scope-hoisting/src/concat.js create mode 100644 packages/shared/scope-hoisting/src/generate.js create mode 100644 packages/shared/scope-hoisting/src/helpers.js create mode 100644 packages/shared/scope-hoisting/src/hoist.js create mode 100644 packages/shared/scope-hoisting/src/index.js create mode 100644 packages/shared/scope-hoisting/src/link.js create mode 100644 packages/shared/scope-hoisting/src/mangler.js create mode 100644 packages/shared/scope-hoisting/src/prelude.js create mode 100644 packages/shared/scope-hoisting/src/renamer.js create mode 100644 packages/shared/scope-hoisting/src/shake.js create mode 100644 packages/shared/scope-hoisting/src/utils.js create mode 100644 packages/transformers/css/.babelrc diff --git a/packages/bundlers/default/.babelrc b/packages/bundlers/default/.babelrc new file mode 100644 index 00000000000..be26495ef30 --- /dev/null +++ b/packages/bundlers/default/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@parcel/babel-preset"] +} diff --git a/packages/core/core/src/Asset.js b/packages/core/core/src/Asset.js index 909b754b625..2cfaf24447b 100644 --- a/packages/core/core/src/Asset.js +++ b/packages/core/core/src/Asset.js @@ -15,6 +15,7 @@ import type { PackageJSON, SourceMap, Stats, + Symbol, TransformerResult } from '@parcel/types'; @@ -33,6 +34,7 @@ import Dependency from './Dependency'; type AssetOptions = {| id?: string, hash?: ?string, + idBase?: string, filePath: FilePath, type: string, content?: Blob, @@ -46,7 +48,9 @@ type AssetOptions = {| outputHash?: string, env: Environment, meta?: Meta, - stats: Stats + stats: Stats, + symbols?: Map | Array<[Symbol, Symbol]>, + sideEffects?: boolean |}; type SerializedOptions = {| @@ -60,6 +64,7 @@ type SerializedOptions = {| export default class Asset { id: string; hash: ?string; + idBase: string; filePath: FilePath; type: string; ast: ?AST; @@ -74,13 +79,16 @@ export default class Asset { stats: Stats; content: Blob; contentKey: ?string; + symbols: Map; + sideEffects: boolean; constructor(options: AssetOptions) { + this.idBase = options.idBase != null ? options.idBase : options.filePath; this.id = options.id != null ? options.id : md5FromString( - options.filePath + options.type + JSON.stringify(options.env) + this.idBase + options.type + JSON.stringify(options.env) ); this.hash = options.hash; this.filePath = options.filePath; @@ -100,6 +108,8 @@ export default class Asset { this.env = options.env; this.meta = options.meta || {}; this.stats = options.stats; + this.symbols = new Map(options.symbols || []); + this.sideEffects = options.sideEffects != null ? options.sideEffects : true; } serialize(): SerializedOptions { @@ -116,7 +126,9 @@ export default class Asset { env: this.env, meta: this.meta, stats: this.stats, - contentKey: this.contentKey + contentKey: this.contentKey, + symbols: [...this.symbols], + sideEffects: this.sideEffects }; } @@ -237,8 +249,12 @@ export default class Asset { env: this.env.merge(env), sourcePath: this.filePath }); - - this.dependencies.set(dep.id, dep); + let existing = this.dependencies.get(dep.id); + if (existing) { + existing.merge(dep); + } else { + this.dependencies.set(dep.id, dep); + } return dep.id; } @@ -282,6 +298,7 @@ export default class Asset { } let asset = new Asset({ + idBase: this.idBase, hash, filePath: this.filePath, type: result.type, @@ -295,7 +312,10 @@ export default class Asset { stats: { time: 0, size - } + }, + symbols: new Map([...this.symbols, ...(result.symbols || [])]), + sideEffects: + result.sideEffects != null ? result.sideEffects : this.sideEffects }); let dependencies = result.dependencies; diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js index fd5d870f136..28b646021a5 100644 --- a/packages/core/core/src/AssetGraph.js +++ b/packages/core/core/src/AssetGraph.js @@ -13,14 +13,17 @@ import type { Dependency as IDependency, File, FilePath, - GraphTraversalCallback, Target, + GraphVisitor, + Symbol, + SymbolResolution, TransformerRequest } from '@parcel/types'; import type Asset from './Asset'; import invariant from 'assert'; +import nullthrows from 'nullthrows'; import Graph from './Graph'; import {md5FromString} from '@parcel/utils'; import Dependency from './Dependency'; @@ -75,6 +78,9 @@ const getDepNodesFromGraph = ( ); }; +const invertMap = (map: Map): Map => + new Map([...map].map(([key, val]) => [val, key])); + type DepUpdates = {| newRequest?: TransformerRequest, prunedFiles: Array @@ -104,6 +110,7 @@ type AssetGraphOpts = {| export default class AssetGraph extends Graph { incompleteNodes: Map = new Map(); invalidNodes: Map = new Map(); + deferredNodes: Set = new Set(); initializeGraph({ entries, @@ -164,9 +171,37 @@ export default class AssetGraph extends Graph { let requestNode = nodeFromTransformerRequest(req); let {added, removed} = this.replaceNodesConnectedTo(depNode, [requestNode]); + // Defer transforming this dependency if it is marked as weak, there are no side effects, + // and no re-exported symbols are used by ancestor dependencies. + // This helps with performance building large libraries like `lodash-es`, which re-exports + // a huge number of functions since we can avoid even transforming the files that aren't used. + let defer = false; + if (dep.isWeak && req.sideEffects === false) { + let assets = this.getNodesConnectedTo(depNode); + let symbols = invertMap(dep.symbols); + invariant( + assets[0].type === 'asset' || assets[0].type === 'asset_reference' + ); + let resolvedAsset = assets[0].value; + let deps = this.getAncestorDependencies(resolvedAsset); + defer = deps.every( + d => + !d.symbols.has('*') && + ![...d.symbols.keys()].some(symbol => { + let assetSymbol = resolvedAsset.symbols.get(symbol); + return assetSymbol != null && symbols.has(assetSymbol); + }) + ); + } + if (added.nodes.size) { + this.deferredNodes.add(requestNode.id); + } + + if (!defer && this.deferredNodes.has(requestNode.id)) { newRequest = req; this.incompleteNodes.set(requestNode.id, requestNode); + this.deferredNodes.delete(requestNode.id); } let prunedFiles = getFilesFromGraph(removed); @@ -211,9 +246,15 @@ export default class AssetGraph extends Graph { let removedFiles = getFilesFromGraph(removed); for (let assetNode of assetNodes) { - let depNodes = assetNode.value - .getDependencies() - .map(dep => nodeFromDep(dep)); + let depNodes = assetNode.value.getDependencies().map(dep => { + let node = this.getNode(dep.id); + if (node && node.type === 'dependency') { + node.value.merge(dep); + return node; + } + + return nodeFromDep(dep); + }); let {removed, added} = this.replaceNodesConnectedTo(assetNode, depNodes); removedFiles = removedFiles.concat(getFilesFromGraph(removed)); newDepNodes = newDepNodes.concat(getDepNodesFromGraph(added)); @@ -281,15 +322,29 @@ export default class AssetGraph extends Graph { return res; } - traverseAssets( - visit: GraphTraversalCallback, - startNode: ?AssetGraphNode - ): ?AssetGraphNode { - return this.traverse((node, ...args) => { - if (node.type === 'asset') { - return visit(node.value, ...args); + getAncestorDependencies(asset: Asset): Array { + let node = this.getNode(asset.id); + if (!node) { + return []; + } + + return this.findAncestors(node, node => node.type === 'dependency').map( + node => { + invariant(node.type === 'dependency'); + return node.value; } - }, startNode); + ); + } + + traverseAssets( + visit: GraphVisitor, + startNode: ?AssetGraphNode + ): ?TContext { + return this.filteredTraverse( + node => (node.type === 'asset' ? node.value : null), + visit, + startNode + ); } getTotalSize(asset?: ?Asset): number { @@ -327,4 +382,37 @@ export default class AssetGraph extends Graph { return referenceId; } + + resolveSymbol(asset: Asset, symbol: Symbol): SymbolResolution { + if (symbol === '*') { + return {asset, exportSymbol: '*', symbol: '*'}; + } + + let identifier = asset.symbols.get(symbol); + + let deps = this.getDependencies(asset).reverse(); + for (let dep of deps) { + // If this is a re-export, find the original module. + let symbolLookup = new Map( + [...dep.symbols].map(([key, val]) => [val, key]) + ); + let depSymbol = symbolLookup.get(identifier); + if (depSymbol != null) { + let resolved = nullthrows(this.getDependencyResolution(dep)); + return this.resolveSymbol(resolved, depSymbol); + } + + // If this module exports wildcards, resolve the original module. + // Default exports are excluded from wildcard exports. + if (dep.symbols.get('*') === '*' && symbol !== 'default') { + let resolved = nullthrows(this.getDependencyResolution(dep)); + let result = this.resolveSymbol(resolved, symbol); + if (result.symbol != null) { + return result; + } + } + } + + return {asset, exportSymbol: symbol, symbol: identifier}; + } } diff --git a/packages/core/core/src/AssetGraphBuilder.js b/packages/core/core/src/AssetGraphBuilder.js index 414e224b6b9..a1cf36564df 100644 --- a/packages/core/core/src/AssetGraphBuilder.js +++ b/packages/core/core/src/AssetGraphBuilder.js @@ -131,9 +131,9 @@ export default class AssetGraphBuilder extends EventEmitter { } async resolve(dep: Dependency, {signal}: BuildOpts) { - let resolvedPath; + let req; try { - resolvedPath = await this.resolverRunner.resolve(dep); + req = await this.resolverRunner.resolve(dep); } catch (err) { if (err.code === 'MODULE_NOT_FOUND' && dep.isOptional) { return; @@ -146,9 +146,7 @@ export default class AssetGraphBuilder extends EventEmitter { throw new BuildAbortError(); } - 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); diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index cffca7347e2..393d8692c0a 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -4,11 +4,11 @@ import type {GraphTraversalCallback} from '@parcel/types'; import type Asset from './Asset'; import type {Bundle, BundleGraphNode} from './types'; -import Graph from './Graph'; +import Graph, {type GraphOpts} from './Graph'; export default class BundleGraph extends Graph { - constructor() { - super(); + constructor(opts?: GraphOpts) { + super(opts); this.setRootNode({ type: 'root', id: 'root', @@ -46,10 +46,25 @@ export default class BundleGraph extends Graph { traverseBundles( visit: GraphTraversalCallback ): ?TContext { - return this.traverse((node, ...args) => { - if (node.type === 'bundle') { - return visit(node.value, ...args); + return this.filteredTraverse( + node => (node.type === 'bundle' ? node.value : null), + visit + ); + } + + isAssetReferenced(asset: Asset) { + let result = false; + + this.traverseBundles((bundle, context, traversal) => { + let referenceNode = bundle.assetGraph.findNode( + node => node.type === 'asset_reference' && node.value.id === asset.id + ); + if (referenceNode) { + result = true; + traversal.stop(); } }); + + return result; } } diff --git a/packages/core/core/src/Dependency.js b/packages/core/core/src/Dependency.js index 5962a5ed92c..4c83254584e 100644 --- a/packages/core/core/src/Dependency.js +++ b/packages/core/core/src/Dependency.js @@ -7,7 +7,8 @@ import type { Meta, Target, ModuleSpecifier, - FilePath + FilePath, + Symbol } from '@parcel/types'; import {md5FromString} from '@parcel/utils'; @@ -25,11 +26,13 @@ export default class Dependency implements IDependency { isEntry: ?boolean; isOptional: ?boolean; isURL: ?boolean; + isWeak: ?boolean; loc: ?SourceLocation; env: IEnvironment; - meta: ?Meta; + meta: Meta; target: ?Target; sourcePath: FilePath; + symbols: Map; constructor(opts: DependencyOpts) { this.moduleSpecifier = opts.moduleSpecifier; @@ -37,15 +40,40 @@ export default class Dependency implements IDependency { this.isEntry = opts.isEntry; this.isOptional = opts.isOptional; this.isURL = opts.isURL; + this.isWeak = opts.isWeak; this.loc = opts.loc; - this.meta = opts.meta; + this.meta = opts.meta || {}; this.target = opts.target; this.env = opts.env; this.sourcePath = opts.sourcePath || ''; // TODO: get from graph? + this.symbols = new Map(opts.symbols || []); this.id = opts.id || md5FromString( `${this.sourcePath}:${this.moduleSpecifier}:${JSON.stringify(this.env)}` ); } + + merge(other: IDependency) { + Object.assign(this.meta, other.meta); + this.symbols = new Map([...this.symbols, ...other.symbols]); + } + + serialize() { + return { + moduleSpecifier: this.moduleSpecifier, + isAsync: this.isAsync, + isEntry: this.isEntry, + isOptional: this.isOptional, + isURL: this.isURL, + isWeak: this.isWeak, + loc: this.loc, + meta: this.meta, + target: this.target, + env: this.env, + sourcePath: this.sourcePath, + symbols: [...this.symbols], + id: this.id + }; + } } diff --git a/packages/core/core/src/Graph.js b/packages/core/core/src/Graph.js index a04aba23864..3d0b66dc27c 100644 --- a/packages/core/core/src/Graph.js +++ b/packages/core/core/src/Graph.js @@ -1,12 +1,10 @@ // @flow import type {Edge, Node, NodeId} from './types'; - -import type {GraphTraversalCallback, TraversalActions} from '@parcel/types'; - +import type {TraversalActions, GraphVisitor} from '@parcel/types'; import nullthrows from 'nullthrows'; -type GraphOpts = {| +export type GraphOpts = {| nodes?: Array<[NodeId, TNode]>, edges?: Array, rootNodeId?: ?NodeId @@ -202,20 +200,43 @@ export default class Graph { return {removed, added}; } - traverse( - visit: GraphTraversalCallback, - startNode: ?TNode - ): ?TContext { + traverse(visit: GraphVisitor, startNode: ?TNode) { return this.dfs({ + // $FlowFixMe visit, startNode, getChildren: this.getNodesConnectedFrom.bind(this) }); } + filteredTraverse( + filter: TNode => ?TValue, + visit: GraphVisitor, + startNode: ?TNode + ): ?TContext { + return this.traverse( + { + enter: (node, ...args) => { + let fn = visit.enter || visit; + let value = filter(node); + if (value != null && typeof fn === 'function') { + return fn(value, ...args); + } + }, + exit: (node, ...args) => { + let value = filter(node); + if (value != null && typeof visit.exit === 'function') { + return visit.exit(value, ...args); + } + } + }, + startNode + ); + } + traverseAncestors( startNode: TNode, - visit: GraphTraversalCallback + visit: GraphVisitor ) { return this.dfs({ visit, @@ -229,7 +250,7 @@ export default class Graph { startNode, getChildren }: { - visit: GraphTraversalCallback, + visit: GraphVisitor, getChildren(node: TNode): Array, startNode?: ?TNode }): ?TContext { @@ -254,9 +275,12 @@ export default class Graph { visited.add(node); skipped = false; - let newContext = visit(node, context, actions); - if (typeof newContext !== 'undefined') { - context = newContext; + let enter = typeof visit === 'function' ? visit : visit.enter; + if (enter) { + let newContext = enter(node, context, actions); + if (typeof newContext !== 'undefined') { + context = newContext; + } } if (skipped) { @@ -278,6 +302,21 @@ export default class Graph { return result; } } + + if (visit.exit) { + let newContext = visit.exit(node, context, actions); + if (typeof newContext !== 'undefined') { + context = newContext; + } + } + + if (skipped) { + return; + } + + if (stopped) { + return context; + } }; return walk(root); @@ -326,6 +365,54 @@ export default class Graph { return graph; } + findAncestor(node: TNode, fn: (node: TNode) => boolean): ?TNode { + let res = null; + this.traverseAncestors(node, (node, ctx, traversal) => { + if (fn(node)) { + res = node; + traversal.stop(); + } + }); + return res; + } + + findAncestors(node: TNode, fn: (node: TNode) => boolean): Array { + let res = []; + this.traverseAncestors(node, (node, ctx, traversal) => { + if (fn(node)) { + res.push(node); + traversal.skipChildren(); + } + }); + return res; + } + + findDescendant(node: TNode, fn: (node: TNode) => boolean): ?TNode { + let res = null; + this.traverse((node, ctx, traversal) => { + if (fn(node)) { + res = node; + traversal.stop(); + } + }, node); + return res; + } + + findDescendants(node: TNode, fn: (node: TNode) => boolean): Array { + let res = []; + this.traverse((node, ctx, traversal) => { + if (fn(node)) { + res.push(node); + traversal.skipChildren(); + } + }, node); + return res; + } + + findNode(predicate: TNode => boolean): ?TNode { + return Array.from(this.nodes.values()).find(predicate); + } + findNodes(predicate: TNode => boolean): Array { return Array.from(this.nodes.values()).filter(predicate); } diff --git a/packages/core/core/src/PackagerRunner.js b/packages/core/core/src/PackagerRunner.js index d9b70ae94a0..eabb7ee244c 100644 --- a/packages/core/core/src/PackagerRunner.js +++ b/packages/core/core/src/PackagerRunner.js @@ -4,6 +4,7 @@ import {Readable} from 'stream'; import type {ParcelOptions, Blob, FilePath} from '@parcel/types'; import type {Bundle as InternalBundle} from './types'; import type Config from './Config'; +import type InternalBundleGraph from './BundleGraph'; import invariant from 'assert'; import {mkdirp, writeFile, writeFileStream} from '@parcel/fs'; @@ -12,6 +13,7 @@ import {NamedBundle} from './public/Bundle'; import nullthrows from 'nullthrows'; import path from 'path'; import {report} from './ReporterRunner'; +import {BundleGraph} from './public/BundleGraph'; type Opts = {| config: Config, @@ -30,9 +32,9 @@ export default class PackagerRunner { this.distExists = new Set(); } - async writeBundle(bundle: InternalBundle) { + async writeBundle(bundle: InternalBundle, bundleGraph: InternalBundleGraph) { let start = Date.now(); - let contents = await this.package(bundle); + let contents = await this.package(bundle, bundleGraph); contents = await this.optimize(bundle, contents); let filePath = nullthrows(bundle.filePath); @@ -60,7 +62,10 @@ export default class PackagerRunner { }; } - async package(internalBundle: InternalBundle): Promise { + async package( + internalBundle: InternalBundle, + bundleGraph: InternalBundleGraph + ): Promise { let bundle = new NamedBundle(internalBundle); report({ type: 'buildProgress', @@ -68,13 +73,18 @@ export default class PackagerRunner { bundle }); - let depToBundlePath = generateDepToBundlePath(internalBundle); - let packager = await this.config.getPackager(bundle.filePath); - let packageContent = await packager.package(bundle, this.options); + let packageContent = await packager.package( + bundle, + new BundleGraph(bundleGraph), + this.options + ); return typeof packageContent === 'string' - ? replaceReferences(packageContent, depToBundlePath) + ? replaceReferences( + packageContent, + generateDepToBundlePath(internalBundle) + ) : packageContent; } diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js index 2e92c03938d..3734e2fe4bb 100644 --- a/packages/core/core/src/Parcel.js +++ b/packages/core/core/src/Parcel.js @@ -26,7 +26,7 @@ export default class Parcel { #initialOptions; // InitialParcelOptions; #reporterRunner; // ReporterRunner #resolvedOptions; // ?ParcelOptions - #runPackage; // (bundle: Bundle) => Promise; + #runPackage; // (bundle: Bundle, bundleGraph: InternalBundleGraph) => Promise; constructor(options: InitialParcelOptions) { this.#initialOptions = clone(options); @@ -169,12 +169,15 @@ export default class Parcel { function packageBundles( bundleGraph: InternalBundleGraph, - runPackage: (bundle: Bundle) => Promise + runPackage: ( + bundle: Bundle, + bundleGraph: InternalBundleGraph + ) => Promise ): Promise { let promises = []; bundleGraph.traverseBundles(bundle => { promises.push( - runPackage(bundle).then(stats => { + runPackage(bundle, bundleGraph).then(stats => { bundle.stats = stats; }) ); diff --git a/packages/core/core/src/ResolverRunner.js b/packages/core/core/src/ResolverRunner.js index e94ea5a42df..9485bbfa4a2 100644 --- a/packages/core/core/src/ResolverRunner.js +++ b/packages/core/core/src/ResolverRunner.js @@ -1,6 +1,10 @@ // @flow -import type {ParcelOptions, Dependency, FilePath} from '@parcel/types'; +import type { + ParcelOptions, + Dependency, + TransformerRequest +} from '@parcel/types'; import path from 'path'; import Config from './Config'; import {report} from './ReporterRunner'; @@ -16,7 +20,7 @@ const getCacheKey = (filename, parent) => export default class ResolverRunner { config: Config; options: ParcelOptions; - cache: Map; + cache: Map; constructor({config, options}: Opts) { this.config = config; @@ -24,7 +28,7 @@ export default class ResolverRunner { this.cache = new Map(); } - async resolve(dependency: Dependency): Promise { + async resolve(dependency: Dependency): Promise { report({ type: 'buildProgress', phase: 'resolving', diff --git a/packages/core/core/src/TransformerRunner.js b/packages/core/core/src/TransformerRunner.js index 4182411586e..07319c410dd 100644 --- a/packages/core/core/src/TransformerRunner.js +++ b/packages/core/core/src/TransformerRunner.js @@ -20,12 +20,12 @@ import { md5FromString } from '@parcel/utils'; import Cache from '@parcel/cache'; -import {createReadStream} from 'fs'; import {TapStream, unique} from '@parcel/utils'; +import {createReadStream} from 'fs'; + import Config from './Config'; import {report} from './ReporterRunner'; -import nullthrows from 'nullthrows'; -import {Asset, MutableAsset, assetToInternalAsset} from './public/Asset'; +import {MutableAsset, assetToInternalAsset} from './public/Asset'; import InternalAsset from './Asset'; type Opts = {| @@ -69,6 +69,9 @@ export default class TransformerRunner { } let input = new InternalAsset({ + // If the transformer request passed code rather than a filename, + // use a hash as the base for the id to ensure it is unique. + idBase: req.code ? hash : req.filePath, filePath: req.filePath, type: path.extname(req.filePath).slice(1), ast: null, @@ -78,7 +81,8 @@ export default class TransformerRunner { stats: { time: 0, size - } + }, + sideEffects: req.sideEffects }); let pipeline = await this.config.getTransformers(req.filePath); @@ -88,16 +92,6 @@ export default 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 = md5FromString( - nullthrows(asset.hash) + JSON.stringify(asset.env) - ); - } - } - cacheEntry = { filePath: req.filePath, env: req.env, @@ -198,7 +192,10 @@ export default class TransformerRunner { // Load config for the transformer. let config = null; if (transformer.getConfig) { - config = await transformer.getConfig(new Asset(input), this.options); + config = await transformer.getConfig( + new MutableAsset(input), + this.options + ); } // If an ast exists on the input, but we cannot reuse it, @@ -217,7 +214,7 @@ export default class TransformerRunner { // Parse if there is no AST available from a previous transform. if (!input.ast && transformer.parse) { input.ast = await transformer.parse( - new Asset(input), + new MutableAsset(input), config, this.options ); diff --git a/packages/core/core/src/public/Asset.js b/packages/core/core/src/public/Asset.js index 6dc57e2fb5a..929cf232556 100644 --- a/packages/core/core/src/public/Asset.js +++ b/packages/core/core/src/public/Asset.js @@ -16,7 +16,8 @@ import type { MutableAsset as IMutableAsset, PackageJSON, SourceMap, - Stats + Stats, + Symbol } from '@parcel/types'; import type InternalAsset from '../Asset'; @@ -72,6 +73,14 @@ class BaseAsset { return this.#asset.isIsolated; } + get sideEffects(): boolean { + return this.#asset.sideEffects; + } + + get symbols(): Map { + return this.#asset.symbols; + } + getConfig( filePaths: Array, options: ?{packageKey?: string, parse?: boolean} diff --git a/packages/core/core/src/public/Bundle.js b/packages/core/core/src/public/Bundle.js index 7c4972fa44d..c9077fec0be 100644 --- a/packages/core/core/src/public/Bundle.js +++ b/packages/core/core/src/public/Bundle.js @@ -1,7 +1,7 @@ // @flow strict-local // flowlint unsafe-getters-setters:off -import type {Bundle as InternalBundle} from '../types'; +import type {Bundle as InternalBundle, AssetGraphNode} from '../types'; import type { Asset as IAsset, Bundle as IBundle, @@ -9,17 +9,18 @@ import type { Dependency, Environment, FilePath, - GraphTraversalCallback, MutableBundle as IMutableBundle, NamedBundle as INamedBundle, Stats, - Target + Target, + Symbol, + GraphVisitor } from '@parcel/types'; import nullthrows from 'nullthrows'; -import {Asset} from './Asset'; -import {getInternalAsset} from './utils'; +import {Asset, assetToInternalAsset} from './Asset'; +import {getInternalAsset, assetGraphVisitorToInternal} from './utils'; // Friendly access for other modules within this package that need access // to the internal bundle. @@ -98,25 +99,51 @@ export class Bundle implements IBundle { } traverse( - visit: GraphTraversalCallback + visit: GraphVisitor ): ?TContext { - return this.#bundle.assetGraph.traverse((node, ...args) => { + return this.#bundle.assetGraph.filteredTraverse(node => { if (node.type === 'asset') { - return visit({type: 'asset', value: node.value}, ...args); + return {type: 'asset', value: node.value}; } else if (node.type === 'asset_reference') { - return visit({type: 'asset_reference', value: node.value}, ...args); + return {type: 'asset_reference', value: node.value}; } - }); + }, visit); } - traverseAssets( - visit: GraphTraversalCallback - ): ?TContext { - return this.#bundle.assetGraph.traverse((node, ...args) => { - if (node.type === 'asset') { - return visit(new Asset(node.value), ...args); + traverseAssets(visit: GraphVisitor) { + return this.#bundle.assetGraph.traverseAssets( + assetGraphVisitorToInternal(visit) + ); + } + + traverseAncestors( + asset: IAsset, + visit: GraphVisitor + ) { + let node = nullthrows( + this.#bundle.assetGraph.getNode(asset.id), + 'Bundle does not contain asset' + ); + return this.#bundle.assetGraph.traverseAncestors(node, visit); + } + + resolveSymbol(asset: IAsset, symbol: Symbol) { + return this.#bundle.assetGraph.resolveSymbol( + assetToInternalAsset(asset), + symbol + ); + } + + hasChildBundles() { + let result = false; + this.#bundle.assetGraph.traverse((node, ctx, actions) => { + if (node.type === 'bundle_reference') { + result = true; + actions.stop(); } }); + + return result; } } diff --git a/packages/core/core/src/public/BundleGraph.js b/packages/core/core/src/public/BundleGraph.js index 8a2f6083ad6..41bce9c4cdb 100644 --- a/packages/core/core/src/public/BundleGraph.js +++ b/packages/core/core/src/public/BundleGraph.js @@ -73,6 +73,10 @@ class BaseBundleGraph { assetToInternalAsset(asset) ); } + + isAssetReferenced(asset: Asset): boolean { + return this.#graph.isAssetReferenced(assetToInternalAsset(asset)); + } } export class BundleGraph extends BaseBundleGraph implements IBundleGraph { diff --git a/packages/core/core/src/public/MainAssetGraph.js b/packages/core/core/src/public/MainAssetGraph.js index d5c8c0e6a44..3c1904050e0 100644 --- a/packages/core/core/src/public/MainAssetGraph.js +++ b/packages/core/core/src/public/MainAssetGraph.js @@ -4,13 +4,14 @@ import type AssetGraph from '../AssetGraph'; import type { Asset as IAsset, Dependency, - GraphTraversalCallback, + GraphVisitor, MainAssetGraph as IMainAssetGraph, MainAssetGraphTraversable } from '@parcel/types'; import {Asset, assetToInternalAsset} from './Asset'; import {MutableBundle} from './Bundle'; +import {assetGraphVisitorToInternal} from './utils'; export default class MainAssetGraph implements IMainAssetGraph { #graph; // AssetGraph @@ -62,24 +63,18 @@ export default class MainAssetGraph implements IMainAssetGraph { } traverse( - visit: GraphTraversalCallback + visit: GraphVisitor ): ?TContext { - return this.#graph.traverse((node, ...args) => { + return this.#graph.filteredTraverse(node => { if (node.type === 'asset') { - return visit({type: 'asset', value: new Asset(node.value)}, ...args); + return {type: 'asset', value: new Asset(node.value)}; } else if (node.type === 'dependency') { - return visit({type: 'dependency', value: node.value}, ...args); + return {type: 'dependency', value: node.value}; } - }); + }, visit); } - traverseAssets( - visit: GraphTraversalCallback - ): ?TContext { - return this.#graph.traverse((node, ...args) => { - if (node.type === 'asset') { - return visit(new Asset(node.value), ...args); - } - }); + traverseAssets(visit: GraphVisitor): ?TContext { + return this.#graph.traverseAssets(assetGraphVisitorToInternal(visit)); } } diff --git a/packages/core/core/src/public/utils.js b/packages/core/core/src/public/utils.js index b7ca9a6206c..4829701ca82 100644 --- a/packages/core/core/src/public/utils.js +++ b/packages/core/core/src/public/utils.js @@ -1,9 +1,15 @@ // @flow strict-local -import type {Asset, BundleGroup} from '@parcel/types'; +import type { + Asset as IAsset, + BundleGroup, + GraphTraversalCallback, + GraphVisitor +} from '@parcel/types'; import type InternalAsset from '../Asset'; import type AssetGraph from '../AssetGraph'; +import {Asset} from './Asset'; import invariant from 'assert'; export const getBundleGroupId = (bundleGroup: BundleGroup) => @@ -11,7 +17,7 @@ export const getBundleGroupId = (bundleGroup: BundleGroup) => export function getInternalAsset( assetGraph: AssetGraph, - publicAsset: Asset + publicAsset: IAsset ): InternalAsset { let node = assetGraph.getNode(publicAsset.id); invariant( @@ -19,3 +25,24 @@ export function getInternalAsset( ); return node.value; } + +export function assetGraphVisitorToInternal( + visit: GraphVisitor +): GraphVisitor { + if (typeof visit === 'function') { + return assetGraphTraversalToInternal(visit); + } + + return { + enter: visit.enter ? assetGraphTraversalToInternal(visit.enter) : undefined, + exit: visit.exit ? assetGraphTraversalToInternal(visit.exit) : undefined + }; +} + +function assetGraphTraversalToInternal( + visit: GraphTraversalCallback +): GraphTraversalCallback { + return (asset: InternalAsset, context: ?TContext, actions) => { + return visit(new Asset(asset), context, actions); + }; +} diff --git a/packages/core/core/src/resolveOptions.js b/packages/core/core/src/resolveOptions.js index 739c6dac229..ab80ae4f576 100644 --- a/packages/core/core/src/resolveOptions.js +++ b/packages/core/core/src/resolveOptions.js @@ -49,6 +49,8 @@ export default async function resolveOptions( entries, rootDir, targets, - logLevel: initialOptions.logLevel != null ? initialOptions.logLevel : 'info' + scopeHoist: + initialOptions.scopeHoist ?? initialOptions.mode === 'production', + logLevel: initialOptions.logLevel ?? 'info' }; } diff --git a/packages/core/core/src/worker.js b/packages/core/core/src/worker.js index 4647a89ef98..21da6a651c5 100644 --- a/packages/core/core/src/worker.js +++ b/packages/core/core/src/worker.js @@ -6,6 +6,7 @@ import type { JSONObject } from '@parcel/types'; import type {Bundle} from './types'; +import type BundleGraph from './BundleGraph'; import TransformerRunner from './TransformerRunner'; import PackagerRunner from './PackagerRunner'; @@ -44,10 +45,10 @@ export function runTransform(req: TransformerRequest) { return transformerRunner.transform(req); } -export function runPackage(bundle: Bundle) { +export function runPackage(bundle: Bundle, bundleGraph: BundleGraph) { if (!packagerRunner) { throw new Error('.runPackage() called before .init()'); } - return packagerRunner.writeBundle(bundle); + return packagerRunner.writeBundle(bundle, bundleGraph); } diff --git a/packages/core/integration-tests/package.json b/packages/core/integration-tests/package.json index 6729eb1b104..77966821792 100644 --- a/packages/core/integration-tests/package.json +++ b/packages/core/integration-tests/package.json @@ -12,7 +12,9 @@ "test-ci": "yarn test --reporter mocha-multi-reporters --reporter-options configFile=./test/mochareporters.json" }, "devDependencies": { - "@babel/core": "^7.2.0", + "@babel/core": "^7.0.0-0", + "@babel/plugin-syntax-export-default-from": "^7.2.0", + "@babel/plugin-syntax-export-namespace-from": "^7.2.0", "@jetbrains/kotlinc-js-api": "^1.2.12", "chalk": "^2.1.0", "codecov": "^3.0.0", diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/a.js new file mode 100644 index 00000000000..062d1398324 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/a.js @@ -0,0 +1,4 @@ +var b = require('./b'); + +b.setValue(2); +module.exports = b.value; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/b.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/b.js new file mode 100644 index 00000000000..a0d2f196b4b --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/export-assign-scope/b.js @@ -0,0 +1,3 @@ +exports.setValue = function (value) { + exports.value = value; +} diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/a.js new file mode 100644 index 00000000000..0952f7666a1 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/a.js @@ -0,0 +1 @@ +module.exports = require('./b'); diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/b.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/b.js new file mode 100644 index 00000000000..842e368a0a2 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/interop-require-es-module/b.js @@ -0,0 +1 @@ +export default 2; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/module-object/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/module-object/a.js index 8e7e26c9669..40b62603807 100644 --- a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/module-object/a.js +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/module-object/a.js @@ -1,6 +1,7 @@ output = { id: module.id, hot: module.hot, + moduleRequire: module.require, type: typeof module, exports: exports, exportsType: typeof exports, diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/a.js new file mode 100644 index 00000000000..efa0e7d7260 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/a.js @@ -0,0 +1,2 @@ +import Test from './b'; +output = Test.create(); diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/b.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/b.js new file mode 100644 index 00000000000..2908f869e18 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/b.js @@ -0,0 +1,9 @@ +export default class Test { + constructor() { + this.foo = 'bar'; + } + + static create() { + return new Test(); + } +} diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/package.json b/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/package.json new file mode 100644 index 00000000000..fb6878912e4 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/default-export-class-rename/package.json @@ -0,0 +1,5 @@ +{ + "name": "default-export-class-rename", + "private": true, + "browserslist": ["last 1 Chrome version"] +} diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/pure-assignment/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/pure-assignment/a.js index 1ce7667c325..229761a608f 100644 --- a/packages/core/integration-tests/test/integration/scope-hoisting/es6/pure-assignment/a.js +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/pure-assignment/a.js @@ -1,3 +1,3 @@ import {foo} from './b'; - + output = foo; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/a.js new file mode 100644 index 00000000000..d07f8414fae --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/a.js @@ -0,0 +1,3 @@ +import {foo} from 'bar'; + +output = foo(2); diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/bar.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/bar.js new file mode 100644 index 00000000000..fa5cee4eca0 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/bar.js @@ -0,0 +1,7 @@ +import shared from './shared'; + +sideEffect(); + +export default function bar() { + return shared; +} diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/foo.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/foo.js new file mode 100644 index 00000000000..e713255c13f --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/foo.js @@ -0,0 +1,5 @@ +import shared from './shared'; + +export default function foo(a) { + return a * a + shared; +} diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/index.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/index.js new file mode 100644 index 00000000000..84651fcca44 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/index.js @@ -0,0 +1,3 @@ +export foo from './foo'; +export bar from './bar'; +export shared from './shared'; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/package.json b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/package.json new file mode 100644 index 00000000000..1ea9bea7dfa --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "bar", + "sideEffects": false +} diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/shared.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/shared.js new file mode 100644 index 00000000000..842e368a0a2 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-duplicate/node_modules/bar/shared.js @@ -0,0 +1 @@ +export default 2; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-wildcards/node_modules/bar/b.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-wildcards/node_modules/bar/b.js index e3862e23973..333ff37e4ec 100644 --- a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-wildcards/node_modules/bar/b.js +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-false-wildcards/node_modules/bar/b.js @@ -1 +1,3 @@ +sideEffect('bar'); + export const bar = 'foo' diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js index 737f645807b..f1331d8e2f4 100644 --- a/packages/core/integration-tests/test/scope-hoisting.js +++ b/packages/core/integration-tests/test/scope-hoisting.js @@ -6,16 +6,7 @@ const fs = require('@parcel/fs'); const bundle = (name, opts = {}) => _bundle(name, Object.assign({scopeHoist: true}, opts)); -describe.skip('scope hoisting', function() { - if (process.platform === 'win32') { - // eslint-disable-next-line no-console - console.warn( - 'WARNING: Scope hoisting tests are disabled on windows due to ' + - 'filesystem errors. Feel free to look into this and contribute a fix!' - ); - return; - } - +describe('scope hoisting', function() { describe('es6', function() { it('supports default imports and exports of expressions', async function() { let b = await bundle( @@ -273,6 +264,18 @@ describe.skip('scope hoisting', function() { assert.equal(await output.default, 5); }); + it('supports nested dynamic imports', async function() { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/dynamic-import-dynamic/a.js' + ) + ); + + let output = await run(b); + assert.equal(await output.default, 123); + }); + it('should not export function arguments', async function() { let b = await bundle( path.join( @@ -321,6 +324,18 @@ describe.skip('scope hoisting', function() { assert.deepEqual(output, 'foobar'); }); + it('supports requiring a re-exported and renamed ES6 import', async function() { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/re-export-renamed/a.js' + ) + ); + + let output = await run(b); + assert.deepEqual(output, 'foobar'); + }); + it('keeps side effects by default', async function() { let b = await bundle( path.join( @@ -366,8 +381,14 @@ describe.skip('scope hoisting', function() { '/integration/scope-hoisting/es6/side-effects-false-wildcards/a.js' ) ); - let output = await run(b); + let called = false; + let output = await run(b, { + sideEffect: () => { + called = true; + } + }); + assert(!called, 'side effect called'); assert.deepEqual(output, 'bar'); }); @@ -390,6 +411,25 @@ describe.skip('scope hoisting', function() { assert.deepEqual(output, 4); }); + it('supports the package.json sideEffects: false flag with shared dependencies', async function() { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-false-duplicate/a.js' + ) + ); + + let called = false; + let output = await run(b, { + sideEffect: () => { + called = true; + } + }); + + assert(!called, 'side effect called'); + assert.deepEqual(output, 6); + }); + it('missing exports should be replaced with an empty object', async function() { let b = await bundle( path.join( @@ -426,13 +466,33 @@ describe.skip('scope hoisting', function() { assert.deepEqual(output.default, 2); let contents = await fs.readFile( - path.join(__dirname, '/dist/a.js'), + path.join(__dirname, '/../dist/a.js'), 'utf8' ); assert(contents.includes('foo')); assert(!contents.includes('bar')); }); + it('removes unused function exports when minified', async function() { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/tree-shaking-functions/a.js' + ), + {minify: true} + ); + + let output = await run(b); + assert.deepEqual(output.default, 9); + + let contents = await fs.readFile( + path.join(__dirname, '/../dist/a.js'), + 'utf8' + ); + assert(/.\+./.test(contents)); + assert(!/.-./.test(contents)); + }); + it('support exporting a ES6 module exported as CommonJS', async function() { let b = await bundle( path.join( @@ -491,12 +551,24 @@ describe.skip('scope hoisting', function() { assert.deepEqual(output, 2); let contents = await fs.readFile( - path.join(__dirname, 'dist/a.js'), + path.join(__dirname, '/../dist/a.js'), 'utf8' ); assert(!/bar/.test(contents)); assert(!/displayName/.test(contents)); }); + + it('should correctly rename references to default exported classes', async function() { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/default-export-class-rename/a.js' + ) + ); + + let output = await run(b); + assert.deepEqual(output.foo, 'bar'); + }); }); describe('commonjs', function() { @@ -820,11 +892,21 @@ describe.skip('scope hoisting', function() { ) ); + let entryBundle; + b.traverseBundles((bundle, ctx, traversal) => { + if (bundle.isEntry) { + entryBundle = bundle; + traversal.stop(); + } + }); + let entryAsset = entryBundle.getEntryAssets()[0]; + // TODO: this test doesn't currently work in older browsers since babel // replaces the typeof calls before we can get to them. let output = await run(b); - assert.equal(output.id, b.entryAsset.id); + assert.equal(output.id, entryAsset.id); assert.equal(output.hot, null); + assert.equal(output.moduleRequire, null); assert.equal(output.type, 'object'); assert.deepEqual(output.exports, {}); assert.equal(output.exportsType, 'object'); @@ -839,11 +921,24 @@ describe.skip('scope hoisting', function() { ) ); + let entryBundle; + b.traverseBundles((bundle, ctx, traversal) => { + if (bundle.isEntry) { + entryBundle = bundle; + traversal.stop(); + } + }); + + let asset; + entryBundle.traverseAssets((a, ctx, traversal) => { + if (a.filePath.endsWith('b.js')) { + asset = a; + traversal.stop(); + } + }); + let output = await run(b); - assert.equal( - output, - Array.from(b.assets).find(a => a.name.endsWith('b.js')).id - ); + assert.equal(output, asset.id); }); it('supports requiring a re-exported ES6 import', async function() { @@ -1063,7 +1158,7 @@ describe.skip('scope hoisting', function() { assert.deepEqual(output, 2); let contents = await fs.readFile( - path.join(__dirname, '/dist/a.js'), + path.join(__dirname, '/../dist/a.js'), 'utf8' ); assert(contents.includes('foo')); @@ -1120,5 +1215,30 @@ describe.skip('scope hoisting', function() { let output = await run(b); assert.deepEqual(output, 42); }); + + it('should insert __esModule interop flag when importing from an ES module', async function() { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/commonjs/interop-require-es-module/a.js' + ) + ); + + let output = await run(b); + assert.equal(output.__esModule, true); + assert.equal(output.default, 2); + }); + + it('should support assigning to exports from inside a function', async function() { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/commonjs/export-assign-scope/a.js' + ) + ); + + let output = await run(b); + assert.deepEqual(output, 2); + }); }); }); diff --git a/packages/core/parcel/src/cli.js b/packages/core/parcel/src/cli.js index 8ebca9f14e6..9972cccafbc 100755 --- a/packages/core/parcel/src/cli.js +++ b/packages/core/parcel/src/cli.js @@ -91,6 +91,7 @@ applyOptions(watch, commonOptions); let build = program .command('build [input...]') .description('bundles for production') + .option('--no-minify', 'disable minification') .action(run); applyOptions(build, commonOptions); diff --git a/packages/core/register/src/hook.js b/packages/core/register/src/hook.js index b90ff173541..fdff92fd904 100644 --- a/packages/core/register/src/hook.js +++ b/packages/core/register/src/hook.js @@ -53,7 +53,7 @@ export default function register(opts = DEFAULT_CLI_OPTS) { let output = ''; let asset = result.assets.find(a => a.type === 'js'); if (asset) { - output = (await asset.getOutput()).code; + output = await asset.getCode(); } return output; } diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 4e1edd37e9d..05265aeaa25 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -114,7 +114,8 @@ export type PackageJSON = { }, dependencies?: PackageDependencies, devDependencies?: PackageDependencies, - peerDependencies?: PackageDependencies + peerDependencies?: PackageDependencies, + sideEffects?: boolean | FilePath | Array }; export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'verbose'; @@ -133,6 +134,7 @@ export type InitialParcelOptions = {| killWorkers?: boolean, mode?: 'development' | 'production' | string, minify?: boolean, + scopeHoist?: boolean, sourceMaps?: boolean, hot?: ServerOptions | false, serve?: ServerOptions | false, @@ -140,7 +142,6 @@ export type InitialParcelOptions = {| logLevel?: LogLevel // contentHash - // scopeHoist // throwErrors // global? // detailedReport @@ -178,16 +179,20 @@ export type Meta = { [string]: JSONValue }; +export type Symbol = string; + export type DependencyOptions = {| moduleSpecifier: ModuleSpecifier, isAsync?: boolean, isEntry?: boolean, isOptional?: boolean, isURL?: boolean, + isWeak?: boolean, loc?: SourceLocation, env?: EnvironmentOpts, meta?: Meta, - target?: Target + target?: Target, + symbols?: Map | Array<[Symbol, Symbol]> |}; export interface Dependency { @@ -197,13 +202,17 @@ export interface Dependency { isEntry: ?boolean; isOptional: ?boolean; isURL: ?boolean; + isWeak: ?boolean; loc: ?SourceLocation; env: Environment; - meta: ?Meta; + meta: Meta; target: ?Target; + symbols: Map; // TODO: get this from graph instead of storing them on dependencies sourcePath: FilePath; + + merge(other: Dependency): void; } export type File = { @@ -214,6 +223,7 @@ export type File = { export type TransformerRequest = { filePath: FilePath, env: Environment, + sideEffects?: boolean, code?: string }; @@ -225,11 +235,14 @@ interface BaseAsset { +meta: Meta; +isIsolated: boolean; +type: string; + +symbols: Map; + +sideEffects: boolean; getCode(): Promise; getBuffer(): Promise; getStream(): Readable; getMap(): ?SourceMap; + getConnectedFiles(): $ReadOnlyArray; getDependencies(): $ReadOnlyArray; getConfig( filePaths: Array, @@ -281,14 +294,23 @@ export interface TransformerResult { isIsolated?: boolean; env?: EnvironmentOpts; meta?: Meta; + symbols?: Map; + sideEffects?: boolean; } type Async = T | Promise; export type Transformer = { - getConfig?: (asset: Asset, opts: ParcelOptions) => Async, + getConfig?: ( + asset: MutableAsset, + opts: ParcelOptions + ) => Async, canReuseAST?: (ast: AST, opts: ParcelOptions) => boolean, - parse?: (asset: Asset, config: ?Config, opts: ParcelOptions) => Async, + parse?: ( + asset: MutableAsset, + config: ?Config, + opts: ParcelOptions + ) => Async, transform( asset: MutableAsset, config: ?Config, @@ -311,19 +333,23 @@ export interface TraversalActions { stop(): void; } +export type GraphVisitor = + | GraphTraversalCallback + | {| + enter?: GraphTraversalCallback, + exit?: GraphTraversalCallback + |}; export type GraphTraversalCallback = ( node: TNode, context: ?TContext, - traversal: TraversalActions + actions: TraversalActions ) => ?TContext; // Not a directly exported interface. interface AssetGraphLike { getDependencies(asset: Asset): Array; getDependencyResolution(dependency: Dependency): ?Asset; - traverseAssets( - visit: GraphTraversalCallback - ): ?TContext; + traverseAssets(visit: GraphVisitor): ?TContext; } export type BundleTraversable = @@ -338,10 +364,16 @@ export type MainAssetGraphTraversable = export interface MainAssetGraph extends AssetGraphLike { createBundle(asset: Asset): MutableBundle; traverse( - visit: GraphTraversalCallback + visit: GraphVisitor ): ?TContext; } +export type SymbolResolution = {| + asset: Asset, + exportSymbol: Symbol | string, + symbol: void | Symbol +|}; + export interface Bundle extends AssetGraphLike { +id: string; +type: string; @@ -353,9 +385,15 @@ export interface Bundle extends AssetGraphLike { +stats: Stats; getEntryAssets(): Array; getTotalSize(asset?: Asset): number; + hasChildBundles(): boolean; traverse( - visit: GraphTraversalCallback + visit: GraphVisitor + ): ?TContext; + traverseAncestors( + asset: Asset, + visit: GraphVisitor<*, TContext> ): ?TContext; + resolveSymbol(asset: Asset, symbol: Symbol): SymbolResolution; } export interface MutableBundle extends Bundle { @@ -384,6 +422,7 @@ export interface BundleGraph { traverseBundles( visit: GraphTraversalCallback ): ?TContext; + isAssetReferenced(asset: Asset): boolean; } export interface MutableBundleGraph { @@ -430,7 +469,11 @@ export type Runtime = {| |}; export type Packager = {| - package(bundle: Bundle, opts: ParcelOptions): Async + package( + bundle: Bundle, + bundleGraph: BundleGraph, + opts: ParcelOptions + ): Async |}; export type Optimizer = {| @@ -438,7 +481,10 @@ export type Optimizer = {| |}; export type Resolver = {| - resolve(dependency: Dependency, opts: ParcelOptions): Async + resolve( + dependency: Dependency, + opts: ParcelOptions + ): Async |}; export type ProgressLogEvent = {| diff --git a/packages/examples/simple/src/index.js b/packages/examples/simple/src/index.js index cd40e85c978..8bb82ba86b4 100644 --- a/packages/examples/simple/src/index.js +++ b/packages/examples/simple/src/index.js @@ -1,10 +1,14 @@ import styles from './styles.css'; import parcel from './parcel.webp'; -import('./async'); -import('./async2'); +// import('./async'); +// import('./async2'); -new Worker('./worker.js'); +// new Worker('./worker.js'); + +import {message} from './message'; + +console.log(message); // const message = require('./message'); // const fs = require('fs'); @@ -12,4 +16,4 @@ new Worker('./worker.js'); // console.log(message); // eslint-disable-line no-console // console.log(fs.readFileSync(__dirname + '/test.txt', 'utf8')); -class Test {} +// class Test {} diff --git a/packages/examples/simple/src/message.js b/packages/examples/simple/src/message.js index e62e0030989..648a3705292 100644 --- a/packages/examples/simple/src/message.js +++ b/packages/examples/simple/src/message.js @@ -1 +1 @@ -module.exports = 'hi!'; +export let message = 'hi!'; diff --git a/packages/namers/default/.babelrc b/packages/namers/default/.babelrc new file mode 100644 index 00000000000..be26495ef30 --- /dev/null +++ b/packages/namers/default/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@parcel/babel-preset"] +} diff --git a/packages/packagers/js/.babelrc b/packages/packagers/js/.babelrc new file mode 100644 index 00000000000..be26495ef30 --- /dev/null +++ b/packages/packagers/js/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@parcel/babel-preset"] +} diff --git a/packages/packagers/js/package.json b/packages/packagers/js/package.json index 04913438a76..f6ee54bbea7 100644 --- a/packages/packagers/js/package.json +++ b/packages/packagers/js/package.json @@ -11,6 +11,7 @@ "parcel": "2.x" }, "dependencies": { - "@parcel/plugin": "^2.0.0" + "@parcel/plugin": "^2.0.0", + "@parcel/scope-hoisting": "^2.0.0" } } diff --git a/packages/packagers/js/src/JSPackager.js b/packages/packagers/js/src/JSPackager.js index a21d6158e6b..9421721ad2c 100644 --- a/packages/packagers/js/src/JSPackager.js +++ b/packages/packagers/js/src/JSPackager.js @@ -2,6 +2,7 @@ import {Packager} from '@parcel/plugin'; import fs from 'fs'; +import {concat, link, generate} from '@parcel/scope-hoisting'; const PRELUDE = fs .readFileSync(__dirname + '/prelude.js', 'utf8') @@ -9,7 +10,16 @@ const PRELUDE = fs .replace(/;$/, ''); export default new Packager({ - async package(bundle) { + async package(bundle, bundleGraph, options) { + // If scope hoisting is enabled, we use a different code path. + if (options.scopeHoist) { + let ast = await concat(bundle, bundleGraph); + ast = link(bundle, ast, options); + return generate(bundle, ast, options); + } + + // For development, we just concatenate all of the code together + // rather then enabling scope hoisting, which would be too slow. let promises = []; bundle.traverse(node => { if (node.type === 'asset') { diff --git a/packages/reporters/cli/src/Table.js b/packages/reporters/cli/src/Table.js index c1ba53ff627..6cfb24f83b0 100644 --- a/packages/reporters/cli/src/Table.js +++ b/packages/reporters/cli/src/Table.js @@ -85,6 +85,10 @@ function getText(node: string | {props: CellProps}): string { return node; } + if (!node.props) { + return ''; + } + let t = ''; React.Children.forEach(node.props.children, n => { t += getText(n); diff --git a/packages/resolvers/default/src/DefaultResolver.js b/packages/resolvers/default/src/DefaultResolver.js index 7d540c83d1c..2c21ec8067e 100644 --- a/packages/resolvers/default/src/DefaultResolver.js +++ b/packages/resolvers/default/src/DefaultResolver.js @@ -1,7 +1,13 @@ // @flow import {Resolver} from '@parcel/plugin'; -import type {ParcelOptions, Dependency, PackageJSON} from '@parcel/types'; +import type { + ParcelOptions, + Dependency, + PackageJSON, + FilePath, + TransformerRequest +} from '@parcel/types'; import path from 'path'; import * as fs from '@parcel/fs'; import {isGlob} from '@parcel/utils'; @@ -16,10 +22,42 @@ export default new Resolver({ options }).resolve(dep); - return resolved ? resolved.path : null; + if (!resolved) { + return null; + } + + let result: TransformerRequest = { + filePath: resolved.path, + env: dep.env + }; + + if (resolved.pkg && !hasSideEffects(resolved.path, resolved.pkg)) { + result.sideEffects = false; + } + + return result; } }); +function hasSideEffects(filePath: FilePath, pkg: InternalPackageJSON) { + switch (typeof pkg.sideEffects) { + case 'boolean': + return pkg.sideEffects; + case 'string': + return micromatch.isMatch( + path.relative(pkg.pkgdir, filePath), + pkg.sideEffects, + {matchBase: true} + ); + case 'object': + return pkg.sideEffects.some(sideEffects => + hasSideEffects(filePath, {...pkg, sideEffects}) + ); + } + + return true; +} + type InternalPackageJSON = PackageJSON & { pkgdir: string }; diff --git a/packages/runtimes/js/.babelrc b/packages/runtimes/js/.babelrc new file mode 100644 index 00000000000..be26495ef30 --- /dev/null +++ b/packages/runtimes/js/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@parcel/babel-preset"] +} diff --git a/packages/shared/scope-hoisting/.babelrc b/packages/shared/scope-hoisting/.babelrc new file mode 100644 index 00000000000..be26495ef30 --- /dev/null +++ b/packages/shared/scope-hoisting/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@parcel/babel-preset"] +} diff --git a/packages/shared/scope-hoisting/.eslintrc.json b/packages/shared/scope-hoisting/.eslintrc.json new file mode 100644 index 00000000000..a75bb856758 --- /dev/null +++ b/packages/shared/scope-hoisting/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@parcel/eslint-config", + "parserOptions": { + "sourceType": "module" + } +} diff --git a/packages/shared/scope-hoisting/package.json b/packages/shared/scope-hoisting/package.json new file mode 100644 index 00000000000..0f7126cf3bf --- /dev/null +++ b/packages/shared/scope-hoisting/package.json @@ -0,0 +1,32 @@ +{ + "name": "@parcel/scope-hoisting", + "version": "2.0.0", + "description": "Blazing fast, zero configuration web application bundler", + "main": "src/index.js", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/parcel-bundler/parcel.git" + }, + "engines": { + "node": ">= 8.0.0" + }, + "scripts": { + "pretest": "yarn build", + "test": "true", + "test-ci": "yarn build && yarn test", + "build": "babel src -d lib", + "prepublish": "yarn build" + }, + "dependencies": { + "@babel/generator": "^7.3.3", + "@babel/template": "^7.2.2", + "@babel/traverse": "^7.2.3", + "@babel/parser": "^7.0.0", + "@babel/types": "^7.3.3", + "@parcel/types": "^2.0.0", + "babylon-walk": "^1.0.2", + "micromatch": "^3.1.10", + "nullthrows": "^1.1.1" + } +} diff --git a/packages/shared/scope-hoisting/src/concat.js b/packages/shared/scope-hoisting/src/concat.js new file mode 100644 index 00000000000..eabbd269838 --- /dev/null +++ b/packages/shared/scope-hoisting/src/concat.js @@ -0,0 +1,326 @@ +// @flow + +import invariant from 'assert'; +import type {Bundle, Asset, Symbol, BundleGraph} from '@parcel/types'; +import * as babylon from '@babel/parser'; +import path from 'path'; +import * as t from '@babel/types'; +import * as walk from 'babylon-walk'; +import {getName, getIdentifier} from './utils'; +import fs from 'fs'; +import nullthrows from 'nullthrows'; + +const HELPERS_PATH = path.join(__dirname, 'helpers.js'); +const HELPERS = fs.readFileSync(HELPERS_PATH, 'utf8'); + +const PRELUDE_PATH = path.join(__dirname, 'prelude.js'); +const PRELUDE = fs.readFileSync(PRELUDE_PATH, 'utf8'); + +type AssetASTMap = Map; +type TraversalContext = {| + parent: ?AssetASTMap, + children: AssetASTMap +|}; + +// eslint-disable-next-line no-unused-vars +export async function concat(bundle: Bundle, bundleGraph: BundleGraph) { + let promises = []; + bundle.traverseAssets(asset => { + promises.push(processAsset(bundle, asset)); + }); + let outputs = new Map(await Promise.all(promises)); + let result = [...parse(HELPERS, HELPERS_PATH)]; + + // If this is an entry bundle and it has child bundles, we need to add the prelude code, which allows + // registering modules dynamically at runtime. + let hasChildBundles = bundle.hasChildBundles(); + let needsPrelude = bundle.isEntry && hasChildBundles; + let registerEntry = !bundle.isEntry || hasChildBundles; + if (needsPrelude) { + result.unshift(...parse(PRELUDE, PRELUDE_PATH)); + } + + let usedExports = getUsedExports(bundle); + + bundle.traverseAssets({ + enter(asset, context) { + if (shouldExcludeAsset(asset, usedExports)) { + return context; + } + + return { + parent: context && context.children, + children: new Map() + }; + }, + exit(asset, context) { + invariant(context != null); + + if (shouldExcludeAsset(asset, usedExports)) { + return; + } + + let statements = nullthrows(outputs.get(asset.id)); + let statementIndices: Map = new Map(); + for (let i = 0; i < statements.length; i++) { + let statement = statements[i]; + if (t.isExpressionStatement(statement)) { + for (let depAsset of findRequires(bundle, asset, statement)) { + if (depAsset && !statementIndices.has(depAsset.id)) { + statementIndices.set(depAsset.id, i); + } + } + } + } + + for (let [assetId, ast] of [...context.children].reverse()) { + let index = statementIndices.has(assetId) + ? nullthrows(statementIndices.get(assetId)) + : 0; + statements.splice(index, 0, ...ast); + } + + // If this module is referenced by another bundle, or is an entry module in a child bundle, + // add code to register the module with the module system. + if ( + bundleGraph.isAssetReferenced(asset) || + (!context.parent && registerEntry) + ) { + let exportsId = getName(asset, 'exports'); + statements.push( + ...parse(` + ${asset.meta.isES6Module ? `${exportsId}.__esModule = true;` : ''} + parcelRequire.register("${asset.id}", ${exportsId}); + `) + ); + } + + if (context.parent) { + context.parent.set(asset.id, statements); + } else { + result.push(...statements); + } + } + }); + + let entry = bundle.getEntryAssets()[0]; + if (entry && bundle.isEntry) { + let exportsIdentifier = getName(entry, 'exports'); + let code = await entry.getCode(); + if (code.includes(exportsIdentifier)) { + result.push( + ...parse(` + if (typeof exports === "object" && typeof module !== "undefined") { + // CommonJS + module.exports = ${exportsIdentifier}; + } else if (typeof define === "function" && define.amd) { + // RequireJS + define(function () { + return ${exportsIdentifier}; + }); + } + `) + ); + } + } + + return t.file(t.program(result)); +} + +async function processAsset(bundle: Bundle, asset: Asset) { + let code = await asset.getCode(); + let statements = parse(code, asset.filePath); + + if (statements[0]) { + addComment(statements[0], ` ASSET: ${asset.filePath}`); + } + + if (shouldWrap(bundle, asset)) { + statements = wrapModule(asset, statements); + } + + return [asset.id, statements]; +} + +function parse(code, filename) { + let ast = babylon.parse(code, { + sourceFilename: filename, + allowReturnOutsideFunction: true + }); + + return ast.program.body; +} + +function addComment(statement, comment) { + if (!statement.leadingComments) { + statement.leadingComments = []; + } + statement.leadingComments.push({ + type: 'CommentLine', + value: comment + }); +} + +function getUsedExports(bundle: Bundle): Map> { + let usedExports: Map> = new Map(); + bundle.traverseAssets(asset => { + for (let dep of bundle.getDependencies(asset)) { + let resolvedAsset = bundle.getDependencyResolution(dep); + if (!resolvedAsset) { + continue; + } + + for (let [symbol, identifier] of dep.symbols) { + if (identifier === '*') { + continue; + } + + if (symbol === '*') { + for (let symbol of resolvedAsset.symbols.keys()) { + markUsed(resolvedAsset, symbol); + } + } + + markUsed(resolvedAsset, symbol); + } + } + }); + + function markUsed(asset, symbol) { + let resolved = bundle.resolveSymbol(asset, symbol); + + let used = usedExports.get(resolved.asset.id); + if (!used) { + used = new Set(); + usedExports.set(resolved.asset.id, used); + } + + used.add(resolved.exportSymbol); + } + + return usedExports; +} + +function shouldExcludeAsset( + asset: Asset, + usedExports: Map> +) { + return ( + asset.sideEffects === false && + (!usedExports.has(asset.id) || + nullthrows(usedExports.get(asset.id)).size === 0) + ); +} + +function findRequires(bundle: Bundle, asset: Asset, ast) { + let result = []; + walk.simple(ast, { + CallExpression(node) { + let {arguments: args, callee} = node; + if (!t.isIdentifier(callee)) { + return; + } + + if (callee.name === '$parcel$require') { + let dep = bundle + .getDependencies(asset) + .find(dep => dep.moduleSpecifier === args[1].value); + if (!dep) { + throw new Error(`Could not find dep for "${args[1].value}`); + } + result.push(bundle.getDependencyResolution(dep)); + } + } + }); + + return result; +} + +function shouldWrap(bundle: Bundle, asset: Asset) { + if (asset.meta.shouldWrap != null) { + return asset.meta.shouldWrap; + } + + // We need to wrap if any of the deps are marked by the hoister, e.g. + // when the dep is required inside a function or conditional. + // We also need to wrap if any of the parents are wrapped - transitive requires + // shouldn't be evaluated until their parents are. + let shouldWrap = false; + bundle.traverseAncestors(asset, (node, context, traversal) => { + switch (node.type) { + case 'dependency': + case 'asset': + if (node.value.meta.shouldWrap) { + shouldWrap = true; + traversal.stop(); + } + break; + } + }); + + asset.meta.shouldWrap = shouldWrap; + return shouldWrap; +} + +function wrapModule(asset: Asset, statements) { + let body = []; + let decls = []; + let fns = []; + for (let node of statements) { + // Hoist all declarations out of the function wrapper + // so that they can be referenced by other modules directly. + if (t.isVariableDeclaration(node)) { + for (let decl of node.declarations) { + decls.push(t.variableDeclarator(decl.id)); + if (decl.init) { + body.push( + t.expressionStatement( + t.assignmentExpression('=', t.identifier(decl.id.name), decl.init) + ) + ); + } + } + } else if (t.isFunctionDeclaration(node)) { + // Function declarations can be hoisted out of the module initialization function + fns.push(node); + } else if (t.isClassDeclaration(node)) { + // Class declarations are not hoisted. We declare a variable outside the + // function and convert to a class expression assignment. + decls.push(t.variableDeclarator(t.identifier(node.id.name))); + body.push( + t.expressionStatement( + t.assignmentExpression( + '=', + t.identifier(node.id.name), + t.toExpression(node) + ) + ) + ); + } else { + body.push(node); + } + } + + let executed = getName(asset, 'executed'); + decls.push( + t.variableDeclarator(t.identifier(executed), t.booleanLiteral(false)) + ); + + let init = t.functionDeclaration( + getIdentifier(asset, 'init'), + [], + t.blockStatement([ + t.ifStatement(t.identifier(executed), t.returnStatement()), + t.expressionStatement( + t.assignmentExpression( + '=', + t.identifier(executed), + t.booleanLiteral(true) + ) + ), + ...body + ]) + ); + + return [t.variableDeclaration('var', decls), ...fns, init]; +} diff --git a/packages/shared/scope-hoisting/src/generate.js b/packages/shared/scope-hoisting/src/generate.js new file mode 100644 index 00000000000..e51c5dd2c0e --- /dev/null +++ b/packages/shared/scope-hoisting/src/generate.js @@ -0,0 +1,17 @@ +// @flow + +import type {AST, Bundle, ParcelOptions} from '@parcel/types'; +import babelGenerate from '@babel/generator'; + +export function generate(bundle: Bundle, ast: AST, options: ParcelOptions) { + let {code} = babelGenerate(ast, { + minified: options.minify, + comments: !options.minify + }); + + if (!options.minify) { + code = `\n${code}\n`; + } + + return `(function(){${code}})();`; +} diff --git a/packages/shared/scope-hoisting/src/helpers.js b/packages/shared/scope-hoisting/src/helpers.js new file mode 100644 index 00000000000..0b3230704dd --- /dev/null +++ b/packages/shared/scope-hoisting/src/helpers.js @@ -0,0 +1,37 @@ +// eslint-disable-next-line no-unused-vars +function $parcel$interopDefault(a) { + return a && a.__esModule ? {d: a.default} : {d: a}; +} + +// eslint-disable-next-line no-unused-vars +function $parcel$defineInteropFlag(a) { + Object.defineProperty(a, '__esModule', {value: true}); +} + +// eslint-disable-next-line no-unused-vars +function $parcel$exportWildcard(dest, source) { + Object.keys(source).forEach(function(key) { + if (key === 'default' || key === '__esModule') { + return; + } + + Object.defineProperty(dest, key, { + enumerable: true, + get: function get() { + return source[key]; + } + }); + }); + + return dest; +} + +// eslint-disable-next-line no-unused-vars +function $parcel$missingModule(name) { + var err = new Error("Cannot find module '" + name + "'"); + err.code = 'MODULE_NOT_FOUND'; + throw err; +} + +// eslint-disable-next-line no-unused-vars +var $parcel$global = this; diff --git a/packages/shared/scope-hoisting/src/hoist.js b/packages/shared/scope-hoisting/src/hoist.js new file mode 100644 index 00000000000..59d8ef4e97a --- /dev/null +++ b/packages/shared/scope-hoisting/src/hoist.js @@ -0,0 +1,619 @@ +// @flow + +import type {Asset, MutableAsset} from '@parcel/types'; + +import * as t from '@babel/types'; +import traverse from '@babel/traverse'; +import template from '@babel/template'; +import rename from './renamer'; +import {getName, getIdentifier, getExportIdentifier} from './utils'; + +const WRAPPER_TEMPLATE = template(` + var NAME = (function () { + var exports = this; + var module = {exports: this}; + BODY; + return module.exports; + }).call({}); +`); + +const ESMODULE_TEMPLATE = template(`exports.__esModule = true;`); + +const EXPORT_ASSIGN_TEMPLATE = template('EXPORTS.NAME = LOCAL'); +const EXPORT_ALL_TEMPLATE = template( + '$parcel$exportWildcard(OLD_NAME, $parcel$require(ID, SOURCE))' +); +const REQUIRE_CALL_TEMPLATE = template('$parcel$require(ID, SOURCE)'); +const REQUIRE_RESOLVE_CALL_TEMPLATE = template( + '$parcel$require$resolve(ID, SOURCE)' +); +const TYPEOF = { + module: 'object', + require: 'function' +}; + +export function hoist(asset: MutableAsset) { + if ( + !asset.ast || + asset.ast.type !== 'babel' || + asset.ast.version !== '7.0.0' + ) { + throw new Error('Asset does not have a babel AST'); + } + + asset.ast.isDirty = true; + traverse(asset.ast.program, VISITOR, null, asset); +} + +const VISITOR = { + Program: { + enter(path, asset: Asset) { + traverse.cache.clearScope(); + path.scope.crawl(); + + let shouldWrap = false; + path.traverse({ + CallExpression(path) { + // If we see an `eval` call, wrap the module in a function. + // Otherwise, local variables accessed inside the eval won't work. + let callee = path.node.callee; + if ( + t.isIdentifier(callee) && + callee.name === 'eval' && + !path.scope.hasBinding('eval', true) + ) { + asset.meta.isCommonJS = true; + shouldWrap = true; + path.stop(); + } + }, + + ReturnStatement(path) { + // Wrap in a function if we see a top-level return statement. + if (!path.getFunctionParent()) { + shouldWrap = true; + asset.meta.isCommonJS = true; + path.replaceWith( + t.returnStatement( + t.memberExpression( + t.identifier('module'), + t.identifier('exports') + ) + ) + ); + path.stop(); + } + }, + + ReferencedIdentifier(path) { + // We must wrap if `module` is referenced as a free identifier rather + // than a statically resolvable member expression. + if ( + path.node.name === 'module' && + (!path.parentPath.isMemberExpression() || path.parent.computed) && + !( + path.parentPath.isUnaryExpression() && + path.parent.operator === 'typeof' + ) && + !path.scope.hasBinding('module') && + !path.scope.getData('shouldWrap') + ) { + asset.meta.isCommonJS = true; + shouldWrap = true; + path.stop(); + } + } + }); + + path.scope.setData('shouldWrap', shouldWrap); + }, + + exit(path, asset: Asset) { + let scope = path.scope; + + if (scope.getData('shouldWrap')) { + if (asset.meta.isES6Module) { + path.unshiftContainer('body', [ESMODULE_TEMPLATE()]); + } + + path.replaceWith( + t.program([ + WRAPPER_TEMPLATE({ + NAME: getIdentifier(asset, 'exports'), + BODY: path.node.body + }) + ]) + ); + + asset.symbols.clear(); + asset.meta.isCommonJS = true; + asset.meta.isES6Module = false; + } else { + // Re-crawl scope so we are sure to have all bindings. + scope.crawl(); + + // Rename each binding in the top-level scope to something unique. + for (let name in scope.bindings) { + if (!name.startsWith('$' + t.toIdentifier(asset.id))) { + let newName = getName(asset, 'var', name); + rename(scope, name, newName); + } + } + + let exportsIdentifier = getIdentifier(asset, 'exports'); + + // Add variable that represents module.exports if it is referenced and not declared. + if ( + scope.hasGlobal(exportsIdentifier.name) && + !scope.hasBinding(exportsIdentifier.name) + ) { + scope.push({id: exportsIdentifier, init: t.objectExpression([])}); + } + } + + path.stop(); + } + }, + + DirectiveLiteral(path) { + // Remove 'use strict' directives, since modules are concatenated - one strict mode + // module should not apply to all other modules in the same scope. + if (path.node.value === 'use strict') { + path.parentPath.remove(); + } + }, + + MemberExpression(path, asset: Asset) { + if (path.scope.hasBinding('module') || path.scope.getData('shouldWrap')) { + return; + } + + if (t.matchesPattern(path.node, 'module.exports')) { + path.replaceWith(getExportsIdentifier(asset, path.scope)); + asset.meta.isCommonJS = true; + } + + if (t.matchesPattern(path.node, 'module.id')) { + path.replaceWith(t.stringLiteral(asset.id)); + } + + if (t.matchesPattern(path.node, 'module.hot')) { + path.replaceWith(t.identifier('null')); + } + + if (t.matchesPattern(path.node, 'module.require') && !asset.env.isNode()) { + path.replaceWith(t.identifier('null')); + } + + if (t.matchesPattern(path.node, 'module.bundle')) { + path.replaceWith(t.identifier('parcelRequire')); + } + }, + + ReferencedIdentifier(path, asset: Asset) { + if ( + path.node.name === 'exports' && + !path.scope.hasBinding('exports') && + !path.scope.getData('shouldWrap') + ) { + path.replaceWith(getExportsIdentifier(asset, path.scope)); + asset.meta.isCommonJS = true; + } + + if (path.node.name === 'global' && !path.scope.hasBinding('global')) { + path.replaceWith(t.identifier('$parcel$global')); + if (asset.meta.globals) { + asset.meta.globals.delete('global'); + } + } + + let globals = asset.meta.globals; + if (!globals) { + return; + } + + let globalCode = globals.get(path.node.name); + if (globalCode) { + path.scope + .getProgramParent() + .path.unshiftContainer('body', [template(globalCode.code)()]); + + globals.delete(path.node.name); + } + }, + + ThisExpression(path, asset: Asset) { + if (!path.scope.parent && !path.scope.getData('shouldWrap')) { + path.replaceWith(getExportsIdentifier(asset, path.scope)); + asset.meta.isCommonJS = true; + } + }, + + AssignmentExpression(path, asset: Asset) { + if (path.scope.hasBinding('exports') || path.scope.getData('shouldWrap')) { + return; + } + + let {left, right} = path.node; + if (t.isIdentifier(left) && left.name === 'exports') { + path.get('left').replaceWith(getExportsIdentifier(asset, path.scope)); + asset.meta.isCommonJS = true; + } + + // If we can statically evaluate the name of a CommonJS export, create an ES6-style export for it. + // This allows us to remove the CommonJS export object completely in many cases. + if ( + t.isMemberExpression(left) && + t.isIdentifier(left.object, {name: 'exports'}) && + ((t.isIdentifier(left.property) && !left.computed) || + t.isStringLiteral(left.property)) + ) { + let name = t.isIdentifier(left.property) + ? left.property.name + : left.property.value; + let identifier = getExportIdentifier(asset, name); + + // Replace the CommonJS assignment with a reference to the ES6 identifier. + path + .get('left.object') + .replaceWith(getExportsIdentifier(asset, path.scope)); + path.get('right').replaceWith(identifier); + + // If this is the first assignment, create a binding for the ES6-style export identifier. + // Otherwise, assign to the existing export binding. + let scope = path.scope.getProgramParent(); + if (!scope.hasBinding(identifier.name)) { + asset.symbols.set(name, identifier.name); + + // If in the program scope, create a variable declaration and initialize with the exported value. + // Otherwise, declare the variable in the program scope, and assign to it here. + if (path.scope === scope) { + let [decl] = path.insertBefore( + t.variableDeclaration('var', [ + t.variableDeclarator(t.clone(identifier), right) + ]) + ); + + scope.registerDeclaration(decl); + } else { + scope.push({id: t.clone(identifier)}); + path.insertBefore( + t.assignmentExpression('=', t.clone(identifier), right) + ); + } + } else { + path.insertBefore( + t.assignmentExpression('=', t.clone(identifier), right) + ); + } + + asset.meta.isCommonJS = true; + } + }, + + UnaryExpression(path) { + // Replace `typeof module` with "object" + if ( + path.node.operator === 'typeof' && + t.isIdentifier(path.node.argument) && + TYPEOF[path.node.argument.name] && + !path.scope.hasBinding(path.node.argument.name) && + !path.scope.getData('shouldWrap') + ) { + path.replaceWith(t.stringLiteral(TYPEOF[path.node.argument.name])); + } + }, + + CallExpression(path, asset: Asset) { + let {callee, arguments: args} = path.node; + let isRequire = t.isIdentifier(callee, {name: 'require'}); + let ignore = + args.length !== 1 || + !t.isStringLiteral(args[0]) || + path.scope.hasBinding('require'); + + if (ignore) { + if (isRequire) { + callee.name = 'parcelRequire'; + } + return; + } + + if (isRequire) { + let source = args[0].value; + // Ignore require calls that were ignored earlier. + let dep = asset + .getDependencies() + .find(dep => dep.moduleSpecifier === source); + if (!dep) { + return; + } + + // If this require call does not occur in the top-level, e.g. in a function + // or inside an if statement, or if it might potentially happen conditionally, + // the module must be wrapped in a function so that the module execution order is correct. + let parent = path.getStatementParent().parentPath; + let bail = path.findParent( + p => + p.isConditionalExpression() || + p.isLogicalExpression() || + p.isSequenceExpression() + ); + if (!parent.isProgram() || bail) { + dep.meta.shouldWrap = true; + } + + dep.symbols.set('*', getName(asset, 'require', source)); + + // Generate a variable name based on the current asset id and the module name to require. + // This will be replaced by the final variable name of the resolved asset in the packager. + path.replaceWith( + REQUIRE_CALL_TEMPLATE({ + ID: t.stringLiteral(asset.id), + SOURCE: t.stringLiteral(args[0].value) + }) + ); + } + + if (t.matchesPattern(callee, 'require.resolve')) { + path.replaceWith( + REQUIRE_RESOLVE_CALL_TEMPLATE({ + ID: t.stringLiteral(asset.id), + SOURCE: args[0] + }) + ); + } + }, + + ImportDeclaration(path, asset: Asset) { + let dep = asset + .getDependencies() + .find(dep => dep.moduleSpecifier === path.node.source.value); + if (!dep) { + path.remove(); + return; + } + + // For each specifier, rename the local variables to point to the imported name. + // This will be replaced by the final variable name of the resolved asset in the packager. + for (let specifier of path.node.specifiers) { + let id = getIdentifier(asset, 'import', specifier.local.name); + rename(path.scope, specifier.local.name, id.name); + + if (t.isImportDefaultSpecifier(specifier)) { + dep.symbols.set('default', id.name); + } else if (t.isImportSpecifier(specifier)) { + dep.symbols.set(specifier.imported.name, id.name); + } else if (t.isImportNamespaceSpecifier(specifier)) { + dep.symbols.set('*', id.name); + } + } + + addImport(asset, path); + path.remove(); + }, + + ExportDefaultDeclaration(path, asset: Asset) { + let {declaration} = path.node; + let identifier = getExportIdentifier(asset, 'default'); + let name = declaration.id ? declaration.id.name : declaration.name; + + if (hasImport(asset, name) || hasExport(asset, name)) { + identifier = t.identifier(name); + } + + // Add assignment to exports object for namespace imports and commonjs. + path.insertAfter( + EXPORT_ASSIGN_TEMPLATE({ + EXPORTS: getExportsIdentifier(asset, path.scope), + NAME: t.identifier('default'), + LOCAL: t.clone(identifier) + }) + ); + + if (t.isIdentifier(declaration)) { + // Rename the variable being exported. + safeRename(path, asset, declaration.name, identifier.name); + path.remove(); + } else if (t.isExpression(declaration) || !declaration.id) { + // Declare a variable to hold the exported value. + path.replaceWith( + t.variableDeclaration('var', [ + t.variableDeclarator(identifier, t.toExpression(declaration)) + ]) + ); + + path.scope.registerDeclaration(path); + } else { + // Rename the declaration to the exported name. + safeRename(path, asset, declaration.id.name, identifier.name); + path.replaceWith(declaration); + } + + if (!asset.symbols.has('default')) { + asset.symbols.set('default', identifier.name); + } + + // Mark the asset as an ES6 module, so we handle imports correctly in the packager. + asset.meta.isES6Module = true; + }, + + ExportNamedDeclaration(path, asset: Asset) { + let {declaration, source, specifiers} = path.node; + + if (source) { + for (let specifier of specifiers) { + let exported = specifier.exported; + let imported; + + if (t.isExportDefaultSpecifier(specifier)) { + imported = 'default'; + } else if (t.isExportNamespaceSpecifier(specifier)) { + imported = '*'; + } else if (t.isExportSpecifier(specifier)) { + imported = specifier.local.name; + } + + let id = getIdentifier(asset, 'import', exported.name); + asset.symbols.set(exported.name, id.name); + + let dep = asset + .getDependencies() + .find(dep => dep.moduleSpecifier === source.value); + if (dep && imported) { + dep.symbols.set(imported, id.name); + dep.isWeak = true; + } + + path.insertAfter( + EXPORT_ASSIGN_TEMPLATE({ + EXPORTS: getExportsIdentifier(asset, path.scope), + NAME: exported, + LOCAL: id + }) + ); + } + + addImport(asset, path); + path.remove(); + } else if (declaration) { + path.replaceWith(declaration); + + if (t.isIdentifier(declaration.id)) { + addExport(asset, path, declaration.id, declaration.id); + } else { + let identifiers = t.getBindingIdentifiers(declaration); + for (let id of Object.keys(identifiers)) { + addExport(asset, path, identifiers[id], identifiers[id]); + } + } + } else if (specifiers.length > 0) { + for (let specifier of specifiers) { + addExport(asset, path, specifier.local, specifier.exported); + } + + path.remove(); + } + + // Mark the asset as an ES6 module, so we handle imports correctly in the packager. + asset.meta.isES6Module = true; + }, + + ExportAllDeclaration(path, asset: Asset) { + let dep = asset + .getDependencies() + .find(dep => dep.moduleSpecifier === path.node.source.value); + if (dep) { + dep.symbols.set('*', '*'); + } + + asset.meta.isES6Module = true; + + path.replaceWith( + EXPORT_ALL_TEMPLATE({ + OLD_NAME: getExportsIdentifier(asset, path.scope), + SOURCE: t.stringLiteral(path.node.source.value), + ID: t.stringLiteral(asset.id) + }) + ); + } +}; + +function addImport(asset: Asset, path) { + // Replace with a $parcel$require call so we know where to insert side effects. + let requireCall = REQUIRE_CALL_TEMPLATE({ + ID: t.stringLiteral(asset.id), + SOURCE: t.stringLiteral(path.node.source.value) + }); + + // Hoist the call to the top of the file. + let lastImport = path.scope.getData('hoistedImport'); + if (lastImport) { + [lastImport] = lastImport.insertAfter(requireCall); + } else { + [lastImport] = path.parentPath.unshiftContainer('body', [requireCall]); + } + + path.scope.setData('hoistedImport', lastImport); +} + +function addExport(asset: Asset, path, local, exported) { + let scope = path.scope.getProgramParent(); + let identifier = getExportIdentifier(asset, exported.name); + + if (hasImport(asset, local.name)) { + identifier = t.identifier(local.name); + } + + if (hasExport(asset, local.name)) { + identifier = t.identifier(local.name); + } + + let assignNode = EXPORT_ASSIGN_TEMPLATE({ + EXPORTS: getExportsIdentifier(asset, scope), + NAME: t.identifier(exported.name), + LOCAL: identifier + }); + + let binding = scope.getBinding(local.name); + let constantViolations = binding + ? binding.constantViolations.concat(path) + : [path]; + + if (!asset.symbols.has(exported.name)) { + asset.symbols.set(exported.name, identifier.name); + } + + rename(scope, local.name, identifier.name); + + constantViolations.forEach(path => path.insertAfter(t.cloneDeep(assignNode))); +} + +function hasImport(asset: Asset, id) { + for (let dep of asset.getDependencies()) { + if (new Set(dep.symbols.values()).has(id)) { + return true; + } + } + + return false; +} + +function hasExport(asset: Asset, id) { + return new Set(asset.symbols.values()).has(id); +} + +function safeRename(path, asset: Asset, from, to) { + if (from === to) { + return; + } + + // If the binding that we're renaming is constant, it's safe to rename it. + // Otherwise, create a new binding that references the original. + let binding = path.scope.getBinding(from); + if (binding && binding.constant) { + rename(path.scope, from, to); + } else { + let [decl] = path.insertAfter( + t.variableDeclaration('var', [ + t.variableDeclarator(t.identifier(to), t.identifier(from)) + ]) + ); + + path.scope.getBinding(from).reference(decl.get('declarations.0.init')); + path.scope.registerDeclaration(decl); + } +} + +function getExportsIdentifier(asset: Asset, scope) { + if (scope.getProgramParent().getData('shouldWrap')) { + return t.identifier('exports'); + } else { + let id = getIdentifier(asset, 'exports'); + if (!scope.hasBinding(id.name)) { + scope.getProgramParent().addGlobal(id); + } + + return id; + } +} diff --git a/packages/shared/scope-hoisting/src/index.js b/packages/shared/scope-hoisting/src/index.js new file mode 100644 index 00000000000..b54d618aaa3 --- /dev/null +++ b/packages/shared/scope-hoisting/src/index.js @@ -0,0 +1,6 @@ +// @flow +export {hoist} from './hoist'; +export {concat} from './concat'; +export {link} from './link'; +export {shake} from './shake'; +export {generate} from './generate'; diff --git a/packages/shared/scope-hoisting/src/link.js b/packages/shared/scope-hoisting/src/link.js new file mode 100644 index 00000000000..77837f365cd --- /dev/null +++ b/packages/shared/scope-hoisting/src/link.js @@ -0,0 +1,378 @@ +// @flow + +import type {Asset, AST, Bundle, ParcelOptions, Symbol} from '@parcel/types'; + +import nullthrows from 'nullthrows'; +import {relative} from 'path'; +import template from '@babel/template'; +import * as t from '@babel/types'; +import traverse from '@babel/traverse'; +import treeShake from './shake'; +import mangleScope from './mangler'; +import {getName, getIdentifier} from './utils'; + +const ESMODULE_TEMPLATE = template(`$parcel$defineInteropFlag(EXPORTS);`); +const DEFAULT_INTEROP_TEMPLATE = template( + 'var NAME = $parcel$interopDefault(MODULE)' +); +const THROW_TEMPLATE = template('$parcel$missingModule(MODULE)'); +const REQUIRE_TEMPLATE = template('parcelRequire(ID)'); + +export function link(bundle: Bundle, ast: AST, options: ParcelOptions) { + let replacements: Map = new Map(); + let imports: Map = new Map(); + let assets: Map = new Map(); + let exportsMap: Map = new Map(); + + // Build a mapping of all imported identifiers to replace. + bundle.traverseAssets(asset => { + assets.set(asset.id, asset); + exportsMap.set(getName(asset, 'exports'), asset); + for (let dep of bundle.getDependencies(asset)) { + let resolved = bundle.getDependencyResolution(dep); + if (resolved) { + for (let [imported, local] of dep.symbols) { + imports.set(local, [resolved, imported]); + } + } + } + }); + + function resolveSymbol(inputAsset, inputSymbol) { + let {asset, exportSymbol, symbol} = bundle.resolveSymbol( + inputAsset, + inputSymbol + ); + let identifier = symbol; + + // If this is a wildcard import, resolve to the exports object. + if (asset && identifier === '*') { + identifier = getName(asset, 'exports'); + } + + if (replacements && identifier && replacements.has(identifier)) { + identifier = replacements.get(identifier); + } + + return {asset: asset, symbol: exportSymbol, identifier}; + } + + function replaceExportNode(module, originalName, path) { + let {asset: mod, symbol, identifier} = resolveSymbol(module, originalName); + let node; + + if (identifier) { + node = findSymbol(path, identifier); + } + + // If the module is not in this bundle, create a `require` call for it. + if (!node && !assets.has(mod.id)) { + node = REQUIRE_TEMPLATE({ID: t.stringLiteral(module.id)}).expression; + return interop(module, symbol, path, node); + } + + // If this is an ES6 module, throw an error if we cannot resolve the module + if (!node && !mod.meta.isCommonJS && mod.meta.isES6Module) { + let relativePath = relative(options.rootDir, mod.filePath); + throw new Error(`${relativePath} does not export '${symbol}'`); + } + + // If it is CommonJS, look for an exports object. + if (!node && mod.meta.isCommonJS) { + node = findSymbol(path, getName(mod, 'exports')); + if (!node) { + return null; + } + + return interop(mod, symbol, path, node); + } + + return node; + } + + function findSymbol(path, symbol) { + if (symbol && replacements.has(symbol)) { + symbol = replacements.get(symbol); + } + + // if the symbol is in the scope there is no need to remap it + if (path.scope.getProgramParent().hasBinding(symbol)) { + return t.identifier(symbol); + } + + return null; + } + + function interop(mod, originalName, path, node) { + // Handle interop for default imports of CommonJS modules. + if (mod.meta.isCommonJS && originalName === 'default') { + let name = getName(mod, '$interop$default'); + if (!path.scope.getBinding(name)) { + let [decl] = path.getStatementParent().insertBefore( + DEFAULT_INTEROP_TEMPLATE({ + NAME: t.identifier(name), + MODULE: node + }) + ); + + let binding = path.scope.getBinding(getName(mod, 'exports')); + if (binding) { + binding.reference(decl.get('declarations.0.init')); + } + + path.scope.registerDeclaration(decl); + } + + return t.memberExpression(t.identifier(name), t.identifier('d')); + } + + // if there is a CommonJS export return $id$exports.name + if (originalName !== '*') { + return t.memberExpression(node, t.identifier(originalName)); + } + + return node; + } + + function isUnusedValue(path) { + return ( + path.parentPath.isExpressionStatement() || + (path.parentPath.isSequenceExpression() && + (path.key !== path.container.length - 1 || + isUnusedValue(path.parentPath))) + ); + } + + traverse(ast, { + CallExpression(path) { + let {arguments: args, callee} = path.node; + if (!t.isIdentifier(callee)) { + return; + } + + // each require('module') call gets replaced with $parcel$require(id, 'module') + if (callee.name === '$parcel$require') { + let [id, source] = args; + if ( + args.length !== 2 || + !t.isStringLiteral(id) || + !t.isStringLiteral(source) + ) { + throw new Error( + 'invariant: invalid signature, expected : $parcel$require(number, string)' + ); + } + + let asset = nullthrows(assets.get(id.value)); + let dep = nullthrows( + bundle + .getDependencies(asset) + .find(dep => dep.moduleSpecifier === source.value) + ); + let mod = bundle.getDependencyResolution(dep); + + if (!mod) { + if (dep.isOptional) { + path.replaceWith( + THROW_TEMPLATE({MODULE: t.stringLiteral(source.value)}) + ); + } else if (dep.isWeak) { + path.remove(); + } else { + throw new Error( + `Cannot find module "${source.value}" in asset ${id.value}` + ); + } + } else { + let node; + if (assets.get(mod.id)) { + // Replace with nothing if the require call's result is not used. + if (!isUnusedValue(path)) { + let name = getName(mod, 'exports'); + node = t.identifier(replacements.get(name) || name); + + // Insert __esModule interop flag if the required module is an ES6 module with a default export. + // This ensures that code generated by Babel and other tools works properly. + if ( + asset.meta.isCommonJS && + mod.meta.isES6Module && + mod.symbols.has('default') + ) { + let binding = path.scope.getBinding(name); + if (binding && !binding.path.getData('hasESModuleFlag')) { + if (binding.path.node.init) { + binding.path + .getStatementParent() + .insertAfter(ESMODULE_TEMPLATE({EXPORTS: name})); + } + + for (let path of binding.constantViolations) { + path.insertAfter(ESMODULE_TEMPLATE({EXPORTS: name})); + } + + binding.path.setData('hasESModuleFlag', true); + } + } + } + + // We need to wrap the module in a function when a require + // call happens inside a non top-level scope, e.g. in a + // function, if statement, or conditional expression. + if (mod.meta.shouldWrap) { + let call = t.callExpression(getIdentifier(mod, 'init'), []); + node = node ? t.sequenceExpression([call, node]) : call; + } + } else { + node = REQUIRE_TEMPLATE({ID: t.stringLiteral(mod.id)}).expression; + } + + if (node) { + path.replaceWith(node); + } else { + path.remove(); + } + } + } else if (callee.name === '$parcel$require$resolve') { + let [id, source] = args; + if ( + args.length !== 2 || + !t.isStringLiteral(id) || + !t.isStringLiteral(source) + ) { + throw new Error( + 'invariant: invalid signature, expected : $parcel$require$resolve(number, string)' + ); + } + + let mapped = nullthrows(assets.get(id.value)); + let dep = nullthrows( + bundle + .getDependencies(mapped) + .find(dep => dep.moduleSpecifier === source.value) + ); + let mod = nullthrows(bundle.getDependencyResolution(dep)); + path.replaceWith(t.valueToNode(mod.id)); + } + }, + VariableDeclarator: { + exit(path) { + // Replace references to declarations like `var x = require('x')` + // with the final export identifier instead. + // This allows us to potentially replace accesses to e.g. `x.foo` with + // a variable like `$id$export$foo` later, avoiding the exports object altogether. + let {id, init} = path.node; + if (!t.isIdentifier(init)) { + return; + } + + let module = exportsMap.get(init.name); + if (!module) { + return; + } + + // Replace patterns like `var {x} = require('y')` with e.g. `$id$export$x`. + if (t.isObjectPattern(id)) { + for (let p of path.get('id.properties')) { + let {computed, key, value} = p.node; + if (computed || !t.isIdentifier(key) || !t.isIdentifier(value)) { + continue; + } + + let {identifier} = resolveSymbol(module, key.name); + if (identifier) { + replace(value.name, identifier, p); + } + } + + if (id.properties.length === 0) { + path.remove(); + } + } else if (t.isIdentifier(id)) { + replace(id.name, init.name, path); + } + + function replace(id, init, path) { + let binding = path.scope.getBinding(id); + if (!binding.constant) { + return; + } + + for (let ref of binding.referencePaths) { + ref.replaceWith(t.identifier(init)); + } + + replacements.set(id, init); + path.remove(); + } + } + }, + MemberExpression: { + exit(path) { + if (!path.isReferenced()) { + return; + } + + let {object, property, computed} = path.node; + if ( + !( + t.isIdentifier(object) && + ((t.isIdentifier(property) && !computed) || + t.isStringLiteral(property)) + ) + ) { + return; + } + + let module = exportsMap.get(object.name); + if (!module) { + return; + } + + // If it's a $id$exports.name expression. + let name = t.isIdentifier(property) ? property.name : property.value; + let {identifier} = resolveSymbol(module, name); + + // Check if $id$export$name exists and if so, replace the node by it. + if (identifier) { + path.replaceWith(t.identifier(identifier)); + } + } + }, + ReferencedIdentifier(path) { + let {name} = path.node; + if (typeof name !== 'string') { + return; + } + + if (imports.has(name)) { + let [asset, symbol] = nullthrows(imports.get(name)); + let node = replaceExportNode(asset, symbol, path); + + // If the export does not exist, replace with an empty object. + if (!node) { + node = t.objectExpression([]); + } + + path.replaceWith(node); + return; + } + + // If it's an undefined $id$exports identifier. + if (exportsMap.has(name) && !path.scope.hasBinding(name)) { + path.replaceWith(t.objectExpression([])); + } + }, + Program: { + // A small optimization to remove unused CommonJS exports as sometimes Uglify doesn't remove them. + exit(path) { + treeShake(path.scope); + + if (options.minify) { + mangleScope(path.scope); + } + } + } + }); + + return ast; +} diff --git a/packages/shared/scope-hoisting/src/mangler.js b/packages/shared/scope-hoisting/src/mangler.js new file mode 100644 index 00000000000..a9e28f94b93 --- /dev/null +++ b/packages/shared/scope-hoisting/src/mangler.js @@ -0,0 +1,70 @@ +import rename from './renamer'; +import * as t from '@babel/types'; + +const CHARSET = ( + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ$_' +).split(''); + +/** + * This is a very specialized mangler designer to mangle only names in the top-level scope. + * Mangling of names in other scopes happens at a file level inside workers, but we can't + * mangle the top-level scope until scope hoisting is complete in the packager. + * + * Based on code from babel-minify! + * https://github.com/babel/minify/blob/master/packages/babel-plugin-minify-mangle-names/src/charset.js + */ +export default function mangleScope(scope) { + let newNames = new Set(); + + // Sort bindings so that more frequently referenced bindings get shorter names. + let sortedBindings = Object.keys(scope.bindings).sort( + (a, b) => + scope.bindings[b].referencePaths.length - + scope.bindings[a].referencePaths.length + ); + + for (let oldName of sortedBindings) { + let i = 0; + let newName = ''; + + do { + newName = getIdentifier(i++); + } while ( + newNames.has(newName) || + !canRename(scope, scope.bindings[oldName], newName) + ); + + rename(scope, oldName, newName); + newNames.add(newName); + } +} + +function getIdentifier(num) { + let ret = ''; + num++; + + do { + num--; + ret += CHARSET[num % CHARSET.length]; + num = Math.floor(num / CHARSET.length); + } while (num > 0); + + return ret; +} + +function canRename(scope, binding, newName) { + if (!t.isValidIdentifier(newName)) { + return false; + } + + // If there are any references where the parent scope has a binding + // for the new name, we cannot rename to this name. + for (let i = 0; i < binding.referencePaths.length; i++) { + const ref = binding.referencePaths[i]; + if (ref.scope.hasBinding(newName) || ref.scope.hasReference(newName)) { + return false; + } + } + + return true; +} diff --git a/packages/shared/scope-hoisting/src/prelude.js b/packages/shared/scope-hoisting/src/prelude.js new file mode 100644 index 00000000000..b50bdf4dc66 --- /dev/null +++ b/packages/shared/scope-hoisting/src/prelude.js @@ -0,0 +1,20 @@ +var $parcel$modules = {}; + +parcelRequire = function(name) { + if (name in $parcel$modules) { + return $parcel$modules[name]; + } + + // Try the node require function if it exists. + if (typeof require === 'function') { + return require(name); + } + + var err = new Error("Cannot find module '" + name + "'"); + err.code = 'MODULE_NOT_FOUND'; + throw err; +}; + +parcelRequire.register = function register(id, exports) { + $parcel$modules[id] = exports; +}; diff --git a/packages/shared/scope-hoisting/src/renamer.js b/packages/shared/scope-hoisting/src/renamer.js new file mode 100644 index 00000000000..84657d82892 --- /dev/null +++ b/packages/shared/scope-hoisting/src/renamer.js @@ -0,0 +1,36 @@ +import * as t from '@babel/types'; + +export default function rename(scope, oldName, newName) { + if (oldName === newName) { + return; + } + + let binding = scope.getBinding(oldName); + + // Rename all constant violations + for (let violation of binding.constantViolations) { + let bindingIds = violation.getBindingIdentifierPaths(true, false); + for (let name in bindingIds) { + if (name === oldName) { + for (let idPath of bindingIds[name]) { + idPath.node.name = newName; + } + } + } + } + + // Rename all references + for (let path of binding.referencePaths) { + if (t.isExportSpecifier(path.parent) && path.parentPath.parent.source) { + continue; + } + if (path.node.name === oldName) { + path.node.name = newName; + } + } + + // Rename binding identifier, and update scope. + scope.removeOwnBinding(oldName); + scope.bindings[newName] = binding; + binding.identifier.name = newName; +} diff --git a/packages/shared/scope-hoisting/src/shake.js b/packages/shared/scope-hoisting/src/shake.js new file mode 100644 index 00000000000..6ab8ea70634 --- /dev/null +++ b/packages/shared/scope-hoisting/src/shake.js @@ -0,0 +1,125 @@ +import * as t from '@babel/types'; + +/** + * This is a small small implementation of dead code removal specialized to handle + * removing unused exports. All other dead code removal happens in workers on each + * individual file by babel-minify. + */ +export default function treeShake(scope) { + // Keep passing over all bindings in the scope until we don't remove any. + // This handles cases where we remove one binding which had a reference to + // another one. That one will get removed in the next pass if it is now unreferenced. + let removed; + do { + removed = false; + + // Recrawl to get all bindings. + scope.crawl(); + Object.keys(scope.bindings).forEach(name => { + let binding = getUnusedBinding(scope.path, name); + + // If it is not safe to remove the binding don't touch it. + if (!binding) { + return; + } + + // Remove the binding and all references to it. + binding.path.remove(); + binding.referencePaths.concat(binding.constantViolations).forEach(remove); + + scope.removeBinding(name); + removed = true; + }); + } while (removed); +} + +// Check if a binding is safe to remove and returns it if it is. +function getUnusedBinding(path, name) { + let binding = path.scope.getBinding(name); + if (!binding) { + return null; + } + + let pure = isPure(binding); + if (!binding.referenced && pure) { + return binding; + } + + // Is there any references which aren't simple assignments? + let bailout = binding.referencePaths.some( + path => !isExportAssignment(path) && !isUnusedWildcard(path) + ); + + if (!bailout && pure) { + return binding; + } + + return null; +} + +function isPure(binding) { + if ( + binding.path.isVariableDeclarator() && + binding.path.get('id').isIdentifier() + ) { + let init = binding.path.get('init'); + return init.isPure() || init.isIdentifier() || init.isThisExpression(); + } + + return binding.path.isPure(); +} + +function isExportAssignment(path) { + return ( + // match "path.any = any;" + path.parentPath.isMemberExpression() && + path.parentPath.parentPath.isAssignmentExpression() && + path.parentPath.parentPath.node.left === path.parentPath.node + ); +} + +function isUnusedWildcard(path) { + let {parent} = path; + + return ( + // match `$parcel$exportWildcard` calls + t.isCallExpression(parent) && + t.isIdentifier(parent.callee, {name: '$parcel$exportWildcard'}) && + parent.arguments[0] === path.node && + // check if the $id$exports variable is used + !getUnusedBinding(path, parent.arguments[1].name) + ); +} + +function remove(path) { + if (path.isAssignmentExpression()) { + if (path.parentPath.isSequenceExpression()) { + if (path.parent.expressions.length == 1) { + // replace sequence expression with it's sole child + path.parentPath.replaceWith(path); + remove(path.parentPath); + } else { + path.remove(); + } + } else if (!path.parentPath.isExpressionStatement()) { + path.replaceWith(path.node.right); + } else { + path.remove(); + } + } else if (isExportAssignment(path)) { + remove(path.parentPath.parentPath); + } else if (isUnusedWildcard(path)) { + remove(path.parentPath); + } else if (!path.removed) { + if ( + path.parentPath.isSequenceExpression() && + path.parent.expressions.length === 1 + ) { + // replace sequence expression with it's sole child + path.parentPath.replaceWith(path); + remove(path.parentPath); + } else { + path.remove(); + } + } +} diff --git a/packages/shared/scope-hoisting/src/utils.js b/packages/shared/scope-hoisting/src/utils.js new file mode 100644 index 00000000000..cd8ece2c59b --- /dev/null +++ b/packages/shared/scope-hoisting/src/utils.js @@ -0,0 +1,24 @@ +import * as t from '@babel/types'; + +export function getName(asset, type, ...rest) { + return ( + '$' + + t.toIdentifier(asset.id) + + '$' + + type + + (rest.length + ? '$' + + rest + .map(name => (name === 'default' ? name : t.toIdentifier(name))) + .join('$') + : '') + ); +} + +export function getIdentifier(asset, type, ...rest) { + return t.identifier(getName(asset, type, ...rest)); +} + +export function getExportIdentifier(asset, name) { + return getIdentifier(asset, 'export', name); +} diff --git a/packages/transformers/babel/src/babelrc.js b/packages/transformers/babel/src/babelrc.js index 2287a23235c..f9ea87ba5d6 100644 --- a/packages/transformers/babel/src/babelrc.js +++ b/packages/transformers/babel/src/babelrc.js @@ -1,14 +1,14 @@ // @flow -import type {Asset, PackageJSON} from '@parcel/types'; +import type {MutableAsset, PackageJSON} from '@parcel/types'; import semver from 'semver'; import logger from '@parcel/logger'; import path from 'path'; import {localResolve} from '@parcel/local-require'; -import installPackage from '@parcel/install-package'; +// import installPackage from '@parcel/install-package'; import micromatch from 'micromatch'; export default async function getBabelConfig( - asset: Asset, + asset: MutableAsset, pkg: ?PackageJSON, isSource: boolean ) { @@ -175,8 +175,8 @@ async function getBabelVersion(asset, pkg, plugins) { // We will attempt to infer a verison of babel and install it based on the dependencies of the plugins // in the config. This should only happen once since we save babel core into package.json for subsequent runs. let inferred = await inferBabelVersion(asset, plugins); - let name = inferred === 6 ? 'babel-core' : `@babel/core`; - await installPackage([name], asset.filePath); + // let name = inferred === 6 ? 'babel-core' : `@babel/core`; + // await installPackage([name], asset.filePath); return inferred; } diff --git a/packages/transformers/babel/src/config.js b/packages/transformers/babel/src/config.js index 6ec9086d577..99b23f2a83c 100644 --- a/packages/transformers/babel/src/config.js +++ b/packages/transformers/babel/src/config.js @@ -1,5 +1,5 @@ // @flow -import type {Asset} from '@parcel/types'; +import type {MutableAsset} from '@parcel/types'; import getBabelRc from './babelrc'; import getEnvConfig from './env'; import getJSXConfig from './jsx'; @@ -9,7 +9,7 @@ import * as fs from '@parcel/fs'; const NODE_MODULES = `${path.sep}node_modules${path.sep}`; -export default async function getBabelConfig(asset: Asset) { +export default async function getBabelConfig(asset: MutableAsset) { // Consider the module source code rather than precompiled if the resolver // used the `source` field, or it is not in node_modules. let pkg = await asset.getPackage(); diff --git a/packages/transformers/babel/src/env.js b/packages/transformers/babel/src/env.js index e75c354cd8f..17aa2964a3e 100644 --- a/packages/transformers/babel/src/env.js +++ b/packages/transformers/babel/src/env.js @@ -1,5 +1,5 @@ // @flow -import type {Asset} from '@parcel/types'; +import type {MutableAsset} from '@parcel/types'; import presetEnv from '@babel/preset-env'; import getTargetEngines from './getTargetEngines'; @@ -9,7 +9,7 @@ import getTargetEngines from './getTargetEngines'; * target engines, and doing a diff to include only the necessary plugins. */ export default async function getEnvConfig( - asset: Asset, + asset: MutableAsset, isSourceModule: boolean ) { // Load the target engines for the app and generate a @babel/preset-env config diff --git a/packages/transformers/babel/src/flow.js b/packages/transformers/babel/src/flow.js index 8daf1edd1e9..3e0a3260764 100644 --- a/packages/transformers/babel/src/flow.js +++ b/packages/transformers/babel/src/flow.js @@ -1,10 +1,10 @@ // @flow -import type {Asset} from '@parcel/types'; +import type {MutableAsset} from '@parcel/types'; /** * Generates a babel config for stripping away Flow types. */ -export default async function getFlowConfig(asset: Asset) { +export default async function getFlowConfig(asset: MutableAsset) { if (/^(\/{2}|\/\*+) *@flow/.test((await asset.getCode()).substring(0, 20))) { return { internal: true, diff --git a/packages/transformers/babel/src/getTargetEngines.js b/packages/transformers/babel/src/getTargetEngines.js index d3b3ca1e1e7..768596f8209 100644 --- a/packages/transformers/babel/src/getTargetEngines.js +++ b/packages/transformers/babel/src/getTargetEngines.js @@ -1,5 +1,5 @@ // @flow -import type {Asset} from '@parcel/types'; +import type {MutableAsset} from '@parcel/types'; import browserslist from 'browserslist'; import semver from 'semver'; @@ -11,7 +11,7 @@ const BROWSER_CONTEXT = new Set(['browser', 'web-worker', 'service-worker']); * - package.json browserslist field * - browserslist or .browserslistrc files */ -export default async function getTargetEngines(asset: Asset) { +export default async function getTargetEngines(asset: MutableAsset) { let targets = {}; let compileTarget = BROWSER_CONTEXT.has(asset.env.context) ? 'browsers' diff --git a/packages/transformers/babel/src/jsx.js b/packages/transformers/babel/src/jsx.js index 8151ef30f01..4e143c23089 100644 --- a/packages/transformers/babel/src/jsx.js +++ b/packages/transformers/babel/src/jsx.js @@ -1,5 +1,5 @@ // @flow -import type {Asset, PackageJSON} from '@parcel/types'; +import type {MutableAsset, PackageJSON} from '@parcel/types'; import path from 'path'; const JSX_EXTENSIONS = { @@ -19,7 +19,7 @@ const JSX_PRAGMA = { * and changes the pragma accordingly. */ export default async function getJSXConfig( - asset: Asset, + asset: MutableAsset, pkg: ?PackageJSON, isSourceModule: boolean ) { diff --git a/packages/transformers/css/.babelrc b/packages/transformers/css/.babelrc new file mode 100644 index 00000000000..be26495ef30 --- /dev/null +++ b/packages/transformers/css/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@parcel/babel-preset"] +} diff --git a/packages/transformers/js/package.json b/packages/transformers/js/package.json index 5583ca1611d..4b476fc27e8 100644 --- a/packages/transformers/js/package.json +++ b/packages/transformers/js/package.json @@ -24,6 +24,7 @@ "@babel/types": "^7.0.0", "@parcel/logger": "^2.0.0", "@parcel/plugin": "^2.0.0", + "@parcel/scope-hoisting": "^2.0.0", "@parcel/utils": "^2.0.0", "babylon-walk": "^1.0.2", "node-libs-browser": "^2.0.0", diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index 9ea8eb071ce..ac5cd2a09a3 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -11,6 +11,7 @@ import {parse} from '@babel/parser'; import traverse from '@babel/traverse'; import * as walk from 'babylon-walk'; import * as babelCore from '@babel/core'; +import {hoist} from '@parcel/scope-hoisting'; const IMPORT_RE = /\b(?:import\b|export\b|require\s*\()/; const ENV_RE = /\b(?:process\.env)\b/; @@ -37,9 +38,14 @@ export default new Transformer({ return ast.type === 'babel' && semver.satisfies(ast.version, '^7.0.0'); }, - async parse(asset /*, config , options */) { + async parse(asset, config, options) { let code = await asset.getCode(); - if (!canHaveDependencies(code) && !ENV_RE.test(code) && !FS_RE.test(code)) { + if ( + !options.scopeHoist && + !canHaveDependencies(code) && + !ENV_RE.test(code) && + !FS_RE.test(code) + ) { return null; } @@ -57,7 +63,7 @@ export default new Transformer({ }; }, - async transform(asset) { + async transform(asset, config, options) { asset.type = 'js'; if (!asset.ast) { return [asset]; @@ -103,8 +109,10 @@ export default new Transformer({ } } - // Convert ES6 modules to CommonJS - if (asset.meta.isES6Module) { + if (options.scopeHoist) { + hoist(asset); + } else if (asset.meta.isES6Module) { + // Convert ES6 modules to CommonJS let res = babelCore.transformFromAst(ast.program, code, { code: false, ast: true, diff --git a/packages/transformers/terser/src/TerserTransformer.js b/packages/transformers/terser/src/TerserTransformer.js index 6d90fef5418..fb1e61b8c47 100644 --- a/packages/transformers/terser/src/TerserTransformer.js +++ b/packages/transformers/terser/src/TerserTransformer.js @@ -22,14 +22,14 @@ export default new Transformer({ }, async transform(asset, config, options) { - if (options.mode !== 'production') { + if (!options.minify) { return [asset]; } let terserOptions = { warnings: true, mangle: { - toplevel: true + toplevel: false } }; diff --git a/yarn.lock b/yarn.lock index 92ea5f688f7..29d0f725d46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,6 +66,26 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.0.0-0": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.4.tgz#84055750b05fcd50f9915a826b44fa347a825250" + integrity sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helpers" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.11" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/core@^7.2.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.0.tgz#248fd6874b7d755010bfe61f557461d4f446d9e9" @@ -150,6 +170,28 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.3.3": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.3.tgz#185962ade59a52e00ca2bdfcfd1d58e528d4e39e" + integrity sha512-aEADYwRRZjJyMnKN7llGIlircxTCofm3dtV5pmY6ob18MSIuipHpA2yZWkPlycwu5HJcx/pADS3zssd8eY7/6A== + dependencies: + "@babel/types" "^7.3.3" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== + dependencies: + "@babel/types" "^7.4.4" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -343,6 +385,13 @@ dependencies: "@babel/types" "^7.4.0" +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + "@babel/helper-wrap-function@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -371,6 +420,15 @@ "@babel/traverse" "^7.4.0" "@babel/types" "^7.4.0" +"@babel/helpers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" + integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== + dependencies: + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -405,6 +463,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.3.tgz#32f5df65744b70888d17872ec106b02434ba1489" integrity sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA== +"@babel/parser@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6" + integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w== + "@babel/plugin-proposal-async-generator-functions@^7.1.0", "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -513,14 +576,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-export-default-from@^7.0.0 <7.4.0": +"@babel/plugin-syntax-export-default-from@^7.0.0 <7.4.0", "@babel/plugin-syntax-export-default-from@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.2.0.tgz#edd83b7adc2e0d059e2467ca96c650ab6d2f3820" integrity sha512-c7nqUnNST97BWPtoe+Ssi+fJukc9P9/JMZ71IOMNQWza2E+Psrd46N6AEvtw6pqK+gt7ChjXyrw4SPDO79f3Lw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-export-namespace-from@^7.0.0 <7.4.0": +"@babel/plugin-syntax-export-namespace-from@^7.0.0 <7.4.0", "@babel/plugin-syntax-export-namespace-from@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.2.0.tgz#8d257838c6b3b779db52c0224443459bd27fb039" integrity sha512-1zGA3UNch6A+A11nIzBVEaE3DDJbjfB+eLIcf0GGOh/BJr/8NxL3546MGhV/r0RhH4xADFIEso39TKCfEMlsGA== @@ -1217,6 +1280,15 @@ "@babel/parser" "^7.4.0" "@babel/types" "^7.4.0" +"@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + "@babel/traverse@^7.0.0": version "7.1.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.1.4.tgz#f4f83b93d649b4b2c91121a9087fa2fa949ec2b4" @@ -1292,6 +1364,21 @@ globals "^11.1.0" lodash "^4.17.11" +"@babel/traverse@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.4.tgz#0776f038f6d78361860b6823887d4f3937133fe8" + integrity sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + "@babel/types@^7.0.0", "@babel/types@^7.1.2", "@babel/types@^7.1.3", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.4", "@babel/types@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.0.tgz#670724f77d24cce6cc7d8cf64599d511d164894c" @@ -1319,6 +1406,24 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" +"@babel/types@^7.3.3": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.3.tgz#6c44d1cdac2a7625b624216657d5bc6c107ab436" + integrity sha512-2tACZ80Wg09UnPg5uGAOUvvInaqLk3l/IAhQzlxLQOIXacr6bMsra5SH6AWw/hIDRCSbCdHP2KzSOD+cT7TzMQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + +"@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + "@choojs/findup@^0.2.0": version "0.2.1" resolved "https://registry.yarnpkg.com/@choojs/findup/-/findup-0.2.1.tgz#ac13c59ae7be6e1da64de0779a0a7f03d75615a3"