diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js index 534d7f0b263..58544538767 100644 --- a/packages/core/core/src/AssetGraph.js +++ b/packages/core/core/src/AssetGraph.js @@ -114,6 +114,7 @@ export default class AssetGraph extends ContentGraph { hash: ?string; envCache: Map; symbolPropagationRan: boolean; + safeToIncrementallyBundle: boolean = true; constructor(opts: ?AssetGraphOpts) { if (opts) { diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index 04dfabbd4bc..6e961d8418b 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -26,6 +26,7 @@ import type { } from './types'; import type AssetGraph from './AssetGraph'; import type {ProjectPath} from './projectPath'; +import {nodeFromAsset} from './AssetGraph'; import assert from 'assert'; import invariant from 'assert'; @@ -1883,8 +1884,7 @@ export default class BundleGraph { otherGraphIdToThisNodeId.set(otherNodeId, existingNodeId); let existingNode = nullthrows(this._graph.getNode(existingNodeId)); - // Merge symbols, recompute dep.exluded based on that - + // Merge symbols, recompute dep.excluded based on that if (existingNode.type === 'asset') { invariant(otherNode.type === 'asset'); existingNode.usedSymbols = new Set([ @@ -1936,6 +1936,21 @@ export default class BundleGraph { .some(n => n.type === 'root'); } + /** + * Update the asset in a Bundle Graph and clear the associated Bundle hash. + */ + updateAsset(asset: Asset) { + this._graph.updateNode( + this._graph.getNodeIdByContentKey(asset.id), + nodeFromAsset(asset), + ); + let bundles = this.getBundlesWithAsset(asset); + for (let bundle of bundles) { + // the bundle content will change with a modified asset + this._bundleContentHashes.delete(bundle.id); + } + } + getEntryRoot(projectRoot: FilePath, target: Target): FilePath { let cached = this._targetEntryRoots.get(target.distDir); if (cached != null) { diff --git a/packages/core/core/src/UncommittedAsset.js b/packages/core/core/src/UncommittedAsset.js index cd0fc9be2b6..581df8b77e1 100644 --- a/packages/core/core/src/UncommittedAsset.js +++ b/packages/core/core/src/UncommittedAsset.js @@ -92,7 +92,7 @@ export default class UncommittedAsset { } /* - * Prepares the asset for being serialized to the cache by commiting its + * Prepares the asset for being serialized to the cache by committing its * content and map of the asset to the cache. */ async commit(pipelineKey: string): Promise { diff --git a/packages/core/core/src/requests/AssetGraphRequest.js b/packages/core/core/src/requests/AssetGraphRequest.js index dca4cff08b9..2b0d55073d5 100644 --- a/packages/core/core/src/requests/AssetGraphRequest.js +++ b/packages/core/core/src/requests/AssetGraphRequest.js @@ -49,7 +49,11 @@ type AssetGraphRequestInput = {| requestedAssetIds?: Set, |}; -type AssetGraphRequestResult = {| +type AssetGraphRequestResult = AssetGraphBuilderResult & {| + previousAssetGraphHash: ?string, +|}; + +type AssetGraphBuilderResult = {| assetGraph: AssetGraph, changedAssets: Map, assetRequests: Array, @@ -63,11 +67,7 @@ type RunInput = {| type AssetGraphRequest = {| id: string, +type: 'asset_graph_request', - run: RunInput => Async<{| - assetGraph: AssetGraph, - changedAssets: Map, - assetRequests: Array, - |}>, + run: RunInput => Async, input: AssetGraphRequestInput, |}; @@ -80,8 +80,23 @@ export default function createAssetGraphRequest( run: async input => { let prevResult = await input.api.getPreviousResult(); + let previousAssetGraphHash = prevResult?.assetGraph.getHash(); + let builder = new AssetGraphBuilder(input, prevResult); - return builder.build(); + let assetGraphRequest = await await builder.build(); + + // early break for incremental bundling if production or flag is off; + if ( + !input.options.shouldBundleIncrementally || + input.options.mode === 'production' + ) { + assetGraphRequest.assetGraph.safeToIncrementallyBundle = false; + } + + return { + ...assetGraphRequest, + previousAssetGraphHash, + }; }, input, }; @@ -109,7 +124,7 @@ export class AssetGraphBuilder { constructor( {input, api, options}: RunInput, - prevResult: ?AssetGraphRequestResult, + prevResult: ?AssetGraphBuilderResult, ) { let { entries, @@ -120,6 +135,7 @@ export class AssetGraphBuilder { shouldBuildLazily, } = input; let assetGraph = prevResult?.assetGraph ?? new AssetGraph(); + assetGraph.safeToIncrementallyBundle = true; assetGraph.setRootConnections({ entries, assetGroups, @@ -138,7 +154,7 @@ export class AssetGraphBuilder { this.queue = new PromiseQueue(); } - async build(): Promise { + async build(): Promise { let errors = []; let rootNodeId = nullthrows( this.assetGraph.rootNodeId, @@ -235,6 +251,7 @@ export class AssetGraphBuilder { if (this.shouldBuildLazily) { let node = nullthrows(this.assetGraph.getNode(nodeId)); let childNode = nullthrows(this.assetGraph.getNode(childNodeId)); + if (node.type === 'asset' && childNode.type === 'dependency') { if (this.requestedAssetIds.has(node.value.id)) { node.requested = true; @@ -284,6 +301,7 @@ export class AssetGraphBuilder { assetSymbolsInverse = new Map>(); for (let [s, {local}] of assetSymbols) { let set = assetSymbolsInverse.get(local); + if (!set) { set = new Set(); assetSymbolsInverse.set(local, set); @@ -512,6 +530,7 @@ export class AssetGraphBuilder { } let local = outgoingDepSymbols.get(s)?.local; + if (local == null) { // Caused by '*' => '*', already handled continue; @@ -917,11 +936,34 @@ export class AssetGraphBuilder { } async runEntryRequest(input: ProjectPath) { + let prevEntries = this.assetGraph.safeToIncrementallyBundle + ? this.assetGraph + .getEntryAssets() + .map(asset => asset.id) + .sort() + : []; + let request = createEntryRequest(input); let result = await this.api.runRequest(request, { force: true, }); this.assetGraph.resolveEntry(request.input, result.entries, request.id); + + if (this.assetGraph.safeToIncrementallyBundle) { + let currentEntries = this.assetGraph + .getEntryAssets() + .map(asset => asset.id) + .sort(); + let didEntriesChange = + prevEntries.length !== currentEntries.length || + prevEntries.every( + (entryId, index) => entryId === currentEntries[index], + ); + + if (didEntriesChange) { + this.assetGraph.safeToIncrementallyBundle = false; + } + } } async runTargetRequest(input: Entry) { @@ -955,11 +997,50 @@ export class AssetGraphBuilder { if (assets != null) { for (let asset of assets) { + if (this.assetGraph.safeToIncrementallyBundle) { + let otherAsset = this.assetGraph.getNodeByContentKey(asset.id); + if (otherAsset != null) { + invariant(otherAsset.type === 'asset'); + if (!this._areDependenciesEqualForAssets(asset, otherAsset.value)) { + this.assetGraph.safeToIncrementallyBundle = false; + } + } else { + // adding a new entry or dependency + this.assetGraph.safeToIncrementallyBundle = false; + } + } this.changedAssets.set(asset.id, asset); } this.assetGraph.resolveAssetGroup(input, assets, request.id); + } else { + this.assetGraph.safeToIncrementallyBundle = false; } } + + /** + * Used for incremental bundling of modified assets + */ + _areDependenciesEqualForAssets(asset: Asset, otherAsset: Asset): boolean { + let assetDependencies = Array.from(asset?.dependencies.keys()).sort(); + let otherAssetDependencies = Array.from( + otherAsset?.dependencies.keys(), + ).sort(); + + if (assetDependencies.length !== otherAssetDependencies.length) { + return false; + } + + return assetDependencies.every((key, index) => { + if (key !== otherAssetDependencies[index]) { + return false; + } + + return equalSet( + new Set(asset?.dependencies.get(key)?.symbols?.keys()), + new Set(otherAsset?.dependencies.get(key)?.symbols?.keys()), + ); + }); + } } function equalMap( diff --git a/packages/core/core/src/requests/BundleGraphRequest.js b/packages/core/core/src/requests/BundleGraphRequest.js index fe07eddd575..248628450f5 100644 --- a/packages/core/core/src/requests/BundleGraphRequest.js +++ b/packages/core/core/src/requests/BundleGraphRequest.js @@ -13,6 +13,7 @@ import type { } from '../types'; import type {ConfigAndCachePath} from './ParcelConfigRequest'; +import invariant from 'assert'; import assert from 'assert'; import nullthrows from 'nullthrows'; import {PluginLogger} from '@parcel/logger'; @@ -56,9 +57,16 @@ import { type BundleGraphRequestInput = {| assetGraph: AssetGraph, + changedAssets: Map, + previousAssetGraphHash: ?string, optionsRef: SharedReference, |}; +type BundleGraphRequestResult = {| + bundleGraph: InternalBundleGraph, + bundlerHash: string, +|}; + type RunInput = {| input: BundleGraphRequestInput, ...StaticRunOpts, @@ -66,6 +74,7 @@ type RunInput = {| type BundleGraphResult = {| bundleGraph: InternalBundleGraph, + bundlerHash: string, changedAssets: Map, |}; @@ -94,7 +103,11 @@ export default function createBundleGraphRequest( invalidateDevDeps(invalidDevDeps, input.options, parcelConfig); let builder = new BundlerRunner(input, parcelConfig, devDeps); - return builder.bundle(input.input.assetGraph); + return builder.bundle({ + graph: input.input.assetGraph, + previousAssetGraphHash: input.input.previousAssetGraphHash, + changedAssets: input.input.changedAssets, + }); }, input, }; @@ -170,7 +183,15 @@ class BundlerRunner { await runDevDepRequest(this.api, devDepRequest); } - async bundle(graph: AssetGraph): Promise { + async bundle({ + graph, + previousAssetGraphHash, + changedAssets, + }: {| + graph: AssetGraph, + previousAssetGraphHash: ?string, + changedAssets: Map, + |}): Promise { report({ type: 'buildProgress', phase: 'bundling', @@ -181,7 +202,7 @@ class BundlerRunner { let plugin = await this.config.getBundler(); let {plugin: bundler, name, resolveFrom} = plugin; - let cacheKey = await this.getCacheKey(graph); + let {cacheKey, bundlerHash} = await this.getHashes(graph); // Check if the cacheKey matches the one already stored in the graph. // This can save time deserializing from cache if the graph is already in memory. @@ -211,27 +232,81 @@ class BundlerRunner { } } - let internalBundleGraph = InternalBundleGraph.fromAssetGraph(graph); - await dumpGraphToGraphViz( - // $FlowFixMe - internalBundleGraph._graph, - 'before_bundle', - bundleGraphEdgeTypes, - ); - let mutableBundleGraph = new MutableBundleGraph( - internalBundleGraph, - this.options, - ); + // if a previous asset graph hash is passed in, check if the bundle graph is also available + let previousBundleGraphResult: ?BundleGraphRequestResult; + if (graph.safeToIncrementallyBundle && previousAssetGraphHash != null) { + try { + previousBundleGraphResult = + await this.api.getRequestResult( + 'BundleGraph:' + previousAssetGraphHash, + ); + } catch { + // if the bundle graph had an error or was removed, don't fail the build + } + } + if ( + previousBundleGraphResult == null || + previousBundleGraphResult?.bundlerHash !== bundlerHash + ) { + graph.safeToIncrementallyBundle = false; + } + + let internalBundleGraph; let logger = new PluginLogger({origin: name}); try { - await bundler.bundle({ - bundleGraph: mutableBundleGraph, - config: this.configs.get(plugin.name)?.result, - options: this.pluginOptions, - logger, - }); + if (graph.safeToIncrementallyBundle) { + internalBundleGraph = nullthrows(previousBundleGraphResult).bundleGraph; + for (let changedAsset of changedAssets.values()) { + internalBundleGraph.updateAsset(changedAsset); + } + } else { + internalBundleGraph = InternalBundleGraph.fromAssetGraph(graph); + invariant(internalBundleGraph != null); // ensures the graph was created + + await dumpGraphToGraphViz( + // $FlowFixMe + internalBundleGraph._graph, + 'before_bundle', + bundleGraphEdgeTypes, + ); + let mutableBundleGraph = new MutableBundleGraph( + internalBundleGraph, + this.options, + ); + + // this the normal bundle workflow (bundle, optimizing, run-times, naming) + await bundler.bundle({ + bundleGraph: mutableBundleGraph, + config: this.configs.get(plugin.name)?.result, + options: this.pluginOptions, + logger, + }); + + if (this.pluginOptions.mode === 'production') { + try { + await bundler.optimize({ + bundleGraph: mutableBundleGraph, + config: this.configs.get(plugin.name)?.result, + options: this.pluginOptions, + logger, + }); + } catch (e) { + throw new ThrowableDiagnostic({ + diagnostic: errorToDiagnostic(e, { + origin: plugin.name, + }), + }); + } finally { + await dumpGraphToGraphViz( + // $FlowFixMe[incompatible-call] + internalBundleGraph._graph, + 'after_optimize', + ); + } + } + } } catch (e) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { @@ -239,6 +314,7 @@ class BundlerRunner { }), }); } finally { + invariant(internalBundleGraph != null); // ensures the graph was created await dumpGraphToGraphViz( // $FlowFixMe[incompatible-call] internalBundleGraph._graph, @@ -247,30 +323,6 @@ class BundlerRunner { ); } - if (this.pluginOptions.mode === 'production') { - try { - await bundler.optimize({ - bundleGraph: mutableBundleGraph, - config: this.configs.get(plugin.name)?.result, - options: this.pluginOptions, - logger, - }); - } catch (e) { - throw new ThrowableDiagnostic({ - diagnostic: errorToDiagnostic(e, { - origin: name, - }), - }); - } finally { - await dumpGraphToGraphViz( - // $FlowFixMe[incompatible-call] - internalBundleGraph._graph, - 'after_optimize', - bundleGraphEdgeTypes, - ); - } - } - // Add dev dependency for the bundler. This must be done AFTER running it due to // the potential for lazy require() that aren't executed until the request runs. let devDepRequest = await createDevDependency( @@ -285,7 +337,7 @@ class BundlerRunner { await this.nameBundles(internalBundleGraph); - let changedAssets = await applyRuntimes({ + let changedRuntimes = await applyRuntimes({ bundleGraph: internalBundleGraph, api: this.api, config: this.config, @@ -311,43 +363,60 @@ class BundlerRunner { cacheSerializedObject(internalBundleGraph); // Recompute the cache key to account for new dev dependencies and invalidations. - cacheKey = await this.getCacheKey(graph); + let {cacheKey: updatedCacheKey} = await this.getHashes(graph); this.api.storeResult( { + bundlerHash, bundleGraph: internalBundleGraph, changedAssets: new Map(), }, - cacheKey, + updatedCacheKey, ); return { bundleGraph: internalBundleGraph, - changedAssets, + bundlerHash, + changedAssets: changedRuntimes, }; } - async getCacheKey(assetGraph: AssetGraph): Promise { - let configs = [...this.configs] - .map(([pluginName, config]) => - getConfigHash(config, pluginName, this.options), + async getHashes(assetGraph: AssetGraph): Promise<{| + cacheKey: string, + bundlerHash: string, + |}> { + // BundleGraphRequest needs hashes based on content (for quick retrieval) + // and not-based on content (determine if the environment / config + // changes that violate incremental bundling). + let configs = ( + await Promise.all( + [...this.configs].map(([pluginName, config]) => + getConfigHash(config, pluginName, this.options), + ), ) - .join(''); + ).join(''); + let devDepRequests = [...this.devDepRequests.values()] .map(d => d.hash) .join(''); + let invalidations = await getInvalidationHash( this.api.getInvalidations(), this.options, ); - return hashString( - PARCEL_VERSION + - assetGraph.getHash() + - configs + - devDepRequests + - invalidations + - this.options.mode, - ); + let plugin = await this.config.getBundler(); + + return { + cacheKey: hashString( + PARCEL_VERSION + + assetGraph.getHash() + + configs + + devDepRequests + + invalidations + + this.options.mode, + ), + bundlerHash: hashString(PARCEL_VERSION + plugin.name + configs), + }; } async nameBundles(bundleGraph: InternalBundleGraph): Promise { diff --git a/packages/core/core/src/requests/ParcelBuildRequest.js b/packages/core/core/src/requests/ParcelBuildRequest.js index 72a51011338..f52a02c119e 100644 --- a/packages/core/core/src/requests/ParcelBuildRequest.js +++ b/packages/core/core/src/requests/ParcelBuildRequest.js @@ -61,15 +61,15 @@ async function run({input, api, options}: RunInput) { shouldBuildLazily: options.shouldBuildLazily, requestedAssetIds, }); - let {assetGraph, changedAssets, assetRequests} = await api.runRequest( - request, - { + let {assetGraph, changedAssets, assetRequests, previousAssetGraphHash} = + await api.runRequest(request, { force: options.shouldBuildLazily && requestedAssetIds.size > 0, - }, - ); + }); let bundleGraphRequest = createBundleGraphRequest({ assetGraph, + previousAssetGraphHash, + changedAssets, optionsRef, }); diff --git a/packages/core/core/src/resolveOptions.js b/packages/core/core/src/resolveOptions.js index 01ee2de6288..561e88aad87 100644 --- a/packages/core/core/src/resolveOptions.js +++ b/packages/core/core/src/resolveOptions.js @@ -140,6 +140,7 @@ export default async function resolveOptions( shouldAutoInstall: initialOptions.shouldAutoInstall ?? false, hmrOptions: initialOptions.hmrOptions ?? null, shouldBuildLazily, + shouldBundleIncrementally: initialOptions.shouldBundleIncrementally ?? true, shouldContentHash, serveOptions: initialOptions.serveOptions ? { diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index 1a23828aba0..f355f636691 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -256,6 +256,7 @@ export type ParcelOptions = {| shouldContentHash: boolean, serveOptions: ServerOptions | false, shouldBuildLazily: boolean, + shouldBundleIncrementally: boolean, shouldAutoInstall: boolean, logLevel: LogLevel, projectRoot: FilePath, diff --git a/packages/core/core/test/test-utils.js b/packages/core/core/test/test-utils.js index 9e0dc6594a0..29caf4900c8 100644 --- a/packages/core/core/test/test-utils.js +++ b/packages/core/core/test/test-utils.js @@ -25,6 +25,7 @@ export const DEFAULT_OPTIONS: ParcelOptions = { hmrOptions: undefined, shouldContentHash: true, shouldBuildLazily: false, + shouldBundleIncrementally: true, serveOptions: false, mode: 'development', env: {}, diff --git a/packages/core/integration-tests/test/incremental-bundling.js b/packages/core/integration-tests/test/incremental-bundling.js new file mode 100644 index 00000000000..ac77b3e5cc8 --- /dev/null +++ b/packages/core/integration-tests/test/incremental-bundling.js @@ -0,0 +1,1025 @@ +// @flow strict-local +import {bundler, getNextBuildSuccess, overlayFS, run} from '@parcel/test-utils'; +import assert from 'assert'; +import path from 'path'; +import sinon from 'sinon'; +import Bundler from '@parcel/bundler-default'; +import ExperimentalBundler from '@parcel/bundler-experimental'; + +import {type Asset} from '@parcel/types'; +// $FlowFixMe[untyped-import] +import CustomBundler from './integration/incremental-bundling/node_modules/parcel-bundler-test'; + +const CONFIG = Symbol.for('parcel-plugin-config'); + +describe('incremental bundling', function () { + let defaultBundlerSpy = + process.env.PARCEL_TEST_EXPERIMENTAL_BUNDLER != null + ? // $FlowFixMe[prop-missing] + sinon.spy(ExperimentalBundler[CONFIG], 'bundle') + : // $FlowFixMe[prop-missing] + sinon.spy(Bundler[CONFIG], 'bundle'); + let customBundlerSpy = sinon.spy(CustomBundler[CONFIG], 'bundle'); + + let assertChangedAssets = (actual: number, expected: number) => { + assert.equal( + actual, + expected, + `the number of changed assets should be ${expected}, not ${actual}`, + ); + }; + + let assertTimesBundled = (actual: number, expected: number) => { + assert.equal( + actual, + expected, + `the bundler should have bundled ${expected} time(s), not ${actual}`, + ); + }; + + let getChangedAssetsBeforeRuntimes = (changedAssets: Array) => { + return changedAssets.filter(a => !a.filePath.includes('runtime')); + }; + beforeEach(() => { + defaultBundlerSpy.resetHistory(); + customBundlerSpy.resetHistory(); + }); + + after(() => { + defaultBundlerSpy.restore(); + customBundlerSpy.restore(); + }); + + describe('non-dependency based changes', () => { + describe('javascript', () => { + it('add a console log should not bundle by default', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import {a} from './a'; +console.log('index.js'); +console.log(a); +console.log('adding a new console');`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(contents.includes(`console.log("adding a new console")`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('disable by setting option to false', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: false, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import {a} from './a'; +console.log('index.js'); +console.log(a); +console.log('adding a new console');`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(contents.includes(`console.log("adding a new console")`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('add a console log should not bundle', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import {a} from './a'; +console.log('index.js'); +console.log(a); +console.log('adding a new console');`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(contents.includes(`console.log("adding a new console")`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('updating a string value should not bundle', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import {a} from './a'; +console.log('index.js - updated string'); +console.log(a); +`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert( + contents.includes(`console.log("index.js - updated string");`), + ); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('adding a comment', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import {a} from './a'; +// test comment +console.log('index.js'); +console.log(a);`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(contents.includes(`// test comment`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + // this case is similar to applying a patch or restarting parcel with changes + it('adds multiple non-dependency related changes', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index-export.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index-export.js'), + `import {a} from './a'; +console.log('adding a new console'); +module.exports = a;`, + ); + + await overlayFS.writeFile( + path.join(fixture, 'a.js'), + `export const a = 'a updated';`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 2); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + + assert(contents.includes(`console.log("adding a new console")`)); + + let bundleOutput = await run(result.bundleGraph); + assert.equal(bundleOutput, 'a updated'); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + }); + + it('update an imported css file', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index-with-css.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'a.css'), + `html { + color: red; +} +`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let bundleCSS = result.bundleGraph.getBundles()[1]; + assert.equal(bundleCSS.type, 'css'); + + let cssContent = await overlayFS.readFile(bundleCSS.filePath, 'utf8'); + assert(cssContent.includes(`color: red;`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('update both the js and imported css file', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index-with-css.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index-with-css.js'), + `import {a} from './a'; +import './a.css'; +console.log('index.js'); +console.log(a, 'updated');`, + ); + + await overlayFS.writeFile( + path.join(fixture, 'a.css'), + `html { + color: red; +}`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 2); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + + assert(contents.includes(`console.log((0, _a.a), "updated");`)); + + let bundleCSS = result.bundleGraph.getBundles()[1]; + assert.equal(bundleCSS.type, 'css'); + + let cssContent = await overlayFS.readFile(bundleCSS.filePath, 'utf8'); + assert(cssContent.includes(`color: red;`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('update the bundles if entry is html and js asset is modified', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.html'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import {a} from './a'; +// test comment +console.log('index.js'); +console.log(a);`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + + let bundleHTML = result.bundleGraph.getBundles()[0]; + assert.equal(bundleHTML.type, 'html'); + let htmlContent = await overlayFS.readFile(bundleHTML.filePath, 'utf8'); + + assert(htmlContent.includes(``)); + + let bundleJS = result.bundleGraph.getBundles()[1]; + assert.equal(bundleJS.type, 'js'); + + let jsContent = await overlayFS.readFile(bundleJS.filePath, 'utf8'); + assert(jsContent.includes(`// test comment`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + }); + + describe('dependency based changes should run the bundler', () => { + it('adding a new dependency', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import a from './a'; +import b from './b'; +console.log('index.js', b); +console.log(a); +`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 2); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + + assert( + contents.includes(`console.log("index.js", (0, _bDefault.default));`), + ); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('adding a new dependency of a different type', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import a from './a'; +import './a.css'; +console.log(a); +`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 2); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let result = await b.run(); + + // one CSS and one JS bundle + assert.equal(result.bundleGraph.getBundles().length, 2); + + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + + assert(contents.includes(`console.log((0, _aDefault.default));`)); + + let bundleCSS = result.bundleGraph.getBundles()[1]; + assert.equal(bundleCSS.type, 'css'); + + let cssContent = await overlayFS.readFile(bundleCSS.filePath, 'utf8'); + assert(cssContent.includes(`color: #00f;`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('adding a new dynamic import', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `import a from './a'; +const b = import('./b'); +console.log(a); +`, + ); + + event = await getNextBuildSuccess(b); + let assets = Array.from(event.changedAssets.values()); + assertChangedAssets(getChangedAssetsBeforeRuntimes(assets).length, 2); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let result = await b.run(); + + // original bundle and new dynamic import bundle JS bundle + assert.equal(result.bundleGraph.getBundles().length, 2); + + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + + assert(contents.includes(`console.log((0, _aDefault.default));`)); + + let dynamicBundle = result.bundleGraph.getBundles()[1]; + assert.equal(dynamicBundle.type, 'js'); + + let dynamicContent = await overlayFS.readFile( + dynamicBundle.filePath, + 'utf8', + ); + assert( + dynamicContent.includes(`parcelHelpers.export(exports, "b", ()=>b); +const b = "b";`), + ); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('removing a dependency', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index.js'), + `// import {a} from './a'; +console.log('index.js');`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let output = await overlayFS.readFile( + path.join(fixture, 'index.js'), + 'utf8', + ); + assert(output.includes(`// import {a} from './a'`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + }); + + describe('other changes that would for a re-bundle', () => { + it('changing the bundler in parcel configs', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + assertTimesBundled(customBundlerSpy.callCount, 0); + + await overlayFS.writeFile( + path.join(fixture, '.parcelrc'), + JSON.stringify({ + extends: '@parcel/config-default', + bundler: 'parcel-bundler-test', + }), + ); + + event = await getNextBuildSuccess(b); + let assets = Array.from(event.changedAssets.values()); + // should contain all the assets + assertChangedAssets(getChangedAssetsBeforeRuntimes(assets).length, 3); + // the default bundler was only called once + assertTimesBundled(defaultBundlerSpy.callCount, 1); + // calls the new bundler to rebundle + assertTimesBundled(customBundlerSpy.callCount, 1); + + let output = await overlayFS.readFile( + path.join(fixture, 'index.js'), + 'utf8', + ); + assert(output.includes(`import {a} from './a'`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('changing bundler options', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let pkgFile = path.join(fixture, 'package.json'); + let pkg = JSON.parse(await overlayFS.readFile(pkgFile)); + await overlayFS.writeFile( + pkgFile, + JSON.stringify({ + ...pkg, + '@parcel/bundler-default': { + http: 1, + }, + }), + ); + + event = await getNextBuildSuccess(b); + + // should contain all the assets + assertChangedAssets(event.changedAssets.size, 3); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + }); + + it('changing the namer', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, '.parcelrc'), + JSON.stringify({ + extends: '@parcel/config-default', + namers: ['parcel-namer-test'], + }), + ); + + event = await getNextBuildSuccess(b); + + // should contain all the assets + assertChangedAssets(event.changedAssets.size, 3); + // the default bundler was only called once + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let bundles = result.bundleGraph.getBundles(); + assert.deepEqual( + bundles.map(b => b.name), + bundles.map(b => `${b.id}.${b.type}`), + ); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('changing the runtimes', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, '.parcelrc'), + JSON.stringify({ + extends: '@parcel/config-default', + runtimes: ['parcel-runtime-test'], + }), + ); + + event = await getNextBuildSuccess(b); + + // should contain all the assets + let assets = Array.from(event.changedAssets.values()); + assertChangedAssets(getChangedAssetsBeforeRuntimes(assets).length, 3); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + let result = await b.run(); + let res = await run(result.bundleGraph, null, {require: false}); + assert.equal(res.runtime_test, true); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('changing target options', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + assertTimesBundled(customBundlerSpy.callCount, 0); + + let pkgFile = path.join(fixture, 'package.json'); + let pkg = JSON.parse(await overlayFS.readFile(pkgFile)); + await overlayFS.writeFile( + pkgFile, + JSON.stringify({ + ...pkg, + targets: { + esmodule: { + outputFormat: 'esmodule', + }, + }, + }), + ); + event = await getNextBuildSuccess(b); + + assertChangedAssets(event.changedAssets.size, 3); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let output = await overlayFS.readFile( + path.join(fixture, 'index.js'), + 'utf8', + ); + assert(output.includes(`import {a} from './a'`)); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + + it('adding a new the entry', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, '*.html'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + assertTimesBundled(customBundlerSpy.callCount, 0); + + await overlayFS.writeFile( + path.join(fixture, 'index-new-entry.html'), + '', + ); + + event = await getNextBuildSuccess(b); + + // should contain all the assets + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + it('changing symbols (adding a new dependency via one symbol)', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index-multi-symbol.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + defaultTargetOptions: { + shouldScopeHoist: true, + }, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index-multi-symbol.js'), + `import {a,b,c} from './multi-symbol-util.js'; + + console.log('index.js'); + console.log(a,b,c); + module.exports = {a, b, c}; + `, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert( + /console\.log\(\(0, [^)]+\), \(0, [^)]+\), \(0, [^)]+\)\);/.test( + contents, + ), + ); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); + it('changing symbols (removing a dependency via one symbol)', async () => { + let subscription; + let fixture = path.join(__dirname, '/integration/incremental-bundling'); + try { + let b = bundler(path.join(fixture, 'index-multi-symbol.js'), { + inputFS: overlayFS, + shouldDisableCache: false, + shouldBundleIncrementally: true, + defaultTargetOptions: { + shouldScopeHoist: true, + }, + }); + + await overlayFS.mkdirp(fixture); + subscription = await b.watch(); + + let event = await getNextBuildSuccess(b); + assertTimesBundled(defaultBundlerSpy.callCount, 1); + + await overlayFS.writeFile( + path.join(fixture, 'index-multi-symbol.js'), + `import {a } from './multi-symbol-util.js'; + +console.log('index.js'); +console.log(a); +module.exports = {a}; +`, + ); + + event = await getNextBuildSuccess(b); + assertChangedAssets(event.changedAssets.size, 1); + assertTimesBundled(defaultBundlerSpy.callCount, 2); + + let result = await b.run(); + let contents = await overlayFS.readFile( + result.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(/console\.log\(\(0, [^)]+\)\);/.test(contents)); + + result.bundleGraph.getBundles()[0].traverseAssets(a => { + assert(!a.filePath.endsWith('b.js')); + }); + } finally { + if (subscription) { + await subscription.unsubscribe(); + subscription = null; + } + } + }); +}); diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/a.css b/packages/core/integration-tests/test/integration/incremental-bundling/a.css new file mode 100644 index 00000000000..a1744c426b8 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/a.css @@ -0,0 +1,3 @@ +html { + color: blue; +} diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/a.js b/packages/core/integration-tests/test/integration/incremental-bundling/a.js new file mode 100644 index 00000000000..c4a2d30fbc2 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/a.js @@ -0,0 +1 @@ +export const a = 'a'; diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/b.js b/packages/core/integration-tests/test/integration/incremental-bundling/b.js new file mode 100644 index 00000000000..137b8ce6429 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/b.js @@ -0,0 +1 @@ +export const b = 'b'; diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/c.js b/packages/core/integration-tests/test/integration/incremental-bundling/c.js new file mode 100644 index 00000000000..93c974371a5 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/c.js @@ -0,0 +1 @@ +export const c = 'c'; diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/index-export.js b/packages/core/integration-tests/test/integration/incremental-bundling/index-export.js new file mode 100644 index 00000000000..8dd931e2a4f --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/index-export.js @@ -0,0 +1,3 @@ +import {a} from './a'; + +module.exports = a; diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/index-multi-symbol.js b/packages/core/integration-tests/test/integration/incremental-bundling/index-multi-symbol.js new file mode 100644 index 00000000000..dcba8c47127 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/index-multi-symbol.js @@ -0,0 +1,5 @@ +import {a,b} from './multi-symbol-util.js'; + +console.log('index.js', b); +console.log(a); +module.exports = {a}; diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/index-with-css.js b/packages/core/integration-tests/test/integration/incremental-bundling/index-with-css.js new file mode 100644 index 00000000000..7937b32cb51 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/index-with-css.js @@ -0,0 +1,5 @@ +import {a} from './a'; +import './a.css'; + +console.log('index.js'); +console.log(a); diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/index.html b/packages/core/integration-tests/test/integration/incremental-bundling/index.html new file mode 100644 index 00000000000..f1776e9c2ae --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/index.html @@ -0,0 +1,9 @@ + + + + Test + + + + + diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/index.js b/packages/core/integration-tests/test/integration/incremental-bundling/index.js new file mode 100644 index 00000000000..79e20ce45f9 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/index.js @@ -0,0 +1,4 @@ +import {a} from './a'; + +console.log('index.js'); +console.log(a); diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/multi-symbol-util.js b/packages/core/integration-tests/test/integration/incremental-bundling/multi-symbol-util.js new file mode 100644 index 00000000000..659c6321df6 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/multi-symbol-util.js @@ -0,0 +1,3 @@ +export * from "./a.js"; +export * from "./b.js"; +export * from "./c.js"; diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-bundler-test/index.js b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-bundler-test/index.js new file mode 100644 index 00000000000..6fe12230859 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-bundler-test/index.js @@ -0,0 +1,39 @@ +// Spying on the Bundler, it did not like the imports +let CONFIG = Symbol.for('parcel-plugin-config'); + +class Bundler { + constructor(opts) { + this[CONFIG] = opts; + } +} + +// A very dumb bundler just for tests... (probably ßdoesn't produce working code) +module.exports = new Bundler({ + bundle({bundleGraph}) { + bundleGraph.traverse((node, context) => { + if (node.type !== 'dependency') { + return context; + } + + let dependency = node.value; + let assets = bundleGraph.getDependencyAssets(dependency); + let bundleGroup = bundleGraph.createBundleGroup(dependency, dependency.target || context.target); + + for (let asset of assets) { + let bundle = bundleGraph.createBundle({ + entryAsset: asset, + isEntry: Boolean(dependency.isEntry), + target: bundleGroup.target, + }); + + bundleGraph.addAssetGraphToBundle(asset, bundle); + bundleGraph.addBundleToBundleGroup(bundle, bundleGroup); + } + + return { + target: dependency.target || context.target + }; + }); + }, + optimize() {} +}); diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-bundler-test/package.json b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-bundler-test/package.json new file mode 100644 index 00000000000..ee742e8db07 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-bundler-test/package.json @@ -0,0 +1,4 @@ +{ + "name": "parcel-bundler-test", + "version": "1.2.3" +} diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-namer-test/index.js b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-namer-test/index.js new file mode 100644 index 00000000000..b0195e3f85e --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-namer-test/index.js @@ -0,0 +1,7 @@ +const {Namer} = require('@parcel/plugin'); + +module.exports = new Namer({ + name({bundle}) { + return bundle.id + '.' + bundle.type; + } +}); diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-namer-test/package.json b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-namer-test/package.json new file mode 100644 index 00000000000..afb7cef75f9 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-namer-test/package.json @@ -0,0 +1,4 @@ +{ + "name": "parcel-namer-test", + "version": "1.2.3" +} diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-runtime-test/index.js b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-runtime-test/index.js new file mode 100644 index 00000000000..3a2a34e6f43 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-runtime-test/index.js @@ -0,0 +1,11 @@ +const {Runtime} = require('@parcel/plugin'); + +module.exports = new Runtime({ + apply({bundle, options}) { + return { + filePath: options.projectRoot + '/runtime.js', + code: 'global.runtime_test = true;', + isEntry: true, + }; + } +}); diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-runtime-test/package.json b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-runtime-test/package.json new file mode 100644 index 00000000000..484cf226f89 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/node_modules/parcel-runtime-test/package.json @@ -0,0 +1,4 @@ +{ + "name": "parcel-runtime-test", + "version": "1.2.3" +} diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/package.json b/packages/core/integration-tests/test/integration/incremental-bundling/package.json new file mode 100644 index 00000000000..1628a715116 --- /dev/null +++ b/packages/core/integration-tests/test/integration/incremental-bundling/package.json @@ -0,0 +1,11 @@ +{ + "@parcel/bundler-default": { + "minBundleSize": 0 + }, + "sideEffects": [ + "index-multi-symbol.js", + "index-export.js", + "index-with-css.js", + "index.js" + ] +} diff --git a/packages/core/integration-tests/test/integration/incremental-bundling/yarn.lock b/packages/core/integration-tests/test/integration/incremental-bundling/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/parcel/src/cli.js b/packages/core/parcel/src/cli.js index 8cdcb04ff54..f0665c3f208 100755 --- a/packages/core/parcel/src/cli.js +++ b/packages/core/parcel/src/cli.js @@ -143,6 +143,10 @@ let serve = program '--lazy', 'Build async bundles on demand, when requested in the browser', ) + .option( + '--no-incremental', + 'Disables bundling skipping. Default builds are faster when modifying a file without adding or removing dependencies', + ) .action(runCommand); applyOptions(serve, hmrOptions); @@ -478,6 +482,7 @@ async function normalizeOptions( logLevel: command.logLevel, shouldProfile: command.profile, shouldBuildLazily: command.lazy, + shouldBundleIncrementally: command.incremental ? true : false, // default is now true, turn off with "--no-incremental" detailedReport: command.detailedReport != null ? { diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index 3fadfe85824..5dcf7a7e66e 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -113,6 +113,8 @@ export function getParcelOptions( entries, shouldDisableCache: true, logLevel: 'none', + shouldBundleIncrementally: + process.env.NO_INCREMENTAL == null ? true : false, defaultConfig: path.join( __dirname, process.env.PARCEL_TEST_EXPERIMENTAL_BUNDLER == null diff --git a/packages/core/types/index.js b/packages/core/types/index.js index a9153efb6de..abe9522ed85 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -300,6 +300,7 @@ export type InitialParcelOptions = {| +shouldProfile?: boolean, +shouldPatchConsole?: boolean, +shouldBuildLazily?: boolean, + +shouldBundleIncrementally?: boolean, +inputFS?: FileSystem, +outputFS?: FileSystem,