Skip to content

Commit

Permalink
Refactor chunking algorithm (#2575)
Browse files Browse the repository at this point in the history
* Split up misc tests

* Create basic bundle information test

* * Make sure "isEntry" is only true for entry facades
* Give precedence to entry facades when deconflicting ids
* Add entryModuleId to entry facades

* * Mark dynamic entry points as such and separate this from actual entry points
* Use internal names for dynamic entry points
* Do not generate dynamic entry points that are not used

* Add entryModuleIds to both static and dynamic entry points

* Add name to output

* Separate execution order from chunk colouring

* Prefer named export and do not mix for better IDE support

* Move chunk colouring behind tree-shaking and add more tests

* Use dynamic import tree-shaking information to optimize chunks

* Create proper facades for dynamic imports if necessary that are actually
used in place of the facaded module. Also no longer inline dynamic imports
that are part of the static graph for now as this logic would fail if
those are then dynamically imported by another chunk.

* As with normal imports, also use single quotes for dynamic imports

* Test we provide the right chunk information for dynamic facades

* Move facadeChunk property to modules

* Refactor entry export generation to prepare for multiple entry modules

* Make entryModuleIds a Set on each module

* Support manual chunks with multiple facades

* Handle name conflicts between dynamic entries in manual chunks

* Simplify tracing

* A chunk may only ever be facade for a single module to simplify the logic
Also delete tests that have been skipped for years. Let's rather make a fresh
start should the problem still persists and someone care to address it.

* Add information about dynamically imported chunks to bundle

* Make "optimizeImports" an experimental option to reflect that the logic
is complicated and may not have been properly tested in all situations.

* Make sure tree-shaken dynamic imports do not lead to the creation of
namespace objects when inlined
  • Loading branch information
lukastaegert committed Dec 11, 2018
1 parent 8d5326b commit c87132b
Show file tree
Hide file tree
Showing 280 changed files with 2,708 additions and 1,522 deletions.
274 changes: 141 additions & 133 deletions src/Chunk.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/ExternalModule.ts
Expand Up @@ -17,7 +17,7 @@ export default class ExternalModule {
renormalizeRenderPath = false;
isExternal = true;
isEntryPoint = false;
name: string;
variableName: string;
mostCommonSuggestion: number = 0;
nameSuggestions: { [name: string]: number };
reexported: boolean = false;
Expand All @@ -30,7 +30,7 @@ export default class ExternalModule {
this.execIndex = Infinity;

const parts = id.split(/[\\/]/);
this.name = makeLegal(parts.pop());
this.variableName = makeLegal(parts.pop());

this.nameSuggestions = Object.create(null);
this.declarations = Object.create(null);
Expand Down Expand Up @@ -58,7 +58,7 @@ export default class ExternalModule {

if (this.nameSuggestions[name] > this.mostCommonSuggestion) {
this.mostCommonSuggestion = this.nameSuggestions[name];
this.name = name;
this.variableName = name;
}
}

Expand Down
176 changes: 76 additions & 100 deletions src/Graph.ts
Expand Up @@ -23,9 +23,10 @@ import {
Watcher
} from './rollup/types';
import { finaliseAsset } from './utils/assetHooks';
import { assignChunkColouringHashes } from './utils/chunkColouring';
import { Uint8ArrayToHexString } from './utils/entryHashing';
import error from './utils/error';
import { analyzeModuleExecution, sortByExecutionOrder } from './utils/execution-order';
import { error } from './utils/error';
import { analyseModuleExecution, sortByExecutionOrder } from './utils/executionOrder';
import { isRelative, resolve } from './utils/path';
import { createPluginDriver, PluginDriver } from './utils/pluginDriver';
import relativeId, { getAliasName } from './utils/relativeId';
Expand All @@ -51,11 +52,11 @@ export default class Graph {
context: string;
externalModules: ExternalModule[] = [];
getModuleContext: (id: string) => string;
hasLoaders: boolean;
isPureExternalModule: (id: string) => boolean;
moduleById = new Map<string, Module | ExternalModule>();
assetsById = new Map<string, Asset>();
modules: Module[] = [];
needsTreeshakingPass: boolean = false;
onwarn: WarningHandler;
deoptimizationTracker: EntityPathTracker;
scope: GlobalScope;
Expand Down Expand Up @@ -154,7 +155,7 @@ export default class Graph {
this.shimMissingExports = options.shimMissingExports;

this.scope = new GlobalScope();
// TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
// Strictly speaking, this only applies with non-ES6, non-default-only bundles
for (const name of ['module', 'exports', '_interopDefault']) {
this.scope.findVariable(name); // creates global variable as side-effect
}
Expand Down Expand Up @@ -262,18 +263,15 @@ export default class Graph {

includeMarked(modules: Module[]) {
if (this.treeshake) {
let needsTreeshakingPass,
treeshakingPass = 1;
let treeshakingPass = 1;
do {
timeStart(`treeshaking pass ${treeshakingPass}`, 3);
needsTreeshakingPass = false;
this.needsTreeshakingPass = false;
for (const module of modules) {
if (module.include()) {
needsTreeshakingPass = true;
}
if (module.isExecuted) module.include();
}
timeEnd(`treeshaking pass ${treeshakingPass++}`, 3);
} while (needsTreeshakingPass);
} while (this.needsTreeshakingPass);
} else {
// Necessary to properly replace namespace imports
for (const module of modules) module.includeAllInBundle();
Expand Down Expand Up @@ -371,17 +369,7 @@ export default class Graph {

this.link();

const {
orderedModules,
dynamicImports,
dynamicImportAliases,
cyclePaths
} = analyzeModuleExecution(
entryModules,
!preserveModules && !inlineDynamicImports,
inlineDynamicImports,
manualChunkModules
);
const { orderedModules, cyclePaths } = analyseModuleExecution(entryModules);
for (const cyclePath of cyclePaths) {
this.warn({
code: 'CIRCULAR_DEPENDENCY',
Expand All @@ -390,41 +378,18 @@ export default class Graph {
});
}

if (entryModuleAliases) {
for (let i = entryModules.length - 1; i >= 0; i--) {
entryModules[i].chunkAlias = entryModuleAliases[i];
}
}
timeEnd('analyse dependency graph', 2);

// Phase 3 – marking. We include all statements that should be included
timeStart('mark included statements', 2);

if (inlineDynamicImports) {
const entryModule = entryModules[0];
if (entryModules.length > 1)
throw new Error(
'Internal Error: can only inline dynamic imports for single-file builds.'
);
for (const dynamicImportModule of dynamicImports) {
if (entryModule !== dynamicImportModule) dynamicImportModule.markPublicExports();
dynamicImportModule.getOrCreateNamespace().include();
}
} else {
for (let i = 0; i < dynamicImports.length; i++) {
const dynamicImportModule = dynamicImports[i];
if (entryModules.indexOf(dynamicImportModule) === -1) {
entryModules.push(dynamicImportModule);
if (!dynamicImportModule.chunkAlias)
dynamicImportModule.chunkAlias = dynamicImportAliases[i];
}
}
}

timeEnd('analyse dependency graph', 2);

// Phase 3 – marking. We include all statements that should be included
timeStart('mark included statements', 2);

for (const entryModule of entryModules) entryModule.markPublicExports();

// only include statements that should appear in the bundle
for (const entryModule of entryModules) entryModule.includeAllExports();
this.includeMarked(orderedModules);

// check for unused external imports
Expand All @@ -436,15 +401,27 @@ export default class Graph {
// entry point graph colouring, before generating the import and export facades
timeStart('generate chunks', 2);

if (!preserveModules && !inlineDynamicImports) {
assignChunkColouringHashes(entryModules, manualChunkModules);
}

if (entryModuleAliases) {
for (let i = entryModules.length - 1; i >= 0; i--) {
entryModules[i].chunkAlias = entryModuleAliases[i];
}
}

// TODO: there is one special edge case unhandled here and that is that any module
// exposed as an unresolvable export * (to a graph external export *,
// either as a namespace import reexported or top-level export *)
// should be made to be its own entry point module before chunking
let chunks: Chunk[] = [];
if (preserveModules) {
for (const module of orderedModules) {
const chunk = new Chunk(this, [module]);
if (module.isEntryPoint || !chunk.isEmpty) chunk.entryModule = module;
const chunk = new Chunk(this, [module], inlineDynamicImports);
if (module.isEntryPoint || !chunk.isEmpty) {
chunk.entryModules = [module];
}
chunks.push(chunk);
}
} else {
Expand All @@ -462,7 +439,7 @@ export default class Graph {
for (const entryHashSum in chunkModules) {
const chunkModulesOrdered = chunkModules[entryHashSum];
sortByExecutionOrder(chunkModulesOrdered);
const chunk = new Chunk(this, chunkModulesOrdered);
const chunk = new Chunk(this, chunkModulesOrdered, inlineDynamicImports);
chunks.push(chunk);
}
}
Expand All @@ -474,30 +451,35 @@ export default class Graph {
}

// filter out empty dependencies
chunks = chunks.filter(chunk => !chunk.isEmpty || chunk.entryModule || chunk.isManualChunk);
chunks = chunks.filter(
chunk => !chunk.isEmpty || chunk.entryModules.length > 0 || chunk.isManualChunk
);

// then go over and ensure all entry chunks export their variables
for (const chunk of chunks) {
if (preserveModules || chunk.entryModule) {
chunk.populateEntryExports(preserveModules);
if (preserveModules || chunk.entryModules.length > 0) {
chunk.generateEntryExportsOrMarkAsTainted();
}
}

// create entry point facades for entry module chunks that have tainted exports
const facades = [];
if (!preserveModules) {
for (const entryModule of entryModules) {
if (!entryModule.chunk.isEntryModuleFacade) {
const entryPointFacade = new Chunk(this, []);
entryPointFacade.linkFacade(entryModule);
chunks.push(entryPointFacade);
for (const chunk of chunks) {
for (const entryModule of chunk.entryModules) {
if (chunk.facadeModule !== entryModule) {
const entryPointFacade = new Chunk(this, [], inlineDynamicImports);
entryPointFacade.turnIntoFacade(entryModule);
facades.push(entryPointFacade);
}
}
}
}

timeEnd('generate chunks', 2);

this.finished = true;
return chunks;
return chunks.concat(facades);
}
);
}
Expand Down Expand Up @@ -605,47 +587,41 @@ export default class Graph {
}

private fetchAllDependencies(module: Module) {
// resolve and fetch dynamic imports where possible
const fetchDynamicImportsPromise = Promise.all(
module.getDynamicImportExpressions().map((dynamicImportExpression, index) => {
return Promise.resolve(
this.pluginDriver.hookFirst('resolveDynamicImport', [dynamicImportExpression, module.id])
).then(replacement => {
if (!replacement) {
module.dynamicImportResolutions[index] = {
alias: undefined,
resolution: undefined
};
return;
}
const alias = getAliasName(
replacement,
typeof dynamicImportExpression === 'string' ? dynamicImportExpression : undefined
);
if (typeof dynamicImportExpression !== 'string') {
module.dynamicImportResolutions[index] = { alias, resolution: replacement };
} else if (this.isExternal(replacement, module.id, true)) {
let externalModule;
if (!this.moduleById.has(replacement)) {
externalModule = new ExternalModule({
graph: this,
id: replacement
});
this.externalModules.push(externalModule);
this.moduleById.set(replacement, module);
module.getDynamicImportExpressions().map((dynamicImportExpression, index) =>
this.pluginDriver
.hookFirst('resolveDynamicImport', [dynamicImportExpression, module.id])
.then(replacement => {
if (!replacement) return;
const dynamicImport = module.dynamicImports[index];
dynamicImport.alias = getAliasName(
replacement,
typeof dynamicImportExpression === 'string' ? dynamicImportExpression : undefined
);
if (typeof dynamicImportExpression !== 'string') {
dynamicImport.resolution = replacement;
} else if (this.isExternal(replacement, module.id, true)) {
let externalModule;
if (!this.moduleById.has(replacement)) {
externalModule = new ExternalModule({
graph: this,
id: replacement
});
this.externalModules.push(externalModule);
this.moduleById.set(replacement, module);
} else {
externalModule = <ExternalModule>this.moduleById.get(replacement);
}
dynamicImport.resolution = externalModule;
externalModule.exportsNamespace = true;
} else {
externalModule = <ExternalModule>this.moduleById.get(replacement);
return this.fetchModule(replacement, module.id).then(depModule => {
dynamicImport.resolution = depModule;
});
}
module.dynamicImportResolutions[index] = { alias, resolution: externalModule };
externalModule.exportsNamespace = true;
} else {
return this.fetchModule(replacement, module.id).then(depModule => {
module.dynamicImportResolutions[index] = { alias, resolution: depModule };
});
}
});
})
).then(() => {});
})
)
);
fetchDynamicImportsPromise.catch(() => {});

return Promise.all(
Expand Down

0 comments on commit c87132b

Please sign in to comment.