diff --git a/src/Bundle.ts b/src/Bundle.ts index 53b1f2b87f4..290dc6b7315 100644 --- a/src/Bundle.ts +++ b/src/Bundle.ts @@ -41,7 +41,29 @@ export default class Bundle { private readonly graph: Graph ) {} + static async fromModules( + outputOptions: NormalizedOutputOptions, + unsetOptions: ReadonlySet, + inputOptions: NormalizedInputOptions, + pluginDriver: PluginDriver, + graph: Graph, + entryModules: Module[], + modulesById: Map + ) { + const bundle = new Bundle(outputOptions, unsetOptions, inputOptions, pluginDriver, graph); + + return bundle.generateInternal(false, entryModules, modulesById); + } + async generate(isWrite: boolean): Promise { + return this.generateInternal(isWrite); + } + + async generateInternal( + isWrite: boolean, + entryModules = this.graph.entryModules, + modulesById = this.graph.modulesById + ) { timeStart('GENERATE', 1); const outputBundle: OutputBundleWithPlaceholders = Object.create(null); this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions, this.facadeChunkByModule); @@ -49,7 +71,7 @@ export default class Bundle { await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]); timeStart('generate chunks', 2); - const chunks = await this.generateChunks(); + const chunks = await this.generateChunks(entryModules, modulesById); if (chunks.length > 1) { validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onwarn); } @@ -155,13 +177,16 @@ export default class Bundle { } } - private assignManualChunks(getManualChunk: GetManualChunk): Map { + private assignManualChunks( + getManualChunk: GetManualChunk, + modulesById: Map + ): Map { const manualChunkAliasesWithEntry: [alias: string, module: Module][] = []; const manualChunksApi = { - getModuleIds: () => this.graph.modulesById.keys(), + getModuleIds: () => modulesById.keys(), getModuleInfo: this.graph.getModuleInfo }; - for (const module of this.graph.modulesById.values()) { + for (const module of modulesById.values()) { if (module instanceof Module) { const manualChunkAlias = getManualChunk(module.id, manualChunksApi); if (typeof manualChunkAlias === 'string') { @@ -203,22 +228,25 @@ export default class Bundle { this.pluginDriver.finaliseAssets(); } - private async generateChunks(): Promise { + private async generateChunks( + entryModules = this.graph.entryModules, + modulesById = this.graph.modulesById + ): Promise { const { manualChunks } = this.outputOptions; const manualChunkAliasByEntry = typeof manualChunks === 'object' ? await this.addManualChunks(manualChunks) - : this.assignManualChunks(manualChunks); + : this.assignManualChunks(manualChunks, modulesById); const chunks: Chunk[] = []; const chunkByModule = new Map(); for (const { alias, modules } of this.outputOptions.inlineDynamicImports - ? [{ alias: null, modules: getIncludedModules(this.graph.modulesById) }] + ? [{ alias: null, modules: getIncludedModules(modulesById) }] : this.outputOptions.preserveModules - ? getIncludedModules(this.graph.modulesById).map(module => ({ + ? getIncludedModules(modulesById).map(module => ({ alias: null, modules: [module] })) - : getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)) { + : getChunkAssignments(entryModules, manualChunkAliasByEntry)) { sortByExecutionOrder(modules); const chunk = new Chunk( modules, @@ -226,7 +254,7 @@ export default class Bundle { this.outputOptions, this.unsetOptions, this.pluginDriver, - this.graph.modulesById, + modulesById, chunkByModule, this.facadeChunkByModule, this.includedNamespaces, diff --git a/src/Graph.ts b/src/Graph.ts index 2bc50b79764..a5369d7dae6 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -135,6 +135,18 @@ export default class Graph { return ast; } + ensureModule(module: Module | ExternalModule) { + // Make sure that the same module / external module can only be added to the + // graph once since it may be requested multiple times over the life of a + // service. + const modules: Array = + module instanceof Module ? this.modules : this.externalModules; + + if (!modules.includes(module)) { + modules.push(module); + } + } + getCache(): RollupCache { // handle plugin cache eviction for (const name in this.pluginCache) { @@ -166,11 +178,7 @@ export default class Graph { throw new Error('You must supply options.input to rollup'); } for (const module of this.modulesById.values()) { - if (module instanceof Module) { - this.modules.push(module); - } else { - this.externalModules.push(module); - } + this.ensureModule(module); } } diff --git a/src/Module.ts b/src/Module.ts index 1e7c4ada2ba..df7aa00f6d9 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -676,12 +676,16 @@ export default class Module { this.addModulesToImportDescriptions(this.reexportDescriptions); const externalExportAllModules: ExternalModule[] = []; for (const source of this.exportAllSources) { - const module = this.graph.modulesById.get(this.resolvedIds[source].id)!; + const module = this.graph.modulesById.get(this.resolvedIds[source].id); if (module instanceof ExternalModule) { externalExportAllModules.push(module); - continue; + } else if (module instanceof Module) { + this.exportAllModules.push(module); + } else { + externalExportAllModules.push( + new ExternalModule(this.options, this.resolvedIds[source].id, true, {}, true) + ); } - this.exportAllModules.push(module); } this.exportAllModules.push(...externalExportAllModules); } @@ -1019,7 +1023,8 @@ export default class Module { ): void { for (const specifier of importDescription.values()) { const { id } = this.resolvedIds[specifier.source]; - specifier.module = this.graph.modulesById.get(id)!; + specifier.module = + this.graph.modulesById.get(id) ?? new ExternalModule(this.options, id, true, {}, true); } } diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 729afd56185..8a5fc27a85b 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -40,6 +40,7 @@ import transform from './utils/transform'; export interface UnresolvedModule { fileName: string | null; id: string; + implicitlyLoadedAfter?: string[]; importer: string | undefined; name: string | null; } @@ -176,12 +177,15 @@ export class ModuleLoader { } public async preloadModule( - resolvedId: { id: string; resolveDependencies?: boolean } & Partial> + resolvedId: { id: string; isEntry?: boolean; resolveDependencies?: boolean } & Partial< + PartialNull + > ): Promise { + const isEntry = resolvedId.isEntry !== false; const module = await this.fetchModule( this.getResolvedIdWithDefaults(resolvedId)!, undefined, - false, + isEntry, resolvedId.resolveDependencies ? RESOLVE_DEPENDENCIES : true ); return module.info; diff --git a/src/browser-entry.ts b/src/browser-entry.ts index 8f03c2d6209..ada21e75244 100644 --- a/src/browser-entry.ts +++ b/src/browser-entry.ts @@ -1,2 +1,2 @@ -export { default as rollup, defineConfig } from './rollup/rollup'; +export { default as rollup, defineConfig, startService } from './rollup/rollup'; export { version as VERSION } from 'package.json'; diff --git a/src/node-entry.ts b/src/node-entry.ts index d2a08e5f631..4a2ff631270 100644 --- a/src/node-entry.ts +++ b/src/node-entry.ts @@ -1,3 +1,3 @@ -export { default as rollup, defineConfig } from './rollup/rollup'; +export { default as rollup, defineConfig, startService } from './rollup/rollup'; export { default as watch } from './watch/watch-proxy'; export { version as VERSION } from 'package.json'; diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index f1981f7f1d5..5da0feddb76 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -23,22 +23,183 @@ import type { RollupBuild, RollupOptions, RollupOutput, + RollupService, RollupWatcher } from './types'; +import Module from '../Module'; export default function rollup(rawInputOptions: GenericConfigObject): Promise { return rollupInternal(rawInputOptions, null); } -export async function rollupInternal( - rawInputOptions: GenericConfigObject, +export async function startService( + { input, ...rawInputOptions }: GenericConfigObject, watcher: RollupWatcher | null -): Promise { +): Promise { + if (input) { + throw new Error( + `The 'input' option must not be specified when starting Rollup in service mode.` + ); + } + + const { graph, inputOptions, unsetInputOptions } = await graphSetup(rawInputOptions, watcher); + + const service: RollupService = { + async build( + input, + { signal, shouldIncludeInBundle: shouldIncludeInBundleFn, ...rawOutputOptions } + ) { + const normalizedInput = input == null ? [] : typeof input === 'string' ? [input] : input; + const seen: Set = new Set(); + const entrypoints = Array.isArray(normalizedInput) + ? [...normalizedInput] + : Object.values(normalizedInput); + + const modulesById: Map = new Map(); + const entryModules: Module[] = []; + + const entrypointIdPromises = entrypoints.map(async source => { + const resolvedId = await graph.moduleLoader.resolveId(source, undefined, {}, true); + + if (!resolvedId) { + throw new Error(`Failed to resolve the entrypoint ${source}`); + } + + if (resolvedId.external) { + throw new Error(`An entrypoint cannot be marked as external ${source}`); + } + + return resolvedId.id; + }); + + const entrypointIds = new Set(await Promise.all(entrypointIdPromises)); + const queue = [...entrypointIds]; + + while (queue.length && signal?.aborted !== true) { + const moduleId = queue.shift()!; + + if (seen.has(moduleId)) { + continue; + } + seen.add(moduleId); + + console.log('preloading', moduleId); + + const moduleInfo = await graph.moduleLoader.preloadModule({ + id: moduleId, + isEntry: false, + resolveDependencies: true + }); + + const module = graph.modulesById.get(moduleInfo.id)!; + + if (!(module instanceof Module)) { + continue; + } + + modulesById.set(module.id, module); + + if (entrypointIds.has(moduleId)) { + entryModules.push(module); + } + + for (const [source, resolvedDependencyId] of Object.entries(module.resolvedIds)) { + // If `source` is nullish, this means it came from a plugin. We want synthetic files + // to be included. + if ( + shouldIncludeInBundleFn && + source != null && + !resolvedDependencyId.id.startsWith('\0') + ) { + if (!shouldIncludeInBundleFn(resolvedDependencyId, source, module.id)) { + continue; + } + } + + queue.push(resolvedDependencyId.id); + } + } + + console.log( + 'finished traversing', + Array.from(modulesById.values()).map(m => m.info.id) + ); + + for (const module of modulesById.values()) { + graph.ensureModule(module); + } + + for (const module of modulesById.values()) { + module.linkImports(); + module.bindReferences(); + module.includeAllInBundle(); + } + + const { + options: outputOptions, + outputPluginDriver, + unsetOptions + } = getOutputOptionsAndPluginDriver( + rawOutputOptions as GenericConfigObject, + graph.pluginDriver, + inputOptions, + unsetInputOptions + ); + + const outputBundle = await Bundle.fromModules( + outputOptions, + unsetOptions, + inputOptions, + outputPluginDriver, + graph, + entryModules, + modulesById + ); + + return createOutput(outputBundle); + }, + async close(err?: any) { + if (service.closed) return; + + service.closed = true; + + if (err) { + const watchFiles = Object.keys(graph.watchFiles); + if (watchFiles.length > 0) { + err.watchFiles = watchFiles; + } + } + + await graph.pluginDriver.hookParallel('buildEnd', err ? [err] : []); + await graph.pluginDriver.hookParallel('closeBundle', []); + }, + closed: false, + getModuleInfo: graph.getModuleInfo, + load(id, options) { + return graph.moduleLoader.preloadModule({ ...options, id }); + }, + resolve(source, importer, options) { + return graph.moduleLoader.resolveId(source, importer, options, false); + } + }; + + await catchUnfinishedHookActions(graph.pluginDriver, async () => { + try { + await graph.pluginDriver.hookParallel('buildStart', [inputOptions]); + } catch (err: any) { + await service.close(err); + throw err; + } + }); + + return service; +} + +async function graphSetup(rawInputOptions: GenericConfigObject, watcher: RollupWatcher | null) { const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions( rawInputOptions, watcher !== null ); - initialiseTimers(inputOptions); const graph = new Graph(inputOptions, watcher); @@ -47,6 +208,25 @@ export async function rollupInternal( delete inputOptions.cache; delete rawInputOptions.cache; + return { + graph, + inputOptions, + unsetInputOptions, + useCache + }; +} + +export async function rollupInternal( + rawInputOptions: GenericConfigObject, + watcher: RollupWatcher | null +): Promise { + const { graph, inputOptions, unsetInputOptions, useCache } = await graphSetup( + rawInputOptions, + watcher + ); + + initialiseTimers(inputOptions); + timeStart('BUILD', 1); await catchUnfinishedHookActions(graph.pluginDriver, async () => { diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index b4c4673212b..5b00f588ec3 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -830,6 +830,29 @@ export interface RollupBuild { write: (options: OutputOptions) => Promise; } +export interface BuildOptions extends OutputOptions { + signal?: { + aborted: boolean; + }; + shouldIncludeInBundle?: (resolvedId: ResolvedId, source: string, fromId: string) => boolean; +} + +export interface RollupService { + build: (input: InputOption, options: BuildOptions) => Promise; + close: (err?: any) => Promise; + closed: boolean; + load: ( + source: string, + options?: { resolveDependencies?: boolean } & Partial> + ) => Promise; + getModuleInfo: GetModuleInfo; + resolve: ( + source: string, + importer?: string, + options?: { custom?: CustomPluginOptions; isEntry?: boolean } + ) => Promise; +} + export interface RollupOptions extends InputOptions { // This is included for compatibility with config files but ignored by rollup.rollup output?: OutputOptions | OutputOptions[]; @@ -841,6 +864,8 @@ export interface MergedRollupOptions extends InputOptions { export function rollup(options: RollupOptions): Promise; +export function startService(options: RollupOptions): Promise; + export interface ChokidarOptions { alwaysStat?: boolean; atomic?: boolean | number;