Skip to content

Commit

Permalink
support unique bundle-time asset emission
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed May 28, 2018
1 parent 9ffb2ff commit 53805de
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 117 deletions.
92 changes: 22 additions & 70 deletions src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,15 @@ import {
TreeshakingOptions,
WarningHandler,
ModuleJSON,
RollupError
RollupError,
OutputBundle
} from './rollup/types';
import Chunk from './Chunk';
import GlobalScope from './ast/scopes/GlobalScope';
import {
randomUint8Array,
Uint8ArrayToHexString,
Uint8ArrayXor,
randomHexString
} from './utils/entryHashing';
import { randomUint8Array, Uint8ArrayToHexString, Uint8ArrayXor } from './utils/entryHashing';
import firstSync from './utils/first-sync';
import { Program } from 'estree';
import { getAssetFileName } from './utils/getAssetFileName';

export interface Asset {
name: string;
source: string | Buffer;
fileName: string;
}
import { createAssetPluginHooks, Asset, finaliseAsset } from './utils/assetHooks';

export default class Graph {
curChunkIndex = 0;
Expand Down Expand Up @@ -71,7 +61,6 @@ export default class Graph {
treeshakingOptions: TreeshakingOptions;
varOrConst: 'var' | 'const';

finalisedAssets = false;
contextParse: (code: string, acornOptions?: acorn.Options) => Program;

// deprecated
Expand Down Expand Up @@ -126,50 +115,22 @@ export default class Graph {
);
};

this.pluginContext = {
isExternal: this.isExternal,
emitAsset: (name: string, source?: string | Buffer) => {
if (this.finalisedAssets)
error({
code: 'ASSETS_ALREADY_FINALISED',
message: 'Plugin error - Unable to emit assets at this stage of the build pipeline.'
});
const assetId = randomHexString(8);
this.assetsById.set(assetId, {
name,
source,
fileName: undefined
});
return assetId;
},
setAssetSource: (assetId: string, source: string | Buffer) => {
if (this.finalisedAssets)
error({
code: 'ASSETS_ALREADY_FINALISED',
message:
'Plugin error - Unable to set asset sources at this stage of the build pipeline.'
});
const asset = this.assetsById.get(assetId);
if (asset.source)
error({
code: 'ASSET_SOURCE_ALREADY_SET',
message: `Plugin error - Unable to set asset source for ${
asset.name
}, source already set.`
});
asset.source = source;
},
resolveId: undefined,
parse: this.contextParse,
warn(warning: RollupWarning | string) {
if (typeof warning === 'string') warning = { message: warning };
this.warn(warning);
this.pluginContext = Object.assign(
{
isExternal: this.isExternal,
resolveId: undefined,
parse: this.contextParse,
warn(warning: RollupWarning | string) {
if (typeof warning === 'string') warning = { message: warning };
this.warn(warning);
},
error(err: RollupError | string) {
if (typeof err === 'string') throw new Error(err);
error(err);
}
},
error(err: RollupError | string) {
if (typeof err === 'string') throw new Error(err);
error(err);
}
};
createAssetPluginHooks(this.assetsById)
);

this.resolveId = first(
[
Expand Down Expand Up @@ -283,18 +244,9 @@ export default class Graph {
}

finaliseAssets(assetFileNames: string) {
this.finalisedAssets = true;
const assets: Record<string, string | Buffer> = Object.create(null);
this.assetsById.forEach(asset => {
const fileName = getAssetFileName(asset.name, asset.source, assets, assetFileNames);
asset.fileName = fileName;
assets[fileName] = asset.source;
});
return assets;
}

getAssetFileName(assetId: string) {
return this.assetsById.get(assetId).fileName;
const outputBundle: OutputBundle = Object.create(null);
this.assetsById.forEach(asset => finaliseAsset(asset, outputBundle, assetFileNames));
return outputBundle;
}

private loadModule(entryName: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export default class Module {
code, // Only needed for debugging
error: this.error.bind(this),
fileName, // Needed for warnings
getAssetFileName: this.graph.getAssetFileName.bind(this.graph),
getAssetFileName: this.graph.pluginContext.getAssetFileName,
getExports: this.getExports.bind(this),
getReexports: this.getReexports.bind(this),
getModuleExecIndex: () => this.execIndex,
Expand Down
25 changes: 20 additions & 5 deletions src/rollup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from './types';
import getExportMode from '../utils/getExportMode';
import Chunk from '../Chunk';
import { finaliseAsset, createAssetPluginHooks } from '../utils/assetHooks';

export const VERSION = '<@VERSION@>';

Expand Down Expand Up @@ -257,9 +258,8 @@ export default function rollup(
timeStart('GENERATE', 1);

// populate asset files into output
let outputBundle: OutputBundle = graph.finaliseAssets(
outputOptions.assetFileNames || 'assets/[name]-[hash][ext]'
);
const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][ext]';
const outputBundle: OutputBundle = graph.finaliseAssets(assetFileNames);

const inputBase = commondir(
chunks.filter(chunk => chunk.entryModule).map(chunk => chunk.entryModule.id)
Expand Down Expand Up @@ -348,16 +348,31 @@ export default function rollup(
// run generateBundle hook
const generateBundlePlugins = graph.plugins.filter(plugin => plugin.generateBundle);
if (generateBundlePlugins.length === 0) return;

// assets emitted during generateBundle are unique to that specific generate call
const assets = new Map(graph.assetsById);
const generateBundleContext = Object.assign(
{},
graph.pluginContext,
createAssetPluginHooks(assets, outputBundle, assetFileNames)
);

return Promise.all(
generateBundlePlugins.map(plugin =>
plugin.generateBundle.call(
graph.pluginContext,
generateBundleContext,
outputOptions,
outputBundle,
isWrite
)
)
);
).then(() => {
// throw errors for assets not finalised with a source
assets.forEach(asset => {
if (asset.fileName === undefined)
finaliseAsset(asset, outputBundle, assetFileNames);
});
});
})
.then(() => {
timeEnd('GENERATE', 1);
Expand Down
1 change: 1 addition & 0 deletions src/rollup/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface PluginContext {
parse: (input: string, options: any) => ESTree.Program;
emitAsset: (name: string, source?: string | Buffer) => string;
setAssetSource: (assetId: string, source: string | Buffer) => void;
getAssetFileName: (assetId: string) => string;
warn(warning: RollupWarning, pos?: { line: number; column: number }): void;
error(err: RollupError, pos?: { line: number; column: number }): void;
}
Expand Down
103 changes: 103 additions & 0 deletions src/utils/assetHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import error from './error';
import { randomHexString } from './entryHashing';
import { isPlainName } from './relativeId';
import { makeUnique, renderNamePattern } from './renderNamePattern';
import sha256 from 'hash.js/lib/hash/sha/256';
import { extname } from './path';
import { OutputBundle } from '../rollup/types';

export interface Asset {
name: string;
source: string | Buffer;
fileName: string;
}

export function getAssetFileName(
asset: Asset,
existingNames: Record<string, any>,
assetFileNames: string
) {
if (asset.source === undefined)
error({
code: 'ASSET_SOURCE_NOT_FOUND',
message: `Plugin error creating asset ${asset.name} - no asset source set.`
});

if (asset.fileName) return asset.fileName;

return makeUnique(
renderNamePattern(assetFileNames, 'assetFileNames', name => {
switch (name) {
case 'hash':
const hash = sha256();
hash.update(name);
hash.update(':');
hash.update(asset.source);
return hash.digest('hex').substr(0, 8);
case 'name':
return asset.name.substr(0, asset.name.length - extname(asset.name).length);
case 'ext':
return extname(asset.name);
}
}),
existingNames
);
}

export function createAssetPluginHooks(
assetsById: Map<string, Asset>,
outputBundle?: OutputBundle,
assetFileNames?: string
) {
return {
emitAsset(name: string, source?: string | Buffer) {
if (typeof name !== 'string' || !isPlainName(name))
error({
code: 'INVALID_ASSET_NAME',
message: `Plugin error creating asset, name is not a plain (non relative or absolute URL) string name.`
});
const assetId = randomHexString(8);
const asset: Asset = { name, source, fileName: undefined };
if (outputBundle) finaliseAsset(asset, outputBundle, assetFileNames);
assetsById.set(assetId, asset);
return assetId;
},
setAssetSource: (assetId: string, source: string | Buffer) => {
const asset = assetsById.get(assetId);
if (!asset)
error({
code: 'ASSET_NOT_FOUND',
message: `Plugin error - Unable to set asset source for unknown asset ${assetId}.`
});
if (asset.source)
error({
code: 'ASSET_SOURCE_ALREADY_SET',
message: `Plugin error - Unable to set asset source for ${
asset.name
}, source already set.`
});
asset.source = source;
if (outputBundle) finaliseAsset(asset, outputBundle, assetFileNames);
},
getAssetFileName: (assetId: string) => {
const asset = assetsById.get(assetId);
if (!asset)
error({
code: 'ASSET_NOT_FOUND',
message: `Plugin error - Unable to get asset filename for unknown asset ${assetId}.`
});
if (asset.fileName === undefined)
error({
code: 'ASSET_NOT_FINALISED',
message: `Plugin error - Unable to get asset file name for asset ${assetId}. Ensure that the source is set and that generate is called first.`
});
return asset.fileName;
}
};
}

export function finaliseAsset(asset: Asset, outputBundle: OutputBundle, assetFileNames: string) {
const fileName = getAssetFileName(asset, outputBundle, assetFileNames);
asset.fileName = fileName;
outputBundle[fileName] = asset.source;
}
41 changes: 0 additions & 41 deletions src/utils/getAssetFileName.ts

This file was deleted.

43 changes: 43 additions & 0 deletions test/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,49 @@ module.exports = input;
});
});

it('supports assets uniquely defined in the generateBundle hook', () => {
return rollup
.rollup({
input: 'input',
experimentalCodeSplitting: true,
experimentalDynamicImport: true,
plugins: [
loader({ input: `alert('hello')` }),
{
generateBundle (options, outputBundle, isWrite) {
if (options.format === 'es') {
const depAssetId = this.emitAsset('lateDepAsset', 'custom source');
const source = `references ${this.getAssetFileName(depAssetId)}`;
this.emitAsset('lateMainAsset', source);
} else {
const depAssetId = this.emitAsset('lateDepAsset', 'different source');
const source = `references ${this.getAssetFileName(depAssetId)}`;
this.emitAsset('lateMainAsset', source);
}
}
}
]
})
.then(bundle =>
bundle.generate({ format: 'es' })
.then(outputBundle1 =>
bundle.generate({ format: 'cjs' })
.then(outputBundle2 => [outputBundle1, outputBundle2])
)
)
.then(([outputBundle1, outputBundle2]) => {
assert.equal(outputBundle1['input.js'].code, `alert('hello');\n`);
assert.equal(outputBundle1['assets/lateDepAsset-671f747d'], `custom source`);
assert.equal(outputBundle1['assets/lateMainAsset-863ea4b5'], `references assets/lateDepAsset-671f747d`);

assert.equal(outputBundle2['input.js'].code, `'use strict';\n\nalert('hello');\n`);
assert.equal(outputBundle2['assets/lateDepAsset-671f747d'], undefined);
assert.equal(outputBundle2['assets/lateMainAsset-863ea4b5'], undefined);
assert.equal(outputBundle2['assets/lateDepAsset-c107f5fc'], `different source`);
assert.equal(outputBundle2['assets/lateMainAsset-6dc2262b'], `references assets/lateDepAsset-c107f5fc`);
});
});

it('supports processBundle hook including reporting tree-shaken exports', () => {
return rollup
.rollup({
Expand Down

0 comments on commit 53805de

Please sign in to comment.