diff --git a/packages/core/core/src/InternalConfig.js b/packages/core/core/src/InternalConfig.js index a8f819e11f7..6ba525b58ef 100644 --- a/packages/core/core/src/InternalConfig.js +++ b/packages/core/core/src/InternalConfig.js @@ -25,6 +25,7 @@ type ConfigOpts = {| invalidateOnOptionChange?: Set, devDeps?: Array, invalidateOnStartup?: boolean, + invalidateOnBuild?: boolean, |}; export function createConfig({ @@ -39,6 +40,7 @@ export function createConfig({ invalidateOnOptionChange, devDeps, invalidateOnStartup, + invalidateOnBuild, }: ConfigOpts): Config { let environment = env ?? createEnvironment(); return { @@ -59,5 +61,6 @@ export function createConfig({ invalidateOnOptionChange: invalidateOnOptionChange ?? new Set(), devDeps: devDeps ?? [], invalidateOnStartup: invalidateOnStartup ?? false, + invalidateOnBuild: invalidateOnBuild ?? false, }; } diff --git a/packages/core/core/src/PackagerRunner.js b/packages/core/core/src/PackagerRunner.js index 3c18a16c10b..e31746f51f2 100644 --- a/packages/core/core/src/PackagerRunner.js +++ b/packages/core/core/src/PackagerRunner.js @@ -38,6 +38,7 @@ import BundleGraph, { bundleGraphToInternalBundleGraph, } from './public/BundleGraph'; import PluginOptions from './public/PluginOptions'; +import PublicConfig from './public/Config'; import {PARCEL_VERSION, HASH_REF_PREFIX, HASH_REF_REGEX} from './constants'; import { fromProjectPath, @@ -50,7 +51,7 @@ import { loadPluginConfig, getConfigHash, getConfigRequests, - type PluginWithLoadConfig, + type PluginWithBundleConfig, } from './requests/ConfigRequest'; import { createDevDependency, @@ -143,12 +144,20 @@ export default class PackagerRunner { ): Promise { invalidateDevDeps(invalidDevDeps, this.options, this.config); - let configs = await this.loadConfigs(bundleGraph, bundle); + let {configs, bundleConfigs} = await this.loadConfigs(bundleGraph, bundle); let bundleInfo = - (await this.getBundleInfoFromCache(bundleGraph, bundle, configs)) ?? - (await this.getBundleInfo(bundle, bundleGraph, configs)); - - let configRequests = getConfigRequests([...configs.values()]); + (await this.getBundleInfoFromCache( + bundleGraph, + bundle, + configs, + bundleConfigs, + )) ?? + (await this.getBundleInfo(bundle, bundleGraph, configs, bundleConfigs)); + + let configRequests = getConfigRequests([ + ...configs.values(), + ...bundleConfigs.values(), + ]); let devDepRequests = getWorkerDevDepRequests([ ...this.devDepRequests.values(), ]); @@ -164,70 +173,110 @@ export default class PackagerRunner { async loadConfigs( bundleGraph: InternalBundleGraph, bundle: InternalBundle, - ): Promise> { + ): Promise<{| + configs: Map, + bundleConfigs: Map, + |}> { let configs = new Map(); + let bundleConfigs = new Map(); - await this.loadConfig(bundle, configs); + await this.loadConfig(bundleGraph, bundle, configs, bundleConfigs); for (let inlineBundle of bundleGraph.getInlineBundles(bundle)) { - await this.loadConfig(inlineBundle, configs); + await this.loadConfig(bundleGraph, inlineBundle, configs, bundleConfigs); } - return configs; + return {configs, bundleConfigs}; } async loadConfig( + bundleGraph: InternalBundleGraph, bundle: InternalBundle, configs: Map, + bundleConfigs: Map, ): Promise { let name = nullthrows(bundle.name); let plugin = await this.config.getPackager(name); - await this.loadPluginConfig(plugin, configs); + await this.loadPluginConfig( + bundleGraph, + bundle, + plugin, + configs, + bundleConfigs, + ); let optimizers = await this.config.getOptimizers(name, bundle.pipeline); - for (let optimizer of optimizers) { - await this.loadPluginConfig(optimizer, configs); + await this.loadPluginConfig( + bundleGraph, + bundle, + optimizer, + configs, + bundleConfigs, + ); } } - async loadPluginConfig( + async loadPluginConfig( + bundleGraph: InternalBundleGraph, + bundle: InternalBundle, plugin: LoadedPlugin, configs: Map, + bundleConfigs: Map, ): Promise { - if (configs.has(plugin.name)) { - return; - } + if (!configs.has(plugin.name)) { + // Only load config for a plugin once per build. + let existing = pluginConfigs.get(plugin.name); + if (existing != null) { + configs.set(plugin.name, existing); + } else { + if (plugin.plugin.loadConfig != null) { + let config = createConfig({ + plugin: plugin.name, + searchPath: toProjectPathUnsafe('index'), + }); + + await loadPluginConfig(plugin, config, this.options); + + for (let devDep of config.devDeps) { + let devDepRequest = await createDevDependency( + devDep, + this.previousDevDeps, + this.options, + ); + let key = `${devDep.specifier}:${fromProjectPath( + this.options.projectRoot, + devDep.resolveFrom, + )}`; + this.devDepRequests.set(key, devDepRequest); + } - // Only load config for a plugin once per build. - let existing = pluginConfigs.get(plugin.name); - if (existing != null) { - configs.set(plugin.name, existing); - return; + pluginConfigs.set(plugin.name, config); + configs.set(plugin.name, config); + } + } } - if (plugin.plugin.loadConfig != null) { + let loadBundleConfig = plugin.plugin.loadBundleConfig; + if (!bundleConfigs.has(plugin.name) && loadBundleConfig != null) { let config = createConfig({ plugin: plugin.name, - searchPath: toProjectPathUnsafe('index'), + searchPath: joinProjectPath( + bundle.target.distDir, + bundle.name ?? bundle.id, + ), }); - - await loadPluginConfig(plugin, config, this.options); - - for (let devDep of config.devDeps) { - let devDepRequest = await createDevDependency( - devDep, - this.previousDevDeps, + config.result = await loadBundleConfig({ + bundle: NamedBundle.get(bundle, bundleGraph, this.options), + bundleGraph: new BundleGraph( + bundleGraph, + NamedBundle.get.bind(NamedBundle), this.options, - ); - let key = `${devDep.specifier}:${fromProjectPath( - this.options.projectRoot, - devDep.resolveFrom, - )}`; - this.devDepRequests.set(key, devDepRequest); - } - - pluginConfigs.set(plugin.name, config); - configs.set(plugin.name, config); + ), + config: new PublicConfig(config, this.options), + options: new PluginOptions(this.options), + logger: new PluginLogger({origin: plugin.name}), + }); + bundleConfigs.set(plugin.name, config); } } @@ -235,6 +284,7 @@ export default class PackagerRunner { bundleGraph: InternalBundleGraph, bundle: InternalBundle, configs: Map, + bundleConfigs: Map, ): Async { if (this.options.shouldDisableCache) { return; @@ -244,6 +294,7 @@ export default class PackagerRunner { bundle, bundleGraph, configs, + bundleConfigs, this.previousInvalidations, ); let infoKey = PackagerRunner.getInfoKey(cacheKey); @@ -254,17 +305,23 @@ export default class PackagerRunner { bundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + bundleConfigs: Map, ): Promise { let {type, contents, map} = await this.getBundleResult( bundle, bundleGraph, configs, + bundleConfigs, ); // Recompute cache keys as they may have changed due to dev dependencies. - let cacheKey = await this.getCacheKey(bundle, bundleGraph, configs, [ - ...this.invalidations.values(), - ]); + let cacheKey = await this.getCacheKey( + bundle, + bundleGraph, + configs, + bundleConfigs, + [...this.invalidations.values()], + ); let cacheKeys = { content: PackagerRunner.getContentKey(cacheKey), map: PackagerRunner.getMapKey(cacheKey), @@ -278,12 +335,18 @@ export default class PackagerRunner { bundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + bundleConfigs: Map, ): Promise<{| type: string, contents: Blob, map: ?string, |}> { - let packaged = await this.package(bundle, bundleGraph, configs); + let packaged = await this.package( + bundle, + bundleGraph, + configs, + bundleConfigs, + ); let type = packaged.type ?? bundle.type; let res = await this.optimize( bundle, @@ -292,6 +355,7 @@ export default class PackagerRunner { packaged.contents, packaged.map, configs, + bundleConfigs, ); let map = @@ -319,6 +383,7 @@ export default class PackagerRunner { internalBundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + bundleConfigs: Map, ): Promise { let bundle = NamedBundle.get(internalBundle, bundleGraph, this.options); this.report({ @@ -332,6 +397,7 @@ export default class PackagerRunner { try { return await plugin.package({ config: configs.get(name)?.result, + bundleConfig: bundleConfigs.get(name)?.result, bundle, bundleGraph: new BundleGraph( bundleGraph, @@ -358,6 +424,7 @@ export default class PackagerRunner { // $FlowFixMe bundleGraphToInternalBundleGraph(bundleGraph), configs, + bundleConfigs, ); return {contents: res.contents}; @@ -395,6 +462,7 @@ export default class PackagerRunner { contents: Blob, map?: ?SourceMap, configs: Map, + bundleConfigs: Map, ): Promise { let bundle = NamedBundle.get( internalBundle, @@ -430,6 +498,7 @@ export default class PackagerRunner { try { let next = await optimizer.plugin.optimize({ config: configs.get(optimizer.name)?.result, + bundleConfig: bundleConfigs.get(optimizer.name)?.result, bundle, bundleGraph, contents: optimized.contents, @@ -535,6 +604,7 @@ export default class PackagerRunner { bundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + bundleConfigs: Map, invalidations: Array, ): Promise { let configResults = {}; @@ -547,6 +617,16 @@ export default class PackagerRunner { ); } } + let globalInfoResults = {}; + for (let [pluginName, config] of bundleConfigs) { + if (config) { + globalInfoResults[pluginName] = await getConfigHash( + config, + pluginName, + this.options, + ); + } + } let devDepHashes = await this.getDevDepHashes(bundle); for (let inlineBundle of bundleGraph.getInlineBundles(bundle)) { @@ -565,6 +645,7 @@ export default class PackagerRunner { bundle.target.publicUrl + bundleGraph.getHash(bundle) + JSON.stringify(configResults) + + JSON.stringify(globalInfoResults) + this.options.mode, ); } diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js index ea0b1219072..8688c081716 100644 --- a/packages/core/core/src/Parcel.js +++ b/packages/core/core/src/Parcel.js @@ -261,6 +261,8 @@ export default class Parcel { type: 'buildStart', }); + this.#requestTracker.graph.invalidateOnBuildNodes(); + let request = createParcelBuildRequest({ optionsRef: this.#optionsRef, requestedAssetIds: this.#requestedAssetIds, diff --git a/packages/core/core/src/ParcelConfig.js b/packages/core/core/src/ParcelConfig.js index cec4ebd4f0a..c69a5f0a9a0 100644 --- a/packages/core/core/src/ParcelConfig.js +++ b/packages/core/core/src/ParcelConfig.js @@ -253,7 +253,7 @@ export default class ParcelConfig { async getPackager( filePath: FilePath, - ): Promise>> { + ): Promise>> { let packager = this.matchGlobMap( toProjectPathUnsafe(filePath), this.packagers, @@ -265,7 +265,7 @@ export default class ParcelConfig { '/packagers', ); } - return this.loadPlugin>(packager); + return this.loadPlugin>(packager); } _getOptimizerNodes( @@ -298,13 +298,13 @@ export default class ParcelConfig { getOptimizers( filePath: FilePath, pipeline: ?string, - ): Promise>>> { + ): Promise>>> { let optimizers = this._getOptimizerNodes(filePath, pipeline); if (optimizers.length === 0) { return Promise.resolve([]); } - return this.loadPlugins>(optimizers); + return this.loadPlugins>(optimizers); } async getCompressors( diff --git a/packages/core/core/src/RequestTracker.js b/packages/core/core/src/RequestTracker.js index 00a950e00dd..08e5c73b453 100644 --- a/packages/core/core/src/RequestTracker.js +++ b/packages/core/core/src/RequestTracker.js @@ -70,6 +70,7 @@ type RequestGraphOpts = {| envNodeIds: Set, optionNodeIds: Set, unpredicatableNodeIds: Set, + invalidateOnBuildNodeIds: Set, |}; type SerializedRequestGraph = {| @@ -80,6 +81,7 @@ type SerializedRequestGraph = {| envNodeIds: Set, optionNodeIds: Set, unpredicatableNodeIds: Set, + invalidateOnBuildNodeIds: Set, |}; type FileNode = {|id: ContentKey, +type: 'file', value: InternalFile|}; @@ -135,6 +137,7 @@ export type RunAPI = {| invalidateOnFileDelete: ProjectPath => void, invalidateOnFileUpdate: ProjectPath => void, invalidateOnStartup: () => void, + invalidateOnBuild: () => void, invalidateOnEnvChange: string => void, invalidateOnOptionChange: string => void, getInvalidations(): Array, @@ -216,6 +219,7 @@ export class RequestGraph extends ContentGraph< // Unpredictable nodes are requests that cannot be predicted whether they should rerun based on // filesystem changes alone. They should rerun on each startup of Parcel. unpredicatableNodeIds: Set = new Set(); + invalidateOnBuildNodeIds: Set = new Set(); // $FlowFixMe[prop-missing] static deserialize(opts: RequestGraphOpts): RequestGraph { @@ -227,6 +231,7 @@ export class RequestGraph extends ContentGraph< deserialized.envNodeIds = opts.envNodeIds; deserialized.optionNodeIds = opts.optionNodeIds; deserialized.unpredicatableNodeIds = opts.unpredicatableNodeIds; + deserialized.invalidateOnBuildNodeIds = opts.invalidateOnBuildNodeIds; return deserialized; } @@ -240,6 +245,7 @@ export class RequestGraph extends ContentGraph< envNodeIds: this.envNodeIds, optionNodeIds: this.optionNodeIds, unpredicatableNodeIds: this.unpredicatableNodeIds, + invalidateOnBuildNodeIds: this.invalidateOnBuildNodeIds, }; } @@ -267,6 +273,7 @@ export class RequestGraph extends ContentGraph< this.incompleteNodeIds.delete(nodeId); this.incompleteNodePromises.delete(nodeId); this.unpredicatableNodeIds.delete(nodeId); + this.invalidateOnBuildNodeIds.delete(nodeId); let node = nullthrows(this.getNode(nodeId)); if (node.type === 'glob') { this.globNodeIds.delete(nodeId); @@ -326,6 +333,14 @@ export class RequestGraph extends ContentGraph< } } + invalidateOnBuildNodes() { + for (let nodeId of this.invalidateOnBuildNodeIds) { + let node = nullthrows(this.getNode(nodeId)); + invariant(node.type !== 'file' && node.type !== 'glob'); + this.invalidateNode(nodeId, STARTUP); + } + } + invalidateEnvNodes(env: EnvMap) { for (let nodeId of this.envNodeIds) { let node = nullthrows(this.getNode(nodeId)); @@ -504,6 +519,11 @@ export class RequestGraph extends ContentGraph< this.unpredicatableNodeIds.add(requestNodeId); } + invalidateOnBuild(requestNodeId: NodeId) { + this.getRequestNode(requestNodeId); + this.invalidateOnBuildNodeIds.add(requestNodeId); + } + invalidateOnEnvChange( requestNodeId: NodeId, env: string, @@ -552,6 +572,7 @@ export class RequestGraph extends ContentGraph< clearInvalidations(nodeId: NodeId) { this.unpredicatableNodeIds.delete(nodeId); + this.invalidateOnBuildNodeIds.delete(nodeId); this.replaceNodeIdsConnectedTo( nodeId, [], @@ -993,6 +1014,7 @@ export default class RequestTracker { invalidateOnFileUpdate: filePath => this.graph.invalidateOnFileUpdate(requestId, filePath), invalidateOnStartup: () => this.graph.invalidateOnStartup(requestId), + invalidateOnBuild: () => this.graph.invalidateOnBuild(requestId), invalidateOnEnvChange: env => this.graph.invalidateOnEnvChange(requestId, env, this.options.env[env]), invalidateOnOptionChange: option => @@ -1120,6 +1142,7 @@ async function loadRequestGraph(options): Async { opts, ); requestGraph.invalidateUnpredictableNodes(); + requestGraph.invalidateOnBuildNodes(); requestGraph.invalidateEnvNodes(options.env); requestGraph.invalidateOptionNodes(options); requestGraph.respondToFSEvents( diff --git a/packages/core/core/src/public/Config.js b/packages/core/core/src/public/Config.js index 1b2eba4edb8..0b1d34242b9 100644 --- a/packages/core/core/src/public/Config.js +++ b/packages/core/core/src/public/Config.js @@ -112,6 +112,10 @@ export default class PublicConfig implements IConfig { this.#config.invalidateOnStartup = true; } + invalidateOnBuild() { + this.#config.invalidateOnBuild = true; + } + async getConfigFrom( searchPath: FilePath, fileNames: Array, diff --git a/packages/core/core/src/requests/ConfigRequest.js b/packages/core/core/src/requests/ConfigRequest.js index 86861d2c7bd..620034b9b17 100644 --- a/packages/core/core/src/requests/ConfigRequest.js +++ b/packages/core/core/src/requests/ConfigRequest.js @@ -4,6 +4,8 @@ import type { Config as IConfig, PluginOptions as IPluginOptions, PluginLogger as IPluginLogger, + NamedBundle as INamedBundle, + BundleGraph as IBundleGraph, } from '@parcel/types'; import type { Config, @@ -32,6 +34,22 @@ export type PluginWithLoadConfig = { ... }; +export type PluginWithBundleConfig = { + loadConfig?: ({| + config: IConfig, + options: IPluginOptions, + logger: IPluginLogger, + |}) => Async, + loadBundleConfig?: ({| + bundle: INamedBundle, + bundleGraph: IBundleGraph, + config: IConfig, + options: IPluginOptions, + logger: IPluginLogger, + |}) => Async, + ... +}; + export type ConfigRequest = { id: string, invalidateOnFileChange: Set, @@ -39,6 +57,7 @@ export type ConfigRequest = { invalidateOnEnvChange: Set, invalidateOnOptionChange: Set, invalidateOnStartup: boolean, + invalidateOnBuild: boolean, ... }; @@ -81,6 +100,7 @@ export async function runConfigRequest( invalidateOnEnvChange, invalidateOnOptionChange, invalidateOnStartup, + invalidateOnBuild, } = configRequest; // If there are no invalidations, then no need to create a node. @@ -88,7 +108,8 @@ export async function runConfigRequest( invalidateOnFileChange.size === 0 && invalidateOnFileCreate.length === 0 && invalidateOnOptionChange.size === 0 && - !invalidateOnStartup + !invalidateOnStartup && + !invalidateOnBuild ) { return; } @@ -117,6 +138,10 @@ export async function runConfigRequest( if (invalidateOnStartup) { api.invalidateOnStartup(); } + + if (invalidateOnBuild) { + api.invalidateOnBuild(); + } }, input: null, }); @@ -178,7 +203,8 @@ export function getConfigRequests( config.invalidateOnFileCreate.length > 0 || config.invalidateOnEnvChange.size > 0 || config.invalidateOnOptionChange.size > 0 || - config.invalidateOnStartup + config.invalidateOnStartup || + config.invalidateOnBuild ); }) .map(config => ({ @@ -188,5 +214,6 @@ export function getConfigRequests( invalidateOnEnvChange: config.invalidateOnEnvChange, invalidateOnOptionChange: config.invalidateOnOptionChange, invalidateOnStartup: config.invalidateOnStartup, + invalidateOnBuild: config.invalidateOnBuild, })); } diff --git a/packages/core/core/src/requests/PackageRequest.js b/packages/core/core/src/requests/PackageRequest.js index e02c605fd30..209f36ebc31 100644 --- a/packages/core/core/src/requests/PackageRequest.js +++ b/packages/core/core/src/requests/PackageRequest.js @@ -7,7 +7,7 @@ import type {SharedReference} from '@parcel/workers'; import type {StaticRunOpts} from '../RequestTracker'; import type {Bundle} from '../types'; import type BundleGraph from '../BundleGraph'; -import type {BundleInfo} from '../PackagerRunner'; +import type {BundleInfo, PackageRequestResult} from '../PackagerRunner'; import type {ConfigAndCachePath} from './ParcelConfigRequest'; import nullthrows from 'nullthrows'; @@ -55,7 +55,7 @@ async function run({input, api, farm}: RunInput) { await api.runRequest(createParcelConfigRequest()), ); let {devDepRequests, configRequests, bundleInfo, invalidations} = - await runPackage({ + (await runPackage({ bundle, bundleGraphReference, optionsRef, @@ -63,7 +63,7 @@ async function run({input, api, farm}: RunInput) { previousDevDeps: devDeps, invalidDevDeps, previousInvalidations: api.getInvalidations(), - }); + }): PackageRequestResult); for (let devDepRequest of devDepRequests) { await runDevDepRequest(api, devDepRequest); @@ -90,6 +90,7 @@ async function run({input, api, farm}: RunInput) { } } + // $FlowFixMe[cannot-write] time is marked read-only, but this is the exception bundleInfo.time = Date.now() - start; api.storeResult(bundleInfo); diff --git a/packages/core/core/src/requests/ValidationRequest.js b/packages/core/core/src/requests/ValidationRequest.js index d657b88ad68..9b7acdd9d10 100644 --- a/packages/core/core/src/requests/ValidationRequest.js +++ b/packages/core/core/src/requests/ValidationRequest.js @@ -47,12 +47,13 @@ export default function createValidationRequest( }); // Schedule validations on workers for all plugins that implement the one-asset-at-a-time "validate" method. - let promises = trackedRequestsDesc.map(async request => - (await farm.createHandle('runValidate'))({ - requests: [request], - optionsRef: optionsRef, - configCachePath: cachePath, - }), + let promises = trackedRequestsDesc.map( + async request => + ((await farm.createHandle('runValidate'))({ + requests: [request], + optionsRef: optionsRef, + configCachePath: cachePath, + }): void), ); // Skip sending validation requests if no validators were configured diff --git a/packages/core/core/src/requests/WriteBundlesRequest.js b/packages/core/core/src/requests/WriteBundlesRequest.js index bdfcbcf1360..799caa346ed 100644 --- a/packages/core/core/src/requests/WriteBundlesRequest.js +++ b/packages/core/core/src/requests/WriteBundlesRequest.js @@ -92,6 +92,7 @@ async function run({input, api, farm, options}: RunInput) { bundleGraphReference: ref, optionsRef, }); + let info = await api.runRequest(request); bundleInfoMap[bundle.id] = info; diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index 3acd4df21eb..82382ad1295 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -436,6 +436,7 @@ export type Config = {| invalidateOnOptionChange: Set, devDeps: Array, invalidateOnStartup: boolean, + invalidateOnBuild: boolean, |}; export type EntryRequest = {| diff --git a/packages/core/core/test/TargetRequest.test.js b/packages/core/core/test/TargetRequest.test.js index 63205e98cf2..b8db4e11e69 100644 --- a/packages/core/core/test/TargetRequest.test.js +++ b/packages/core/core/test/TargetRequest.test.js @@ -77,6 +77,7 @@ describe('TargetResolver', () => { invalidateOnEnvChange() {}, invalidateOnOptionChange() {}, invalidateOnStartup() {}, + invalidateOnBuild() {}, getInvalidations() { return []; }, diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/.parcelrc b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/.parcelrc new file mode 100644 index 00000000000..8d13690ba88 --- /dev/null +++ b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/.parcelrc @@ -0,0 +1,9 @@ +{ + "extends": "@parcel/config-default", + "transformers": { + "*.txt": ["@parcel/transformer-raw"] + }, + "packagers": { + "*.txt": "parcel-packager-config" + } +} diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/a.txt b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/a.txt new file mode 100644 index 00000000000..a661f8d4fff --- /dev/null +++ b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/a.txt @@ -0,0 +1 @@ +Hello from a diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/b.txt b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/b.txt new file mode 100644 index 00000000000..a5ae4447f96 --- /dev/null +++ b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/b.txt @@ -0,0 +1 @@ +Hello from b diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/index.2.html b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/index.2.html new file mode 100644 index 00000000000..8b9833e4eaf --- /dev/null +++ b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/index.2.html @@ -0,0 +1,2 @@ +a +a diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/index.html b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/index.html new file mode 100644 index 00000000000..4d38cd63b80 --- /dev/null +++ b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/index.html @@ -0,0 +1 @@ +a diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/node_modules/parcel-packager-config/index.js b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/node_modules/parcel-packager-config/index.js new file mode 100644 index 00000000000..34b182ac2e9 --- /dev/null +++ b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/node_modules/parcel-packager-config/index.js @@ -0,0 +1,24 @@ +// @flow strict-local + +const invariant = require('assert'); +const {Packager} = require('@parcel/plugin'); + +module.exports = (new Packager({ + loadBundleConfig({bundle, bundleGraph, config}) { + config.invalidateOnBuild(); + let x = bundleGraph + .getBundles() + .filter(b => b.type === 'txt' && b.needsStableName) + .map(b => b.name); + console.log(bundle.name, x); + return x; + }, + async package({bundle, bundleConfig}) { + let contents = await bundle.getMainEntry()?.getCode(); + invariant(contents != null); + + return { + contents: `Bundles: ${bundleConfig.join(',')}. Contents: ${contents}`, + }; + }, +}) /*: Packager */); diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/node_modules/parcel-packager-config/package.json b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/node_modules/parcel-packager-config/package.json new file mode 100644 index 00000000000..3eda97b42bc --- /dev/null +++ b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/node_modules/parcel-packager-config/package.json @@ -0,0 +1,13 @@ +{ + "name": "parcel-packager-config", + "version": "1.0.0", + "private": true, + "main": "index.js", + "engines": { + "parcel": "^2.0.0-beta.1" + }, + "dependencies": { + "@parcel/plugin": "^2.0.0-beta.1", + "@parcel/utils": "^2.0.0-beta.1" + } +} diff --git a/packages/core/integration-tests/test/integration/packager-loadBundleConfig/yarn.lock b/packages/core/integration-tests/test/integration/packager-loadBundleConfig/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/integration-tests/test/plugin.js b/packages/core/integration-tests/test/plugin.js index 5b86ca5fbe9..3310449a55a 100644 --- a/packages/core/integration-tests/test/plugin.js +++ b/packages/core/integration-tests/test/plugin.js @@ -1,14 +1,17 @@ // @flow import assert from 'assert'; +import invariant from 'assert'; import path from 'path'; import nullthrows from 'nullthrows'; import { assertBundles, bundle, + bundler, distDir, findAsset, findDependency, + getNextBuild, outputFS as fs, overlayFS, run, @@ -87,6 +90,67 @@ parcel-transformer-b`, assert.deepEqual(calls, [1234]); }); + it('invalidate the cache based on loadBundleConfig in a packager', async function () { + let fixture = path.join( + __dirname, + '/integration/packager-loadBundleConfig', + ); + let entry = path.join(fixture, 'index.html'); + + let b = await bundler(entry, { + inputFS: overlayFS, + shouldDisableCache: false, + }); + + let subscription = await b.watch(); + try { + let bundleEvent = await getNextBuild(b); + invariant(bundleEvent.type === 'buildSuccess'); + + assert.strictEqual( + await overlayFS.readFile( + nullthrows( + bundleEvent.bundleGraph + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith('a.txt')), + ).filePath, + 'utf8', + ), + `Bundles: a.txt. Contents: Hello from a\n`, + ); + + await overlayFS.copyFile(path.join(fixture, 'index.2.html'), entry); + + bundleEvent = await getNextBuild(b); + invariant(bundleEvent.type === 'buildSuccess'); + + assert.strictEqual( + await overlayFS.readFile( + nullthrows( + bundleEvent.bundleGraph + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith('a.txt')), + ).filePath, + 'utf8', + ), + `Bundles: a.txt,b.txt. Contents: Hello from a\n`, + ); + assert.strictEqual( + await overlayFS.readFile( + nullthrows( + bundleEvent.bundleGraph + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith('b.txt')), + ).filePath, + 'utf8', + ), + `Bundles: a.txt,b.txt. Contents: Hello from b\n`, + ); + } finally { + await subscription.unsubscribe(); + } + }); + it('invalidate the cache based on loadConfig in a packager', async function () { let fixture = path.join(__dirname, '/integration/packager-loadConfig'); let entry = path.join(fixture, 'index.txt'); diff --git a/packages/core/plugin/src/PluginAPI.js b/packages/core/plugin/src/PluginAPI.js index 07d7ea1a0e5..d0bcd49f7f0 100644 --- a/packages/core/plugin/src/PluginAPI.js +++ b/packages/core/plugin/src/PluginAPI.js @@ -58,14 +58,14 @@ export class Validator { } export class Packager { - constructor(opts: PackagerOpts) { + constructor(opts: PackagerOpts) { // $FlowFixMe this[CONFIG] = opts; } } export class Optimizer { - constructor(opts: OptimizerOpts) { + constructor(opts: OptimizerOpts) { // $FlowFixMe this[CONFIG] = opts; } diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 29aa68ee9fd..a9153efb6de 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -808,8 +808,10 @@ export interface Config { invalidateOnFileCreate(FileCreateInvalidation): void; /** Invalidates the config when the given environment variable changes. */ invalidateOnEnvChange(string): void; - /** Invalidates the config when Parcel restarts. */ + /** Invalidates the config only when Parcel restarts. */ invalidateOnStartup(): void; + /** Invalidates the config on every build. */ + invalidateOnBuild(): void; /** * Adds a dev dependency to the config. If the dev dependency or any of its * dependencies change, the config will be invalidated. @@ -1591,18 +1593,26 @@ export type Runtime = {| /** * @section packager */ -export type Packager = {| +export type Packager = {| loadConfig?: ({| config: Config, options: PluginOptions, logger: PluginLogger, - |}) => Promise | ConfigType, + |}) => Async, + loadBundleConfig?: ({| + bundle: NamedBundle, + bundleGraph: BundleGraph, + config: Config, + options: PluginOptions, + logger: PluginLogger, + |}) => Async, package({| bundle: NamedBundle, bundleGraph: BundleGraph, options: PluginOptions, logger: PluginLogger, config: ConfigType, + bundleConfig: BundleConfigType, getInlineBundleContents: ( Bundle, BundleGraph, @@ -1614,12 +1624,19 @@ export type Packager = {| /** * @section optimizer */ -export type Optimizer = {| +export type Optimizer = {| loadConfig?: ({| config: Config, options: PluginOptions, logger: PluginLogger, - |}) => Promise | ConfigType, + |}) => Async, + loadBundleConfig?: ({| + bundle: NamedBundle, + bundleGraph: BundleGraph, + config: Config, + options: PluginOptions, + logger: PluginLogger, + |}) => Async, optimize({| bundle: NamedBundle, bundleGraph: BundleGraph, @@ -1628,6 +1645,7 @@ export type Optimizer = {| options: PluginOptions, logger: PluginLogger, config: ConfigType, + bundleConfig: BundleConfigType, getSourceMapReference: (map: ?SourceMap) => Async, |}): Async, |};