diff --git a/src/Graph.ts b/src/Graph.ts index 79e3cfbf4e8..72b08503202 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -23,15 +23,10 @@ import { Watcher } from './rollup/types'; import { finaliseAsset } from './utils/assetHooks'; -import { - randomUint8Array, - Uint8ArrayEqual, - Uint8ArrayToHexString, - Uint8ArrayXor -} from './utils/entryHashing'; +import { Uint8ArrayToHexString } from './utils/entryHashing'; import error from './utils/error'; -import { sortByExecutionOrder } from './utils/execution-order'; -import { isRelative, relative, resolve } from './utils/path'; +import { analyzeModuleExecution, sortByExecutionOrder } from './utils/execution-order'; +import { isRelative, resolve } from './utils/path'; import { createPluginDriver, PluginDriver } from './utils/pluginDriver'; import relativeId, { getAliasName } from './utils/relativeId'; import { timeEnd, timeStart } from './utils/timers'; @@ -376,12 +371,24 @@ export default class Graph { this.link(); - const { orderedModules, dynamicImports, dynamicImportAliases } = this.analyseExecution( + const { + orderedModules, + dynamicImports, + dynamicImportAliases, + cyclePaths + } = analyzeModuleExecution( entryModules, !preserveModules && !inlineDynamicImports, inlineDynamicImports, manualChunkModules ); + for (const cyclePath of cyclePaths) { + this.warn({ + code: 'CIRCULAR_DEPENDENCY', + importer: cyclePath[0], + message: `Circular dependency: ${cyclePath.join(' -> ')}` + }); + } if (entryModuleAliases) { for (let i = entryModules.length - 1; i >= 0; i--) { @@ -495,139 +502,6 @@ export default class Graph { ); } - private analyseExecution( - entryModules: Module[], - graphColouring: boolean, - inlineDynamicImports: boolean, - chunkModules?: Record - ) { - let curEntry: Module, curEntryHash: Uint8Array; - const allSeen: { [id: string]: boolean } = {}; - - const orderedModules: Module[] = []; - let analyzedModuleCount = 0; - - const dynamicImports: Module[] = []; - const dynamicImportAliases: string[] = []; - - let parents: { [id: string]: string }; - - const visit = (module: Module) => { - // Track entry point graph colouring by tracing all modules loaded by a given - // entry point and colouring those modules by the hash of its id. Colours are mixed as - // hash xors, providing the unique colouring of the graph into unique hash chunks. - // This is really all there is to automated chunking, the rest is chunk wiring. - if (graphColouring) { - if (!curEntry.chunkAlias) { - Uint8ArrayXor(module.entryPointsHash, curEntryHash); - } else { - // manual chunks are indicated in this phase by having a chunk alias - // they are treated as a single colour in the colouring - // and aren't divisable by future colourings - module.chunkAlias = curEntry.chunkAlias; - module.entryPointsHash = curEntryHash; - } - } - - for (const depModule of module.dependencies) { - if (depModule instanceof ExternalModule) { - depModule.execIndex = analyzedModuleCount++; - continue; - } - - if (depModule.id in parents) { - if (!allSeen[depModule.id]) { - this.warnCycle(depModule.id, module.id, parents); - } - continue; - } - - parents[depModule.id] = module.id; - if (!depModule.isEntryPoint && !depModule.chunkAlias) visit(depModule); - } - - for (const dynamicModule of module.dynamicImportResolutions) { - if (!(dynamicModule.resolution instanceof Module)) continue; - // If the parent module of a dynamic import is to a child module whose graph - // colouring is the same as the parent module, then that dynamic import does - // not need to be treated as a new entry point as it is in the static graph - if ( - !graphColouring || - (!dynamicModule.resolution.chunkAlias && - !Uint8ArrayEqual(dynamicModule.resolution.entryPointsHash, curEntry.entryPointsHash)) - ) { - if (dynamicImports.indexOf(dynamicModule.resolution) === -1) { - dynamicImports.push(dynamicModule.resolution); - dynamicImportAliases.push(dynamicModule.alias); - } - } - } - - if (allSeen[module.id]) return; - allSeen[module.id] = true; - - module.execIndex = analyzedModuleCount++; - orderedModules.push(module); - }; - - if (graphColouring && chunkModules) { - for (const chunkName of Object.keys(chunkModules)) { - curEntryHash = randomUint8Array(10); - - for (curEntry of chunkModules[chunkName]) { - if (curEntry.chunkAlias) { - error({ - code: 'INVALID_CHUNK', - message: `Cannot assign ${relative( - process.cwd(), - curEntry.id - )} to the "${chunkName}" chunk as it is already in the "${curEntry.chunkAlias}" chunk. -Try defining "${chunkName}" first in the manualChunks definitions of the Rollup configuration.` - }); - } - curEntry.chunkAlias = chunkName; - parents = { [curEntry.id]: null }; - visit(curEntry); - } - } - } - - for (curEntry of entryModules) { - curEntry.isEntryPoint = true; - curEntryHash = randomUint8Array(10); - parents = { [curEntry.id]: null }; - visit(curEntry); - } - - // new items can be added during this loop - for (curEntry of dynamicImports) { - if (curEntry.isEntryPoint) continue; - if (!inlineDynamicImports) curEntry.isEntryPoint = true; - curEntryHash = randomUint8Array(10); - parents = { [curEntry.id]: null }; - visit(curEntry); - } - - return { orderedModules, dynamicImports, dynamicImportAliases }; - } - - private warnCycle(id: string, parentId: string, parents: { [id: string]: string | null }) { - const path = [relativeId(id)]; - let curId = parentId; - while (curId !== id) { - path.push(relativeId(curId)); - curId = parents[curId]; - if (!curId) break; - } - path.push(path[0]); - path.reverse(); - this.warn({ - code: 'CIRCULAR_DEPENDENCY', - importer: path[0], - message: `Circular dependency: ${path.join(' -> ')}` - }); - } - private fetchModule(id: string, importer: string): Promise { // short-circuit cycles const existingModule = this.moduleById.get(id); diff --git a/src/utils/execution-order.ts b/src/utils/execution-order.ts index 070297bdf2b..4b96e9daa5c 100644 --- a/src/utils/execution-order.ts +++ b/src/utils/execution-order.ts @@ -1,3 +1,10 @@ +import ExternalModule from '../ExternalModule'; +import Module from '../Module'; +import { randomUint8Array, Uint8ArrayEqual, Uint8ArrayXor } from './entryHashing'; +import error from './error'; +import { relative } from './path'; +import relativeId from './relativeId'; + interface OrderedExecutionUnit { execIndex: number; } @@ -8,3 +15,133 @@ const compareExecIndex = (unitA: T, unitB: T) => export function sortByExecutionOrder(units: OrderedExecutionUnit[]) { units.sort(compareExecIndex); } + +export function analyzeModuleExecution( + entryModules: Module[], + generateChunkHashes: boolean, + inlineDynamicImports: boolean, + manualChunkModules: Record +) { + let curEntry: Module, curEntryHash: Uint8Array; + const cyclePaths: string[][] = []; + const allSeen: { [id: string]: boolean } = {}; + + const orderedModules: Module[] = []; + let analyzedModuleCount = 0; + + const dynamicImports: Module[] = []; + const dynamicImportAliases: string[] = []; + + let parents: { [id: string]: string }; + + const visit = (module: Module) => { + // Track entry point graph colouring by tracing all modules loaded by a given + // entry point and colouring those modules by the hash of its id. Colours are mixed as + // hash xors, providing the unique colouring of the graph into unique hash chunks. + // This is really all there is to automated chunking, the rest is chunk wiring. + if (generateChunkHashes) { + if (!curEntry.chunkAlias) { + Uint8ArrayXor(module.entryPointsHash, curEntryHash); + } else { + // manual chunks are indicated in this phase by having a chunk alias + // they are treated as a single colour in the colouring + // and aren't divisable by future colourings + module.chunkAlias = curEntry.chunkAlias; + module.entryPointsHash = curEntryHash; + } + } + + for (const depModule of module.dependencies) { + if (depModule instanceof ExternalModule) { + depModule.execIndex = analyzedModuleCount++; + continue; + } + + if (depModule.id in parents) { + if (!allSeen[depModule.id]) { + cyclePaths.push(getCyclePath(depModule.id, module.id, parents)); + } + continue; + } + + parents[depModule.id] = module.id; + if (!depModule.isEntryPoint && !depModule.chunkAlias) visit(depModule); + } + + for (const dynamicModule of module.dynamicImportResolutions) { + if (!(dynamicModule.resolution instanceof Module)) continue; + // If the parent module of a dynamic import is to a child module whose graph + // colouring is the same as the parent module, then that dynamic import does + // not need to be treated as a new entry point as it is in the static graph + if ( + !generateChunkHashes || + (!dynamicModule.resolution.chunkAlias && + !Uint8ArrayEqual(dynamicModule.resolution.entryPointsHash, curEntry.entryPointsHash)) + ) { + if (dynamicImports.indexOf(dynamicModule.resolution) === -1) { + dynamicImports.push(dynamicModule.resolution); + dynamicImportAliases.push(dynamicModule.alias); + } + } + } + + if (allSeen[module.id]) return; + allSeen[module.id] = true; + + module.execIndex = analyzedModuleCount++; + orderedModules.push(module); + }; + + if (generateChunkHashes && manualChunkModules) { + for (const chunkName of Object.keys(manualChunkModules)) { + curEntryHash = randomUint8Array(10); + + for (curEntry of manualChunkModules[chunkName]) { + if (curEntry.chunkAlias) { + error({ + code: 'INVALID_CHUNK', + message: `Cannot assign ${relative( + process.cwd(), + curEntry.id + )} to the "${chunkName}" chunk as it is already in the "${curEntry.chunkAlias}" chunk. +Try defining "${chunkName}" first in the manualChunks definitions of the Rollup configuration.` + }); + } + curEntry.chunkAlias = chunkName; + parents = { [curEntry.id]: null }; + visit(curEntry); + } + } + } + + for (curEntry of entryModules) { + curEntry.isEntryPoint = true; + curEntryHash = randomUint8Array(10); + parents = { [curEntry.id]: null }; + visit(curEntry); + } + + // new items can be added during this loop + for (curEntry of dynamicImports) { + if (curEntry.isEntryPoint) continue; + if (!inlineDynamicImports) curEntry.isEntryPoint = true; + curEntryHash = randomUint8Array(10); + parents = { [curEntry.id]: null }; + visit(curEntry); + } + + return { orderedModules, dynamicImports, dynamicImportAliases, cyclePaths }; +} + +function getCyclePath(id: string, parentId: string, parents: { [id: string]: string | null }) { + const path = [relativeId(id)]; + let curId = parentId; + while (curId !== id) { + path.push(relativeId(curId)); + curId = parents[curId]; + if (!curId) break; + } + path.push(path[0]); + path.reverse(); + return path; +} diff --git a/test/function/index.js b/test/function/index.js index e84882f6721..18e206ba90c 100644 --- a/test/function/index.js +++ b/test/function/index.js @@ -30,20 +30,26 @@ function runSingleFileTest(code, configContext) { } function runCodeSplitTest(output, configContext) { - function requireFromOutput(fileName) { - const outputId = path.relative('.', fileName); + const requireFromOutputVia = importer => importee => { + const outputId = path.join(path.dirname(importer), importee); const chunk = output[outputId]; if (chunk) { - return requireWithContext(chunk.code, context); + return requireWithContext( + chunk.code, + Object.assign({ require: requireFromOutputVia(outputId) }, context) + ); } else { - return require(fileName); + return require(importee); } - } + }; - const context = Object.assign({ require: requireFromOutput, assert }, configContext); + const context = Object.assign({ assert }, configContext); let exports; try { - exports = requireWithContext(output['main.js'].code, context); + exports = requireWithContext( + output['main.js'].code, + Object.assign({ require: requireFromOutputVia('main.js') }, context) + ); } catch (error) { return { error, exports: error.exports }; }