Skip to content

Commit

Permalink
Unified file emission api (#2999)
Browse files Browse the repository at this point in the history
* Use new FileEmitter for basic cases around assets. TODO: Replace asset
hooks completely, create tests for assets using the new hooks.

* Migrate assets to new file emitter

* Remove assetsById from Graph

* Implement emitFile for assets

* Internally use EmittedFile in the file emitter

* Deprecate emitAsset and ROLLUP_ASSET_URL

* Deprecate getAssetFileName

* Merge chunk emission into unified API and deprecated previous API

* Allow emitting files with fixed names

* Support unnamed assets

* Improve chunk name assignment

* Initial support for chunk file names

* Allow specifying explicit file names for emitted chunks

* Fix some TODOs

* Test ids remain stable when the transform hook is cached and make test
more stable

* Refine error handling

* Test some more errors

* Refine file emission

* Refactor file emitter to have a single code path for asset finalization

* Deduplicated emitted assets without a specific file name

* Only use the alias as name for a manual chunk if the chunk is not facade
for an entry point

* Generate separate facades for duplicate named user-defined entry points

* Always create facades for explicit file names

* Test edge cases

* Test and refactor handling of dynamic relative imports

* Use async-await in generate function, remove error condition

* Improve and test pattern validation

* Test file emitter edge cases

* Improve plugin error handling

* Add documentation
  • Loading branch information
lukastaegert committed Aug 5, 2019
1 parent 57dd0b9 commit 2443783
Show file tree
Hide file tree
Showing 1,188 changed files with 6,974 additions and 2,475 deletions.
131 changes: 81 additions & 50 deletions docs/05-plugin-development.md

Large diffs are not rendered by default.

201 changes: 114 additions & 87 deletions src/Chunk.ts
Expand Up @@ -77,6 +77,11 @@ export interface ImportSpecifier {
local: string;
}

interface FacadeName {
fileName?: string;
name?: string;
}

function getGlobalName(
module: ExternalModule,
globals: GlobalsOption,
Expand Down Expand Up @@ -110,12 +115,19 @@ export function isChunkRendered(chunk: Chunk): boolean {
}

export default class Chunk {
static generateFacade(graph: Graph, facadedModule: Module): Chunk {
private static generateFacade(
graph: Graph,
facadedModule: Module,
facadeName: FacadeName
): Chunk {
const chunk = new Chunk(graph, []);
chunk.assignFacadeName(facadeName, facadedModule);
if (!facadedModule.facadeChunk) {
facadedModule.facadeChunk = chunk;
}
chunk.dependencies = [facadedModule.chunk as Chunk];
chunk.dynamicDependencies = [];
chunk.facadeModule = facadedModule;
facadedModule.facadeChunk = chunk;
for (const exportName of facadedModule.getAllExportNames()) {
const tracedVariable = facadedModule.getVariableForExportName(exportName);
chunk.exports.add(tracedVariable);
Expand All @@ -129,7 +141,7 @@ export default class Chunk {
exportMode: 'none' | 'named' | 'default' = 'named';
facadeModule: Module | null = null;
graph: Graph;
id: string = undefined as any;
id: string | null = null;
indentString: string = undefined as any;
isEmpty: boolean;
manualChunkAlias: string | null = null;
Expand All @@ -138,14 +150,15 @@ export default class Chunk {
[moduleId: string]: RenderedModule;
};
usedModules: Module[] = undefined as any;
variableName = 'chunk';

variableName: string;
private chunkName?: string;
private dependencies: (ExternalModule | Chunk)[] = undefined as any;
private dynamicDependencies: (ExternalModule | Chunk)[] = undefined as any;
private exportNames: { [name: string]: Variable } = Object.create(null);
private exports = new Set<Variable>();
private fileName: string | null = null;
private imports = new Set<Variable>();
private name: string | null = null;
private needsExportsShim = false;
private renderedDeclarations: {
dependencies: ChunkDependencies;
Expand Down Expand Up @@ -179,25 +192,20 @@ export default class Chunk {
}
}

const entryModule = this.entryModules[0];
if (entryModule) {
const moduleForNaming =
this.entryModules[0] || this.orderedModules[this.orderedModules.length - 1];
if (moduleForNaming) {
this.variableName = makeLegal(
basename(
entryModule.chunkAlias || entryModule.manualChunkAlias || getAliasName(entryModule.id)
moduleForNaming.chunkName ||
moduleForNaming.manualChunkAlias ||
getAliasName(moduleForNaming.id)
)
);
} else {
this.variableName = '__chunk_' + ++graph.curChunkIndex;
}
}

canModuleBeFacade(
moduleExportNamesByVariable: Map<Variable, string[]>,
moduleChunkAlias: string | null
): boolean {
if (this.manualChunkAlias && moduleChunkAlias && this.manualChunkAlias !== moduleChunkAlias) {
return false;
}
canModuleBeFacade(moduleExportNamesByVariable: Map<Variable, string[]>): boolean {
for (const exposedVariable of this.exports) {
if (!moduleExportNamesByVariable.has(exposedVariable)) {
return false;
Expand All @@ -209,22 +217,33 @@ export default class Chunk {
generateFacades(): Chunk[] {
const facades: Chunk[] = [];
for (const module of this.entryModules) {
const requiredFacades: FacadeName[] = Array.from(module.userChunkNames).map(name => ({
name
}));
if (requiredFacades.length === 0 && module.isUserDefinedEntryPoint) {
requiredFacades.push({});
}
requiredFacades.push(...Array.from(module.chunkFileNames).map(fileName => ({ fileName })));
if (requiredFacades.length === 0) {
requiredFacades.push({});
}
if (!this.facadeModule) {
const exportNamesByVariable = module.getExportNamesByVariable();
if (
this.graph.preserveModules ||
this.canModuleBeFacade(exportNamesByVariable, module.chunkAlias)
) {
if (this.graph.preserveModules || this.canModuleBeFacade(exportNamesByVariable)) {
this.facadeModule = module;
module.facadeChunk = this;
for (const [variable, exportNames] of exportNamesByVariable) {
for (const exportName of exportNames) {
this.exportNames[exportName] = variable;
}
}
continue;
this.assignFacadeName(requiredFacades.shift() as FacadeName, module);
}
}
facades.push(Chunk.generateFacade(this.graph, module));

for (const facadeName of requiredFacades) {
facades.push(Chunk.generateFacade(this.graph, module, facadeName));
}
}
return facades;
}
Expand All @@ -234,30 +253,27 @@ export default class Chunk {
patternName: string,
addons: Addons,
options: OutputOptions,
existingNames: Record<string, true>
) {
this.id = makeUnique(
renderNamePattern(pattern, patternName, type => {
switch (type) {
case 'format':
return options.format === 'es' ? 'esm' : options.format;
case 'hash':
return this.computeContentHashWithDependencies(addons, options);
case 'name':
return this.getChunkName();
}
return undefined as any;
existingNames: Record<string, any>
): string {
if (this.fileName !== null) {
return this.fileName;
}
return makeUnique(
renderNamePattern(pattern, patternName, {
format: () => (options.format === 'es' ? 'esm' : (options.format as string)),
hash: () => this.computeContentHashWithDependencies(addons, options),
name: () => this.getChunkName()
}),
existingNames
);
}

generateIdPreserveModules(
preserveModulesRelativeDir: string,
existingNames: Record<string, true>
) {
existingNames: Record<string, any>
): string {
const sanitizedId = sanitizeFileName(this.orderedModules[0].id);
this.id = makeUnique(
return makeUnique(
normalize(
isAbsolute(this.orderedModules[0].id)
? relative(preserveModulesRelativeDir, sanitizedId)
Expand Down Expand Up @@ -304,11 +320,11 @@ export default class Chunk {
}

getChunkName(): string {
return this.chunkName || (this.chunkName = this.computeChunkName());
return this.name || (this.name = sanitizeFileName(this.getFallbackChunkName()));
}

getDynamicImportIds(): string[] {
return this.dynamicDependencies.map(chunk => chunk.id).filter(Boolean);
return this.dynamicDependencies.map(chunk => chunk.id).filter(Boolean) as string[];
}

getExportNames(): string[] {
Expand All @@ -318,7 +334,7 @@ export default class Chunk {
}

getImportIds(): string[] {
return this.dependencies.map(chunk => chunk.id);
return this.dependencies.map(chunk => chunk.id).filter(Boolean) as string[];
}

getRenderedHash(): string {
Expand Down Expand Up @@ -616,13 +632,9 @@ export default class Chunk {
if (dep instanceof ExternalModule && !dep.renormalizeRenderPath) continue;

const renderedDependency = this.renderedDeclarations.dependencies[i];

const depId = dep instanceof ExternalModule ? renderedDependency.id : dep.id;
let relPath = this.id ? normalize(relative(dirname(this.id), depId)) : depId;
if (!relPath.startsWith('../')) relPath = './' + relPath;

const depId = dep instanceof ExternalModule ? renderedDependency.id : (dep.id as string);
if (dep instanceof Chunk) renderedDependency.namedExportsMode = dep.exportMode !== 'default';
renderedDependency.id = relPath;
renderedDependency.id = this.getRelativePath(depId);
}

this.finaliseDynamicImports(format);
Expand Down Expand Up @@ -697,23 +709,18 @@ export default class Chunk {

let file: string;
if (options.file) file = resolve(options.sourcemapFile || options.file);
else if (options.dir) file = resolve(options.dir, this.id);
else file = resolve(this.id);

if (this.graph.pluginDriver.hasLoadersOrTransforms) {
const decodedMap = magicString.generateDecodedMap({});
map = collapseSourcemaps(
this,
file,
decodedMap,
this.usedModules,
chunkSourcemapChain,
options.sourcemapExcludeSources as boolean
);
} else {
map = magicString.generateMap({ file, includeContent: !options.sourcemapExcludeSources });
}

else if (options.dir) file = resolve(options.dir, this.id as string);
else file = resolve(this.id as string);

const decodedMap = magicString.generateDecodedMap({});
map = collapseSourcemaps(
this,
file,
decodedMap,
this.usedModules,
chunkSourcemapChain,
options.sourcemapExcludeSources as boolean
);
map.sources = map.sources.map(sourcePath =>
normalize(
options.sourcemapPathTransform ? options.sourcemapPathTransform(sourcePath) : sourcePath
Expand Down Expand Up @@ -782,17 +789,14 @@ export default class Chunk {
}
}

private computeChunkName(): string {
if (this.manualChunkAlias) {
return sanitizeFileName(this.manualChunkAlias);
}
if (this.facadeModule !== null) {
return sanitizeFileName(this.facadeModule.chunkAlias || getAliasName(this.facadeModule.id));
}
for (const module of this.orderedModules) {
if (module.chunkAlias) return sanitizeFileName(module.chunkAlias);
private assignFacadeName({ fileName, name }: FacadeName, facadedModule: Module) {
if (fileName) {
this.fileName = fileName;
} else {
this.name = sanitizeFileName(
name || facadedModule.chunkName || getAliasName(facadedModule.id)
);
}
return 'chunk';
}

private computeContentHashWithDependencies(addons: Addons, options: OutputOptions): string {
Expand All @@ -817,19 +821,24 @@ export default class Chunk {
if (resolution instanceof Module) {
if (resolution.chunk !== this && isChunkRendered(resolution.chunk as Chunk)) {
const resolutionChunk = resolution.facadeChunk || (resolution.chunk as Chunk);
let relPath = normalize(relative(dirname(this.id), resolutionChunk.id));
if (!relPath.startsWith('../')) relPath = './' + relPath;
node.renderFinalResolution(code, `'${relPath}'`, format);
node.renderFinalResolution(
code,
`'${this.getRelativePath(resolutionChunk.id as string)}'`,
format
);
}
} else if (resolution instanceof ExternalModule) {
let resolutionId = resolution.id;
if (resolution.renormalizeRenderPath) {
resolutionId = normalize(relative(dirname(this.id), resolution.renderPath));
if (!resolutionId.startsWith('../')) resolutionId = './' + resolutionId;
}
node.renderFinalResolution(code, `'${resolutionId}'`, format);
} else {
node.renderFinalResolution(code, resolution, format);
node.renderFinalResolution(
code,
resolution instanceof ExternalModule
? `'${
resolution.renormalizeRenderPath
? this.getRelativePath(resolution.renderPath)
: resolution.id
}'`
: resolution,
format
);
}
}
}
Expand All @@ -838,7 +847,7 @@ export default class Chunk {
private finaliseImportMetas(format: string): void {
for (const [module, code] of this.renderedModuleSources) {
for (const importMeta of module.importMetas) {
importMeta.renderFinalMechanism(code, this.id, format, this.graph.pluginDriver);
importMeta.renderFinalMechanism(code, this.id as string, format, this.graph.pluginDriver);
}
}
}
Expand Down Expand Up @@ -983,6 +992,24 @@ export default class Chunk {
return exports;
}

private getFallbackChunkName(): string {
if (this.manualChunkAlias) {
return this.manualChunkAlias;
}
if (this.fileName) {
return getAliasName(this.fileName);
}
for (const module of this.orderedModules) {
if (module.chunkName) return module.chunkName;
}
return 'chunk';
}

private getRelativePath(targetPath: string): string {
const relativePath = normalize(relative(dirname(this.id as string), targetPath));
return relativePath.startsWith('../') ? relativePath : './' + relativePath;
}

private inlineChunkDependencies(chunk: Chunk, deep: boolean) {
for (const dep of chunk.dependencies) {
if (dep instanceof ExternalModule) {
Expand Down

0 comments on commit 2443783

Please sign in to comment.