Skip to content

Commit

Permalink
Implement this.load context function
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Nov 2, 2021
1 parent dda4346 commit d092514
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 40 deletions.
29 changes: 19 additions & 10 deletions docs/05-plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Notifies a plugin when watcher process closes and all open resources should be c

#### `load`

**Type:** `(id: string) => string | null | {code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`<br> **Kind:** `async, first`<br> **Previous Hook:** [`resolveId`](guide/en/#resolveid) or [`resolveDynamicImport`](guide/en/#resolvedynamicimport) where the loaded id was resolved.<br> **Next Hook:** [`transform`](guide/en/#transform) to transform the loaded file.
**Type:** `(id: string) => string | null | {code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`<br> **Kind:** `async, first`<br> **Previous Hook:** [`resolveId`](guide/en/#resolveid) or [`resolveDynamicImport`](guide/en/#resolvedynamicimport) where the loaded id was resolved. Additionally, this hook can be triggered at any time from plugin hooks by calling [`this.load`](guide/en/#thisload) to preload the module corresponding to an id.<br> **Next Hook:** [`transform`](guide/en/#transform) to transform the loaded file.

Defines a custom loader. Returning `null` defers to other `load` functions (and eventually the default behavior of loading from the file system). To prevent additional parsing overhead in case e.g. this hook already used `this.parse` to generate an AST for some reason, this hook can optionally return a `{ code, ast, map }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node. If the transformation does not move code, you can preserve existing sourcemaps by setting `map` to `null`. Otherwise you might need to generate the source map. See [the section on source code transformations](#source-code-transformations).

Expand Down Expand Up @@ -162,7 +162,7 @@ the source will be `"../bar.js""`.

The `importer` is the fully resolved id of the importing module. When resolving entry points, importer will usually be `undefined`. An exception here are entry points generated via [`this.emitFile`](guide/en/#thisemitfile) as here, you can provide an `importer` argument.

For those cases, the `isEntry` option will tell you if we are resolving a user defined entry point, an emitted chunk, or if the `isEntry` parameter was provided for the [`this.resolve(source, importer)`](guide/en/#thisresolve) context function.
For those cases, the `isEntry` option will tell you if we are resolving a user defined entry point, an emitted chunk, or if the `isEntry` parameter was provided for the [`this.resolve`](guide/en/#thisresolve) context function.

You can use this for instance as a mechanism to define custom proxy modules for entry points. The following plugin will only expose the default export from entry points while still keeping named exports available for internal usage:

Expand Down Expand Up @@ -218,7 +218,9 @@ See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect

See [custom module meta-data](guide/en/#custom-module-meta-data) for how to use the `meta` option. If `null` is returned or the option is omitted, then `meta` will default to an empty object. The `load` and `transform` hooks can add or replace properties of this object.

When triggering this hook from a plugin via [`this.resolve(source, importer, options)`](guide/en/#thisresolve), it is possible to pass a custom options object to this hook. While this object will be passed unmodified, plugins should follow the convention of adding a `custom` property with an object where the keys correspond to the names of the plugins that the options are intended for. For details see [custom resolver options](guide/en/#custom-resolver-options).
Note that while `resolveId` will be called for each import of a module and can therefore resolve to the same `id` many times, values for `external`, `moduleSideEffects`, `syntheticNamedExports` or `meta` can only be set once before the module is loaded. The reason is that after this call, Rollup will continue with the [`load`](guide/en/#load) and [`transform`](guide/en/#transform) hooks for that module that may override these values and should take precedence if they do so.

When triggering this hook from a plugin via [`this.resolve`](guide/en/#thisresolve), it is possible to pass a custom options object to this hook. While this object will be passed unmodified, plugins should follow the convention of adding a `custom` property with an object where the keys correspond to the names of the plugins that the options are intended for. For details see [custom resolver options](guide/en/#custom-resolver-options).

#### `transform`

Expand Down Expand Up @@ -691,18 +693,23 @@ Loads and parses the module corresponding to the given id, attaching additional
This allows you to inspect the final content of modules before deciding how to resolve them in the [`resolveId`](guide/en/#resolveid) hook and e.g. resolve to a proxy module instead. If the module becomes part of the graph later, there is no additional overhead from using this context function as the module will not be parsed again. The signature allows you to directly pass the return value of [`this.resolve`](guide/en/#thisresolve) to this function as long as it is neither `null` nor external.
Note that with regard to the `moduleSideEffects`, `syntheticNamedExports` and `meta` options, the same restrictions apply as for the `resolveId` hook: Their values only have an effect if the module has not been loaded yet. Thus, it is very important to use `this.resolve` first to find out if any plugins want to set special values for these options in their `resolveId` hook, and pass these options on to `this.load` if appropriate. The example below showcases how this can be handled to add a proxy module for modules containing a special code comment:
```js
export default function addProxyPlugin() {
return {
async resolveId(source, importer) {
const resolution = await this.resolve(source, importer, { skipSelf: true });
async resolveId(source, importer, options) {
// We make sure to pass on any resolveId options to this.resolve to get the module id
const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
// We can only pre-load existing and non-external ids
if (!resolution?.external) {
const moduleInfo = await this.load(resolution);
if (moduleInfo.code.indexOf('/* use proxy */') >= 0) {
return `${resolution.id}?proxy`;
}
}
return null;
// As we already fully resolved the module, there is no reason to resolve it again
return resolution;
},
load(id) {
if (id.endsWith('?proxy')) {
Expand All @@ -715,7 +722,9 @@ export default function addProxyPlugin() {
}
```
If the module was already loaded, this will just wait for the parsing to complete and then return its module information. If the module was not yet imported by another module, this will not automatically trigger loading other modules imported by this module. Instead, imported modules will only be loaded once this module has actually been imported at least once.
If the module was already loaded, this will just wait for the parsing to complete and then return its module information. If the module was not yet imported by another module, this will not automatically trigger loading other modules imported by this module. Instead, static and dynamic dependencies will only be loaded once this module has actually been imported at least once.
Be aware that you cannot await calling `this.load` for a module during that module's own `load` or `transform` hook as that would essentially wait for those hooks to complete first and lead to a deadlock.
#### `this.meta`
Expand Down Expand Up @@ -1000,11 +1009,11 @@ At some point when using many dedicated plugins, there may be the need for unrel
#### Custom resolver options
Assume you have a plugin that should resolve a module to different ids depending on how it is imported. One way to achieve this would be to use special proxy ids when importing this module, e.g. a transpiled import via `require("foo")` could be denoted with an id `foo?require=true` so that a resolver plugin knows this.
Assume you have a plugin that should resolve an import to different ids depending on how the import was generated by another plugin. One way to achieve this would be to rewrite the import to use special proxy ids, e.g. a transpiled import via `require("foo")` in a CommonJS file could become a regular import with a special id `import "foo?require=true"` so that a resolver plugin knows this.
The problem here, however, is that this proxy id may or may not cause unintended side-effects when passed to other resolvers. Moreover, if the id is created by plugin `A` and the resolution happens in plugin `B`, it creates a dependency between these plugins so that one `A` is not usable without `B`.
The problem here, however, is that this proxy id may or may not cause unintended side effects when passed to other resolvers because it does not really correspond to a file. Moreover, if the id is created by plugin `A` and the resolution happens in plugin `B`, it creates a dependency between these plugins so that `A` is not usable without `B`.
Custom resolver option offer a solution here by allowing to pass additional options for plugins when manually resolving a module. This happens without changing the id and thus without impairing the ability for other plugins to resolve the module correctly if the intended target plugin is not present.
Custom resolver option offer a solution here by allowing to pass additional options for plugins when manually resolving a module via `this resolve`. This happens without changing the id and thus without impairing the ability for other plugins to resolve the module correctly if the intended target plugin is not present.
```js
function requestingPlugin() {
Expand Down
115 changes: 85 additions & 30 deletions src/ModuleLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CustomPluginOptions,
EmittedChunk,
HasModuleSideEffects,
ModuleInfo,
ModuleOptions,
NormalizedInputOptions,
PartialNull,
Expand Down Expand Up @@ -48,11 +49,30 @@ type NormalizedResolveIdWithoutDefaults = Partial<PartialNull<ModuleOptions>> &
id: string;
};

type ResolveStaticDependencyPromise = Promise<[source: string, resolvedId: ResolvedId]>;
type ResolveDynamicDependencyPromise = Promise<
[dynamicImport: DynamicImport, resolvedId: ResolvedId | string | null]
>;
type LoadModulePromise = Promise<
[
resolveStaticDependencies: ResolveStaticDependencyPromise[],
resolveDynamicDependencies: ResolveDynamicDependencyPromise[]
]
>;

export class ModuleLoader {
private readonly hasModuleSideEffects: HasModuleSideEffects;
private readonly implicitEntryModules = new Set<Module>();
private readonly indexedEntryModules: { index: number; module: Module }[] = [];
private latestLoadModulesPromise: Promise<unknown> = Promise.resolve();
private moduleLoadingState = new Map<
Module,
{
loadAndResolveDependenciesPromise: Promise<void>;
// Set to null once/if dependencies will be loaded as well
loadPromise: null | LoadModulePromise;
}
>();
private nextEntryModuleIndex = 0;
private readQueue = new Queue();

Expand Down Expand Up @@ -145,6 +165,12 @@ export class ModuleLoader {
return module;
}

public preloadModule(resolvedId: NormalizedResolveIdWithoutDefaults): Promise<ModuleInfo> {
return this.fetchModule(this.addDefaultsToResolvedId(resolvedId)!, undefined, false, true).then(
module => module.info
);
}

resolveId = async (
source: string,
importer: string | undefined,
Expand Down Expand Up @@ -280,9 +306,7 @@ export class ModuleLoader {

private async fetchDynamicDependencies(
module: Module,
resolveDynamicImportPromises: Promise<
[dynamicImport: DynamicImport, resolvedId: ResolvedId | string | null]
>[]
resolveDynamicImportPromises: ResolveDynamicDependencyPromise[]
): Promise<void> {
const dependencies = await Promise.all(
resolveDynamicImportPromises.map(resolveDynamicImportPromise =>
Expand All @@ -308,21 +332,18 @@ export class ModuleLoader {
}
}

// If this is a preload, then this method always waits for the dependencies of the module to be resolved.
// Otherwise if the module does not exist, it waits for the module and all its dependencies to be loaded.
// Otherwise it returns immediately.
private async fetchModule(
{ id, meta, moduleSideEffects, syntheticNamedExports }: ResolvedId,
importer: string | undefined,
isEntry: boolean
isEntry: boolean,
isPreload: boolean
): Promise<Module> {
const existingModule = this.modulesById.get(id);
if (existingModule instanceof Module) {
if (isEntry) {
existingModule.info.isEntry = true;
this.implicitEntryModules.delete(existingModule);
for (const dependant of existingModule.implicitlyLoadedAfter) {
dependant.implicitlyLoadedBefore.delete(existingModule);
}
existingModule.implicitlyLoadedAfter.clear();
}
await this.handleExistingModule(existingModule, isEntry, isPreload);
return existingModule;
}

Expand All @@ -337,23 +358,38 @@ export class ModuleLoader {
);
this.modulesById.set(id, module);
this.graph.watchFiles[id] = true;
await this.addModuleSource(id, importer, module);
const resolveStaticDependencyPromises = this.getResolveStaticDependencyPromises(module);
const resolveDynamicImportPromises = this.getResolveDynamicImportPromises(module);
Promise.all([
...(resolveStaticDependencyPromises as Promise<never>[]),
...(resolveDynamicImportPromises as Promise<never>[])
])
const loadPromise: LoadModulePromise = this.addModuleSource(id, importer, module).then(() => [
this.getResolveStaticDependencyPromises(module),
this.getResolveDynamicImportPromises(module)
]);
const loadAndResolveDependenciesPromise = loadPromise
.then(([resolveStaticDependencyPromises, resolveDynamicImportPromises]) =>
Promise.all<unknown>([...resolveStaticDependencyPromises, ...resolveDynamicImportPromises])
)
.then(() => this.pluginDriver.hookParallel('moduleParsed', [module.info]))
.catch(() => {
/* rejections thrown here are also handled within PluginDriver - they are safe to ignore */
});
if (isPreload) {
this.moduleLoadingState.set(module, { loadAndResolveDependenciesPromise, loadPromise });
await loadAndResolveDependenciesPromise;
} else {
this.moduleLoadingState.set(module, { loadAndResolveDependenciesPromise, loadPromise: null });
await this.fetchModuleDependencies(module, ...(await loadPromise));
}
return module;
}

private async fetchModuleDependencies(
module: Module,
resolveStaticDependencyPromises: ResolveStaticDependencyPromise[],
resolveDynamicDependencyPromises: ResolveDynamicDependencyPromise[]
) {
await Promise.all([
this.fetchStaticDependencies(module, resolveStaticDependencyPromises),
this.fetchDynamicDependencies(module, resolveDynamicImportPromises)
this.fetchDynamicDependencies(module, resolveDynamicDependencyPromises)
]);
module.linkImports();
return module;
}

private fetchResolvedDependency(
Expand Down Expand Up @@ -382,13 +418,13 @@ export class ModuleLoader {
}
return Promise.resolve(externalModule);
} else {
return this.fetchModule(resolvedId, importer, false);
return this.fetchModule(resolvedId, importer, false, false);
}
}

private async fetchStaticDependencies(
module: Module,
resolveStaticDependencyPromises: Promise<[source: string, resolvedId: ResolvedId]>[]
resolveStaticDependencyPromises: ResolveStaticDependencyPromise[]
): Promise<void> {
for (const dependency of await Promise.all(
resolveStaticDependencyPromises.map(resolveStaticDependencyPromise =>
Expand Down Expand Up @@ -456,9 +492,7 @@ export class ModuleLoader {
};
}

private getResolveDynamicImportPromises(
module: Module
): Promise<[dynamicImport: DynamicImport, resolvedId: ResolvedId | string | null]>[] {
private getResolveDynamicImportPromises(module: Module): ResolveDynamicDependencyPromise[] {
return module.dynamicImports.map(async dynamicImport => {
const resolvedId = await this.resolveDynamicImport(
module,
Expand All @@ -474,9 +508,7 @@ export class ModuleLoader {
});
}

private getResolveStaticDependencyPromises(
module: Module
): Promise<[source: string, resolvedId: ResolvedId]>[] {
private getResolveStaticDependencyPromises(module: Module): ResolveStaticDependencyPromise[] {
return Array.from(
module.sources,
async source =>
Expand All @@ -493,6 +525,28 @@ export class ModuleLoader {
);
}

private async handleExistingModule(module: Module, isEntry: boolean, isPreload: boolean) {
const loadingState = this.moduleLoadingState.get(module)!;
if (isPreload) {
await loadingState.loadAndResolveDependenciesPromise;
return;
}
if (isEntry) {
module.info.isEntry = true;
this.implicitEntryModules.delete(module);
for (const dependant of module.implicitlyLoadedAfter) {
dependant.implicitlyLoadedBefore.delete(module);
}
module.implicitlyLoadedAfter.clear();
}
const { loadPromise } = loadingState;
if (loadPromise) {
loadingState.loadPromise = null;
await this.fetchModuleDependencies(module, ...(await loadPromise));
}
return;
}

private handleResolveId(
resolvedId: ResolvedId | null,
source: string,
Expand Down Expand Up @@ -558,7 +612,8 @@ export class ModuleLoader {
: { id: resolveIdResult }
)!,
undefined,
isEntry
isEntry,
false
);
}

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 @@ -198,6 +198,7 @@ export interface PluginContext extends MinimalPluginContext {
getWatchFiles: () => string[];
/** @deprecated Use `this.resolve` instead */
isExternal: IsExternal;
load: (options: { id: string } & Partial<PartialNull<ModuleOptions>>) => Promise<ModuleInfo>;
/** @deprecated Use `this.getModuleIds` instead */
moduleIds: IterableIterator<string>;
parse: (input: string, options?: any) => AcornNode;
Expand Down
4 changes: 4 additions & 0 deletions src/utils/PluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,16 @@ export function getPluginContext(
true,
options
),
load(resolvedId) {
return graph.moduleLoader.preloadModule(resolvedId);
},
meta: {
rollupVersion,
watchMode: graph.watchMode
},
get moduleIds() {
function* wrappedModuleIds() {
// We are wrapping this in a generator to only show the message once we are actually iterating
warnDeprecation(
{
message: `Accessing "this.moduleIds" on the plugin context by plugin ${plugin.name} is deprecated. The "this.getModuleIds" plugin context function should be used instead.`,
Expand Down

0 comments on commit d092514

Please sign in to comment.