diff --git a/browser/fs.ts b/browser/fs.ts index 5cc22ebd873..fbd18d02214 100644 --- a/browser/fs.ts +++ b/browser/fs.ts @@ -1,5 +1,10 @@ -const nope = (method: string) => (..._args: any[]): any => { - throw new Error(`Cannot use fs.${method} inside browser`); +const nope = (method: string) => (..._args: any[]): never => { + throw Object.assign( + new Error( + `Cannot access the file system (via "fs.${method}") when using the browser build of Rollup. Make sure you supply a plugin with custom resolveId and load hooks to Rollup.` + ), + { code: 'NO_FS_IN_BROWSER', url: 'https://rollupjs.org/guide/en/#a-simple-example' } + ); }; export const lstatSync = nope('lstatSync'); diff --git a/cli/help.md b/cli/help.md index b391ab154da..583de840666 100644 --- a/cli/help.md +++ b/cli/help.md @@ -26,7 +26,6 @@ Basic options: --chunkFileNames Name pattern for emitted secondary chunks --compact Minify wrapper code --context Specify top-level `this` value ---dynamicImportFunction Rename the dynamic `import()` function --entryFileNames Name pattern for emitted entry chunks --environment Settings passed to config file (see example) --no-esModule Do not add __esModule property diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index dcd8a61efb5..c113a7112d8 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -85,7 +85,6 @@ export default { // can be an array (for multiple inputs) // danger zone amd, - dynamicImportFunction, esModule, exports, externalLiveBindings, @@ -229,7 +228,6 @@ Many options have command line equivalents. In those cases, any arguments passed --chunkFileNames Name pattern for emitted secondary chunks --compact Minify wrapper code --context Specify top-level `this` value ---dynamicImportFunction Rename the dynamic `import()` function --entryFileNames Name pattern for emitted entry chunks --environment Settings passed to config file (see example) --no-esModule Do not add __esModule property diff --git a/docs/02-javascript-api.md b/docs/02-javascript-api.md index f7a1ad7f2a3..0ed7b888b29 100755 --- a/docs/02-javascript-api.md +++ b/docs/02-javascript-api.md @@ -136,7 +136,6 @@ const outputOptions = { // danger zone amd, - dynamicImportFunction, esModule, exports, freeze, diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index cdd099b5295..6142c499f1b 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -238,8 +238,8 @@ The first hook of the output generation phase is [outputOptions](guide/en/#outpu #### `augmentChunkHash` Type: `(preRenderedChunk: PreRenderedChunk) => string`
Kind: `sync, sequential`
-Previous Hook: [`banner`](guide/en/#banner), [`footer`](guide/en/#footer), [`intro`](guide/en/#intro), [`outro`](guide/en/#outro).
-Next Hook: [`resolveFileUrl`](guide/en/#resolvefileurl) for each use of `import.meta.ROLLUP_FILE_URL_referenceId` and [`resolveImportMeta`](guide/en/#resolveimportmeta) for all other accesses to `import.meta`. Then [`renderChunk`](guide/en/#renderchunk) for each chunk. +Previous Hook: [`renderDynamicImport`](guide/en/#renderdynamicimport) for each dynamic import expression.
+Next Hook: [`resolveFileUrl`](guide/en/#resolvefileurl) for each use of `import.meta.ROLLUP_FILE_URL_referenceId` and [`resolveImportMeta`](guide/en/#resolveimportmeta) for all other accesses to `import.meta`. Can be used to augment the hash of individual chunks. Called for each Rollup output chunk. Returning a falsy value will not modify the hash. Truthy values will be passed to [`hash.update`](https://nodejs.org/dist/latest-v12.x/docs/api/crypto.html#crypto_hash_update_data_inputencoding). @@ -258,7 +258,7 @@ augmentChunkHash(chunkInfo) { Type: `string | (() => string)`
Kind: `async, parallel`
Previous Hook: [`renderStart`](guide/en/#renderstart)
-Next Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) for each chunk that would contain a hash in the file name. Then [`resolveFileUrl`](guide/en/#resolvefileurl) for each use of `import.meta.ROLLUP_FILE_URL_referenceId` and [`resolveImportMeta`](guide/en/#resolveimportmeta) for all other accesses to `import.meta`. Then [`renderChunk`](guide/en/#renderchunk) for each chunk. +Next Hook: [`renderDynamicImport`](guide/en/#renderdynamicimport) for each dynamic import expression. Cf. [`output.banner/output.footer`](guide/en/#outputbanneroutputfooter). @@ -266,14 +266,14 @@ Cf. [`output.banner/output.footer`](guide/en/#outputbanneroutputfooter). Type: `string | (() => string)`
Kind: `async, parallel`
Previous Hook: [`renderStart`](guide/en/#renderstart)
-Next Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) for each chunk that would contain a hash in the file name. Then [`resolveFileUrl`](guide/en/#resolvefileurl) for each use of `import.meta.ROLLUP_FILE_URL_referenceId` and [`resolveImportMeta`](guide/en/#resolveimportmeta) for all other accesses to `import.meta`. Then [`renderChunk`](guide/en/#renderchunk) for each chunk. +Next Hook: [`renderDynamicImport`](guide/en/#renderdynamicimport) for each dynamic import expression. Cf. [`output.banner/output.footer`](guide/en/#outputbanneroutputfooter). #### `generateBundle` Type: `(options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }, isWrite: boolean) => void`
Kind: `async, sequential`
-Previous Hook: [`renderChunk`](guide/en/#renderchunk)
+Previous Hook: [`renderChunk`](guide/en/#renderchunk) for each chunk.
Next Hook: [`writeBundle`](guide/en/#writebundle) if the output was generated via `bundle.write(...)`, otherwise this is the last hook of the output generation phase and may again be followed by [`outputOptions`](guide/en/#outputoptions) if another output is generated. Called at the end of `bundle.generate()` or immediately before the files are written in `bundle.write()`. To modify the files after they have been written, use the [`writeBundle`](guide/en/#writebundle) hook. `bundle` provides the full list of files being written or generated along with their details: @@ -316,7 +316,7 @@ You can prevent files from being emitted by deleting them from the bundle object Type: `string | (() => string)`
Kind: `async, parallel`
Previous Hook: [`renderStart`](guide/en/#renderstart)
-Next Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) for each chunk that would contain a hash in the file name. Then [`resolveFileUrl`](guide/en/#resolvefileurl) for each use of `import.meta.ROLLUP_FILE_URL_referenceId` and [`resolveImportMeta`](guide/en/#resolveimportmeta) for all other accesses to `import.meta`. Then [`renderChunk`](guide/en/#renderchunk) for each chunk. +Next Hook: [`renderDynamicImport`](guide/en/#renderdynamicimport) for each dynamic import expression. Cf. [`output.intro/output.outro`](guide/en/#outputintrooutputoutro). @@ -332,18 +332,69 @@ Replaces or manipulates the output options object passed to `bundle.generate()` Type: `string | (() => string)`
Kind: `async, parallel`
Previous Hook: [`renderStart`](guide/en/#renderstart)
-Next Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) for each chunk that would contain a hash in the file name. Then [`resolveFileUrl`](guide/en/#resolvefileurl) for each use of `import.meta.ROLLUP_FILE_URL_referenceId` and [`resolveImportMeta`](guide/en/#resolveimportmeta) for all other accesses to `import.meta`. Then [`renderChunk`](guide/en/#renderchunk) for each chunk. +Next Hook: [`renderDynamicImport`](guide/en/#renderdynamicimport) for each dynamic import expression. Cf. [`output.intro/output.outro`](guide/en/#outputintrooutputoutro). #### `renderChunk` Type: `(code: string, chunk: ChunkInfo, options: OutputOptions) => string | { code: string, map: SourceMap } | null`
Kind: `async, sequential`
-Previous Hook: [`resolveFileUrl`](guide/en/#resolvefileurl) or [`resolveImportMeta`](guide/en/#resolveimportmeta) if `import.meta` properties are used. Before that [`augmentChunkHash`](guide/en/#augmentchunkhash) if there are chunks that would contain a hash in the file name. Before that [`banner`](guide/en/#banner), [`footer`](guide/en/#footer), [`intro`](guide/en/#intro), [`outro`](guide/en/#outro).
+Previous Hook: [`resolveFileUrl`](guide/en/#resolvefileurl) for each use of `import.meta.ROLLUP_FILE_URL_referenceId` and [`resolveImportMeta`](guide/en/#resolveimportmeta) for all other accesses to `import.meta`.
Next Hook: [`generateBundle`](guide/en/#generatebundle). Can be used to transform individual chunks. Called for each Rollup output chunk file. Returning `null` will apply no transformations. +#### `renderDynamicImport` +Type: `({format: string, moduleId: string, targetModuleId: string | null, customResolution: string | null}) => {left: string, right: string} | null`
+Kind: `sync, first`
+Previous Hook: [`banner`](guide/en/#banner), [`footer`](guide/en/#footer), [`intro`](guide/en/#intro), [`outro`](guide/en/#outro).
+Next Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) for each chunk that would contain a hash in the file name. + +This hook provides fine-grained control over how dynamic imports are rendered by providing replacements for the code to the left (`import(`) and right (`)`) of the argument of the import expression. Returning `null` defers to other hooks of this type and ultimately renders a format-specific default. + +`format` is the rendered output format, `moduleId` the id of the module performing the dynamic import. If the import could be resolved to an internal or external id, then `targetModuleId` will be set to this id, otherwise it will be `null`. If the dynamic import contained a non-string expression that was resolved by a [`resolveDynamicImport`](guide/en/#resolvedynamicimport) hook to a replacement string, then `customResolution` will contain that string. + +The following code will replace all dynamic imports with a custom handler, adding `import.meta.url` as a second argument to allow the handler to resolve relative imports correctly: + +```js +// plugin +const plugin = { + name: 'dynamic-import-polyfill', + renderDynamicImport() { + return { + left: 'dynamicImportPolyfill(', + right: ', import.meta.url)' + } + } +}; + +// input +import('./lib.js'); + +// output +dynamicImportPolyfill('./lib.js', import.meta.url); +``` + +The next plugin will make sure all dynamic imports of `esm-lib` are marked as external and retained as import expressions to e.g. allow a CommonJS build to import an ES module in Node 13+, cf. how to [import ES modules from CommonJS](https://nodejs.org/api/esm.html#esm_import_expressions) in the Node documentation. + +```js +// plugin +const plugin = { + name: 'retain-import-expression', + resolveDynamicImport(specifier) { + if (specifier === 'esm-lib') return false; + return null; + }, + renderDynamicImport({targetModuleId}) { + if (targetModuleId === 'esm-lib') + return { + left: 'import(', + right: ')' + } + } +}; +``` + #### `renderError` Type: `(error: Error) => void`
Kind: `async, parallel`
@@ -363,7 +414,7 @@ Called initially each time `bundle.generate()` or `bundle.write()` is called. To #### `resolveFileUrl` Type: `({chunkId: string, fileName: string, format: string, moduleId: string, referenceId: string, relativePath: string}) => string | null`
Kind: `sync, first`
-Previous Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) if there are chunks that would contain a hash in the file name. Before that [`banner`](guide/en/#banner), [`footer`](guide/en/#footer), [`intro`](guide/en/#intro), [`outro`](guide/en/#outro).
+Previous Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) for each chunk that would contain a hash in the file name.
Next Hook: [`renderChunk`](guide/en/#renderchunk) for each chunk. Allows to customize how Rollup resolves URLs of files that were emitted by plugins via `this.emitFile`. By default, Rollup will generate code for `import.meta.ROLLUP_FILE_URL_referenceId` that should correctly generate absolute URLs of emitted files independent of the output format and the host system where the code is deployed. @@ -391,7 +442,7 @@ resolveFileUrl({fileName}) { #### `resolveImportMeta` Type: `(property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null`
Kind: `sync, first`
-Previous Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) if there are chunks that would contain a hash in the file name. Before that [`banner`](guide/en/#banner), [`footer`](guide/en/#footer), [`intro`](guide/en/#intro), [`outro`](guide/en/#outro).
+Previous Hook: [`augmentChunkHash`](guide/en/#augmentchunkhash) for each chunk that would contain a hash in the file name.
Next Hook: [`renderChunk`](guide/en/#renderchunk) for each chunk. Allows to customize how Rollup handles `import.meta` and `import.meta.someProperty`, in particular `import.meta.url`. In ES modules, `import.meta` is an object and `import.meta.url` contains the URL of the current module, e.g. `http://server.net/bundle.js` for browsers or `file:///path/to/bundle.js` in Node. @@ -496,7 +547,8 @@ Returns additional information about the module in question in the form id: string, // the id of the module, for convenience isEntry: boolean, // is this a user- or plugin-defined entry point isExternal: boolean, // for external modules that are not included in the graph - importedIds: string[], // the module ids imported by this module + importedIds: string[], // the module ids statically imported by this module + dynamicallyImportedIds: string[], // the module ids imported by this module via dynamic import() hasModuleSideEffects: boolean // are imports of this module included if nothing is imported from it } ``` diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 6d8caf03f26..3cbe9f33bc5 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -848,13 +848,6 @@ Default: `true` Whether to include the 'use strict' pragma at the top of generated non-ES bundles. Strictly speaking, ES modules are *always* in strict mode, so you shouldn't disable this without good reason. -#### output.dynamicImportFunction -Type: `string`
-CLI: `--dynamicImportFunction `
-Default: `import` - -This will rename the dynamic import function to the chosen name when outputting ES bundles. This is useful for generating code that uses a dynamic import polyfill such as [this one](https://github.com/uupaa/dynamic-import-polyfill). - #### preserveSymlinks Type: `boolean`
CLI: `--preserveSymlinks`
@@ -1140,6 +1133,7 @@ export default { ☢️ These options have been deprecated and may be removed in a future Rollup version. #### treeshake.pureExternalModules +_Use [`treeshake.moduleSideEffects: 'no-external'`](guide/en/#treeshake) instead._
Type: `boolean | string[] | (id: string) => boolean | null`
CLI: `--treeshake.pureExternalModules`/`--no-treeshake.pureExternalModules`
Default: `false` @@ -1166,3 +1160,11 @@ console.log(42); ``` You can also supply a list of external ids to be considered pure or a function that is called whenever an external import could be removed. + +#### output.dynamicImportFunction +_Use the [`renderDynamicImport`](guide/en/#renderdynamicimport) plugin hook instead._
+Type: `string`
+CLI: `--dynamicImportFunction `
+Default: `import` + +This will rename the dynamic import function to the chosen name when outputting ES bundles. This is useful for generating code that uses a dynamic import polyfill such as [this one](https://github.com/uupaa/dynamic-import-polyfill). diff --git a/src/Chunk.ts b/src/Chunk.ts index 4c126ef256e..7e5f24e72fa 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -16,6 +16,7 @@ import Module from './Module'; import { DecodedSourceMapOrMissing, GlobalsOption, + InternalModuleFormat, OutputOptions, PreRenderedChunk, RenderedChunk, @@ -366,7 +367,17 @@ export default class Chunk { getRenderedHash(outputPluginDriver: PluginDriver): string { if (this.renderedHash) return this.renderedHash; const hash = createHash(); - const hashAugmentation = this.calculateHashAugmentation(outputPluginDriver); + const hashAugmentation = outputPluginDriver.hookReduceValueSync( + 'augmentChunkHash', + '', + [this.getPrerenderedChunk()], + (hashAugmentation, pluginHash) => { + if (pluginHash) { + hashAugmentation += pluginHash; + } + return hashAugmentation; + } + ); hash.update(hashAugmentation); hash.update(this.renderedSource!.toString()); hash.update( @@ -401,7 +412,7 @@ export default class Chunk { } // prerender allows chunk hashes and names to be generated before finalizing - preRender(options: OutputOptions, inputBase: string) { + preRender(options: OutputOptions, inputBase: string, outputPluginDriver: PluginDriver) { timeStart('render modules', 3); const magicString = new MagicStringBundle({ separator: options.compact ? '' : '\n\n' }); @@ -414,10 +425,11 @@ export default class Chunk { const renderOptions: RenderOptions = { compact: options.compact as boolean, dynamicImportFunction: options.dynamicImportFunction as string, - format: options.format as string, + format: options.format as InternalModuleFormat, freeze: options.freeze !== false, indent: this.indentString, namespaceToStringTag: options.namespaceToStringTag === true, + outputPluginDriver, varOrConst: options.preferConst ? 'const' : 'var' }; @@ -445,19 +457,18 @@ export default class Chunk { let renderedLength = 0; if (module.isIncluded()) { const source = module.render(renderOptions).trim(); - if (options.compact && source.lastLine().indexOf('//') !== -1) source.append('\n'); - const namespace = module.getOrCreateNamespace(); - if (namespace.included || source.length() > 0) { - renderedLength = source.length(); + renderedLength = source.length(); + if (renderedLength) { + if (options.compact && source.lastLine().indexOf('//') !== -1) source.append('\n'); this.renderedModuleSources.set(module, source); magicString.addSource(source); this.usedModules.push(module); - - if (namespace.included && !this.graph.preserveModules) { - const rendered = namespace.renderBlock(renderOptions); - if (namespace.renderFirst()) hoistedSource += n + rendered; - else magicString.addSource(new MagicString(rendered)); - } + } + const namespace = module.getOrCreateNamespace(); + if (namespace.included && !this.graph.preserveModules) { + const rendered = namespace.renderBlock(renderOptions); + if (namespace.renderFirst()) hoistedSource += n + rendered; + else magicString.addSource(new MagicString(rendered)); } } const { renderedExports, removedExports } = module.getRenderedExports(); @@ -510,7 +521,7 @@ export default class Chunk { timeStart('render format', 3); const chunkId = this.id!; - const format = options.format as string; + const format = options.format as InternalModuleFormat; const finalise = finalisers[format]; if (options.dynamicImportFunction && format !== 'es') { this.graph.warn({ @@ -527,10 +538,10 @@ export default class Chunk { const depId = dependency instanceof ExternalModule ? renderedDependency.id : dependency.id!; if (dependency instanceof Chunk) renderedDependency.namedExportsMode = dependency.exportMode !== 'default'; - renderedDependency.id = this.getRelativePath(depId); + renderedDependency.id = this.getRelativePath(depId, false); } - this.finaliseDynamicImports(format); + this.finaliseDynamicImports(format === 'amd'); this.finaliseImportMetas(format, outputPluginDriver); const hasExports = @@ -655,34 +666,6 @@ export default class Chunk { } } - private calculateHashAugmentation(outputPluginDriver: PluginDriver): string { - const facadeModule = this.facadeModule; - const getChunkName = this.getChunkName.bind(this); - const preRenderedChunk = { - dynamicImports: this.getDynamicImportIds(), - exports: this.getExportNames(), - facadeModuleId: facadeModule && facadeModule.id, - imports: this.getImportIds(), - isDynamicEntry: facadeModule !== null && facadeModule.dynamicallyImportedBy.length > 0, - isEntry: facadeModule !== null && facadeModule.isEntryPoint, - modules: this.renderedModules, - get name() { - return getChunkName(); - } - } as PreRenderedChunk; - return outputPluginDriver.hookReduceValueSync( - 'augmentChunkHash', - '', - [preRenderedChunk], - (hashAugmentation, pluginHash) => { - if (pluginHash) { - hashAugmentation += pluginHash; - } - return hashAugmentation; - } - ); - } - private computeContentHashWithDependencies( addons: Addons, options: OutputOptions, @@ -710,37 +693,35 @@ export default class Chunk { return hash.digest('hex').substr(0, 8); } - private finaliseDynamicImports(format: string) { + private finaliseDynamicImports(stripKnownJsExtensions: boolean) { for (const [module, code] of this.renderedModuleSources) { for (const { node, resolution } of module.dynamicImports) { - if (!resolution) continue; - if (resolution instanceof Module) { - if (resolution.chunk && resolution.chunk !== this) { - const resolutionChunk = resolution.facadeChunk || resolution.chunk; - node.renderFinalResolution( - code, - `'${this.getRelativePath(resolutionChunk.id!)}'`, - format - ); - } - } else { - node.renderFinalResolution( - code, - resolution instanceof ExternalModule - ? `'${ - resolution.renormalizeRenderPath - ? this.getRelativePath(resolution.renderPath) - : resolution.id - }'` - : resolution, - format - ); + if ( + !resolution || + !node.included || + (resolution instanceof Module && resolution.chunk === this) + ) { + continue; } + const renderedResolution = + resolution instanceof Module + ? `'${this.getRelativePath(resolution.facadeChunk!.id!, stripKnownJsExtensions)}'` + : resolution instanceof ExternalModule + ? `'${ + resolution.renormalizeRenderPath + ? this.getRelativePath(resolution.renderPath, stripKnownJsExtensions) + : resolution.id + }'` + : resolution; + node.renderFinalResolution(code, renderedResolution); } } } - private finaliseImportMetas(format: string, outputPluginDriver: PluginDriver): void { + private finaliseImportMetas( + format: InternalModuleFormat, + outputPluginDriver: PluginDriver + ): void { for (const [module, code] of this.renderedModuleSources) { for (const importMeta of module.importMetas) { importMeta.renderFinalMechanism(code, this.id!, format, outputPluginDriver); @@ -896,8 +877,28 @@ export default class Chunk { return getAliasName(this.orderedModules[this.orderedModules.length - 1].id); } - private getRelativePath(targetPath: string): string { - const relativePath = normalize(relative(dirname(this.id!), targetPath)); + private getPrerenderedChunk(): PreRenderedChunk { + const facadeModule = this.facadeModule; + const getChunkName = this.getChunkName.bind(this); + return { + dynamicImports: this.getDynamicImportIds(), + exports: this.getExportNames(), + facadeModuleId: facadeModule && facadeModule.id, + imports: this.getImportIds(), + isDynamicEntry: facadeModule !== null && facadeModule.dynamicallyImportedBy.length > 0, + isEntry: facadeModule !== null && facadeModule.isEntryPoint, + modules: this.renderedModules!, + get name() { + return getChunkName(); + } + }; + } + + private getRelativePath(targetPath: string, stripJsExtension: boolean): string { + let relativePath = normalize(relative(dirname(this.id!), targetPath)); + if (stripJsExtension && relativePath.endsWith('.js')) { + relativePath = relativePath.slice(0, -3); + } return relativePath.startsWith('../') ? relativePath : './' + relativePath; } @@ -923,12 +924,12 @@ export default class Chunk { if (resolution instanceof Module) { if (resolution.chunk === this) { const namespace = resolution.getOrCreateNamespace(); - node.setResolution('named', namespace); + node.setResolution('named', resolution, namespace); } else { - node.setResolution(resolution.chunk!.exportMode); + node.setResolution(resolution.chunk!.exportMode, resolution); } } else { - node.setResolution('auto'); + node.setResolution('auto', resolution); } } } diff --git a/src/Graph.ts b/src/Graph.ts index 1458189e0d9..7edff3f5c9c 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -126,12 +126,7 @@ export default class Graph { ...this.acornOptions }); - this.pluginDriver = new PluginDriver( - this, - options.plugins!, - this.pluginCache, - options.preserveSymlinks === true - ); + this.pluginDriver = new PluginDriver(this, options.plugins!, this.pluginCache); if (watcher) { const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]); @@ -178,6 +173,7 @@ export default class Graph { this, this.moduleById, this.pluginDriver, + options.preserveSymlinks === true, options.external!, (typeof options.manualChunks === 'function' && options.manualChunks) as GetManualChunk | null, (this.treeshakingOptions ? this.treeshakingOptions.moduleSideEffects : null)!, diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 7740914fbc0..8415b17c78e 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -26,9 +26,11 @@ import { errUnresolvedImport, errUnresolvedImportTreatedAsExternal } from './utils/error'; +import { readFile } from './utils/fs'; import { isRelative, resolve } from './utils/path'; import { PluginDriver } from './utils/PluginDriver'; import relativeId from './utils/relativeId'; +import { resolveId } from './utils/resolveId'; import { timeEnd, timeStart } from './utils/timers'; import transform from './utils/transform'; @@ -97,27 +99,22 @@ function getHasModuleSideEffects( export class ModuleLoader { readonly isExternal: IsExternal; private readonly getManualChunk: GetManualChunk; - private readonly graph: Graph; private readonly hasModuleSideEffects: (id: string, external: boolean) => boolean; private readonly indexedEntryModules: { index: number; module: Module }[] = []; private latestLoadModulesPromise: Promise = Promise.resolve(); private readonly manualChunkModules: Record = {}; - private readonly modulesById: Map; private nextEntryModuleIndex = 0; - private readonly pluginDriver: PluginDriver; constructor( - graph: Graph, - modulesById: Map, - pluginDriver: PluginDriver, + private readonly graph: Graph, + private readonly modulesById: Map, + private readonly pluginDriver: PluginDriver, + private readonly preserveSymlinks: boolean, external: ExternalOption, getManualChunk: GetManualChunk | null, moduleSideEffects: ModuleSideEffectsOption, pureExternalModules: PureModulesOption ) { - this.graph = graph; - this.modulesById = modulesById; - this.pluginDriver = pluginDriver; this.isExternal = getIdMatcher(external); this.hasModuleSideEffects = getHasModuleSideEffects( moduleSideEffects, @@ -201,12 +198,13 @@ export class ModuleLoader { async resolveId( source: string, importer: string | undefined, - skip?: number | null + skip: number | null = null ): Promise { return this.normalizeResolveIdResult( this.isExternal(source, importer, false) ? false - : await this.pluginDriver.hookFirst('resolveId', [source, importer], null, skip), + : await resolveId(source, importer, this.preserveSymlinks, this.pluginDriver, skip), + importer, source ); @@ -301,6 +299,7 @@ export class ModuleLoader { timeStart('load modules', 3); return Promise.resolve(this.pluginDriver.hookFirst('load', [id])) + .then(source => (source != null ? source : readFile(id))) .catch((err: Error) => { timeEnd('load modules', 3); let msg = `Could not load ${id}`; @@ -423,23 +422,25 @@ export class ModuleLoader { isEntry: boolean, importer: string | undefined ): Promise => - this.pluginDriver.hookFirst('resolveId', [unresolvedId, importer]).then(resolveIdResult => { - if ( - resolveIdResult === false || - (resolveIdResult && typeof resolveIdResult === 'object' && resolveIdResult.external) - ) { - return error(errEntryCannotBeExternal(unresolvedId)); - } - const id = - resolveIdResult && typeof resolveIdResult === 'object' - ? resolveIdResult.id - : resolveIdResult; + resolveId(unresolvedId, importer, this.preserveSymlinks, this.pluginDriver, null).then( + resolveIdResult => { + if ( + resolveIdResult === false || + (resolveIdResult && typeof resolveIdResult === 'object' && resolveIdResult.external) + ) { + return error(errEntryCannotBeExternal(unresolvedId)); + } + const id = + resolveIdResult && typeof resolveIdResult === 'object' + ? resolveIdResult.id + : resolveIdResult; - if (typeof id === 'string') { - return this.fetchModule(id, undefined as any, true, false, isEntry); + if (typeof id === 'string') { + return this.fetchModule(id, undefined as any, true, false, isEntry); + } + return error(errUnresolvedEntry(unresolvedId)); } - return error(errUnresolvedEntry(unresolvedId)); - }); + ); private normalizeResolveIdResult( resolveIdResult: ResolveIdResult, diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index d27cc87e81a..be4a895f41c 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -1,4 +1,6 @@ import MagicString from 'magic-string'; +import ExternalModule from '../../ExternalModule'; +import Module from '../../Module'; import { findFirstOccurrenceOutsideComment, RenderOptions } from '../../utils/renderHelpers'; import { INTEROP_NAMESPACE_VARIABLE } from '../../utils/variableNames'; import { InclusionContext } from '../ExecutionContext'; @@ -12,11 +14,12 @@ interface DynamicImportMechanism { } export default class Import extends NodeBase { - inlineNamespace?: NamespaceVariable; + inlineNamespace: NamespaceVariable | null = null; source!: ExpressionNode; type!: NodeType.tImportExpression; private exportMode: 'none' | 'named' | 'default' | 'auto' = 'auto'; + private resolution: Module | ExternalModule | string | null = null; hasEffects(): boolean { return true; @@ -59,20 +62,17 @@ export default class Import extends NodeBase { this.source.render(code, options); } - renderFinalResolution(code: MagicString, resolution: string, format: string) { - if (this.included) { - if (format === 'amd' && resolution.startsWith("'.") && resolution.endsWith(".js'")) { - resolution = resolution.slice(0, -4) + "'"; - } - code.overwrite(this.source.start, this.source.end, resolution); - } + renderFinalResolution(code: MagicString, resolution: string) { + code.overwrite(this.source.start, this.source.end, resolution); } setResolution( exportMode: 'none' | 'named' | 'default' | 'auto', - inlineNamespace?: NamespaceVariable + resolution: Module | ExternalModule | string | null, + inlineNamespace: NamespaceVariable | false = false ): void { this.exportMode = exportMode; + this.resolution = resolution; if (inlineNamespace) { this.inlineNamespace = inlineNamespace; } else { @@ -91,6 +91,18 @@ export default class Import extends NodeBase { } private getDynamicImportMechanism(options: RenderOptions): DynamicImportMechanism | null { + const mechanism = options.outputPluginDriver.hookFirstSync('renderDynamicImport', [ + { + customResolution: typeof this.resolution === 'string' ? this.resolution : null, + format: options.format, + moduleId: this.context.module.id, + targetModuleId: + this.resolution && typeof this.resolution !== 'string' ? this.resolution.id : null + } + ]); + if (mechanism) { + return mechanism; + } switch (options.format) { case 'cjs': { const _ = options.compact ? '' : ' '; diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index e7b38ab8212..ff169fae919 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -1,5 +1,5 @@ import MagicString from 'magic-string'; -import { accessedFileUrlGlobals, accessedMetaUrlGlobals } from '../../utils/defaultPlugin'; +import { InternalModuleFormat } from '../../rollup/types'; import { dirname, normalize, relative } from '../../utils/path'; import { PluginDriver } from '../../utils/PluginDriver'; import { ObjectPathKey } from '../utils/PathTracker'; @@ -57,7 +57,7 @@ export default class MetaProperty extends NodeBase { renderFinalMechanism( code: MagicString, chunkId: string, - format: string, + format: InternalModuleFormat, outputPluginDriver: PluginDriver ): void { if (!this.included) return; @@ -106,18 +106,19 @@ export default class MetaProperty extends NodeBase { ]); } if (!replacement) { - replacement = outputPluginDriver.hookFirstSync<'resolveFileUrl'>('resolveFileUrl', [ - { - assetReferenceId, - chunkId, - chunkReferenceId, - fileName, - format, - moduleId: this.context.module.id, - referenceId: referenceId || assetReferenceId || chunkReferenceId!, - relativePath - } - ]) as string; + replacement = + outputPluginDriver.hookFirstSync('resolveFileUrl', [ + { + assetReferenceId, + chunkId, + chunkReferenceId, + fileName, + format, + moduleId: this.context.module.id, + referenceId: referenceId || assetReferenceId || chunkReferenceId!, + relativePath + } + ]) || relativeUrlMechanisms[format](relativePath); } code.overwrite( @@ -129,14 +130,16 @@ export default class MetaProperty extends NodeBase { return; } - const replacement = outputPluginDriver.hookFirstSync('resolveImportMeta', [ - metaProperty, - { - chunkId, - format, - moduleId: this.context.module.id - } - ]); + const replacement = + outputPluginDriver.hookFirstSync('resolveImportMeta', [ + metaProperty, + { + chunkId, + format, + moduleId: this.context.module.id + } + ]) || + (importMetaMechanisms[format] && importMetaMechanisms[format](metaProperty, chunkId)); if (typeof replacement === 'string') { if (parent instanceof MemberExpression) { code.overwrite(parent.start, parent.end, replacement, { contentOnly: true }); @@ -146,3 +149,77 @@ export default class MetaProperty extends NodeBase { } } } + +const accessedMetaUrlGlobals = { + amd: ['document', 'module', 'URL'], + cjs: ['document', 'require', 'URL'], + iife: ['document', 'URL'], + system: ['module'], + umd: ['document', 'require', 'URL'] +}; + +const accessedFileUrlGlobals = { + amd: ['document', 'require', 'URL'], + cjs: ['document', 'require', 'URL'], + iife: ['document', 'URL'], + system: ['module', 'URL'], + umd: ['document', 'require', 'URL'] +}; + +const getResolveUrl = (path: string, URL = 'URL') => `new ${URL}(${path}).href`; + +const getRelativeUrlFromDocument = (relativePath: string) => + getResolveUrl( + `'${relativePath}', document.currentScript && document.currentScript.src || document.baseURI` + ); + +const getGenericImportMetaMechanism = (getUrl: (chunkId: string) => string) => ( + prop: string | null, + chunkId: string +) => { + const urlMechanism = getUrl(chunkId); + return prop === null ? `({ url: ${urlMechanism} })` : prop === 'url' ? urlMechanism : 'undefined'; +}; + +const getUrlFromDocument = (chunkId: string) => + `(document.currentScript && document.currentScript.src || new URL('${chunkId}', document.baseURI).href)`; + +const relativeUrlMechanisms: Record string> = { + amd: relativePath => { + if (relativePath[0] !== '.') relativePath = './' + relativePath; + return getResolveUrl(`require.toUrl('${relativePath}'), document.baseURI`); + }, + cjs: relativePath => + `(typeof document === 'undefined' ? ${getResolveUrl( + `'file:' + __dirname + '/${relativePath}'`, + `(require('u' + 'rl').URL)` + )} : ${getRelativeUrlFromDocument(relativePath)})`, + es: relativePath => getResolveUrl(`'${relativePath}', import.meta.url`), + iife: relativePath => getRelativeUrlFromDocument(relativePath), + system: relativePath => getResolveUrl(`'${relativePath}', module.meta.url`), + umd: relativePath => + `(typeof document === 'undefined' ? ${getResolveUrl( + `'file:' + __dirname + '/${relativePath}'`, + `(require('u' + 'rl').URL)` + )} : ${getRelativeUrlFromDocument(relativePath)})` +}; + +const importMetaMechanisms: Record string> = { + amd: getGenericImportMetaMechanism(() => getResolveUrl(`module.uri, document.baseURI`)), + cjs: getGenericImportMetaMechanism( + chunkId => + `(typeof document === 'undefined' ? ${getResolveUrl( + `'file:' + __filename`, + `(require('u' + 'rl').URL)` + )} : ${getUrlFromDocument(chunkId)})` + ), + iife: getGenericImportMetaMechanism(chunkId => getUrlFromDocument(chunkId)), + system: prop => (prop === null ? `module.meta` : `module.meta.${prop}`), + umd: getGenericImportMetaMechanism( + chunkId => + `(typeof document === 'undefined' ? ${getResolveUrl( + `'file:' + __filename`, + `(require('u' + 'rl').URL)` + )} : ${getUrlFromDocument(chunkId)})` + ) +}; diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index f60ddae3ed2..cb61b03d9b6 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -197,6 +197,12 @@ export async function rollupInternal( ): Promise { timeStart('GENERATE', 1); + if (outputOptions.dynamicImportFunction) { + graph.warnDeprecation( + `The "output.dynamicImportFunction" option is deprecated. Use the "renderDynamicImport" plugin hook instead.`, + false + ); + } const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]'; const inputBase = commondir(getAbsoluteEntryModulePaths(chunks)); const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null); @@ -212,7 +218,7 @@ export async function rollupInternal( chunk.exportMode = getExportMode(chunk, outputOptions, chunk.facadeModule!.id); } for (const chunk of chunks) { - chunk.preRender(outputOptions, inputBase); + chunk.preRender(outputOptions, inputBase, outputPluginDriver); } assignChunkIds( chunks, diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 5a0c821fbcd..17a61eff786 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -164,6 +164,7 @@ export interface PluginContext extends MinimalPluginContext { getModuleInfo: ( moduleId: string ) => { + dynamicallyImportedIds: string[]; hasModuleSideEffects: boolean; id: string; importedIds: string[]; @@ -261,7 +262,7 @@ export type ResolveDynamicImportHook = ( export type ResolveImportMetaHook = ( this: PluginContext, prop: string | null, - options: { chunkId: string; format: string; moduleId: string } + options: { chunkId: string; format: InternalModuleFormat; moduleId: string } ) => string | null | undefined; export type ResolveAssetUrlHook = ( @@ -269,7 +270,7 @@ export type ResolveAssetUrlHook = ( options: { assetFileName: string; chunkId: string; - format: string; + format: InternalModuleFormat; moduleId: string; relativeAssetPath: string; } @@ -282,7 +283,7 @@ export type ResolveFileUrlHook = ( chunkId: string; chunkReferenceId: string | null; fileName: string; - format: string; + format: InternalModuleFormat; moduleId: string; referenceId: string; relativePath: string; @@ -316,6 +317,17 @@ export interface OutputBundleWithPlaceholders { [fileName: string]: OutputAsset | OutputChunk | FilePlaceholder; } +export interface PluginHooks extends OutputPluginHooks { + buildEnd: (this: PluginContext, err?: Error) => Promise | void; + buildStart: (this: PluginContext, options: InputOptions) => Promise | void; + load: LoadHook; + options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; + resolveDynamicImport: ResolveDynamicImportHook; + resolveId: ResolveIdHook; + transform: TransformHook; + watchChange: (id: string) => void; +} + interface OutputPluginHooks { augmentChunkHash: (this: PluginContext, chunk: PreRenderedChunk) => string | void; generateBundle: ( @@ -326,6 +338,15 @@ interface OutputPluginHooks { ) => void | Promise; outputOptions: (this: PluginContext, options: OutputOptions) => OutputOptions | null | undefined; renderChunk: RenderChunkHook; + renderDynamicImport: ( + this: PluginContext, + options: { + customResolution: string | null; + format: InternalModuleFormat; + moduleId: string; + targetModuleId: string | null; + } + ) => { left: string; right: string } | null | undefined; renderError: (this: PluginContext, err?: Error) => Promise | void; renderStart: ( this: PluginContext, @@ -334,8 +355,8 @@ interface OutputPluginHooks { ) => Promise | void; /** @deprecated Use `resolveFileUrl` instead */ resolveAssetUrl: ResolveAssetUrlHook; - resolveDynamicImport: ResolveDynamicImportHook; resolveFileUrl: ResolveFileUrlHook; + resolveImportMeta: ResolveImportMetaHook; writeBundle: ( this: PluginContext, options: OutputOptions, @@ -343,17 +364,6 @@ interface OutputPluginHooks { ) => void | Promise; } -export interface PluginHooks extends OutputPluginHooks { - buildEnd: (this: PluginContext, err?: Error) => Promise | void; - buildStart: (this: PluginContext, options: InputOptions) => Promise | void; - load: LoadHook; - options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; - resolveId: ResolveIdHook; - resolveImportMeta: ResolveImportMetaHook; - transform: TransformHook; - watchChange: (id: string) => void; -} - export type AsyncPluginHooks = | 'buildEnd' | 'buildStart' @@ -373,6 +383,7 @@ export type SyncPluginHooks = Exclude; export type FirstPluginHooks = | 'load' + | 'renderDynamicImport' | 'resolveAssetUrl' | 'resolveDynamicImport' | 'resolveFileUrl' @@ -456,17 +467,9 @@ export interface InputOptions { watch?: WatcherOptions; } -export type ModuleFormat = - | 'amd' - | 'cjs' - | 'commonjs' - | 'es' - | 'esm' - | 'iife' - | 'module' - | 'system' - | 'systemjs' - | 'umd'; +export type InternalModuleFormat = 'amd' | 'cjs' | 'es' | 'iife' | 'system' | 'umd'; + +export type ModuleFormat = InternalModuleFormat | 'commonjs' | 'esm' | 'module' | 'systemjs'; export type OptionsPaths = Record | ((id: string) => string); @@ -481,6 +484,7 @@ export interface OutputOptions { compact?: boolean; // only required for bundle.write dir?: string; + /** @deprecated Use the "renderDynamicImport" plugin hook instead. */ dynamicImportFunction?: string; entryFileNames?: string; esModule?: boolean; @@ -490,8 +494,6 @@ export interface OutputOptions { // only required for bundle.write file?: string; footer?: string | (() => string | Promise); - // this is optional at the base-level of RollupWatchOptions, - // which extends from this interface through config merge format?: ModuleFormat; freeze?: boolean; globals?: GlobalsOption; diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 3d832cb5652..09c94ff3635 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -127,14 +127,23 @@ export function getPluginContexts( if (foundModule == null) { throw new Error(`Unable to find module ${moduleId}`); } - + const importedIds: string[] = []; + const dynamicallyImportedIds: string[] = []; + if (foundModule instanceof Module) { + for (const source of foundModule.sources) { + importedIds.push(foundModule.resolvedIds[source].id); + } + for (const { resolution } of foundModule.dynamicImports) { + if (resolution instanceof Module || resolution instanceof ExternalModule) { + dynamicallyImportedIds.push(resolution.id); + } + } + } return { + dynamicallyImportedIds, hasModuleSideEffects: foundModule.moduleSideEffects, id: foundModule.id, - importedIds: - foundModule instanceof ExternalModule - ? [] - : Array.from(foundModule.sources).map(id => foundModule.resolvedIds[id].id), + importedIds, isEntry: foundModule instanceof Module && foundModule.isEntryPoint, isExternal: foundModule instanceof ExternalModule }; diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index b3f6a804a3d..1c1445e1081 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -5,6 +5,7 @@ import { EmitFile, FirstPluginHooks, OutputBundleWithPlaceholders, + OutputPluginHooks, ParallelPluginHooks, Plugin, PluginContext, @@ -14,7 +15,6 @@ import { SerializablePluginCache, SyncPluginHooks } from '../rollup/types'; -import { getRollupDefaultPlugin } from './defaultPlugin'; import { errInputHookInOutputPlugin, error } from './error'; import { FileEmitter } from './FileEmitter'; import { getPluginContexts } from './PluginContext'; @@ -36,6 +36,22 @@ type EnsurePromise = Promise>; */ type Arg0 = Parameters[0]; +// This will make sure no input hook is omitted +type Subtract = T extends U ? never : T; +const inputHookNames: { + [P in Subtract]: 1; +} = { + buildEnd: 1, + buildStart: 1, + load: 1, + options: 1, + resolveDynamicImport: 1, + resolveId: 1, + transform: 1, + watchChange: 1 +}; +const inputHooks = Object.keys(inputHookNames); + type ReplaceContext = (context: PluginContext, plugin: Plugin) => PluginContext; function throwInvalidHookError(hookName: string, pluginName: string) { @@ -59,32 +75,26 @@ export class PluginDriver { private pluginCache: Record | undefined; private pluginContexts: PluginContext[]; private plugins: Plugin[]; - private preserveSymlinks: boolean; - private previousHooks = new Set(['options']); constructor( graph: Graph, userPlugins: Plugin[], pluginCache: Record | undefined, - preserveSymlinks: boolean, basePluginDriver?: PluginDriver ) { warnDeprecatedHooks(userPlugins, graph); this.graph = graph; this.pluginCache = pluginCache; - this.preserveSymlinks = preserveSymlinks; this.fileEmitter = new FileEmitter(graph, basePluginDriver && basePluginDriver.fileEmitter); this.emitFile = this.fileEmitter.emitFile; this.getFileName = this.fileEmitter.getFileName; this.finaliseAssets = this.fileEmitter.assertAssetsFinalized; this.setOutputBundle = this.fileEmitter.setOutputBundle; - this.plugins = userPlugins.concat( - basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)] - ); + this.plugins = userPlugins.concat(basePluginDriver ? basePluginDriver.plugins : []); this.pluginContexts = this.plugins.map(getPluginContexts(pluginCache, graph, this.fileEmitter)); if (basePluginDriver) { for (const plugin of userPlugins) { - for (const hook of basePluginDriver.previousHooks) { + for (const hook of inputHooks) { if (hook in plugin) { graph.warn(errInputHookInOutputPlugin(plugin.name, hook)); } @@ -94,7 +104,7 @@ export class PluginDriver { } public createOutputPluginDriver(plugins: Plugin[]): PluginDriver { - return new PluginDriver(this.graph, plugins, this.pluginCache, this.preserveSymlinks, this); + return new PluginDriver(this.graph, plugins, this.pluginCache, this); } // chains, first non-null result stops and returns @@ -279,7 +289,6 @@ export class PluginDriver { permitValues: boolean, hookContext?: ReplaceContext | null ): EnsurePromise> { - this.previousHooks.add(hookName); const plugin = this.plugins[pluginIndex]; const hook = plugin[hookName]; if (!hook) return undefined as any; @@ -314,7 +323,6 @@ export class PluginDriver { pluginIndex: number, hookContext?: ReplaceContext ): ReturnType { - this.previousHooks.add(hookName); const plugin = this.plugins[pluginIndex]; const hook = plugin[hookName]; if (!hook) return undefined as any; diff --git a/src/utils/defaultPlugin.ts b/src/utils/defaultPlugin.ts deleted file mode 100644 index 1e30b349eff..00000000000 --- a/src/utils/defaultPlugin.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Plugin, ResolveIdHook } from '../rollup/types'; -import { error } from './error'; -import { lstatSync, readdirSync, readFile, realpathSync } from './fs'; -import { basename, dirname, isAbsolute, resolve } from './path'; - -export function getRollupDefaultPlugin(preserveSymlinks: boolean): Plugin { - return { - name: 'Rollup Core', - resolveId: createResolveId(preserveSymlinks) as ResolveIdHook, - load(id) { - return readFile(id); - }, - resolveFileUrl({ relativePath, format }) { - return relativeUrlMechanisms[format](relativePath); - }, - resolveImportMeta(prop, { chunkId, format }) { - const mechanism = importMetaMechanisms[format] && importMetaMechanisms[format](prop, chunkId); - if (mechanism) { - return mechanism; - } - } - }; -} - -function findFile(file: string, preserveSymlinks: boolean): string | void { - try { - const stats = lstatSync(file); - if (!preserveSymlinks && stats.isSymbolicLink()) - return findFile(realpathSync(file), preserveSymlinks); - if ((preserveSymlinks && stats.isSymbolicLink()) || stats.isFile()) { - // check case - const name = basename(file); - const files = readdirSync(dirname(file)); - - if (files.indexOf(name) !== -1) return file; - } - } catch { - // suppress - } -} - -function addJsExtensionIfNecessary(file: string, preserveSymlinks: boolean) { - let found = findFile(file, preserveSymlinks); - if (found) return found; - found = findFile(file + '.mjs', preserveSymlinks); - if (found) return found; - found = findFile(file + '.js', preserveSymlinks); - return found; -} - -function createResolveId(preserveSymlinks: boolean) { - return function(source: string, importer: string) { - if (typeof process === 'undefined') { - return error({ - code: 'MISSING_PROCESS', - message: `It looks like you're using Rollup in a non-Node.js environment. This means you must supply a plugin with custom resolveId and load functions`, - url: 'https://rollupjs.org/guide/en/#a-simple-example' - }); - } - - // external modules (non-entry modules that start with neither '.' or '/') - // are skipped at this stage. - if (importer !== undefined && !isAbsolute(source) && source[0] !== '.') return null; - - // `resolve` processes paths from right to left, prepending them until an - // absolute path is created. Absolute importees therefore shortcircuit the - // resolve call and require no special handing on our part. - // See https://nodejs.org/api/path.html#path_path_resolve_paths - return addJsExtensionIfNecessary( - resolve(importer ? dirname(importer) : resolve(), source), - preserveSymlinks - ); - }; -} - -const getResolveUrl = (path: string, URL = 'URL') => `new ${URL}(${path}).href`; - -const getUrlFromDocument = (chunkId: string) => - `(document.currentScript && document.currentScript.src || new URL('${chunkId}', document.baseURI).href)`; - -const getGenericImportMetaMechanism = (getUrl: (chunkId: string) => string) => ( - prop: string | null, - chunkId: string -) => { - const urlMechanism = getUrl(chunkId); - return prop === null ? `({ url: ${urlMechanism} })` : prop === 'url' ? urlMechanism : 'undefined'; -}; - -const importMetaMechanisms: Record string> = { - amd: getGenericImportMetaMechanism(() => getResolveUrl(`module.uri, document.baseURI`)), - cjs: getGenericImportMetaMechanism( - chunkId => - `(typeof document === 'undefined' ? ${getResolveUrl( - `'file:' + __filename`, - `(require('u' + 'rl').URL)` - )} : ${getUrlFromDocument(chunkId)})` - ), - iife: getGenericImportMetaMechanism(chunkId => getUrlFromDocument(chunkId)), - system: prop => (prop === null ? `module.meta` : `module.meta.${prop}`), - umd: getGenericImportMetaMechanism( - chunkId => - `(typeof document === 'undefined' ? ${getResolveUrl( - `'file:' + __filename`, - `(require('u' + 'rl').URL)` - )} : ${getUrlFromDocument(chunkId)})` - ) -}; - -const getRelativeUrlFromDocument = (relativePath: string) => - getResolveUrl( - `'${relativePath}', document.currentScript && document.currentScript.src || document.baseURI` - ); - -const relativeUrlMechanisms: Record string> = { - amd: relativePath => { - if (relativePath[0] !== '.') relativePath = './' + relativePath; - return getResolveUrl(`require.toUrl('${relativePath}'), document.baseURI`); - }, - cjs: relativePath => - `(typeof document === 'undefined' ? ${getResolveUrl( - `'file:' + __dirname + '/${relativePath}'`, - `(require('u' + 'rl').URL)` - )} : ${getRelativeUrlFromDocument(relativePath)})`, - es: relativePath => getResolveUrl(`'${relativePath}', import.meta.url`), - iife: relativePath => getRelativeUrlFromDocument(relativePath), - system: relativePath => getResolveUrl(`'${relativePath}', module.meta.url`), - umd: relativePath => - `(typeof document === 'undefined' ? ${getResolveUrl( - `'file:' + __dirname + '/${relativePath}'`, - `(require('u' + 'rl').URL)` - )} : ${getRelativeUrlFromDocument(relativePath)})` -}; - -export const accessedMetaUrlGlobals = { - amd: ['document', 'module', 'URL'], - cjs: ['document', 'require', 'URL'], - iife: ['document', 'URL'], - system: ['module'], - umd: ['document', 'require', 'URL'] -}; - -export const accessedFileUrlGlobals = { - amd: ['document', 'require', 'URL'], - cjs: ['document', 'require', 'URL'], - iife: ['document', 'URL'], - system: ['module', 'URL'], - umd: ['document', 'require', 'URL'] -}; diff --git a/src/utils/renderHelpers.ts b/src/utils/renderHelpers.ts index 1363ce535e4..74a764a47d7 100644 --- a/src/utils/renderHelpers.ts +++ b/src/utils/renderHelpers.ts @@ -1,14 +1,17 @@ import MagicString from 'magic-string'; import { Node, StatementNode } from '../ast/nodes/shared/Node'; +import { InternalModuleFormat } from '../rollup/types'; +import { PluginDriver } from './PluginDriver'; import { treeshakeNode } from './treeshakeNode'; export interface RenderOptions { compact: boolean; dynamicImportFunction: string; - format: string; + format: InternalModuleFormat; freeze: boolean; indent: string; namespaceToStringTag: boolean; + outputPluginDriver: PluginDriver; varOrConst: 'var' | 'const'; } diff --git a/src/utils/resolveId.ts b/src/utils/resolveId.ts new file mode 100644 index 00000000000..4fb6e911041 --- /dev/null +++ b/src/utils/resolveId.ts @@ -0,0 +1,53 @@ +import { lstatSync, readdirSync, realpathSync } from './fs'; +import { basename, dirname, isAbsolute, resolve } from './path'; +import { PluginDriver } from './PluginDriver'; + +export async function resolveId( + source: string, + importer: string | undefined, + preserveSymlinks: boolean, + pluginDriver: PluginDriver, + skip: number | null +) { + const pluginResult = await pluginDriver.hookFirst('resolveId', [source, importer], null, skip); + if (pluginResult != null) return pluginResult; + + // external modules (non-entry modules that start with neither '.' or '/') + // are skipped at this stage. + if (importer !== undefined && !isAbsolute(source) && source[0] !== '.') return null; + + // `resolve` processes paths from right to left, prepending them until an + // absolute path is created. Absolute importees therefore shortcircuit the + // resolve call and require no special handing on our part. + // See https://nodejs.org/api/path.html#path_path_resolve_paths + return addJsExtensionIfNecessary( + resolve(importer ? dirname(importer) : resolve(), source), + preserveSymlinks + ); +} + +function addJsExtensionIfNecessary(file: string, preserveSymlinks: boolean) { + let found = findFile(file, preserveSymlinks); + if (found) return found; + found = findFile(file + '.mjs', preserveSymlinks); + if (found) return found; + found = findFile(file + '.js', preserveSymlinks); + return found; +} + +function findFile(file: string, preserveSymlinks: boolean): string | undefined { + try { + const stats = lstatSync(file); + if (!preserveSymlinks && stats.isSymbolicLink()) + return findFile(realpathSync(file), preserveSymlinks); + if ((preserveSymlinks && stats.isSymbolicLink()) || stats.isFile()) { + // check case + const name = basename(file); + const files = readdirSync(dirname(file)); + + if (files.indexOf(name) !== -1) return file; + } + } catch { + // suppress + } +} diff --git a/test/chunking-form/samples/deprecated/dynamic-import-comments/_config.js b/test/chunking-form/samples/deprecated/dynamic-import-comments/_config.js new file mode 100644 index 00000000000..2bc7c8df417 --- /dev/null +++ b/test/chunking-form/samples/deprecated/dynamic-import-comments/_config.js @@ -0,0 +1,16 @@ +module.exports = { + description: 'should not remove inline comments inside dynamic import', + options: { + strictDeprecations: false, + input: 'main.js', + onwarn() {}, + plugins: { + resolveDynamicImport() { + return false; + } + }, + output: { + dynamicImportFunction: 'foobar' + } + } +}; diff --git a/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/amd/main.js b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/amd/main.js new file mode 100644 index 00000000000..24110f00861 --- /dev/null +++ b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/amd/main.js @@ -0,0 +1,26 @@ +define(['require'], function (require) { 'use strict'; + + function _interopNamespace(e) { + if (e && e.__esModule) { return e; } else { + var n = {}; + if (e) { + Object.keys(e).forEach(function (k) { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { + return e[k]; + } + }); + }); + } + n['default'] = e; + return n; + } + } + + new Promise(function (resolve, reject) { require([ + /* webpackChunkName: "chunk-name" */ + './foo'/*suffix*/], function (m) { resolve(_interopNamespace(m)); }, reject) }); + +}); diff --git a/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/cjs/main.js b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/cjs/main.js new file mode 100644 index 00000000000..7e7e20c640d --- /dev/null +++ b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/cjs/main.js @@ -0,0 +1,24 @@ +'use strict'; + +function _interopNamespace(e) { + if (e && e.__esModule) { return e; } else { + var n = {}; + if (e) { + Object.keys(e).forEach(function (k) { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { + return e[k]; + } + }); + }); + } + n['default'] = e; + return n; + } +} + +new Promise(function (resolve) { resolve(_interopNamespace(require( +/* webpackChunkName: "chunk-name" */ +'./foo.js'/*suffix*/))); }); diff --git a/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/es/main.js b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/es/main.js new file mode 100644 index 00000000000..8f23863149d --- /dev/null +++ b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/es/main.js @@ -0,0 +1,3 @@ +foobar( +/* webpackChunkName: "chunk-name" */ +'./foo.js'/*suffix*/); diff --git a/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/system/main.js b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/system/main.js new file mode 100644 index 00000000000..5967f1516fb --- /dev/null +++ b/test/chunking-form/samples/deprecated/dynamic-import-comments/_expected/system/main.js @@ -0,0 +1,12 @@ +System.register([], function (exports, module) { + 'use strict'; + return { + execute: function () { + + module.import( + /* webpackChunkName: "chunk-name" */ + './foo.js'/*suffix*/); + + } + }; +}); diff --git a/test/chunking-form/samples/deprecated/dynamic-import-comments/main.js b/test/chunking-form/samples/deprecated/dynamic-import-comments/main.js new file mode 100644 index 00000000000..9519c0a650f --- /dev/null +++ b/test/chunking-form/samples/deprecated/dynamic-import-comments/main.js @@ -0,0 +1,3 @@ +import /* () should not break */ ( +/* webpackChunkName: "chunk-name" */ +'./foo.js'/*suffix*/); diff --git a/test/chunking-form/samples/dynamic-import-name/_config.js b/test/chunking-form/samples/deprecated/dynamic-import-name/_config.js similarity index 89% rename from test/chunking-form/samples/dynamic-import-name/_config.js rename to test/chunking-form/samples/deprecated/dynamic-import-name/_config.js index 12af4716181..601e3b9d0d7 100644 --- a/test/chunking-form/samples/dynamic-import-name/_config.js +++ b/test/chunking-form/samples/deprecated/dynamic-import-name/_config.js @@ -1,6 +1,7 @@ module.exports = { description: 'allows specifying a custom importer function', options: { + strictDeprecations: false, input: 'main.js', onwarn() {}, plugins: { diff --git a/test/chunking-form/samples/dynamic-import-name/_expected/amd/main.js b/test/chunking-form/samples/deprecated/dynamic-import-name/_expected/amd/main.js similarity index 100% rename from test/chunking-form/samples/dynamic-import-name/_expected/amd/main.js rename to test/chunking-form/samples/deprecated/dynamic-import-name/_expected/amd/main.js diff --git a/test/chunking-form/samples/dynamic-import-name/_expected/cjs/main.js b/test/chunking-form/samples/deprecated/dynamic-import-name/_expected/cjs/main.js similarity index 100% rename from test/chunking-form/samples/dynamic-import-name/_expected/cjs/main.js rename to test/chunking-form/samples/deprecated/dynamic-import-name/_expected/cjs/main.js diff --git a/test/chunking-form/samples/dynamic-import-name/_expected/es/main.js b/test/chunking-form/samples/deprecated/dynamic-import-name/_expected/es/main.js similarity index 100% rename from test/chunking-form/samples/dynamic-import-name/_expected/es/main.js rename to test/chunking-form/samples/deprecated/dynamic-import-name/_expected/es/main.js diff --git a/test/chunking-form/samples/dynamic-import-name/_expected/system/main.js b/test/chunking-form/samples/deprecated/dynamic-import-name/_expected/system/main.js similarity index 100% rename from test/chunking-form/samples/dynamic-import-name/_expected/system/main.js rename to test/chunking-form/samples/deprecated/dynamic-import-name/_expected/system/main.js diff --git a/test/chunking-form/samples/dynamic-import-name/main.js b/test/chunking-form/samples/deprecated/dynamic-import-name/main.js similarity index 100% rename from test/chunking-form/samples/dynamic-import-name/main.js rename to test/chunking-form/samples/deprecated/dynamic-import-name/main.js diff --git a/test/chunking-form/samples/dynamic-import-comments/_config.js b/test/chunking-form/samples/dynamic-import-comments/_config.js index d279ae3a1bf..9e74bcab779 100644 --- a/test/chunking-form/samples/dynamic-import-comments/_config.js +++ b/test/chunking-form/samples/dynamic-import-comments/_config.js @@ -1,15 +1,15 @@ module.exports = { - description: 'should not remove inline comments inside dynamic import', + description: 'does not remove inline comments inside dynamic imports', options: { input: 'main.js', onwarn() {}, plugins: { + renderDynamicImport() { + return { left: 'foobar(', right: ')' }; + }, resolveDynamicImport() { return false; } - }, - output: { - dynamicImportFunction: 'foobar' } } }; diff --git a/test/chunking-form/samples/dynamic-import-comments/_expected/amd/main.js b/test/chunking-form/samples/dynamic-import-comments/_expected/amd/main.js index 24110f00861..686837209f9 100644 --- a/test/chunking-form/samples/dynamic-import-comments/_expected/amd/main.js +++ b/test/chunking-form/samples/dynamic-import-comments/_expected/amd/main.js @@ -19,8 +19,8 @@ define(['require'], function (require) { 'use strict'; } } - new Promise(function (resolve, reject) { require([ + foobar( /* webpackChunkName: "chunk-name" */ - './foo'/*suffix*/], function (m) { resolve(_interopNamespace(m)); }, reject) }); + './foo'/*suffix*/); }); diff --git a/test/chunking-form/samples/dynamic-import-comments/_expected/cjs/main.js b/test/chunking-form/samples/dynamic-import-comments/_expected/cjs/main.js index 7e7e20c640d..5949b7802b5 100644 --- a/test/chunking-form/samples/dynamic-import-comments/_expected/cjs/main.js +++ b/test/chunking-form/samples/dynamic-import-comments/_expected/cjs/main.js @@ -19,6 +19,6 @@ function _interopNamespace(e) { } } -new Promise(function (resolve) { resolve(_interopNamespace(require( +foobar( /* webpackChunkName: "chunk-name" */ -'./foo.js'/*suffix*/))); }); +'./foo.js'/*suffix*/); diff --git a/test/chunking-form/samples/dynamic-import-comments/_expected/system/main.js b/test/chunking-form/samples/dynamic-import-comments/_expected/system/main.js index 5967f1516fb..6a642eabfb8 100644 --- a/test/chunking-form/samples/dynamic-import-comments/_expected/system/main.js +++ b/test/chunking-form/samples/dynamic-import-comments/_expected/system/main.js @@ -3,7 +3,7 @@ System.register([], function (exports, module) { return { execute: function () { - module.import( + foobar( /* webpackChunkName: "chunk-name" */ './foo.js'/*suffix*/); diff --git a/test/chunking-form/samples/entry-point-without-own-code/_expected/amd/generated-m1.js b/test/chunking-form/samples/entry-point-without-own-code/_expected/amd/generated-m1.js index 52ed74a5c5c..83a962f60e6 100644 --- a/test/chunking-form/samples/entry-point-without-own-code/_expected/amd/generated-m1.js +++ b/test/chunking-form/samples/entry-point-without-own-code/_expected/amd/generated-m1.js @@ -1,7 +1,5 @@ define(['exports', './m2'], function (exports, m2) { 'use strict'; - - var ms = /*#__PURE__*/Object.freeze({ __proto__: null, m2: m2 diff --git a/test/chunking-form/samples/entry-point-without-own-code/_expected/cjs/generated-m1.js b/test/chunking-form/samples/entry-point-without-own-code/_expected/cjs/generated-m1.js index f8fc76bc360..d23d73ad3a4 100644 --- a/test/chunking-form/samples/entry-point-without-own-code/_expected/cjs/generated-m1.js +++ b/test/chunking-form/samples/entry-point-without-own-code/_expected/cjs/generated-m1.js @@ -2,8 +2,6 @@ var m2 = require('./m2.js'); - - var ms = /*#__PURE__*/Object.freeze({ __proto__: null, m2: m2 diff --git a/test/chunking-form/samples/entry-point-without-own-code/_expected/es/generated-m1.js b/test/chunking-form/samples/entry-point-without-own-code/_expected/es/generated-m1.js index 4197cabf46a..4e392790970 100644 --- a/test/chunking-form/samples/entry-point-without-own-code/_expected/es/generated-m1.js +++ b/test/chunking-form/samples/entry-point-without-own-code/_expected/es/generated-m1.js @@ -1,8 +1,6 @@ import m2 from './m2.js'; export { default as a } from './m2.js'; - - var ms = /*#__PURE__*/Object.freeze({ __proto__: null, m2: m2 diff --git a/test/chunking-form/samples/entry-point-without-own-code/_expected/system/generated-m1.js b/test/chunking-form/samples/entry-point-without-own-code/_expected/system/generated-m1.js index 591300d8a86..78363f16b3c 100644 --- a/test/chunking-form/samples/entry-point-without-own-code/_expected/system/generated-m1.js +++ b/test/chunking-form/samples/entry-point-without-own-code/_expected/system/generated-m1.js @@ -8,8 +8,6 @@ System.register(['./m2.js'], function (exports) { }], execute: function () { - - var ms = /*#__PURE__*/Object.freeze({ __proto__: null, m2: m2 diff --git a/test/chunking-form/samples/render-dynamic-import/_config.js b/test/chunking-form/samples/render-dynamic-import/_config.js new file mode 100644 index 00000000000..c841123e304 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_config.js @@ -0,0 +1,22 @@ +const path = require('path'); + +module.exports = { + description: 'supports custom rendering for dynamic imports', + options: { + plugins: { + name: 'test-plugin', + resolveDynamicImport(specifier) { + if (typeof specifier === 'object' && specifier.name === 'someResolvedVariable') { + return 'someCustomlyResolvedVariable'; + } + }, + renderDynamicImport({ customResolution, format, moduleId, targetModuleId }) { + return { + left: `${format}SpecialHandler(`, + right: `, '${path.relative(__dirname, moduleId)}', '${targetModuleId && + path.relative(__dirname, targetModuleId)}', ${customResolution})` + }; + } + } + } +}; diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/amd/generated-imported-via-special-handler.js b/test/chunking-form/samples/render-dynamic-import/_expected/amd/generated-imported-via-special-handler.js new file mode 100644 index 00000000000..96ce7e1d3f0 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/amd/generated-imported-via-special-handler.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + console.log('special'); + +}); diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/amd/main.js b/test/chunking-form/samples/render-dynamic-import/_expected/amd/main.js new file mode 100644 index 00000000000..bf711f8dc59 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/amd/main.js @@ -0,0 +1,26 @@ +define(['require'], function (require) { 'use strict'; + + function _interopNamespace(e) { + if (e && e.__esModule) { return e; } else { + var n = {}; + if (e) { + Object.keys(e).forEach(function (k) { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { + return e[k]; + } + }); + }); + } + n['default'] = e; + return n; + } + } + + amdSpecialHandler('./generated-imported-via-special-handler', 'main.js', 'imported-via-special-handler.js', null); + amdSpecialHandler(someVariable, 'main.js', 'null', null); + amdSpecialHandler(someCustomlyResolvedVariable, 'main.js', 'null', someCustomlyResolvedVariable); + +}); diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/cjs/generated-imported-via-special-handler.js b/test/chunking-form/samples/render-dynamic-import/_expected/cjs/generated-imported-via-special-handler.js new file mode 100644 index 00000000000..2963ee147dd --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/cjs/generated-imported-via-special-handler.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('special'); diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/cjs/main.js b/test/chunking-form/samples/render-dynamic-import/_expected/cjs/main.js new file mode 100644 index 00000000000..64e4c86028d --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/cjs/main.js @@ -0,0 +1,24 @@ +'use strict'; + +function _interopNamespace(e) { + if (e && e.__esModule) { return e; } else { + var n = {}; + if (e) { + Object.keys(e).forEach(function (k) { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { + return e[k]; + } + }); + }); + } + n['default'] = e; + return n; + } +} + +cjsSpecialHandler('./generated-imported-via-special-handler.js', 'main.js', 'imported-via-special-handler.js', null); +cjsSpecialHandler(someVariable, 'main.js', 'null', null); +cjsSpecialHandler(someCustomlyResolvedVariable, 'main.js', 'null', someCustomlyResolvedVariable); diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/es/generated-imported-via-special-handler.js b/test/chunking-form/samples/render-dynamic-import/_expected/es/generated-imported-via-special-handler.js new file mode 100644 index 00000000000..6c8553ec696 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/es/generated-imported-via-special-handler.js @@ -0,0 +1 @@ +console.log('special'); diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/es/main.js b/test/chunking-form/samples/render-dynamic-import/_expected/es/main.js new file mode 100644 index 00000000000..ddbe789b8b5 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/es/main.js @@ -0,0 +1,3 @@ +esSpecialHandler('./generated-imported-via-special-handler.js', 'main.js', 'imported-via-special-handler.js', null); +esSpecialHandler(someVariable, 'main.js', 'null', null); +esSpecialHandler(someCustomlyResolvedVariable, 'main.js', 'null', someCustomlyResolvedVariable); diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/system/generated-imported-via-special-handler.js b/test/chunking-form/samples/render-dynamic-import/_expected/system/generated-imported-via-special-handler.js new file mode 100644 index 00000000000..8dd5d86ac58 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/system/generated-imported-via-special-handler.js @@ -0,0 +1,10 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + console.log('special'); + + } + }; +}); diff --git a/test/chunking-form/samples/render-dynamic-import/_expected/system/main.js b/test/chunking-form/samples/render-dynamic-import/_expected/system/main.js new file mode 100644 index 00000000000..52306b00fc1 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/_expected/system/main.js @@ -0,0 +1,12 @@ +System.register([], function (exports, module) { + 'use strict'; + return { + execute: function () { + + systemSpecialHandler('./generated-imported-via-special-handler.js', 'main.js', 'imported-via-special-handler.js', null); + systemSpecialHandler(someVariable, 'main.js', 'null', null); + systemSpecialHandler(someCustomlyResolvedVariable, 'main.js', 'null', someCustomlyResolvedVariable); + + } + }; +}); diff --git a/test/chunking-form/samples/render-dynamic-import/imported-via-special-handler.js b/test/chunking-form/samples/render-dynamic-import/imported-via-special-handler.js new file mode 100644 index 00000000000..6c8553ec696 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/imported-via-special-handler.js @@ -0,0 +1 @@ +console.log('special'); diff --git a/test/chunking-form/samples/render-dynamic-import/main.js b/test/chunking-form/samples/render-dynamic-import/main.js new file mode 100644 index 00000000000..72129c63bd5 --- /dev/null +++ b/test/chunking-form/samples/render-dynamic-import/main.js @@ -0,0 +1,3 @@ +import('./imported-via-special-handler.js'); +import(someVariable); +import(someResolvedVariable); diff --git a/test/form/samples/dynamic-import-unresolvable/_config.js b/test/form/samples/dynamic-import-unresolvable/_config.js index 7d3836af3ea..66a9e5692d6 100644 --- a/test/form/samples/dynamic-import-unresolvable/_config.js +++ b/test/form/samples/dynamic-import-unresolvable/_config.js @@ -6,9 +6,15 @@ module.exports = { plugins: [ { resolveDynamicImport(specifier) { + if (specifier === './seven.js') { + return false; + } assert.ok(specifier); assert.strictEqual(typeof specifier, 'object'); - if (specifier.type !== 'TemplateLiteral' && specifier.type !== 'Literal') { + if (specifier.type === 'Literal') { + return "'./seven.js'"; + } + if (specifier.type !== 'TemplateLiteral') { throw new Error(`Unexpected specifier type ${specifier.type}.`); } return false; diff --git a/test/form/samples/dynamic-import-unresolvable/_expected/amd.js b/test/form/samples/dynamic-import-unresolvable/_expected/amd.js index 23dd8232c03..04bf2c06c62 100644 --- a/test/form/samples/dynamic-import-unresolvable/_expected/amd.js +++ b/test/form/samples/dynamic-import-unresolvable/_expected/amd.js @@ -21,6 +21,7 @@ define(['require'], function (require) { 'use strict'; new Promise(function (resolve, reject) { require([`${globalThis.unknown}`], function (m) { resolve(_interopNamespace(m)); }, reject) }); new Promise(function (resolve, reject) { require([`My ${globalThis.unknown}`], function (m) { resolve(_interopNamespace(m)); }, reject) }); - new Promise(function (resolve, reject) { require([7], function (m) { resolve(_interopNamespace(m)); }, reject) }); + new Promise(function (resolve, reject) { require(['./seven.js'], function (m) { resolve(_interopNamespace(m)); }, reject) }); + new Promise(function (resolve, reject) { require(['./seven'], function (m) { resolve(_interopNamespace(m)); }, reject) }); }); diff --git a/test/form/samples/dynamic-import-unresolvable/_expected/cjs.js b/test/form/samples/dynamic-import-unresolvable/_expected/cjs.js index 0290948c069..5ae04fc8c33 100644 --- a/test/form/samples/dynamic-import-unresolvable/_expected/cjs.js +++ b/test/form/samples/dynamic-import-unresolvable/_expected/cjs.js @@ -21,4 +21,5 @@ function _interopNamespace(e) { new Promise(function (resolve) { resolve(_interopNamespace(require(`${globalThis.unknown}`))); }); new Promise(function (resolve) { resolve(_interopNamespace(require(`My ${globalThis.unknown}`))); }); -new Promise(function (resolve) { resolve(_interopNamespace(require(7))); }); +new Promise(function (resolve) { resolve(_interopNamespace(require('./seven.js'))); }); +new Promise(function (resolve) { resolve(_interopNamespace(require('./seven.js'))); }); diff --git a/test/form/samples/dynamic-import-unresolvable/_expected/es.js b/test/form/samples/dynamic-import-unresolvable/_expected/es.js index 2c04ceceac4..4ab677be931 100644 --- a/test/form/samples/dynamic-import-unresolvable/_expected/es.js +++ b/test/form/samples/dynamic-import-unresolvable/_expected/es.js @@ -1,3 +1,4 @@ import(`${globalThis.unknown}`); import(`My ${globalThis.unknown}`); -import(7); +import('./seven.js'); +import('./seven.js'); diff --git a/test/form/samples/dynamic-import-unresolvable/_expected/iife.js b/test/form/samples/dynamic-import-unresolvable/_expected/iife.js index c8fccafa406..83008aee524 100644 --- a/test/form/samples/dynamic-import-unresolvable/_expected/iife.js +++ b/test/form/samples/dynamic-import-unresolvable/_expected/iife.js @@ -3,6 +3,7 @@ import(`${globalThis.unknown}`); import(`My ${globalThis.unknown}`); - import(7); + import('./seven.js'); + import('./seven.js'); }()); diff --git a/test/form/samples/dynamic-import-unresolvable/_expected/system.js b/test/form/samples/dynamic-import-unresolvable/_expected/system.js index f8b7f8294c5..ec2f668e4b4 100644 --- a/test/form/samples/dynamic-import-unresolvable/_expected/system.js +++ b/test/form/samples/dynamic-import-unresolvable/_expected/system.js @@ -5,7 +5,8 @@ System.register([], function (exports, module) { module.import(`${globalThis.unknown}`); module.import(`My ${globalThis.unknown}`); - module.import(7); + module.import('./seven.js'); + module.import('./seven.js'); } }; diff --git a/test/form/samples/dynamic-import-unresolvable/_expected/umd.js b/test/form/samples/dynamic-import-unresolvable/_expected/umd.js index efd0b19b8a1..d5aee0821a3 100644 --- a/test/form/samples/dynamic-import-unresolvable/_expected/umd.js +++ b/test/form/samples/dynamic-import-unresolvable/_expected/umd.js @@ -5,6 +5,7 @@ import(`${globalThis.unknown}`); import(`My ${globalThis.unknown}`); - import(7); + import('./seven.js'); + import('./seven.js'); }))); diff --git a/test/form/samples/dynamic-import-unresolvable/main.js b/test/form/samples/dynamic-import-unresolvable/main.js index 2c04ceceac4..5f6ec579e20 100644 --- a/test/form/samples/dynamic-import-unresolvable/main.js +++ b/test/form/samples/dynamic-import-unresolvable/main.js @@ -1,3 +1,4 @@ import(`${globalThis.unknown}`); import(`My ${globalThis.unknown}`); import(7); +import('./seven.js'); diff --git a/test/function/index.js b/test/function/index.js index 4cb614eb7b4..6ae7178b447 100644 --- a/test/function/index.js +++ b/test/function/index.js @@ -3,8 +3,7 @@ const assert = require('assert'); const rollup = require('../../dist/rollup'); const { compareError, compareWarnings, extend, runTestSuiteWithSamples } = require('../utils.js'); -function requireWithContext(code, context) { - const module = { exports: {} }; +function requireWithContext(code, context, module) { const contextWithExports = Object.assign({}, context, { module, exports: module.exports }); const contextKeys = Object.keys(contextWithExports); const contextValues = contextKeys.map(key => contextWithExports[key]); @@ -19,14 +18,20 @@ function requireWithContext(code, context) { } function runCodeSplitTest(codeMap, entryId, configContext) { + const exportsMap = Object.create(null); + const requireFromOutputVia = importer => importee => { const outputId = path.posix.join(path.posix.dirname(importer), importee); + if (outputId in exportsMap) { + return exportsMap[outputId]; + } const code = codeMap[outputId]; if (typeof code !== 'undefined') { - return requireWithContext( + return (exportsMap[outputId] = requireWithContext( code, - Object.assign({ require: requireFromOutputVia(outputId) }, context) - ); + Object.assign({ require: requireFromOutputVia(outputId) }, context), + (exportsMap[outputId] = { exports: {} }) + )); } else { return require(importee); } @@ -35,10 +40,7 @@ function runCodeSplitTest(codeMap, entryId, configContext) { const context = Object.assign({ assert }, configContext); let exports; try { - exports = requireWithContext( - codeMap[entryId], - Object.assign({ require: requireFromOutputVia(entryId) }, context) - ); + exports = requireFromOutputVia(entryId)(entryId); } catch (error) { return { error, exports: error.exports }; } diff --git a/test/function/samples/dynamic-import-name-warn/_config.js b/test/function/samples/deprecated/dynamic-import-name-warn/_config.js similarity index 95% rename from test/function/samples/dynamic-import-name-warn/_config.js rename to test/function/samples/deprecated/dynamic-import-name-warn/_config.js index 581dde7a8fa..12b146bac6e 100644 --- a/test/function/samples/dynamic-import-name-warn/_config.js +++ b/test/function/samples/deprecated/dynamic-import-name-warn/_config.js @@ -9,6 +9,7 @@ module.exports = { } }, options: { + strictDeprecations: false, input: 'main.js', plugins: { resolveDynamicImport() { diff --git a/test/function/samples/dynamic-import-name-warn/main.js b/test/function/samples/deprecated/dynamic-import-name-warn/main.js similarity index 100% rename from test/function/samples/dynamic-import-name-warn/main.js rename to test/function/samples/deprecated/dynamic-import-name-warn/main.js diff --git a/test/function/samples/dynamic-import-name/_config.js b/test/function/samples/deprecated/dynamic-import-name/_config.js similarity index 93% rename from test/function/samples/dynamic-import-name/_config.js rename to test/function/samples/deprecated/dynamic-import-name/_config.js index 917b20a396f..afb4f3ba114 100644 --- a/test/function/samples/dynamic-import-name/_config.js +++ b/test/function/samples/deprecated/dynamic-import-name/_config.js @@ -10,6 +10,7 @@ module.exports = { } }, options: { + strictDeprecations: false, input: 'main.js', plugins: { resolveDynamicImport() { diff --git a/test/function/samples/dynamic-import-name/main.js b/test/function/samples/deprecated/dynamic-import-name/main.js similarity index 100% rename from test/function/samples/dynamic-import-name/main.js rename to test/function/samples/deprecated/dynamic-import-name/main.js diff --git a/test/function/samples/deprecated/emit-chunk/chunk-filename-not-available/_config.js b/test/function/samples/deprecated/emit-chunk/chunk-filename-not-available/_config.js index 52a5ee32ae1..1315b984bbe 100644 --- a/test/function/samples/deprecated/emit-chunk/chunk-filename-not-available/_config.js +++ b/test/function/samples/deprecated/emit-chunk/chunk-filename-not-available/_config.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { description: 'Throws when accessing the filename before it has been generated', options: { @@ -17,6 +19,7 @@ module.exports = { message: 'Plugin error - Unable to get file name for chunk "chunk.js". Ensure that generate is called first.', plugin: 'test-plugin', - pluginCode: 'CHUNK_NOT_GENERATED' + pluginCode: 'CHUNK_NOT_GENERATED', + watchFiles: [path.resolve(__dirname, 'chunk.js')] } }; diff --git a/test/function/samples/deprecations/dynamicImportFunction/_config.js b/test/function/samples/deprecations/dynamicImportFunction/_config.js new file mode 100644 index 00000000000..10db1585d19 --- /dev/null +++ b/test/function/samples/deprecations/dynamicImportFunction/_config.js @@ -0,0 +1,13 @@ +module.exports = { + description: 'marks the "output.dynamicImportFunction" option as deprecated', + options: { + output: { + dynamicImportFunction: 'foo' + } + }, + generateError: { + code: 'DEPRECATED_FEATURE', + message: + 'The "output.dynamicImportFunction" option is deprecated. Use the "renderDynamicImport" plugin hook instead.' + } +}; diff --git a/test/function/samples/deprecations/dynamicImportFunction/main.js b/test/function/samples/deprecations/dynamicImportFunction/main.js new file mode 100644 index 00000000000..f8a2d88d245 --- /dev/null +++ b/test/function/samples/deprecations/dynamicImportFunction/main.js @@ -0,0 +1,11 @@ +const foo = {}; + +function doIt(x) { + if (foo[x]) { + return true; + } + foo[x] = true; +} + +doIt('x'); +assert.ok(doIt('x'), 'foo was not reassigned'); diff --git a/test/function/samples/emit-file/chunk-filename-not-available/_config.js b/test/function/samples/emit-file/chunk-filename-not-available/_config.js index 2575784f8d7..48d5280a8f1 100644 --- a/test/function/samples/emit-file/chunk-filename-not-available/_config.js +++ b/test/function/samples/emit-file/chunk-filename-not-available/_config.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { description: 'Throws when accessing the filename before it has been generated', options: { @@ -16,6 +18,7 @@ module.exports = { message: 'Plugin error - Unable to get file name for chunk "chunk.js". Ensure that generate is called first.', plugin: 'test-plugin', - pluginCode: 'CHUNK_NOT_GENERATED' + pluginCode: 'CHUNK_NOT_GENERATED', + watchFiles: [path.resolve(__dirname, 'chunk.js')] } }; diff --git a/test/function/samples/emit-file/set-asset-source-chunk/_config.js b/test/function/samples/emit-file/set-asset-source-chunk/_config.js index 27b97f89fb1..b13d837e6d2 100644 --- a/test/function/samples/emit-file/set-asset-source-chunk/_config.js +++ b/test/function/samples/emit-file/set-asset-source-chunk/_config.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { description: 'throws when trying to set the asset source of a chunk', options: { @@ -14,6 +16,7 @@ module.exports = { hook: 'buildStart', message: 'Asset sources can only be set for emitted assets but "6c87f683" is an emitted chunk.', plugin: 'test-plugin', - pluginCode: 'VALIDATION_ERROR' + pluginCode: 'VALIDATION_ERROR', + watchFiles: [path.resolve(__dirname, 'chunk.js')] } }; diff --git a/test/function/samples/per-output-plugins-warn-hooks/_config.js b/test/function/samples/per-output-plugins-warn-hooks/_config.js index 3583c674719..446269bb422 100644 --- a/test/function/samples/per-output-plugins-warn-hooks/_config.js +++ b/test/function/samples/per-output-plugins-warn-hooks/_config.js @@ -36,7 +36,7 @@ module.exports = { { code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', message: - 'The "options" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + 'The "buildEnd" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' }, { code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', @@ -46,22 +46,27 @@ module.exports = { { code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', message: - 'The "resolveId" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + 'The "load" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' }, { code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', message: - 'The "load" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + 'The "options" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' }, { code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', message: - 'The "transform" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + 'The "resolveDynamicImport" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' }, { code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', message: - 'The "buildEnd" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + 'The "resolveId" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "transform" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' }, { code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', diff --git a/test/function/samples/plugin-module-information/_config.js b/test/function/samples/plugin-module-information/_config.js index 6a9a20b4734..793d25a1469 100644 --- a/test/function/samples/plugin-module-information/_config.js +++ b/test/function/samples/plugin-module-information/_config.js @@ -15,6 +15,7 @@ module.exports = { plugins: { load(id) { assert.deepStrictEqual(this.getModuleInfo(id), { + dynamicallyImportedIds: [], hasModuleSideEffects: true, id, importedIds: [], @@ -24,15 +25,17 @@ module.exports = { }, renderStart() { rendered = true; - assert.deepStrictEqual(Array.from(this.moduleIds), [ID_MAIN, ID_FOO, ID_NESTED, ID_PATH]); + assert.deepStrictEqual(Array.from(this.moduleIds), [ID_MAIN, ID_FOO, ID_PATH, ID_NESTED]); assert.deepStrictEqual(this.getModuleInfo(ID_MAIN), { + dynamicallyImportedIds: [ID_NESTED, ID_PATH], hasModuleSideEffects: true, id: ID_MAIN, - importedIds: [ID_FOO, ID_NESTED], + importedIds: [ID_FOO], isEntry: true, isExternal: false }); assert.deepStrictEqual(this.getModuleInfo(ID_FOO), { + dynamicallyImportedIds: [], hasModuleSideEffects: true, id: ID_FOO, importedIds: [ID_PATH], @@ -40,6 +43,7 @@ module.exports = { isExternal: false }); assert.deepStrictEqual(this.getModuleInfo(ID_NESTED), { + dynamicallyImportedIds: [], hasModuleSideEffects: true, id: ID_NESTED, importedIds: [ID_FOO], @@ -47,6 +51,7 @@ module.exports = { isExternal: false }); assert.deepStrictEqual(this.getModuleInfo(ID_PATH), { + dynamicallyImportedIds: [], hasModuleSideEffects: true, id: ID_PATH, importedIds: [], @@ -56,6 +61,9 @@ module.exports = { } } }, + context: { + thePath: 'path' + }, bundle() { assert.ok(rendered); } diff --git a/test/function/samples/plugin-module-information/main.js b/test/function/samples/plugin-module-information/main.js index 73b23d68a98..216b345a828 100644 --- a/test/function/samples/plugin-module-information/main.js +++ b/test/function/samples/plugin-module-information/main.js @@ -1,4 +1,4 @@ -export {foo} from './foo.js'; -import { nested } from './nested/nested'; - -export {nested}; +export { foo } from './foo.js'; +export const nested = import('./nested/nested'); +export const path = import('path'); +export const pathAgain = import(thePath);