From aeb1edd0e1fac07efdfc2d19c724f574497c77cc Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 12 Nov 2021 06:09:32 +0100 Subject: [PATCH] Plugin context function for pre-loading modules (#4234) * Create documentation * Write documentation * Implement this.load context function * Abort the build for moduleParsed errors * 2.59.0-0 * Do not wait for import resolution and refine docs * 2.59.0-1 * Fix typo --- docs/05-plugin-development.md | 64 ++++++++-- package-lock.json | 2 +- package.json | 2 +- src/Module.ts | 2 +- src/ModuleLoader.ts | 117 +++++++++++++----- src/rollup/types.d.ts | 1 + src/utils/PluginContext.ts | 4 + .../plugin-error-module-parsed/_config.js | 22 ++++ .../plugin-error-module-parsed/main.js | 13 ++ .../samples/preload-cyclic-module/_config.js | 37 ++++++ .../samples/preload-cyclic-module/main.js | 6 + .../samples/preload-module/_config.js | 96 ++++++++++++++ test/function/samples/preload-module/dep.js | 1 + test/function/samples/preload-module/main.js | 2 + .../samples/preload-module/other-dep.js | 1 + test/function/samples/preload-module/other.js | 2 + test/utils.js | 2 +- 17 files changed, 329 insertions(+), 45 deletions(-) create mode 100644 test/function/samples/plugin-error-module-parsed/_config.js create mode 100644 test/function/samples/plugin-error-module-parsed/main.js create mode 100644 test/function/samples/preload-cyclic-module/_config.js create mode 100644 test/function/samples/preload-cyclic-module/main.js create mode 100644 test/function/samples/preload-module/_config.js create mode 100644 test/function/samples/preload-module/dep.js create mode 100644 test/function/samples/preload-module/main.js create mode 100644 test/function/samples/preload-module/other-dep.js create mode 100644 test/function/samples/preload-module/other.js diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index a2c2ce7fed9..d49eaf4b5c1 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -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}`
**Kind:** `async, first`
**Previous Hook:** [`resolveId`](guide/en/#resolveid) or [`resolveDynamicImport`](guide/en/#resolvedynamicimport) where the loaded id was resolved.
**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}`
**Kind:** `async, first`
**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.
**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). @@ -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: @@ -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` @@ -685,6 +687,54 @@ Returns `null` if the module id cannot be found. Get ids of the files which has been watched previously. Include both files added by plugins with `this.addWatchFile` and files added implicitly by rollup during the build. +#### `this.load` + +**Type:** `({id: string, moduleSideEffects?: boolean | 'no-treeshake' | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}) => Promise` + +Loads and parses the module corresponding to the given id, attaching additional meta information to the module if provided. This will trigger the same [`load`](guide/en/#load), [`transform`](guide/en/#transform) and [`moduleParsed`](guide/en/#moduleparsed) hooks that would be triggered if the module were imported by another module. + +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. + +The returned promise will resolve once the module has been fully transformed and parsed but before any imports have been resolved. That means that the resulting `ModuleInfo` will have empty `importedIds` and `dynamicallyImportedIds`. This helps to avoid deadlock situations when awaiting `this.load` in a `resolveId` hook. If you are interested in `importedIds` and `dynamicallyImportedIds`, you should implement a `moduleParsed` hook. + +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, options) { + if (importer?.endsWith('?proxy')) { + // Do not proxy ids used in proxies + return null; + } + // 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 && !resolution.external) { + // we pass on the entire resolution information + const moduleInfo = await this.load(resolution); + if (moduleInfo.code.indexOf('/* use proxy */') >= 0) { + return `${resolution.id}?proxy`; + } + } + // As we already fully resolved the module, there is no reason to resolve it again + return resolution; + }, + load(id) { + if (id.endsWith('?proxy')) { + const importee = id.slice(0, -'?proxy'.length); + return `console.log('proxy for ${importee}'); export * from ${JSON.stringify(importee)};`; + } + return null; + } + }; +} +``` + +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. + +While it is safe to use `this.load` in a `resolveId` hook, you should be very careful when awaiting it in a `load` or `transform` hook. If there are cyclic dependencies in the module graph, this can easily lead to a deadlock, so any plugin needs to manually take care to avoid waiting for `this.load` inside the `load` or `transform` of the any module that is in a cycle with the loaded module. + #### `this.meta` **Type:** `{rollupVersion: string, watchMode: boolean}` @@ -704,7 +754,7 @@ Use Rollup's internal acorn instance to parse code to an AST. #### `this.resolve` -**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, meta: {[plugin: string]: any}} | null>` +**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, isEntry?: boolean, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, meta: {[plugin: string]: any}} | null>` Resolve imports to module ids (i.e. file names) using the same plugins that Rollup uses, and determine if an import should be external. If `null` is returned, the import could not be resolved by Rollup or any plugin but was not explicitly marked as external by the user. If an absolute external id is returned that should remain absolute in the output either via the [`makeAbsoluteExternalsRelative`](guide/en/#makeabsoluteexternalsrelative) option or by explicit plugin choice in the [`resolveId`](guide/en/#resolveid) hook, `external` will be `"absolute"` instead of `true`. @@ -968,11 +1018,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() { diff --git a/package-lock.json b/package-lock.json index a1e1883d0e7..13db753e0f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.59.0", + "version": "2.59.0-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b97b5d8a541..1656b104543 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.59.0", + "version": "2.59.0-1", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js", diff --git a/src/Module.ts b/src/Module.ts index 4c48420228e..98fd5e8d4dc 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -280,7 +280,7 @@ export default class Module { return Array.from(module.implicitlyLoadedBefore, getId); }, get importedIds() { - return Array.from(module.sources, source => module.resolvedIds[source].id); + return Array.from(module.sources, source => module.resolvedIds[source]?.id).filter(Boolean); }, get importers() { return module.importers.sort(); diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 1386485fda9..3442b5f1061 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -6,6 +6,7 @@ import { CustomPluginOptions, EmittedChunk, HasModuleSideEffects, + ModuleInfo, ModuleOptions, NormalizedInputOptions, PartialNull, @@ -48,11 +49,23 @@ type NormalizedResolveIdWithoutDefaults = Partial> & 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(); private readonly indexedEntryModules: { index: number; module: Module }[] = []; private latestLoadModulesPromise: Promise = Promise.resolve(); + private moduleLoadPromises = new Map(); private nextEntryModuleIndex = 0; private readQueue = new Queue(); @@ -145,6 +158,12 @@ export class ModuleLoader { return module; } + public preloadModule(resolvedId: NormalizedResolveIdWithoutDefaults): Promise { + return this.fetchModule(this.addDefaultsToResolvedId(resolvedId)!, undefined, false, true).then( + module => module.info + ); + } + resolveId = async ( source: string, importer: string | undefined, @@ -280,9 +299,7 @@ export class ModuleLoader { private async fetchDynamicDependencies( module: Module, - resolveDynamicImportPromises: Promise< - [dynamicImport: DynamicImport, resolvedId: ResolvedId | string | null] - >[] + resolveDynamicImportPromises: ResolveDynamicDependencyPromise[] ): Promise { const dependencies = await Promise.all( resolveDynamicImportPromises.map(resolveDynamicImportPromise => @@ -308,21 +325,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 { 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; } @@ -337,23 +351,40 @@ 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[]), - ...(resolveDynamicImportPromises as Promise[]) - ]) - .then(() => this.pluginDriver.hookParallel('moduleParsed', [module.info])) - .catch(() => { - /* rejections thrown here are also handled within PluginDriver - they are safe to ignore */ - }); + const loadPromise: LoadModulePromise = this.addModuleSource(id, importer, module).then(() => [ + this.getResolveStaticDependencyPromises(module), + this.getResolveDynamicImportPromises(module) + ]); + const loadAndResolveDependenciesPromise = loadPromise + .then(([resolveStaticDependencyPromises, resolveDynamicImportPromises]) => + Promise.all([...resolveStaticDependencyPromises, ...resolveDynamicImportPromises]) + ) + .then(() => this.pluginDriver.hookParallel('moduleParsed', [module.info])); + loadAndResolveDependenciesPromise.catch(() => { + /* avoid unhandled promise rejections */ + }); + + if (isPreload) { + this.moduleLoadPromises.set(module, loadPromise); + await loadPromise; + } else { + await this.fetchModuleDependencies(module, ...(await loadPromise)); + // To handle errors when resolving dependencies or in moduleParsed + await loadAndResolveDependenciesPromise; + } + 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( @@ -382,13 +413,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 { for (const dependency of await Promise.all( resolveStaticDependencyPromises.map(resolveStaticDependencyPromise => @@ -456,9 +487,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, @@ -474,9 +503,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 => @@ -493,6 +520,27 @@ export class ModuleLoader { ); } + private async handleExistingModule(module: Module, isEntry: boolean, isPreload: boolean) { + const loadPromise = this.moduleLoadPromises.get(module); + if (isPreload) { + await loadPromise; + return; + } + if (isEntry) { + module.info.isEntry = true; + this.implicitEntryModules.delete(module); + for (const dependant of module.implicitlyLoadedAfter) { + dependant.implicitlyLoadedBefore.delete(module); + } + module.implicitlyLoadedAfter.clear(); + } + if (loadPromise) { + this.moduleLoadPromises.delete(module); + await this.fetchModuleDependencies(module, ...(await loadPromise)); + } + return; + } + private handleResolveId( resolvedId: ResolvedId | null, source: string, @@ -558,7 +606,8 @@ export class ModuleLoader { : { id: resolveIdResult } )!, undefined, - isEntry + isEntry, + false ); } diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 2d0b3d9fad1..a4458ef9058 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -198,6 +198,7 @@ export interface PluginContext extends MinimalPluginContext { getWatchFiles: () => string[]; /** @deprecated Use `this.resolve` instead */ isExternal: IsExternal; + load: (options: { id: string } & Partial>) => Promise; /** @deprecated Use `this.getModuleIds` instead */ moduleIds: IterableIterator; parse: (input: string, options?: any) => AcornNode; diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 24b1abe735f..5afd1d7dfc1 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -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.`, diff --git a/test/function/samples/plugin-error-module-parsed/_config.js b/test/function/samples/plugin-error-module-parsed/_config.js new file mode 100644 index 00000000000..b2190253239 --- /dev/null +++ b/test/function/samples/plugin-error-module-parsed/_config.js @@ -0,0 +1,22 @@ +const path = require('path'); + +module.exports = { + description: 'errors in moduleParsed abort the build', + options: { + plugins: [ + { + name: 'testPlugin', + moduleParsed() { + throw new Error('broken'); + } + } + ] + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'moduleParsed', + message: 'broken', + plugin: 'testPlugin', + watchFiles: [path.join(__dirname, 'main.js')] + } +}; diff --git a/test/function/samples/plugin-error-module-parsed/main.js b/test/function/samples/plugin-error-module-parsed/main.js new file mode 100644 index 00000000000..69b8f8f7946 --- /dev/null +++ b/test/function/samples/plugin-error-module-parsed/main.js @@ -0,0 +1,13 @@ +let effect = false; + +var b = { + get a() { + effect = true; + } +}; + +function X() {} +X.prototype = b; +new X().a; + +assert.ok(effect); diff --git a/test/function/samples/preload-cyclic-module/_config.js b/test/function/samples/preload-cyclic-module/_config.js new file mode 100644 index 00000000000..6de3be6c77f --- /dev/null +++ b/test/function/samples/preload-cyclic-module/_config.js @@ -0,0 +1,37 @@ +module.exports = { + description: 'handles pre-loading a cyclic module in the resolveId hook', + warnings: [ + { + code: 'CIRCULAR_DEPENDENCY', + cycle: ['main.js', 'main.js?proxy', 'main.js'], + importer: 'main.js', + message: 'Circular dependency: main.js -> main.js?proxy -> main.js' + } + ], + options: { + plugins: [ + { + async resolveId(source, importer, options) { + if (!importer || importer.endsWith('?proxy')) { + return null; + } + const resolution = await this.resolve(source, importer, { skipSelf: true, ...options }); + if (resolution && !resolution.external) { + const moduleInfo = await this.load(resolution); + if (moduleInfo.code.indexOf('/* use proxy */') >= 0) { + return `${resolution.id}?proxy`; + } + } + return resolution; + }, + load(id) { + if (id.endsWith('?proxy')) { + const importee = id.slice(0, -'?proxy'.length); + return `export * from ${JSON.stringify(importee)}; export const extra = 'extra';`; + } + return null; + } + } + ] + } +}; diff --git a/test/function/samples/preload-cyclic-module/main.js b/test/function/samples/preload-cyclic-module/main.js new file mode 100644 index 00000000000..57f7fea2ae5 --- /dev/null +++ b/test/function/samples/preload-cyclic-module/main.js @@ -0,0 +1,6 @@ +/* main *//* use proxy */ +import { foo as bar, extra } from './main.js'; +export const foo = 'foo'; +assert.strictEqual(bar, 'foo'); +assert.strictEqual(extra, 'extra'); + diff --git a/test/function/samples/preload-module/_config.js b/test/function/samples/preload-module/_config.js new file mode 100644 index 00000000000..f9326371ae0 --- /dev/null +++ b/test/function/samples/preload-module/_config.js @@ -0,0 +1,96 @@ +const assert = require('assert'); +const path = require('path'); +const ID_MAIN = path.join(__dirname, 'main.js'); +const ID_DEP = path.join(__dirname, 'dep.js'); +const ID_OTHER = path.join(__dirname, 'other.js'); + +const loadedModules = []; +const transformedModules = []; +const parsedModules = []; + +module.exports = { + description: 'allows pre-loading modules via this.load', + options: { + plugins: [ + { + name: 'test-plugin', + load(id) { + loadedModules.push(id); + }, + async transform(code, id) { + transformedModules.push(id); + }, + moduleParsed({ id }) { + parsedModules.push(id); + }, + async resolveId(source, importer, options) { + if (source.endsWith('main.js')) { + const resolvedId = await this.resolve(source, importer, { skipSelf: true, ...options }); + const { ast, ...moduleInfo } = await this.load({ + ...resolvedId, + meta: { testPlugin: 'first' } + }); + assert.deepStrictEqual(moduleInfo, { + code: "import './dep';\nassert.ok(true);\n", + dynamicImporters: [], + dynamicallyImportedIds: [], + hasModuleSideEffects: true, + id: ID_MAIN, + implicitlyLoadedAfterOneOf: [], + implicitlyLoadedBefore: [], + importedIds: [], + importers: [], + isEntry: false, + isExternal: false, + meta: { testPlugin: 'first' }, + syntheticNamedExports: false + }); + assert.strictEqual(loadedModules.filter(id => id === ID_MAIN, 'loaded').length, 1); + assert.strictEqual( + transformedModules.filter(id => id === ID_MAIN, 'transformed').length, + 1 + ); + assert.strictEqual(parsedModules.filter(id => id === ID_MAIN, 'parsed').length, 0); + // No dependencies have been loaded yet + assert.deepStrictEqual([...this.getModuleIds()], [ID_MAIN]); + await this.load({ id: ID_OTHER }); + assert.deepStrictEqual([...this.getModuleIds()], [ID_MAIN, ID_OTHER]); + return resolvedId; + } + }, + async buildEnd(err) { + if (err) { + return; + } + const { ast, ...moduleInfo } = await this.load({ + id: ID_DEP, + // This should be ignored as the module was already loaded + meta: { testPlugin: 'second' } + }); + assert.deepStrictEqual(moduleInfo, { + code: 'assert.ok(true);\n', + dynamicImporters: [], + dynamicallyImportedIds: [], + hasModuleSideEffects: true, + id: ID_DEP, + implicitlyLoadedAfterOneOf: [], + implicitlyLoadedBefore: [], + importedIds: [], + importers: [ID_MAIN], + isEntry: false, + isExternal: false, + meta: {}, + syntheticNamedExports: false + }); + assert.strictEqual(loadedModules.filter(id => id === ID_DEP, 'loaded').length, 1); + assert.strictEqual( + transformedModules.filter(id => id === ID_DEP, 'transformed').length, + 1 + ); + assert.strictEqual(parsedModules.filter(id => id === ID_DEP, 'parsed').length, 1); + assert.deepStrictEqual([...this.getModuleIds()], [ID_MAIN, ID_OTHER, ID_DEP]); + } + } + ] + } +}; diff --git a/test/function/samples/preload-module/dep.js b/test/function/samples/preload-module/dep.js new file mode 100644 index 00000000000..cc1d88a24fa --- /dev/null +++ b/test/function/samples/preload-module/dep.js @@ -0,0 +1 @@ +assert.ok(true); diff --git a/test/function/samples/preload-module/main.js b/test/function/samples/preload-module/main.js new file mode 100644 index 00000000000..03c9802af09 --- /dev/null +++ b/test/function/samples/preload-module/main.js @@ -0,0 +1,2 @@ +import './dep'; +assert.ok(true); diff --git a/test/function/samples/preload-module/other-dep.js b/test/function/samples/preload-module/other-dep.js new file mode 100644 index 00000000000..cd6715c9386 --- /dev/null +++ b/test/function/samples/preload-module/other-dep.js @@ -0,0 +1 @@ +throw new Error('Should not be executed'); diff --git a/test/function/samples/preload-module/other.js b/test/function/samples/preload-module/other.js new file mode 100644 index 00000000000..675eabdd86b --- /dev/null +++ b/test/function/samples/preload-module/other.js @@ -0,0 +1,2 @@ +import './other-dep'; +throw new Error('Should not be executed'); diff --git a/test/utils.js b/test/utils.js index 5711b67bae9..beb2c60d97b 100644 --- a/test/utils.js +++ b/test/utils.js @@ -106,7 +106,7 @@ function runTestSuiteWithSamples(suiteName, samplesDir, runTest, onTeardown) { describe(suiteName, () => runSamples(samplesDir, runTest, onTeardown)); } -// You can run only or skip certain kinds of tests be appending .only or .skip +// You can run only or skip certain kinds of tests by appending .only or .skip runTestSuiteWithSamples.only = function (suiteName, samplesDir, runTest, onTeardown) { describe.only(suiteName, () => runSamples(samplesDir, runTest, onTeardown)); };