Skip to content

Commit

Permalink
Plugin system refactoring (#2382)
Browse files Browse the repository at this point in the history
* asset fixes and refactoring for caching

* revert cacheType

* refactor plugin hooks behind a single interface, plugin error augmentations

* ensureArray -> set checking external

* remove name handling and cache code

* pr feedback

* Remove unnecessary export
  • Loading branch information
guybedford authored and lukastaegert committed Aug 12, 2018
1 parent 8f999d7 commit 2d17db9
Show file tree
Hide file tree
Showing 28 changed files with 576 additions and 547 deletions.
10 changes: 3 additions & 7 deletions bin/src/run/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import * as rollup from 'rollup';
import tc from 'turbocolor';
import {
InputOptions,
OutputBundle,
OutputChunk,
OutputOptions,
RollupBuild,
RollupSingleFileBuild
} from '../../../src/rollup/types';
import { mapSequence } from '../../../src/utils/promise';
import relativeId from '../../../src/utils/relativeId';
import { handleError, stderr } from '../logging';
import SOURCEMAPPING_URL from '../sourceMappingUrl';
Expand Down Expand Up @@ -67,10 +64,9 @@ export default function build(
});
}

return mapSequence<OutputOptions, Promise<OutputChunk | { output: OutputBundle }>>(
outputOptions,
output => bundle.write(output)
).then(() => bundle);
return Promise.all(outputOptions.map(output => <Promise<any>>bundle.write(output))).then(
() => bundle
);
})
.then((bundle?: RollupSingleFileBuild | RollupBuild) => {
warnings.flush();
Expand Down
5 changes: 1 addition & 4 deletions src/Chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1115,10 +1115,7 @@ export default class Chunk {
else if (options.dir) file = resolve(options.dir, this.id);
else file = resolve(this.id);

if (
this.graph.hasLoaders ||
this.graph.plugins.find(plugin => Boolean(plugin.transform || plugin.transformBundle))
) {
if (this.graph.pluginDriver.hasLoadersOrTransforms) {
const decodedMap = magicString.generateDecodedMap({});
map = collapseSourcemaps(this, file, decodedMap, this.usedModules, chunkSourcemapChain);
} else {
Expand Down
171 changes: 69 additions & 102 deletions src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,39 @@ import {
Asset,
InputOptions,
IsExternal,
LoadHook,
ModuleJSON,
OutputBundle,
Plugin,
PluginContext,
ResolveDynamicImportHook,
ResolveIdHook,
RollupError,
RollupWarning,
SourceDescription,
TreeshakingOptions,
WarningHandler,
Watcher
} from './rollup/types';
import { createAssetPluginHooks, finaliseAsset } from './utils/assetHooks';
import { load, makeOnwarn, resolveId } from './utils/defaults';
import ensureArray from './utils/ensureArray';
import { finaliseAsset } from './utils/assetHooks';
import {
randomUint8Array,
Uint8ArrayEqual,
Uint8ArrayToHexString,
Uint8ArrayXor
} from './utils/entryHashing';
import error from './utils/error';
import first from './utils/first';
import { isRelative, relative, resolve } from './utils/path';
import { createPluginDriver, PluginDriver } from './utils/pluginDriver';
import relativeId, { getAliasName } from './utils/relativeId';
import { timeEnd, timeStart } from './utils/timers';
import transform from './utils/transform';

function makeOnwarn() {
const warned = Object.create(null);

return (warning: any) => {
const str = warning.toString();
if (str in warned) return;
console.error(str); //eslint-disable-line no-console
warned[str] = true;
};
}

export default class Graph {
curChunkIndex = 0;
acornOptions: acorn.Options;
Expand All @@ -51,26 +54,24 @@ export default class Graph {
externalModules: ExternalModule[] = [];
getModuleContext: (id: string) => string;
hasLoaders: boolean;
isExternal: IsExternal;
isPureExternalModule: (id: string) => boolean;
load: LoadHook;
moduleById = new Map<string, Module | ExternalModule>();
assetsById = new Map<string, Asset>();
modules: Module[] = [];
onwarn: WarningHandler;
plugins: Plugin[];
pluginContext: PluginContext;
deoptimizationTracker: EntityPathTracker;
resolveDynamicImport: ResolveDynamicImportHook;
resolveId: (id: string, parent: string) => Promise<string | boolean | void>;
scope: GlobalScope;
shimMissingExports: boolean;
exportShimVariable: GlobalVariable;
treeshakingOptions: TreeshakingOptions;
varOrConst: 'var' | 'const';

isExternal: IsExternal;

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

pluginDriver: PluginDriver;

// deprecated
treeshake: boolean;

Expand All @@ -80,15 +81,11 @@ export default class Graph {
this.cachedModules = new Map();
if (options.cache) {
if (options.cache.modules) {
options.cache.modules.forEach(module => {
this.cachedModules.set(module.id, module);
});
for (const module of options.cache.modules) this.cachedModules.set(module.id, module);
}
}
delete options.cache; // TODO not deleting it here causes a memory leak; needs further investigation

this.plugins = options.plugins;

if (!options.input) {
throw new Error('You must supply options.input to rollup');
}
Expand Down Expand Up @@ -121,44 +118,15 @@ export default class Graph {
return this.acornParse(code, { ...defaultAcornOptions, ...options, ...this.acornOptions });
};

const assetPluginHooks = createAssetPluginHooks(this.assetsById);

this.pluginContext = {
watcher,
isExternal: undefined,
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);
},
...assetPluginHooks
};
this.pluginDriver = createPluginDriver(this, options, watcher);

this.resolveId = first(
[
((id: string, parentId: string) =>
this.isExternal(id, parentId, false) ? false : null) as ResolveIdHook
]
.concat(
this.plugins
.map(plugin => plugin.resolveId)
.filter(Boolean)
.map(resolveId => resolveId.bind(this.pluginContext))
)
.concat(resolveId(options))
);

this.pluginContext.resolveId = this.resolveId;

const loaders = this.plugins.map(plugin => plugin.load).filter(Boolean);
this.hasLoaders = loaders.length !== 0;

this.load = first([...loaders, load]);
if (typeof options.external === 'function') {
this.isExternal = options.external;
} else {
const external = options.external;
const ids = new Set(Array.isArray(external) ? external : external ? [external] : []);
this.isExternal = id => ids.has(id);
}

this.shimMissingExports = options.shimMissingExports;

Expand All @@ -184,29 +152,13 @@ export default class Graph {
this.getModuleContext = () => this.context;
}

if (typeof options.external === 'function') {
this.isExternal = options.external;
} else {
const ids = ensureArray(options.external);
this.isExternal = id => ids.indexOf(id) !== -1;
}
this.pluginContext.isExternal = this.isExternal;

this.onwarn = options.onwarn || makeOnwarn();

this.varOrConst = options.preferConst ? 'const' : 'var';

this.acornOptions = options.acorn || {};
const acornPluginsToInject = [];

this.resolveDynamicImport = first(
[
...this.plugins.map(plugin => plugin.resolveDynamicImport).filter(Boolean),
<ResolveDynamicImportHook>function(specifier, parentId) {
return typeof specifier === 'string' && this.resolveId(specifier, parentId);
}
].map(resolveDynamicImport => resolveDynamicImport.bind(this.pluginContext))
);
acornPluginsToInject.push(injectDynamicImportPlugin);
acornPluginsToInject.push(injectImportMeta);
this.acornOptions.plugins = this.acornOptions.plugins || {};
Expand All @@ -217,7 +169,14 @@ export default class Graph {
(<any>this.acornOptions).allowAwaitOutsideFunction = true;
}

acornPluginsToInject.push(...ensureArray(options.acornInjectPlugins));
const acornInjectPlugins = options.acornInjectPlugins;
acornPluginsToInject.push(
...(Array.isArray(acornInjectPlugins)
? acornInjectPlugins
: acornInjectPlugins
? [acornInjectPlugins]
: [])
);
this.acornParse = acornPluginsToInject.reduce((acc, plugin) => plugin(acc), acorn).parse;
}

Expand All @@ -244,23 +203,25 @@ export default class Graph {
}

private loadModule(entryName: string) {
return this.resolveId(entryName, undefined).then(id => {
if (id === false) {
error({
code: 'UNRESOLVED_ENTRY',
message: `Entry module cannot be external`
});
}
return this.pluginDriver
.hookFirst<string | boolean | void>('resolveId', [entryName, undefined])
.then(id => {
if (id === false) {
error({
code: 'UNRESOLVED_ENTRY',
message: `Entry module cannot be external`
});
}

if (id == null) {
error({
code: 'UNRESOLVED_ENTRY',
message: `Could not resolve entry (${entryName})`
});
}
if (id == null) {
error({
code: 'UNRESOLVED_ENTRY',
message: `Could not resolve entry (${entryName})`
});
}

return this.fetchModule(<string>id, undefined);
});
return this.fetchModule(<string>id, undefined);
});
}

private link() {
Expand Down Expand Up @@ -647,7 +608,7 @@ Try defining "${chunkName}" first in the manualChunks definitions of the Rollup
this.moduleById.set(id, module);

timeStart('load modules', 3);
return Promise.resolve(this.load.call(this.pluginContext, id))
return Promise.resolve(this.pluginDriver.hookFirst('load', [id]))
.catch((err: Error) => {
timeEnd('load modules', 3);
let msg = `Could not load ${id}`;
Expand Down Expand Up @@ -683,7 +644,7 @@ Try defining "${chunkName}" first in the manualChunks definitions of the Rollup
// re-emit transform assets
if (cachedModule.transformAssets) {
for (const asset of cachedModule.transformAssets) {
this.pluginContext.emitAsset(
this.pluginDriver.emitAsset(
asset.name,
<string[]>(asset.dependencies ? asset.dependencies : asset.source),
asset.dependencies && asset.source
Expand All @@ -693,7 +654,7 @@ Try defining "${chunkName}" first in the manualChunks definitions of the Rollup
return cachedModule;
}

return transform(this, sourceDescription, module, this.plugins);
return transform(this, sourceDescription, module);
})
.then((source: ModuleJSON) => {
module.setSource(source);
Expand Down Expand Up @@ -742,7 +703,7 @@ Try defining "${chunkName}" first in the manualChunks definitions of the Rollup
const fetchDynamicImportsPromise = Promise.all(
module.getDynamicImportExpressions().map((dynamicImportExpression, index) => {
return Promise.resolve(
this.resolveDynamicImport.call(this.pluginContext, dynamicImportExpression, module.id)
this.pluginDriver.hookFirst('resolveDynamicImport', [dynamicImportExpression, module.id])
).then(replacement => {
if (!replacement) {
module.dynamicImportResolutions[index] = {
Expand Down Expand Up @@ -783,17 +744,24 @@ Try defining "${chunkName}" first in the manualChunks definitions of the Rollup

return Promise.all(
module.sources.map(source => {
const resolvedId = module.resolvedIds[source];
return (resolvedId ? Promise.resolve(resolvedId) : this.resolveId(source, module.id)).then(
resolvedId => {
return Promise.resolve()
.then(() => {
const resolvedId = module.resolvedIds[source];
if (resolvedId) return resolvedId;
const isExternal = this.isExternal(source, module.id, false);
if (isExternal) return false;
return this.pluginDriver.hookFirst<string | boolean | void>('resolveId', [
source,
module.id
]);
})
.then(resolvedId => {
// TODO types of `resolvedId` are not compatable with 'externalId'.
// `this.resolveId` returns `string`, `void`, and `boolean`
const externalId =
<string>resolvedId ||
(isRelative(source) ? resolve(module.id, '..', source) : source);
let isExternal =
resolvedId === false ||
this.isExternal.call(this.pluginContext, externalId, module.id, true);
let isExternal = resolvedId === false || this.isExternal(externalId, module.id, true);

if (!resolvedId && !isExternal) {
if (isRelative(source)) {
Expand Down Expand Up @@ -849,8 +817,7 @@ Try defining "${chunkName}" first in the manualChunks definitions of the Rollup
module.resolvedIds[source] = <string>resolvedId;
return this.fetchModule(<string>resolvedId, module.id);
}
}
);
});
})
).then(() => fetchDynamicImportsPromise);
}
Expand Down
19 changes: 17 additions & 2 deletions src/Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import Chunk from './Chunk';
import ExternalModule from './ExternalModule';
import Graph from './Graph';
import { Asset, IdMap, ModuleJSON, RawSourceMap, RollupError, RollupWarning } from './rollup/types';
import { handleMissingExport } from './utils/defaults';
import error from './utils/error';
import getCodeFrame from './utils/getCodeFrame';
import { getOriginalLocation } from './utils/getOriginalLocation';
Expand Down Expand Up @@ -144,6 +143,22 @@ function includeFully(node: Node) {
}
}

function handleMissingExport(
exportName: string,
importingModule: Module,
importedModule: string,
importerStart?: number
) {
importingModule.error(
{
code: 'MISSING_EXPORT',
message: `'${exportName}' is not exported by ${relativeId(importedModule)}`,
url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module`
},
importerStart
);
}

export default class Module {
type: 'Module';
private graph: Graph;
Expand Down Expand Up @@ -262,7 +277,7 @@ export default class Module {
code, // Only needed for debugging
error: this.error.bind(this),
fileName, // Needed for warnings
getAssetFileName: this.graph.pluginContext.getAssetFileName,
getAssetFileName: this.graph.pluginDriver.getAssetFileName,
getExports: this.getExports.bind(this),
getReexports: this.getReexports.bind(this),
getModuleExecIndex: () => this.execIndex,
Expand Down

0 comments on commit 2d17db9

Please sign in to comment.