diff --git a/LICENSE.md b/LICENSE.md index f173e24c97e..fd5c20a53a0 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -52,6 +52,13 @@ Repository: https://github.com/acornjs/acorn.git --------------------------------------- +## acorn-import-assertions +License: MIT +By: Sven Sauleau +Repository: https://github.com/xtuc/acorn-import-assertions + +--------------------------------------- + ## acorn-walk License: MIT By: Marijn Haverbeke, Ingvar Stepanyan, Adrian Heine diff --git a/browser/LICENSE.md b/browser/LICENSE.md index 8d9cc1268eb..c393ff66b59 100644 --- a/browser/LICENSE.md +++ b/browser/LICENSE.md @@ -52,6 +52,13 @@ Repository: https://github.com/acornjs/acorn.git --------------------------------------- +## acorn-import-assertions +License: MIT +By: Sven Sauleau +Repository: https://github.com/xtuc/acorn-import-assertions + +--------------------------------------- + ## acorn-walk License: MIT By: Marijn Haverbeke, Ingvar Stepanyan, Adrian Heine diff --git a/browser/src/resolveId.ts b/browser/src/resolveId.ts index 494a3792941..da1803f5632 100644 --- a/browser/src/resolveId.ts +++ b/browser/src/resolveId.ts @@ -1,9 +1,5 @@ -import type { - CustomPluginOptions, - Plugin, - ResolvedId, - ResolveIdResult -} from '../../src/rollup/types'; +import { ModuleLoaderResolveId } from '../../src/ModuleLoader'; +import type { CustomPluginOptions, Plugin, ResolveIdResult } from '../../src/rollup/types'; import type { PluginDriver } from '../../src/utils/PluginDriver'; import { resolveIdViaPlugins } from '../../src/utils/resolveIdViaPlugins'; import { throwNoFileSystem } from './error'; @@ -13,16 +9,11 @@ export async function resolveId( importer: string | undefined, _preserveSymlinks: boolean, pluginDriver: PluginDriver, - moduleLoaderResolveId: ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null - ) => Promise, + moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, - isEntry: boolean + isEntry: boolean, + assertions: Record ): Promise { const pluginResult = await resolveIdViaPlugins( source, @@ -31,7 +22,8 @@ export async function resolveId( moduleLoaderResolveId, skip, customOptions, - isEntry + isEntry, + assertions ); if (pluginResult == null) { throwNoFileSystem('path.resolve'); diff --git a/cli/help.md b/cli/help.md index eeea7cce45e..069d4b91d71 100644 --- a/cli/help.md +++ b/cli/help.md @@ -36,6 +36,7 @@ Basic options: --exports Specify export mode (auto, default, named, none) --extend Extend global variable defined by --name --no-externalLiveBindings Do not generate code to support live bindings +--no-externalImportAssertions Omit import assertions in "es" output --failAfterWarnings Exit with an error if the build produced warnings --footer Code to insert at end of bundle (outside wrapper) --no-freeze Do not freeze namespace objects @@ -68,8 +69,8 @@ Basic options: --no-systemNullSetters Do not replace empty SystemJS setters with `null` --no-treeshake Disable tree-shaking optimisations --no-treeshake.annotations Ignore pure call annotations ---no-treeshake.moduleSideEffects Assume modules have no side-effects ---no-treeshake.propertyReadSideEffects Ignore property access side-effects +--no-treeshake.moduleSideEffects Assume modules have no side effects +--no-treeshake.propertyReadSideEffects Ignore property access side effects --no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking --no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw --waitForBundleInput Wait for bundle input files diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index 7690bf07965..5df499cd3c6 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -366,6 +366,7 @@ Many options have command line equivalents. In those cases, any arguments passed --no-esModule Do not add __esModule property --exports Specify export mode (auto, default, named, none) --extend Extend global variable defined by --name +--no-externalImportAssertions Omit import assertions in "es" output --no-externalLiveBindings Do not generate code to support live bindings --failAfterWarnings Exit with an error if the build produced warnings --footer Code to insert at end of bundle (outside wrapper) @@ -399,8 +400,8 @@ Many options have command line equivalents. In those cases, any arguments passed --no-systemNullSetters Do not replace empty SystemJS setters with `null` --no-treeshake Disable tree-shaking optimisations --no-treeshake.annotations Ignore pure call annotations ---no-treeshake.moduleSideEffects Assume modules have no side-effects ---no-treeshake.propertyReadSideEffects Ignore property access side-effects +--no-treeshake.moduleSideEffects Assume modules have no side effects +--no-treeshake.propertyReadSideEffects Ignore property access side effects --no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking --no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw --waitForBundleInput Wait for bundle input files diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index e927847fe84..a0fc1da4174 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -148,17 +148,19 @@ Notifies a plugin when the watcher process will close so that all open resources #### `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. 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 if no cache was used, or there was no cached copy with the same `code`, otherwise [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule). +**Type:** `(id: string) => string | null | {code: string, map?: string | SourceMap, ast? : ESTree.Program, assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | 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 if no cache was used, or there was no cached copy with the same `code`, otherwise [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule). 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). -If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included in the bundle even if the module would have side-effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. The `transform` hook can override this. +If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included in the bundle even if the module would have side effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. The `transform` hook can override this. + +`assertions` contain the import assertions that were used when this module was imported. At the moment, they do not influence rendering for bundled modules but rather serve documentation purposes. If `null` is returned or the flag is omitted, then `assertions` will be determined by the first `resolveId` hook that resolved this module, or the assertions present in the first import of this module. The `transform` hook can override this. See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect of the `syntheticNamedExports` option. If `null` is returned or the flag is omitted, then `syntheticNamedExports` will be determined by the first `resolveId` hook that resolved this module or eventually default to `false`. The `transform` hook can override this. See [custom module meta-data](guide/en/#custom-module-meta-data) for how to use the `meta` option. If a `meta` object is returned by this hook, it will be merged shallowly with any `meta` object returned by the resolveId hook. If no hook returns a `meta` object it will default to an empty object. The `transform` hook can further add or replace properties of this object. -You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `moduleSideEffects`, `syntheticNamedExports` and `meta` inside this hook. +You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` inside this hook. #### `moduleParsed` @@ -180,10 +182,12 @@ This is the only hook that does not have access to most [plugin context](guide/e #### `resolveDynamicImport` -**Type:** `(specifier: string | ESTree.Node, importer: string) => string | false | null | {id: string, external?: boolean}`
**Kind:** `async, first`
**Previous Hook:** [`moduleParsed`](guide/en/#moduleparsed) for the importing file.
**Next Hook:** [`load`](guide/en/#load) if the hook resolved with an id that has not yet been loaded, [`resolveId`](guide/en/#resolveid) if the dynamic import contains a string and was not resolved by the hook, otherwise [`buildEnd`](guide/en/#buildend). +**Type:** `(specifier: string | ESTree.Node, importer: string, {assertions: {[key: string]: string}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`
**Kind:** `async, first`
**Previous Hook:** [`moduleParsed`](guide/en/#moduleparsed) for the importing file.
**Next Hook:** [`load`](guide/en/#load) if the hook resolved with an id that has not yet been loaded, [`resolveId`](guide/en/#resolveid) if the dynamic import contains a string and was not resolved by the hook, otherwise [`buildEnd`](guide/en/#buildend). Defines a custom resolver for dynamic imports. Returning `false` signals that the import should be kept as it is and not be passed to other resolvers thus making it external. Similar to the [`resolveId`](guide/en/#resolveid) hook, you can also return an object to resolve the import to a different id while marking it as external at the same time. +`assertions` tells you which import assertions were present in the import. I.e. `import("foo", {assert: {type: "json"}})` will pass along `assertions: {type: "json"}`. + In case a dynamic import is passed a string as argument, a string returned from this hook will be interpreted as an existing module id while returning `null` will defer to other resolvers and eventually to `resolveId` . In case a dynamic import is not passed a string as argument, this hook gets access to the raw AST nodes to analyze and behaves slightly different in the following ways: @@ -196,7 +200,7 @@ Note that the return value of this hook will not be passed to `resolveId` afterw #### `resolveId` -**Type:** `(source: string, importer: string | undefined, options: {isEntry: boolean, custom?: {[plugin: string]: any}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`
**Kind:** `async, first`
**Previous Hook:** [`buildStart`](guide/en/#buildstart) if we are resolving an entry point, [`moduleParsed`](guide/en/#moduleparsed) if we are resolving an import, or as fallback for [`resolveDynamicImport`](guide/en/#resolvedynamicimport). Additionally, this hook can be triggered during the build phase from plugin hooks by calling [`this.emitFile`](guide/en/#thisemitfile) to emit an entry point or at any time by calling [`this.resolve`](guide/en/#thisresolve) to manually resolve an id.
**Next Hook:** [`load`](guide/en/#load) if the resolved id that has not yet been loaded, otherwise [`buildEnd`](guide/en/#buildend). +**Type:** `(source: string, importer: string | undefined, options: {isEntry: boolean, assertions: {[key: string]: string}, custom?: {[plugin: string]: any}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`
**Kind:** `async, first`
**Previous Hook:** [`buildStart`](guide/en/#buildstart) if we are resolving an entry point, [`moduleParsed`](guide/en/#moduleparsed) if we are resolving an import, or as fallback for [`resolveDynamicImport`](guide/en/#resolvedynamicimport). Additionally, this hook can be triggered during the build phase from plugin hooks by calling [`this.emitFile`](guide/en/#thisemitfile) to emit an entry point or at any time by calling [`this.resolve`](guide/en/#thisresolve) to manually resolve an id.
**Next Hook:** [`load`](guide/en/#load) if the resolved id that has not yet been loaded, otherwise [`buildEnd`](guide/en/#buildend). Defines a custom resolver. A resolver can be useful for e.g. locating third-party dependencies. Here `source` is the importee exactly as it is written in the import statement, i.e. for @@ -278,6 +282,8 @@ function injectPolyfillPlugin() { } ``` +`assertions` tells you which import assertions were present in the import. I.e. `import "foo" assert {type: "json"}` will pass along `assertions: {type: "json"}`. + Returning `null` defers to other `resolveId` functions and eventually the default resolution behavior. Returning `false` signals that `source` should be treated as an external module and not included in the bundle. If this happens for a relative import, the id will be renormalized the same way as when the `external` option is used. If you return an object, then it is possible to resolve an import to a different id while excluding it from the bundle at the same time. This allows you to replace dependencies with external dependencies without the need for the user to mark them as "external" manually via the `external` option: @@ -298,13 +304,15 @@ function externalizeDependencyPlugin() { If `external` is `true`, then absolute ids will be converted to relative ids based on the user's choice for the [`makeAbsoluteExternalsRelative`](guide/en/#makeabsoluteexternalsrelative) option. This choice can be overridden by passing either `external: "relative"` to always convert an absolute id to a relative id or `external: "absolute"` to keep it as an absolute id. When returning an object, relative external ids, i.e. ids starting with `./` or `../`, will _not_ be internally converted to an absolute id and converted back to a relative id in the output, but are instead included in the output unchanged. If you want relative ids to be renormalised and deduplicated instead, return an absolute file system location as `id` and choose `external: "relative"`. -If `false` is returned for `moduleSideEffects` in the first hook that resolves a module id and no other module imports anything from this module, then this module will not be included even if the module would have side-effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `treeshake.moduleSideEffects` option or default to `true`. The `load` and `transform` hooks can override this. +If `false` is returned for `moduleSideEffects` in the first hook that resolves a module id and no other module imports anything from this module, then this module will not be included even if the module would have side effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `treeshake.moduleSideEffects` option or default to `true`. The `load` and `transform` hooks can override this. + +If you return a value for `assertions` for an external module, this will determine how imports of this module will be rendered when generating `"es"` output. E.g. `{id: "foo", external: true, assertions: {type: "json"}}` will cause imports of this module appear as `import "foo" assert {type: "json"}`. If you do not pass a value, the value of the `assertions` input parameter will be used. Pass an empty object to remove any assertions. While `assertions` do not influence rendering for bundled modules, they still need to be consistent across all imports of a module, otherwise a warning is emitted. The `load` and `transform` hooks can override this. See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect of the `syntheticNamedExports` option. If `null` is returned or the flag is omitted, then `syntheticNamedExports` will default to `false`. The `load` and `transform` hooks can override this. 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. -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. +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`, `assertions`, `meta`, `moduleSideEffects` or `syntheticNamedExports` 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). @@ -312,7 +320,7 @@ In watch mode or when using the cache explicitly, the resolved imports of a cach #### `shouldTransformCachedModule` -**Type:** `({id: string, code: string, ast: ESTree.Program, resoledSources: {[source: string]: ResolvedId}, meta: {[plugin: string]: any}, moduleSideEffects: boolean | "no-treeshake", syntheticNamedExports: string | boolean}) => boolean`
**Kind:** `async, first`
**Previous Hook:** [`load`](guide/en/#load) where the cached file was loaded to compare its code with the cached version.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) if no plugin returns `true`, otherwise [`transform`](guide/en/#transform). +**Type:** `({id: string, code: string, ast: ESTree.Program, resolvedSources: {[source: string]: ResolvedId}, assertions: {[key: string]: string}, meta: {[plugin: string]: any}, moduleSideEffects: boolean | "no-treeshake", syntheticNamedExports: boolean | string}) => boolean`
**Kind:** `async, first`
**Previous Hook:** [`load`](guide/en/#load) where the cached file was loaded to compare its code with the cached version.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) if no plugin returns `true`, otherwise [`transform`](guide/en/#transform). If the Rollup cache is used (e.g. in watch mode or explicitly via the JavaScript API), Rollup will skip the [`transform`](guide/en/#transform) hook of a module if after the [`load`](guide/en/#transform) hook, the loaded `code` is identical to the code of the cached copy. To prevent this, discard the cached copy and instead transform a module, plugins can implement this hook and return `true`. @@ -322,7 +330,7 @@ If a plugin does not return `true`, Rollup will trigger this hook for other plug #### `transform` -**Type:** `(code: string, 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, sequential`
**Previous Hook:** [`load`](guide/en/#load) where the currently handled file was loaded. If caching is used and there was a cached copy of that module, [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule) if a plugin returned `true` for that hook.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) once the file has been processed and parsed. +**Type:** `(code: string, id: string) => string | null | {code?: string, map?: string | SourceMap, ast? : ESTree.Program, assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`
**Kind:** `async, sequential`
**Previous Hook:** [`load`](guide/en/#load) where the currently handled file was loaded. If caching is used and there was a cached copy of that module, [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule) if a plugin returned `true` for that hook.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) once the file has been processed and parsed. Can be used to transform individual modules. 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). @@ -332,19 +340,21 @@ In all other cases, the [`shouldTransformCachedModule`](guide/en/#shouldtransfor You can also use the object form of the return value to configure additional properties of the module. Note that it's possible to return only properties and no code transformations. -If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included even if the module would have side-effects. +If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included even if the module would have side effects. -If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). +If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. +`assertions` contain the import assertions that were used when this module was imported. At the moment, they do not influence rendering for bundled modules but rather serve documentation purposes. If `null` is returned or the flag is omitted, then `assertions` will be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module, or the assertions present in the first import of this module. + See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect of the `syntheticNamedExports` option. If `null` is returned or the flag is omitted, then `syntheticNamedExports` will be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `false`. 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 be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module or eventually default to an empty object. -You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `moduleSideEffects`, `syntheticNamedExports` and `meta` inside this hook. +You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` inside this hook. #### `watchChange` @@ -779,6 +789,7 @@ type ModuleInfo = { dynamicImporters: string[]; // the ids of all modules that import this module via dynamic import() implicitlyLoadedAfterOneOf: string[]; // implicit relationships, declared via this.emitFile implicitlyLoadedBefore: string[]; // implicit relationships, declared via this.emitFile + assertions: { [key: string]: string }; // import assertions for this module meta: { [plugin: string]: any }; // custom module meta-data moduleSideEffects: boolean | 'no-treeshake'; // are imports of this module included if nothing is imported from it syntheticNamedExports: boolean | string; // final value of synthetic named exports @@ -787,9 +798,10 @@ type ModuleInfo = { type ResolvedId = { id: string; // the id of the imported module external: boolean | 'absolute'; // is this module external, "absolute" means it will not be rendered as relative in the module + assertions: { [key: string]: string }; // import assertions for this import + meta: { [plugin: string]: any }; // custom module meta-data when resolving the module moduleSideEffects: boolean | 'no-treeshake'; // are side effects of the module observed, is tree-shaking enabled syntheticNamedExports: boolean | string; // does the module allow importing non-existing named exports - meta: { [plugin: string]: any }; // custom module meta-data when resolving the module }; ``` @@ -802,7 +814,7 @@ During the build, this object represents currently available information about t - `importers`, `dynamicImporters` and `implicitlyLoadedBefore` will start as empty arrays, which receive additional entries as new importers and implicit dependents are discovered. They will no longer change after `buildEnd`. - `isIncluded` is only available after `buildEnd`, at which point it will no longer change. - `importedIds`, `importedIdResolutions`, `dynamicallyImportedIds` and `dynamicallyImportedIdResolutions` are available when a module has been parsed and its dependencies have been resolved. This is the case in the `moduleParsed` hook or after awaiting [`this.load`](guide/en/#thisload) with the `resolveDependencies` flag. At that point, they will no longer change. -- `meta`, `moduleSideEffects` and `syntheticNamedExports` can be changed by [`load`](guide/en/#load) and [`transform`](guide/en/#transform) hooks. Moreover, while most properties are read-only, `moduleSideEffects` is writable and changes will be picked up if they occur before the `buildEnd` hook is triggered. `meta` should not be overwritten, but it is ok to mutate its properties at any time to store meta information about a module. The advantage of doing this instead of keeping state in a plugin is that `meta` is persisted to and restored from the cache if it is used, e.g. when using watch mode from the CLI. +- `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` can be changed by [`load`](guide/en/#load) and [`transform`](guide/en/#transform) hooks. Moreover, while most properties are read-only, these properties are writable and changes will be picked up if they occur before the `buildEnd` hook is triggered. `meta` itself should not be overwritten, but it is ok to mutate its properties at any time to store meta information about a module. The advantage of doing this instead of keeping state in a plugin is that `meta` is persisted to and restored from the cache if it is used, e.g. when using watch mode from the CLI. Returns `null` if the module id cannot be found. @@ -814,7 +826,7 @@ Get ids of the files which has been watched previously. Include both files added #### `this.load` -**Type:** `({id: string, moduleSideEffects?: boolean | 'no-treeshake' | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null, resolveDependencies?: boolean}) => Promise` +**Type:** `({id: string, resolveDependencies?: boolean, assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | 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. @@ -822,7 +834,7 @@ This allows you to inspect the final content of modules before deciding how to r 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`, `dynamicallyImportedIds`, `importedIdResolutions` and `dynamicallyImportedIdResolutions`. This helps to avoid deadlock situations when awaiting `this.load` in a `resolveId` hook. If you are interested in `importedIds` and `dynamicallyImportedIds`, you can either implement a `moduleParsed` hook or pass the `resolveDependencies` flag, which will make the Promise returned by `this.load` wait until all dependency ids have been resolved. -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. Note the special handling for re-exporting the default export: +Note that with regard to the `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` 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. Note the special handling for re-exporting the default export: ```js export default function addProxyPlugin() { @@ -955,7 +967,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, assertions?: {[key: string]: string}, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", assertions: {[key: string]: string}, meta: {[plugin: string]: any} | null, moduleSideEffects: boolean | "no-treeshake", syntheticNamedExports: boolean | string>` 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`. @@ -965,7 +977,9 @@ You can also pass an object of plugin-specific options via the `custom` option, The value for `isEntry` you pass here will be passed along to the [`resolveId`](guide/en/#resolveid) hooks handling this call, otherwise `false` will be passed if there is an importer and `true` if there is not. -When calling this function from a `resolveId` hook, you should always check if it makes sense for you to pass along the `isEntry` and `custom` options. +If you pass an object for `assertions`, it will simulate resolving an import with an assertion, e.g. `assertions: {type: "json"}` simulates resolving `import "foo" assert {type: "json"}`. This will be passed to any [`resolveId`](guide/en/#resolveid) hooks handling this call and may ultimately become part of the returned object. + +When calling this function from a `resolveId` hook, you should always check if it makes sense for you to pass along the `isEntry`, `custom` and `assertions` options. #### `this.setAssetSource` diff --git a/docs/08-troubleshooting.md b/docs/08-troubleshooting.md index 7ed5f3e30c1..8f4e12f4de2 100644 --- a/docs/08-troubleshooting.md +++ b/docs/08-troubleshooting.md @@ -36,9 +36,9 @@ Using the [Function constructor](https://developer.mozilla.org/en-US/docs/Web/Ja Sometimes, you'll end up with code in your bundle that doesn't seem like it should be there. For example, if you import a utility from `lodash-es`, you might expect that you'll get the bare minimum of code necessary for that utility to work. -But Rollup has to be conservative about what code it removes in order to guarantee that the end result will run correctly. If an imported module appears to have _side-effects_, either on bits of the module that you're using or on the global environment, Rollup plays it safe and includes those side-effects. +But Rollup has to be conservative about what code it removes in order to guarantee that the end result will run correctly. If an imported module appears to have _side effects_, either on bits of the module that you're using or on the global environment, Rollup plays it safe and includes those side effects. -Because static analysis in a dynamic language like JavaScript is hard, there will occasionally be false positives. Lodash is a good example of a module that _looks_ like it has lots of side-effects, even in places that it doesn't. You can often mitigate those false positives by importing submodules (e.g. `import map from 'lodash-es/map'` rather than `import { map } from 'lodash-es'`). +Because static analysis in a dynamic language like JavaScript is hard, there will occasionally be false positives. Lodash is a good example of a module that _looks_ like it has lots of side effects, even in places that it doesn't. You can often mitigate those false positives by importing submodules (e.g. `import map from 'lodash-es/map'` rather than `import { map } from 'lodash-es'`). Rollup's static analysis will improve over time, but it will never be perfect in all cases – that's just JavaScript. diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 3d74dfecba3..16514383d8e 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -532,6 +532,12 @@ Type: `boolean`
CLI: `--extend`/`--no-extend`
Default: `false` Whether to extend the global variable defined by the `name` option in `umd` or `iife` formats. When `true`, the global variable will be defined as `(global.name = global.name || {})`. When false, the global defined by `name` will be overwritten like `(global.name = {})`. +#### output.externalImportAssertions + +Type: `boolean`
CLI: `--externalImportAssertions`/`--no-externalImportAssertions`
Default: `true` + +Whether to add import assertions to external imports in the output if the output format is `es`. By default, assertions are taken from the input files, but plugins can add or remove assertions later. E.g. `import "foo" assert {type: "json"}` will cause the same import to appear in the output unless the option is set to `false`. Note that all imports of a module need to have consistent assertions, otherwise a warning is emitted. + #### output.generatedCode Type: `"es5" | "es2015" | { arrowFunctions?: boolean, constBindings?: boolean, objectShorthand?: boolean, preset?: "es5" | "es2015", reservedNamesAsProps?: boolean, symbols?: boolean }`
CLI: `--generatedCode `
Default: `"es5"` @@ -1800,7 +1806,7 @@ export default { **treeshake.propertyReadSideEffects**
Type: `boolean | 'always'`
CLI: `--treeshake.propertyReadSideEffects`/`--no-treeshake.propertyReadSideEffects`
Default: `true` -If `true`, retain unused property reads that Rollup can determine to have side-effects. This includes accessing properties of `null` or `undefined` or triggering explicit getters via property access. Note that this does not cover destructuring assignment or getters on objects passed as function parameters. +If `true`, retain unused property reads that Rollup can determine to have side effects. This includes accessing properties of `null` or `undefined` or triggering explicit getters via property access. Note that this does not cover destructuring assignment or getters on objects passed as function parameters. If `false`, assume reading a property of an object never has side effects. Depending on your code, disabling this option can significantly reduce bundle size but can potentially break functionality if you rely on getters or errors from illegal property access. diff --git a/package-lock.json b/package-lock.json index 28b0dd0533d..9ecbf1ff6d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rollup", - "version": "3.0.0-4", + "version": "3.0.0-7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "rollup", - "version": "3.0.0-4", + "version": "3.0.0-7", "license": "MIT", "bin": { "rollup": "dist/bin/rollup" @@ -27,6 +27,7 @@ "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "acorn": "^8.8.0", + "acorn-import-assertions": "^1.8.0", "acorn-jsx": "^5.3.2", "acorn-walk": "^8.2.0", "buble": "^0.20.0", @@ -1327,6 +1328,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -8374,6 +8384,13 @@ "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", diff --git a/package.json b/package.json index 526a2fecbf4..9d72206c070 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "acorn": "^8.8.0", + "acorn-import-assertions": "^1.8.0", "acorn-jsx": "^5.3.2", "acorn-walk": "^8.2.0", "buble": "^0.20.0", diff --git a/src/Chunk.ts b/src/Chunk.ts index d45880e854d..2f8182455b2 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -89,6 +89,7 @@ export type ResolvedDynamicImport = ( ) & { node: ImportExpression }; export interface ChunkDependency { + assertions: string | null; defaultVariableName: string | undefined; globalName: string | false | undefined; importPath: string; @@ -853,6 +854,20 @@ export default class Chunk { ); } + private getDynamicImportStringAndAssertions( + resolution: ExternalModule | string | null, + fileName: string + ): [importPath: string, assertions: string | null | true] { + if (resolution instanceof ExternalModule) { + const chunk = this.externalChunkByModule.get(resolution)!; + return [`'${chunk.getImportPath(fileName)}'`, chunk.getImportAssertions(this.snippets)]; + } + return [ + resolution || '', + (this.outputOptions.format === 'es' && this.outputOptions.externalImportAssertions) || null + ]; + } + private getFallbackChunkName(): string { if (this.manualChunkAlias) { return this.manualChunkAlias; @@ -1032,6 +1047,7 @@ export default class Chunk { const importPath = dep.getImportPath(fileName); renderedDependencies.set(dep, { + assertions: dep instanceof ExternalChunk ? dep.getImportAssertions(this.snippets) : null, defaultVariableName: dep.defaultVariableName, globalName: dep instanceof ExternalChunk && @@ -1181,11 +1197,16 @@ export default class Chunk { pluginDriver, accessedGlobalsByScope, `'${(facadeChunk || chunk).getImportPath(fileName)}'`, - !facadeChunk?.strictFacade && chunk.exportNamesByVariable.get(resolution.namespace)![0] + !facadeChunk?.strictFacade && chunk.exportNamesByVariable.get(resolution.namespace)![0], + null ); } } else { const { resolution } = resolvedDynamicImport; + const [resolutionString, assertions] = this.getDynamicImportStringAndAssertions( + resolution, + fileName + ); resolvedDynamicImport.node.setExternalResolution( 'external', resolution, @@ -1193,10 +1214,9 @@ export default class Chunk { snippets, pluginDriver, accessedGlobalsByScope, - resolution instanceof ExternalModule - ? `'${this.externalChunkByModule.get(resolution)!.getImportPath(fileName)}'` - : resolution || '', - false + resolutionString, + false, + assertions ); } } diff --git a/src/ExternalChunk.ts b/src/ExternalChunk.ts index 2e76832b61e..43a07d913ac 100644 --- a/src/ExternalChunk.ts +++ b/src/ExternalChunk.ts @@ -1,6 +1,7 @@ import ExternalModule from './ExternalModule'; -import { NormalizedOutputOptions } from './rollup/types'; +import { ModuleInfo, NormalizedOutputOptions } from './rollup/types'; import { escapeId } from './utils/escapeId'; +import { GenerateCodeSnippets } from './utils/generateCodeSnippets'; import { normalize, relative } from './utils/path'; import { getImportPath } from './utils/relativeId'; @@ -12,6 +13,8 @@ export default class ExternalChunk { variableName = ''; private fileName: string | null = null; + private importAssertions: string | null = null; + private moduleInfo: ModuleInfo; private renormalizeRenderPath: boolean; constructor( @@ -20,6 +23,7 @@ export default class ExternalChunk { private inputBase: string ) { this.id = module.id; + this.moduleInfo = module.info; this.renormalizeRenderPath = module.renormalizeRenderPath; this.suggestedVariableName = module.suggestedVariableName; } @@ -34,6 +38,15 @@ export default class ExternalChunk { (this.renormalizeRenderPath ? normalize(relative(this.inputBase, this.id)) : this.id)); } + getImportAssertions(snippets: GenerateCodeSnippets): string | null { + return (this.importAssertions ||= formatAssertions( + this.options.format === 'es' && + this.options.externalImportAssertions && + this.moduleInfo.assertions, + snippets + )); + } + getImportPath(importer: string): string { return escapeId( this.renormalizeRenderPath @@ -42,3 +55,19 @@ export default class ExternalChunk { ); } } + +function formatAssertions( + assertions: Record | null | void | false, + { getObject }: GenerateCodeSnippets +): string | null { + if (!assertions) { + return null; + } + const assertionEntries: [key: string, value: string][] = Object.entries(assertions).map( + ([key, value]) => [key, `'${value}'`] + ); + if (assertionEntries.length) { + return getObject(assertionEntries, { lineBreakIndent: null }); + } + return null; +} diff --git a/src/ExternalModule.ts b/src/ExternalModule.ts index b80a1aac79a..78d8a3e2e92 100644 --- a/src/ExternalModule.ts +++ b/src/ExternalModule.ts @@ -23,12 +23,14 @@ export default class ExternalModule { public readonly id: string, moduleSideEffects: boolean | 'no-treeshake', meta: CustomPluginOptions, - public readonly renormalizeRenderPath: boolean + public readonly renormalizeRenderPath: boolean, + assertions: Record ) { this.suggestedVariableName = makeLegal(id.split(/[\\/]/).pop()!); const { importers, dynamicImporters } = this; const info: ModuleInfo = (this.info = { + assertions, ast: null, code: null, dynamicallyImportedIdResolutions: EMPTY_ARRAY, diff --git a/src/Module.ts b/src/Module.ts index 12da5bbdd11..2fbbe653d6f 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -51,6 +51,7 @@ import { augmentCodeLocation, errAmbiguousExternalNamespaces, errCircularReexport, + errInconsistentImportAssertions, errInvalidFormatForTopLevelAwait, errInvalidSourcemapForError, errMissingExport, @@ -65,6 +66,10 @@ import { getId } from './utils/getId'; import { getOrCreate } from './utils/getOrCreate'; import { getOriginalLocation } from './utils/getOriginalLocation'; import { makeLegal } from './utils/identifierHelpers'; +import { + doAssertionsDiffer, + getAssertionsFromImportExportDeclaration +} from './utils/parseAssertions'; import { basename, extname } from './utils/path'; import type { RenderOptions } from './utils/renderHelpers'; import { timeEnd, timeStart } from './utils/timers'; @@ -223,7 +228,7 @@ export default class Module { declare scope: ModuleScope; readonly sideEffectDependenciesByVariable = new Map>(); declare sourcemapChain: DecodedSourceMapOrMissing[]; - readonly sources = new Set(); + readonly sourcesWithAssertions = new Map>(); declare transformFiles?: EmittedFile[]; private allExportNames: Set | null = null; @@ -255,7 +260,8 @@ export default class Module { isEntry: boolean, moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, - meta: CustomPluginOptions + meta: CustomPluginOptions, + assertions: Record ) { this.excludeFromSourcemap = /\0/.test(id); this.context = options.moduleContext(id); @@ -270,10 +276,11 @@ export default class Module { implicitlyLoadedBefore, importers, reexportDescriptions, - sources + sourcesWithAssertions } = this; this.info = { + assertions, ast: null, code: null, get dynamicallyImportedIdResolutions() { @@ -312,12 +319,18 @@ export default class Module { return Array.from(implicitlyLoadedBefore, getId).sort(); }, get importedIdResolutions() { - return Array.from(sources, source => module.resolvedIds[source]).filter(Boolean); + return Array.from( + sourcesWithAssertions.keys(), + source => module.resolvedIds[source] + ).filter(Boolean); }, get importedIds() { // We cannot use this.dependencies because this is needed before // dependencies are populated - return Array.from(sources, source => module.resolvedIds[source]?.id).filter(Boolean); + return Array.from( + sourcesWithAssertions.keys(), + source => module.resolvedIds[source]?.id + ).filter(Boolean); }, get importers() { return importers.sort(); @@ -784,6 +797,7 @@ export default class Module { toJSON(): ModuleJSON { return { + assertions: this.info.assertions, ast: this.ast!.esTreeNode, code: this.info.code!, customTransformCache: this.customTransformCache, @@ -900,7 +914,7 @@ export default class Module { }); } else if (node instanceof ExportAllDeclaration) { const source = node.source.value; - this.sources.add(source); + this.addSource(source, node); if (node.exported) { // export * as name from './other' @@ -920,7 +934,7 @@ export default class Module { // export { name } from './other' const source = node.source.value; - this.sources.add(source); + this.addSource(source, node); for (const specifier of node.specifiers) { const name = specifier.exported.name; this.reexportDescriptions.set(name, { @@ -960,7 +974,7 @@ export default class Module { private addImport(node: ImportDeclaration): void { const source = node.source.value; - this.sources.add(source); + this.addSource(source, node); for (const specifier of node.specifiers) { const isDefault = specifier.type === NodeType.ImportDefaultSpecifier; const isNamespace = specifier.type === NodeType.ImportNamespaceSpecifier; @@ -1039,6 +1053,24 @@ export default class Module { addSideEffectDependencies(alwaysCheckedDependencies); } + private addSource( + source: string, + declaration: ImportDeclaration | ExportNamedDeclaration | ExportAllDeclaration + ) { + const parsedAssertions = getAssertionsFromImportExportDeclaration(declaration.assertions); + const existingAssertions = this.sourcesWithAssertions.get(source); + if (existingAssertions) { + if (doAssertionsDiffer(existingAssertions, parsedAssertions)) { + this.warn( + errInconsistentImportAssertions(existingAssertions, parsedAssertions, source, this.id), + declaration.start + ); + } + } else { + this.sourcesWithAssertions.set(source, parsedAssertions); + } + } + private getVariableFromNamespaceReexports( name: string, importerForSideEffects?: Module, diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 8546bfa62f0..00dab89391a 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -22,6 +22,7 @@ import { errEntryCannotBeExternal, errExternalSyntheticExports, errImplicitDependantCannotBeExternal, + errInconsistentImportAssertions, errInternalIdCannotBeExternal, error, errUnresolvedEntry, @@ -30,6 +31,7 @@ import { errUnresolvedImportTreatedAsExternal } from './utils/error'; import { promises as fs } from './utils/fs'; +import { doAssertionsDiffer, getAssertionsFromImportExpression } from './utils/parseAssertions'; import { isAbsolute, isRelative, resolve } from './utils/path'; import relativeId from './utils/relativeId'; import { resolveId } from './utils/resolveId'; @@ -42,6 +44,15 @@ export interface UnresolvedModule { name: string | null; } +export type ModuleLoaderResolveId = ( + source: string, + importer: string | undefined, + customOptions: CustomPluginOptions | undefined, + isEntry: boolean | undefined, + assertions: Record, + skip?: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null +) => Promise; + type NormalizedResolveIdWithoutDefaults = Partial> & { external?: boolean | 'absolute'; id: string; @@ -174,7 +185,7 @@ export class ModuleLoader { resolvedId: { id: string; resolveDependencies?: boolean } & Partial> ): Promise { const module = await this.fetchModule( - this.getResolvedIdWithDefaults(resolvedId)!, + this.getResolvedIdWithDefaults(resolvedId, EMPTY_OBJECT)!, undefined, false, resolvedId.resolveDependencies ? RESOLVE_DEPENDENCIES : true @@ -182,14 +193,15 @@ export class ModuleLoader { return module.info; } - resolveId = async ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null = null - ): Promise => { - return this.getResolvedIdWithDefaults( + resolveId: ModuleLoaderResolveId = async ( + source, + importer, + customOptions, + isEntry, + assertions, + skip = null + ) => + this.getResolvedIdWithDefaults( this.getNormalizedResolvedIdWithoutDefaults( this.options.external(source, importer, false) ? false @@ -201,14 +213,14 @@ export class ModuleLoader { this.resolveId, skip, customOptions, - typeof isEntry === 'boolean' ? isEntry : !importer + typeof isEntry === 'boolean' ? isEntry : !importer, + assertions ), - importer, source - ) + ), + assertions ); - }; private addEntryWithImplicitDependants( unresolvedModule: UnresolvedModule, @@ -340,17 +352,24 @@ 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. + // 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, + { assertions, id, meta, moduleSideEffects, syntheticNamedExports }: ResolvedId, importer: string | undefined, isEntry: boolean, isPreload: PreloadType ): Promise { const existingModule = this.modulesById.get(id); if (existingModule instanceof Module) { + if (importer && doAssertionsDiffer(assertions, existingModule.info.assertions)) { + this.options.onwarn( + errInconsistentImportAssertions(existingModule.info.assertions, assertions, id, importer) + ); + } await this.handleExistingModule(existingModule, isEntry, isPreload); return existingModule; } @@ -362,7 +381,8 @@ export class ModuleLoader { isEntry, moduleSideEffects, syntheticNamedExports, - meta + meta, + assertions ); this.modulesById.set(id, module); this.graph.watchFiles[id] = true; @@ -412,23 +432,29 @@ export class ModuleLoader { resolvedId: ResolvedId ): Promise { if (resolvedId.external) { - const { external, id, moduleSideEffects, meta } = resolvedId; - if (!this.modulesById.has(id)) { - this.modulesById.set( + const { assertions, external, id, moduleSideEffects, meta } = resolvedId; + let externalModule = this.modulesById.get(id); + if (!externalModule) { + externalModule = new ExternalModule( + this.options, id, - new ExternalModule( - this.options, - id, - moduleSideEffects, - meta, - external !== 'absolute' && isAbsolute(id) - ) + moduleSideEffects, + meta, + external !== 'absolute' && isAbsolute(id), + assertions ); - } - - const externalModule = this.modulesById.get(id); - if (!(externalModule instanceof ExternalModule)) { + this.modulesById.set(id, externalModule); + } else if (!(externalModule instanceof ExternalModule)) { return error(errInternalIdCannotBeExternal(source, importer)); + } else if (doAssertionsDiffer(externalModule.info.assertions, assertions)) { + this.options.onwarn( + errInconsistentImportAssertions( + externalModule.info.assertions, + assertions, + source, + importer + ) + ); } return Promise.resolve(externalModule); } @@ -512,7 +538,8 @@ export class ModuleLoader { typeof dynamicImport.argument === 'string' ? dynamicImport.argument : dynamicImport.argument.esTreeNode, - module.id + module.id, + getAssertionsFromImportExpression(dynamicImport.node) ); if (resolvedId && typeof resolvedId === 'object') { dynamicImport.id = resolvedId.id; @@ -523,29 +550,32 @@ export class ModuleLoader { private getResolveStaticDependencyPromises(module: Module): ResolveStaticDependencyPromise[] { return Array.from( - module.sources, - async source => + module.sourcesWithAssertions, + async ([source, assertions]) => [ source, (module.resolvedIds[source] = module.resolvedIds[source] || - this.handleResolveId( - await this.resolveId(source, module.id, EMPTY_OBJECT, false), + this.handleInvalidResolvedId( + await this.resolveId(source, module.id, EMPTY_OBJECT, false, assertions), source, - module.id + module.id, + assertions )) ] as [string, ResolvedId] ); } private getResolvedIdWithDefaults( - resolvedId: NormalizedResolveIdWithoutDefaults | null + resolvedId: NormalizedResolveIdWithoutDefaults | null, + assertions: Record ): ResolvedId | null { if (!resolvedId) { return null; } const external = resolvedId.external || false; return { + assertions: resolvedId.assertions || assertions, external, id: resolvedId.id, meta: resolvedId.meta || {}, @@ -573,10 +603,11 @@ export class ModuleLoader { return this.fetchModuleDependencies(module, ...(await loadPromise)); } - private handleResolveId( + private handleInvalidResolvedId( resolvedId: ResolvedId | null, source: string, - importer: string + importer: string, + assertions: Record ): ResolvedId { if (resolvedId === null) { if (isRelative(source)) { @@ -584,6 +615,7 @@ export class ModuleLoader { } this.options.onwarn(errUnresolvedImportTreatedAsExternal(source, importer)); return { + assertions, external: true, id: source, meta: {}, @@ -610,7 +642,8 @@ export class ModuleLoader { this.resolveId, null, EMPTY_OBJECT, - true + true, + EMPTY_OBJECT ); if (resolveIdResult == null) { return error( @@ -633,7 +666,8 @@ export class ModuleLoader { this.getResolvedIdWithDefaults( typeof resolveIdResult === 'object' ? (resolveIdResult as NormalizedResolveIdWithoutDefaults) - : { id: resolveIdResult } + : { id: resolveIdResult }, + EMPTY_OBJECT )!, undefined, isEntry, @@ -644,11 +678,13 @@ export class ModuleLoader { private async resolveDynamicImport( module: Module, specifier: string | acorn.Node, - importer: string + importer: string, + assertions: Record ): Promise { const resolution = await this.pluginDriver.hookFirst('resolveDynamicImport', [ specifier, - importer + importer, + { assertions } ]); if (typeof specifier !== 'string') { if (typeof resolution === 'string') { @@ -657,25 +693,41 @@ export class ModuleLoader { if (!resolution) { return null; } - return { - external: false, - moduleSideEffects: true, - ...resolution - } as ResolvedId; + return this.getResolvedIdWithDefaults( + resolution as NormalizedResolveIdWithoutDefaults, + assertions + ); } if (resolution == null) { - return (module.resolvedIds[specifier] ??= this.handleResolveId( - await this.resolveId(specifier, module.id, EMPTY_OBJECT, false), + const existingResolution = module.resolvedIds[specifier]; + if (existingResolution) { + if (doAssertionsDiffer(existingResolution.assertions, assertions)) { + this.options.onwarn( + errInconsistentImportAssertions( + existingResolution.assertions, + assertions, + specifier, + importer + ) + ); + } + return existingResolution; + } + return (module.resolvedIds[specifier] = this.handleInvalidResolvedId( + await this.resolveId(specifier, module.id, EMPTY_OBJECT, false, assertions), specifier, - module.id + module.id, + assertions )); } - return this.handleResolveId( + return this.handleInvalidResolvedId( this.getResolvedIdWithDefaults( - this.getNormalizedResolvedIdWithoutDefaults(resolution, importer, specifier) + this.getNormalizedResolvedIdWithoutDefaults(resolution, importer, specifier), + assertions ), specifier, - importer + importer, + assertions ); } } diff --git a/src/ast/keys.ts b/src/ast/keys.ts index e9f9d437952..32764a3aa28 100644 --- a/src/ast/keys.ts +++ b/src/ast/keys.ts @@ -3,6 +3,9 @@ import type { GenericEsTreeNode } from './nodes/shared/Node'; export const keys: { [name: string]: string[]; } = { + // TODO this should be removed once ImportExpression follows official ESTree + // specs with "null" as default + ImportExpression: ['arguments'], Literal: [], Program: ['body'] }; diff --git a/src/ast/nodes/ExportAllDeclaration.ts b/src/ast/nodes/ExportAllDeclaration.ts index d4828620bf7..2768321eda6 100644 --- a/src/ast/nodes/ExportAllDeclaration.ts +++ b/src/ast/nodes/ExportAllDeclaration.ts @@ -1,11 +1,13 @@ import type MagicString from 'magic-string'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import type Identifier from './Identifier'; +import ImportAttribute from './ImportAttribute'; import type Literal from './Literal'; import type * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class ExportAllDeclaration extends NodeBase { + declare assertions: ImportAttribute[]; declare exported: Identifier | null; declare needsBoundaries: true; declare source: Literal; diff --git a/src/ast/nodes/ExportNamedDeclaration.ts b/src/ast/nodes/ExportNamedDeclaration.ts index 2b63e982618..f4257d43d6d 100644 --- a/src/ast/nodes/ExportNamedDeclaration.ts +++ b/src/ast/nodes/ExportNamedDeclaration.ts @@ -4,12 +4,14 @@ import type { HasEffectsContext } from '../ExecutionContext'; import type ClassDeclaration from './ClassDeclaration'; import type ExportSpecifier from './ExportSpecifier'; import type FunctionDeclaration from './FunctionDeclaration'; +import ImportAttribute from './ImportAttribute'; import type Literal from './Literal'; import type * as NodeType from './NodeType'; import type VariableDeclaration from './VariableDeclaration'; import { type Node, NodeBase } from './shared/Node'; export default class ExportNamedDeclaration extends NodeBase { + declare assertions: ImportAttribute[]; declare declaration: FunctionDeclaration | ClassDeclaration | VariableDeclaration | null; declare needsBoundaries: true; declare source: Literal | null; diff --git a/src/ast/nodes/ImportAttribute.ts b/src/ast/nodes/ImportAttribute.ts new file mode 100644 index 00000000000..a1e0c3bef49 --- /dev/null +++ b/src/ast/nodes/ImportAttribute.ts @@ -0,0 +1,10 @@ +import Identifier from './Identifier'; +import type Literal from './Literal'; +import type * as NodeType from './NodeType'; +import { NodeBase } from './shared/Node'; + +export default class ImportAttribute extends NodeBase { + declare key: Identifier | Literal; + declare type: NodeType.tImportAttribute; + declare value: Literal; +} diff --git a/src/ast/nodes/ImportDeclaration.ts b/src/ast/nodes/ImportDeclaration.ts index 9a4d98aff71..249271d0aed 100644 --- a/src/ast/nodes/ImportDeclaration.ts +++ b/src/ast/nodes/ImportDeclaration.ts @@ -1,5 +1,6 @@ import type MagicString from 'magic-string'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; +import ImportAttribute from './ImportAttribute'; import type ImportDefaultSpecifier from './ImportDefaultSpecifier'; import type ImportNamespaceSpecifier from './ImportNamespaceSpecifier'; import type ImportSpecifier from './ImportSpecifier'; @@ -8,12 +9,13 @@ import type * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class ImportDeclaration extends NodeBase { + declare assertions?: ImportAttribute[]; declare needsBoundaries: true; declare source: Literal; declare specifiers: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]; declare type: NodeType.tImportDeclaration; - // Do not bind specifiers + // Do not bind specifiers or assertions bind(): void {} hasEffects(): boolean { diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 10346a5651a..c0a379afc74 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -13,6 +13,7 @@ import type { InclusionContext } from '../ExecutionContext'; import type ChildScope from '../scopes/ChildScope'; import type NamespaceVariable from '../variables/NamespaceVariable'; import type * as NodeType from './NodeType'; +import ObjectExpression from './ObjectExpression'; import { type ExpressionNode, type IncludeChildren, NodeBase } from './shared/Node'; interface DynamicImportMechanism { @@ -20,16 +21,25 @@ interface DynamicImportMechanism { right: string; } +// TODO once ImportExpression follows official ESTree specs with "null" as +// default, keys.ts should be updated export default class ImportExpression extends NodeBase { + declare arguments: ObjectExpression[] | undefined; inlineNamespace: NamespaceVariable | null = null; declare source: ExpressionNode; declare type: NodeType.tImportExpression; + private assertions: string | null | true = null; private mechanism: DynamicImportMechanism | null = null; private namespaceExportName: string | false | undefined = undefined; private resolution: Module | ExternalModule | string | null = null; private resolutionString: string | null = null; + // Do not bind assertions + bind(): void { + this.source.bind(); + } + hasEffects(): boolean { return true; } @@ -49,7 +59,7 @@ export default class ImportExpression extends NodeBase { render(code: MagicString, options: RenderOptions): void { const { - snippets: { getDirectReturnFunction, getPropertyAccess } + snippets: { _, getDirectReturnFunction, getObject, getPropertyAccess } } = options; if (this.inlineNamespace) { const [left, right] = getDirectReturnFunction([], { @@ -60,20 +70,17 @@ export default class ImportExpression extends NodeBase { code.overwrite( this.start, this.end, - `Promise.resolve().then(${left}${this.inlineNamespace.getName(getPropertyAccess)}${right})`, - { contentOnly: true } + `Promise.resolve().then(${left}${this.inlineNamespace.getName(getPropertyAccess)}${right})` ); return; } - if (this.mechanism) { code.overwrite( this.start, findFirstOccurrenceOutsideComment(code.original, '(', this.start + 6) + 1, - this.mechanism.left, - { contentOnly: true } + this.mechanism.left ); - code.overwrite(this.end - 1, this.end, this.mechanism.right, { contentOnly: true }); + code.overwrite(this.end - 1, this.end, this.mechanism.right); } if (this.resolutionString) { code.overwrite(this.source.start, this.source.end, this.resolutionString); @@ -88,6 +95,19 @@ export default class ImportExpression extends NodeBase { } else { this.source.render(code, options); } + if (this.assertions !== true) { + if (this.arguments) { + code.overwrite(this.source.end, this.end - 1, '', { contentOnly: true }); + } + if (this.assertions) { + code.appendLeft( + this.end - 1, + `,${_}${getObject([['assert', this.assertions]], { + lineBreakIndent: null + })}` + ); + } + } } setExternalResolution( @@ -98,13 +118,15 @@ export default class ImportExpression extends NodeBase { pluginDriver: PluginDriver, accessedGlobalsByScope: Map>, resolutionString: string, - namespaceExportName: string | false | undefined + namespaceExportName: string | false | undefined, + assertions: string | null | true ): void { const { format } = options; this.inlineNamespace = null; this.resolution = resolution; this.resolutionString = resolutionString; this.namespaceExportName = namespaceExportName; + this.assertions = assertions; const accessedGlobals = [...(accessedImportGlobals[format] || [])]; let helper: string | null; ({ helper, mechanism: this.mechanism } = this.getDynamicImportMechanismAndHelper( diff --git a/src/ast/nodes/NodeType.ts b/src/ast/nodes/NodeType.ts index 6d75b6ac7c2..1fd93f3ca8f 100644 --- a/src/ast/nodes/NodeType.ts +++ b/src/ast/nodes/NodeType.ts @@ -30,6 +30,7 @@ export type tFunctionExpression = 'FunctionExpression'; export type tIdentifier = 'Identifier'; export type tIfStatement = 'IfStatement'; export type tImport = 'Import'; +export type tImportAttribute = 'ImportAttribute'; export type tImportDeclaration = 'ImportDeclaration'; export type tImportExpression = 'ImportExpression'; export type tImportDefaultSpecifier = 'ImportDefaultSpecifier'; @@ -101,6 +102,7 @@ export const FunctionExpression: tFunctionExpression = 'FunctionExpression'; export const Identifier: tIdentifier = 'Identifier'; export const IfStatement: tIfStatement = 'IfStatement'; export const Import: tImport = 'Import'; +export const ImportAttribute: tImportAttribute = 'ImportAttribute'; export const ImportDeclaration: tImportDeclaration = 'ImportDeclaration'; export const ImportExpression: tImportExpression = 'ImportExpression'; export const ImportDefaultSpecifier: tImportDefaultSpecifier = 'ImportDefaultSpecifier'; diff --git a/src/ast/nodes/index.ts b/src/ast/nodes/index.ts index 47f1ea5d09c..517ca63f8ba 100644 --- a/src/ast/nodes/index.ts +++ b/src/ast/nodes/index.ts @@ -29,6 +29,7 @@ import FunctionDeclaration from './FunctionDeclaration'; import FunctionExpression from './FunctionExpression'; import Identifier from './Identifier'; import IfStatement from './IfStatement'; +import ImportAttribute from './ImportAttribute'; import ImportDeclaration from './ImportDeclaration'; import ImportDefaultSpecifier from './ImportDefaultSpecifier'; import ImportExpression from './ImportExpression'; @@ -104,6 +105,7 @@ export const nodeConstructors: { FunctionExpression, Identifier, IfStatement, + ImportAttribute, ImportDeclaration, ImportDefaultSpecifier, ImportExpression, diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index cb5337284f5..a83a2606177 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -164,12 +164,11 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { bind(): void { for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; - if (value === null) continue; if (Array.isArray(value)) { for (const child of value) { child?.bind(); } - } else { + } else if (value) { value.bind(); } } diff --git a/src/finalisers/es.ts b/src/finalisers/es.ts index 668f2000322..51eaa1a8b5d 100644 --- a/src/finalisers/es.ts +++ b/src/finalisers/es.ts @@ -10,9 +10,9 @@ export default function es( { accessedGlobals, indent: t, intro, outro, dependencies, exports, snippets }: FinaliserOptions, { externalLiveBindings, freeze, namespaceToStringTag }: NormalizedOutputOptions ): void { - const { _, n } = snippets; + const { n } = snippets; - const importBlock = getImportBlock(dependencies, _); + const importBlock = getImportBlock(dependencies, snippets); if (importBlock.length > 0) intro += importBlock.join(n) + n + n; intro += getHelpersBlock( null, @@ -32,11 +32,13 @@ export default function es( magicString.trim(); } -function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { +function getImportBlock(dependencies: ChunkDependency[], { _ }: GenerateCodeSnippets): string[] { const importBlock: string[] = []; - for (const { importPath, reexports, imports, name } of dependencies) { + for (const { importPath, reexports, imports, name, assertions } of dependencies) { + const assertion = assertions ? `${_}assert${_}${assertions}` : ''; + const pathWithAssertion = `'${importPath}'${assertion};`; if (!reexports && !imports) { - importBlock.push(`import${_}'${importPath}';`); + importBlock.push(`import${_}${pathWithAssertion}`); continue; } if (imports) { @@ -53,10 +55,10 @@ function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { } } if (starImport) { - importBlock.push(`import${_}*${_}as ${starImport.local} from${_}'${importPath}';`); + importBlock.push(`import${_}*${_}as ${starImport.local} from${_}${pathWithAssertion}`); } if (defaultImport && importedNames.length === 0) { - importBlock.push(`import ${defaultImport.local} from${_}'${importPath}';`); + importBlock.push(`import ${defaultImport.local} from${_}${pathWithAssertion}`); } else if (importedNames.length > 0) { importBlock.push( `import ${defaultImport ? `${defaultImport.local},${_}` : ''}{${_}${importedNames @@ -67,7 +69,7 @@ function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { return `${specifier.imported} as ${specifier.local}`; } }) - .join(`,${_}`)}${_}}${_}from${_}'${importPath}';` + .join(`,${_}`)}${_}}${_}from${_}${pathWithAssertion}` ); } } @@ -85,14 +87,14 @@ function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { } } if (starExport) { - importBlock.push(`export${_}*${_}from${_}'${importPath}';`); + importBlock.push(`export${_}*${_}from${_}${pathWithAssertion}`); } if (namespaceReexports.length > 0) { if ( !imports || !imports.some(specifier => specifier.imported === '*' && specifier.local === name) ) { - importBlock.push(`import${_}*${_}as ${name} from${_}'${importPath}';`); + importBlock.push(`import${_}*${_}as ${name} from${_}${pathWithAssertion}`); } for (const specifier of namespaceReexports) { importBlock.push( @@ -112,7 +114,7 @@ function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { return `${specifier.imported} as ${specifier.reexported}`; } }) - .join(`,${_}`)}${_}}${_}from${_}'${importPath}';` + .join(`,${_}`)}${_}}${_}from${_}${pathWithAssertion}` ); } } diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 1086dd6c900..18dcf4311a4 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -83,6 +83,7 @@ type PartialNull = { }; interface ModuleOptions { + assertions: Record; meta: CustomPluginOptions; moduleSideEffects: boolean | 'no-treeshake'; syntheticNamedExports: boolean | string; @@ -189,7 +190,12 @@ export interface PluginContext extends MinimalPluginContext { resolve: ( source: string, importer?: string, - options?: { custom?: CustomPluginOptions; isEntry?: boolean; skipSelf?: boolean } + options?: { + assertions?: Record; + custom?: CustomPluginOptions; + isEntry?: boolean; + skipSelf?: boolean; + } ) => Promise; setAssetSource: (assetReferenceId: string, source: string | Uint8Array) => void; warn: (warning: RollupWarning | string, pos?: number | { column: number; line: number }) => void; @@ -220,7 +226,7 @@ export type ResolveIdHook = ( this: PluginContext, source: string, importer: string | undefined, - options: { custom?: CustomPluginOptions; isEntry: boolean } + options: { assertions: Record; custom?: CustomPluginOptions; isEntry: boolean } ) => ResolveIdResult; export type ShouldTransformCachedModuleHook = ( @@ -275,7 +281,8 @@ export type RenderChunkHook = ( export type ResolveDynamicImportHook = ( this: PluginContext, specifier: string | AcornNode, - importer: string + importer: string, + options: { assertions: Record } ) => ResolveIdResult; export type ResolveImportMetaHook = ( @@ -618,6 +625,7 @@ export interface OutputOptions { esModule?: boolean | 'if-default-prop'; exports?: 'default' | 'named' | 'none' | 'auto'; extend?: boolean; + externalImportAssertions?: boolean; externalLiveBindings?: boolean; // only required for bundle.write file?: string; @@ -669,6 +677,7 @@ export interface NormalizedOutputOptions { esModule: boolean | 'if-default-prop'; exports: 'default' | 'named' | 'none' | 'auto'; extend: boolean; + externalImportAssertions: boolean; externalLiveBindings: boolean; file: string | undefined; footer: AddonFunction; diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 8a871c4e734..64f866a2836 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -9,7 +9,7 @@ import type { } from '../rollup/types'; import type { FileEmitter } from './FileEmitter'; import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; -import { BLANK } from './blank'; +import { BLANK, EMPTY_OBJECT } from './blank'; import { BuildPhase } from './buildPhase'; import { errInvalidRollupPhaseForAddWatchFile, @@ -93,12 +93,13 @@ export function getPluginContext( return wrappedModuleIds(); }, parse: graph.contextParse.bind(graph), - resolve(source, importer, { custom, isEntry, skipSelf } = BLANK) { + resolve(source, importer, { assertions, custom, isEntry, skipSelf } = BLANK) { return graph.moduleLoader.resolveId( source, importer, custom, isEntry, + assertions || EMPTY_OBJECT, skipSelf ? [{ importer, plugin, source }] : null ); }, diff --git a/src/utils/error.ts b/src/utils/error.ts index 925f1a7eeba..d5feabb5113 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -69,6 +69,7 @@ const ADDON_ERROR = 'ADDON_ERROR', FILE_NOT_FOUND = 'FILE_NOT_FOUND', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', ILLEGAL_REASSIGNMENT = 'ILLEGAL_REASSIGNMENT', + INCONSISTENT_IMPORT_ASSERTIONS = 'INCONSISTENT_IMPORT_ASSERTIONS', INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_CONFIG_MODULE_FORMAT = 'INVALID_CONFIG_MODULE_FORMAT', @@ -344,6 +345,30 @@ export function errIllegalImportReassignment(name: string, importingId: string): }; } +export function errInconsistentImportAssertions( + existingAssertions: Record, + newAssertions: Record, + source: string, + importer: string +): RollupLog { + return { + code: INCONSISTENT_IMPORT_ASSERTIONS, + message: `Module "${relativeId(importer)}" tried to import "${relativeId( + source + )}" with ${formatAssertions( + newAssertions + )} assertions, but it was already imported elsewhere with ${formatAssertions( + existingAssertions + )} assertions. Please ensure that import assertions for the same module are always consistent.` + }; +} + +const formatAssertions = (assertions: Record): string => { + const entries = Object.entries(assertions); + if (entries.length === 0) return 'no'; + return entries.map(([key, value]) => `"${key}": "${value}"`).join(', '); +}; + export function errInputHookInOutputPlugin(pluginName: string, hookName: string): RollupLog { return { code: INPUT_HOOK_IN_OUTPUT_PLUGIN, diff --git a/src/utils/options/mergeOptions.ts b/src/utils/options/mergeOptions.ts index ba4e129b722..bf3ba84824b 100644 --- a/src/utils/options/mergeOptions.ts +++ b/src/utils/options/mergeOptions.ts @@ -106,7 +106,7 @@ function mergeInputOptions( overrides: CommandConfigObject, defaultOnWarnHandler: WarningHandler ): InputOptions { - const getOption = (name: string): any => overrides[name] ?? config[name]; + const getOption = (name: keyof InputOptions): any => overrides[name] ?? config[name]; const inputOptions: CompleteInputOptions = { acorn: getOption('acorn'), acornInjectPlugins: config.acornInjectPlugins as @@ -222,7 +222,7 @@ function mergeOutputOptions( overrides: GenericConfigObject, warn: WarningHandler ): OutputOptions { - const getOption = (name: string): any => overrides[name] ?? config[name]; + const getOption = (name: keyof OutputOptions): any => overrides[name] ?? config[name]; const outputOptions: CompleteOutputOptions = { amd: getObjectOption(config, overrides, 'amd'), assetFileNames: getOption('assetFileNames'), @@ -236,6 +236,7 @@ function mergeOutputOptions( esModule: getOption('esModule'), exports: getOption('exports'), extend: getOption('extend'), + externalImportAssertions: getOption('externalImportAssertions'), externalLiveBindings: getOption('externalLiveBindings'), file: getOption('file'), footer: getOption('footer'), diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index e357f6e89ae..267442f95ab 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -1,4 +1,5 @@ import * as acorn from 'acorn'; +import { importAssertions } from 'acorn-import-assertions'; import type { HasModuleSideEffects, InputOptions, @@ -101,7 +102,10 @@ const getAcorn = (config: InputOptions): acorn.Options => ({ const getAcornInjectPlugins = ( config: InputOptions -): NormalizedInputOptions['acornInjectPlugins'] => ensureArray(config.acornInjectPlugins); +): NormalizedInputOptions['acornInjectPlugins'] => [ + importAssertions, + ...ensureArray(config.acornInjectPlugins) +]; const getCache = (config: InputOptions): NormalizedInputOptions['cache'] => (config.cache as unknown as RollupBuild)?.cache || config.cache; diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index b9ad0b5b3d2..6451375503f 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -48,6 +48,7 @@ export function normalizeOutputOptions( esModule: config.esModule ?? 'if-default-prop', exports: getExports(config, unsetOptions), extend: config.extend || false, + externalImportAssertions: config.externalImportAssertions ?? true, externalLiveBindings: config.externalLiveBindings ?? true, file, footer: getAddon(config, 'footer'), diff --git a/src/utils/parseAssertions.ts b/src/utils/parseAssertions.ts new file mode 100644 index 00000000000..4a0f535477a --- /dev/null +++ b/src/utils/parseAssertions.ts @@ -0,0 +1,61 @@ +import Identifier from '../ast/nodes/Identifier'; +import ImportAttribute from '../ast/nodes/ImportAttribute'; +import ImportExpression from '../ast/nodes/ImportExpression'; +import Literal, { LiteralValue } from '../ast/nodes/Literal'; +import ObjectExpression from '../ast/nodes/ObjectExpression'; +import Property from '../ast/nodes/Property'; +import SpreadElement from '../ast/nodes/SpreadElement'; +import { EMPTY_OBJECT } from './blank'; + +export function getAssertionsFromImportExpression(node: ImportExpression): Record { + const assertProperty = node.arguments?.[0]?.properties.find( + (property): property is Property => getPropertyKey(property) === 'assert' + )?.value; + if (!assertProperty) { + return EMPTY_OBJECT; + } + const assertFields = (assertProperty as ObjectExpression).properties + .map(property => { + const key = getPropertyKey(property); + if ( + typeof key === 'string' && + typeof ((property as Property).value as Literal).value === 'string' + ) { + return [key, ((property as Property).value as Literal).value] as [string, string]; + } + return null; + }) + .filter((property): property is [string, string] => !!property); + if (assertFields.length > 0) { + return Object.fromEntries(assertFields); + } + return EMPTY_OBJECT; +} + +const getPropertyKey = ( + property: Property | SpreadElement | ImportAttribute +): LiteralValue | undefined => { + const key = (property as Property | ImportAttribute).key; + return key && ((key as Identifier).name || (key as Literal).value); +}; + +export function getAssertionsFromImportExportDeclaration( + assertions: ImportAttribute[] | undefined +) { + return assertions?.length + ? Object.fromEntries( + assertions.map(assertion => [getPropertyKey(assertion), assertion.value.value]) + ) + : EMPTY_OBJECT; +} + +export function doAssertionsDiffer( + assertionA: Record, + assertionB: Record +): boolean { + const keysA = Object.keys(assertionA); + return ( + keysA.length !== Object.keys(assertionB).length || + keysA.some(key => assertionA[key] !== assertionB[key]) + ); +} diff --git a/src/utils/resolveId.ts b/src/utils/resolveId.ts index d2848d13acd..b0edba5e68a 100644 --- a/src/utils/resolveId.ts +++ b/src/utils/resolveId.ts @@ -1,4 +1,5 @@ -import type { CustomPluginOptions, Plugin, ResolvedId, ResolveIdResult } from '../rollup/types'; +import { ModuleLoaderResolveId } from '../ModuleLoader'; +import type { CustomPluginOptions, Plugin, ResolveIdResult } from '../rollup/types'; import type { PluginDriver } from './PluginDriver'; import { promises as fs } from './fs'; import { basename, dirname, isAbsolute, resolve } from './path'; @@ -9,16 +10,11 @@ export async function resolveId( importer: string | undefined, preserveSymlinks: boolean, pluginDriver: PluginDriver, - moduleLoaderResolveId: ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null - ) => Promise, + moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, - isEntry: boolean + isEntry: boolean, + assertions: Record ): Promise { const pluginResult = await resolveIdViaPlugins( source, @@ -27,7 +23,8 @@ export async function resolveId( moduleLoaderResolveId, skip, customOptions, - isEntry + isEntry, + assertions ); if (pluginResult != null) return pluginResult; diff --git a/src/utils/resolveIdViaPlugins.ts b/src/utils/resolveIdViaPlugins.ts index 02a4975a8ca..7f65cc2dabb 100644 --- a/src/utils/resolveIdViaPlugins.ts +++ b/src/utils/resolveIdViaPlugins.ts @@ -1,27 +1,17 @@ -import type { - CustomPluginOptions, - Plugin, - PluginContext, - ResolvedId, - ResolveIdResult -} from '../rollup/types'; +import { ModuleLoaderResolveId } from '../ModuleLoader'; +import type { CustomPluginOptions, Plugin, PluginContext, ResolveIdResult } from '../rollup/types'; import type { PluginDriver, ReplaceContext } from './PluginDriver'; -import { BLANK } from './blank'; +import { BLANK, EMPTY_OBJECT } from './blank'; export function resolveIdViaPlugins( source: string, importer: string | undefined, pluginDriver: PluginDriver, - moduleLoaderResolveId: ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null - ) => Promise, + moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, - isEntry: boolean + isEntry: boolean, + assertions: Record ): Promise { let skipped: Set | null = null; let replaceContext: ReplaceContext | null = null; @@ -34,12 +24,13 @@ export function resolveIdViaPlugins( } replaceContext = (pluginContext, plugin): PluginContext => ({ ...pluginContext, - resolve: (source, importer, { custom, isEntry, skipSelf } = BLANK) => { + resolve: (source, importer, { assertions, custom, isEntry, skipSelf } = BLANK) => { return moduleLoaderResolveId( source, importer, custom, isEntry, + assertions || EMPTY_OBJECT, skipSelf ? [...skip, { importer, plugin, source }] : skip ); } @@ -47,7 +38,7 @@ export function resolveIdViaPlugins( } return pluginDriver.hookFirst( 'resolveId', - [source, importer, { custom: customOptions, isEntry }], + [source, importer, { assertions, custom: customOptions, isEntry }], replaceContext, skipped ); diff --git a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js index 7d43493ea6d..939da5f5eaa 100644 --- a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js @@ -25,6 +25,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN))), { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -75,11 +77,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, @@ -96,6 +98,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -146,11 +150,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, diff --git a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js index 3c0a5fd5345..682022286ab 100644 --- a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js @@ -21,6 +21,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN))), { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -71,11 +73,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, @@ -92,6 +94,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -142,11 +146,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, diff --git a/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js b/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js index 6abadc7fdce..0682a960244 100644 --- a/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js @@ -34,6 +34,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN1))), { + id: ID_MAIN1, + assertions: {}, ast: { type: 'Program', start: 0, @@ -119,11 +121,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN1, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [ID_DEP], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB1, meta: {}, @@ -131,6 +133,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB1B, meta: {}, @@ -138,6 +141,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB2, meta: {}, @@ -154,6 +158,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN2))), { + id: ID_MAIN2, + assertions: {}, ast: { type: 'Program', start: 0, @@ -239,11 +245,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN2, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [ID_DEP], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB1, meta: {}, @@ -251,6 +257,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB1B, meta: {}, @@ -258,6 +265,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB3, meta: {}, @@ -274,6 +282,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -358,11 +368,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [ID_MAIN1, ID_MAIN2], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB1, meta: {}, @@ -370,6 +380,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB2, meta: {}, @@ -377,6 +388,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB3, meta: {}, diff --git a/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js b/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js index 0a1b56b3f85..5569a48cb99 100644 --- a/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js @@ -20,6 +20,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN))), { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -70,11 +72,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [ID_DEP], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, @@ -91,6 +93,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -141,11 +145,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [ID_MAIN], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, diff --git a/test/form/samples/import-assertions/assertion-shapes/_config.js b/test/form/samples/import-assertions/assertion-shapes/_config.js new file mode 100644 index 00000000000..3686c79e002 --- /dev/null +++ b/test/form/samples/import-assertions/assertion-shapes/_config.js @@ -0,0 +1,7 @@ +module.exports = { + description: 'handles special shapes of assertions', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: () => true + } +}; diff --git a/test/form/samples/import-assertions/assertion-shapes/_expected.js b/test/form/samples/import-assertions/assertion-shapes/_expected.js new file mode 100644 index 00000000000..b5d8cbaa42d --- /dev/null +++ b/test/form/samples/import-assertions/assertion-shapes/_expected.js @@ -0,0 +1,4 @@ +import('external-a', { assert: { type: 'json' } }); +import('external-b'); +import('external-c'); +import('external-d'); diff --git a/test/form/samples/import-assertions/assertion-shapes/main.js b/test/form/samples/import-assertions/assertion-shapes/main.js new file mode 100644 index 00000000000..a82cad812bc --- /dev/null +++ b/test/form/samples/import-assertions/assertion-shapes/main.js @@ -0,0 +1,4 @@ +import('external-a', { 'assert': { 'type': 'json', foo: 1, ...{} } }); +import('external-b', { assert: {} }); +import('external-c', { ...{} }); +import('external-d', {}); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js new file mode 100644 index 00000000000..e2571b057c7 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js @@ -0,0 +1,27 @@ +module.exports = { + description: 'keep import assertions for dynamic imports', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: id => { + if (id === 'unresolved') return null; + return true; + }, + plugins: [ + { + resolveDynamicImport(specifier, importer) { + if (typeof specifier === 'object') { + if (specifier.type === 'TemplateLiteral') { + return "'resolvedString'"; + } + if (specifier.type === 'BinaryExpression') { + return { id: 'resolved-id', external: true }; + } + } else if (specifier === 'external-resolved') { + return { id: 'resolved-different', external: true }; + } + return null; + } + } + ] + } +}; diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js new file mode 100644 index 00000000000..e9478c8fc48 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js @@ -0,0 +1,27 @@ +define(['require'], (function (require) { 'use strict'; + + function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); + } + + new Promise(function (resolve, reject) { require(['external'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); + (function (t) { return new Promise(function (resolve, reject) { require([t], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); })(globalThis.unknown); + (function (t) { return new Promise(function (resolve, reject) { require([t], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); })('resolvedString'); + new Promise(function (resolve, reject) { require(['resolved-id'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); + new Promise(function (resolve, reject) { require(['resolved-different'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); + new Promise(function (resolve, reject) { require(['unresolved'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); + +})); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js new file mode 100644 index 00000000000..e1cd2b6d0da --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js @@ -0,0 +1,8 @@ +'use strict'; + +import('external'); +import(globalThis.unknown); +import('resolvedString'); +import('resolved-id'); +import('resolved-different'); +import('unresolved'); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js new file mode 100644 index 00000000000..87164b7871f --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js @@ -0,0 +1,6 @@ +import('external', { assert: { type: 'special' } }); +import(globalThis.unknown, { assert: { type: 'special' } }); +import('resolvedString', { assert: { type: 'special' } }); +import('resolved-id', { assert: { type: 'special' } }); +import('resolved-different', { assert: { type: 'special' } }); +import('unresolved', { assert: { type: 'special' } }); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js new file mode 100644 index 00000000000..0da8002824f --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js @@ -0,0 +1,11 @@ +(function () { + 'use strict'; + + import('external'); + import(globalThis.unknown); + import('resolvedString'); + import('resolved-id'); + import('resolved-different'); + import('unresolved'); + +})(); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js new file mode 100644 index 00000000000..44c758c2912 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js @@ -0,0 +1,15 @@ +System.register([], (function (exports, module) { + 'use strict'; + return { + execute: (function () { + + module.import('external'); + module.import(globalThis.unknown); + module.import('resolvedString'); + module.import('resolved-id'); + module.import('resolved-different'); + module.import('unresolved'); + + }) + }; +})); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js new file mode 100644 index 00000000000..cd9d7642deb --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js @@ -0,0 +1,13 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +})((function () { 'use strict'; + + import('external'); + import(globalThis.unknown); + import('resolvedString'); + import('resolved-id'); + import('resolved-different'); + import('unresolved'); + +})); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/main.js b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js new file mode 100644 index 00000000000..38bfed638c0 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js @@ -0,0 +1,6 @@ +import('external', { assert: { type: 'special' } }); +import(globalThis.unknown, { assert: { type: 'special' } }); +import(`external-${globalThis.unknown}`, { assert: { type: 'special' } }); +import('external' + globalThis.unknown, { assert: { type: 'special' } }); +import('external-resolved', { assert: { type: 'special' } }); +import('unresolved', { assert: { type: 'special' } }); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_config.js b/test/form/samples/import-assertions/keeps-static-assertions/_config.js new file mode 100644 index 00000000000..7940684a958 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'keeps any import assertions on input', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: id => { + if (id === 'unresolved') return null; + return true; + }, + output: { name: 'bundle' } + } +}; diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/amd.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/amd.js new file mode 100644 index 00000000000..418dca68f57 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/amd.js @@ -0,0 +1,35 @@ +define(['exports', 'a', 'b', 'c', 'd', 'unresolved'], (function (exports, a, b, c, d$1, unresolved) { 'use strict'; + + function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); + } + + var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); + + console.log(a.a, b__namespace, d); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); + }); + +})); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/cjs.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/cjs.js new file mode 100644 index 00000000000..cb59fa6ed49 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/cjs.js @@ -0,0 +1,39 @@ +'use strict'; + +var a = require('a'); +var b = require('b'); +var c = require('c'); +var d$1 = require('d'); +require('unresolved'); + +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); + +console.log(a.a, b__namespace, d); + +Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } +}); +Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); +}); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/es.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/es.js new file mode 100644 index 00000000000..ba6c7ba4532 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/es.js @@ -0,0 +1,7 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'unresolved' assert { type: 'e' }; + +console.log(a, b, d); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/iife.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/iife.js new file mode 100644 index 00000000000..8398b9e18ab --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/iife.js @@ -0,0 +1,38 @@ +var bundle = (function (exports, a, b, c, d$1) { + 'use strict'; + + function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); + } + + var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); + + console.log(a.a, b__namespace, d); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); + }); + + return exports; + +})({}, a, b, c, d$1); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/system.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/system.js new file mode 100644 index 00000000000..e1e3a6ae431 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/system.js @@ -0,0 +1,28 @@ +System.register('bundle', ['a', 'b', 'c', 'd', 'unresolved'], (function (exports) { + 'use strict'; + var _starExcludes = { + default: 1, + c: 1 + }; + var a, b; + return { + setters: [function (module) { + a = module.a; + }, function (module) { + b = module; + }, function (module) { + exports('c', module.c); + }, function (module) { + var setter = {}; + for (var name in module) { + if (!_starExcludes[name]) setter[name] = module[name]; + } + exports(setter); + }, null], + execute: (function () { + + console.log(a, b, d); + + }) + }; +})); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/umd.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/umd.js new file mode 100644 index 00000000000..f30436f32f2 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/umd.js @@ -0,0 +1,39 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('a'), require('b'), require('c'), require('d'), require('unresolved')) : + typeof define === 'function' && define.amd ? define(['exports', 'a', 'b', 'c', 'd', 'unresolved'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {}, global.a, global.b, global.c, global.d$1)); +})(this, (function (exports, a, b, c, d$1) { 'use strict'; + + function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); + } + + var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); + + console.log(a.a, b__namespace, d); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); + }); + +})); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/main.js b/test/form/samples/import-assertions/keeps-static-assertions/main.js new file mode 100644 index 00000000000..02b154c863a --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/main.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'unresolved' assert { type: 'e' }; + +console.log(a, b, d); + + diff --git a/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_config.js b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_config.js new file mode 100644 index 00000000000..efbc46143d0 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_config.js @@ -0,0 +1,22 @@ +module.exports = { + description: 'allows plugins to read and write import assertions in resolveDynamicImport', + options: { + plugins: [ + { + resolveDynamicImport(specifier, importer, { assertions }) { + const resolutionOptions = { + external: true, + assertions: Object.fromEntries(Object.keys(assertions).map(key => [key, 'changed'])) + }; + if (typeof specifier === 'object') { + if (specifier.type === 'TemplateLiteral') { + return { id: 'resolved-a', ...resolutionOptions }; + } + return { id: 'resolved-b', ...resolutionOptions }; + } + return { id: specifier, ...resolutionOptions }; + } + } + ] + } +}; diff --git a/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_expected.js b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_expected.js new file mode 100644 index 00000000000..279b0c9a4f0 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_expected.js @@ -0,0 +1,4 @@ +import('a', { assert: { type: 'changed' } }); +import('resolved-b', { assert: { type: 'changed', extra: 'changed' } }); +import('b'); +import('resolved-a'); diff --git a/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/main.js b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/main.js new file mode 100644 index 00000000000..6d8a892a6d4 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/main.js @@ -0,0 +1,4 @@ +import('a', { assert: { type: 'special' } }); +import(globalThis.unknown, { assert: { type: 'special', extra: 'value' } }); +import('b'); +import(`external-${globalThis.unknown}`); diff --git a/test/form/samples/import-assertions/plugin-assertions-resolveid/_config.js b/test/form/samples/import-assertions/plugin-assertions-resolveid/_config.js new file mode 100644 index 00000000000..77c68a879e1 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolveid/_config.js @@ -0,0 +1,17 @@ +module.exports = { + description: 'allows plugins to read and write import assertions in resolveId', + options: { + output: { name: 'bundle' }, + plugins: [ + { + resolveId(source, importer, { assertions, isEntry }) { + return { + id: source, + external: !isEntry, + assertions: Object.fromEntries(Object.keys(assertions).map(key => [key, 'changed'])) + }; + } + } + ] + } +}; diff --git a/test/form/samples/import-assertions/plugin-assertions-resolveid/_expected.js b/test/form/samples/import-assertions/plugin-assertions-resolveid/_expected.js new file mode 100644 index 00000000000..200aa397375 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolveid/_expected.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'changed', extra: 'changed' }; +import * as b from 'b' assert { type: 'changed' }; +export { c } from 'c' assert { type: 'changed' }; +export * from 'd' assert { type: 'changed' }; +import 'e'; + +console.log(a, b, d); +import('f', { assert: { type: 'changed' } }); +import('g'); diff --git a/test/form/samples/import-assertions/plugin-assertions-resolveid/main.js b/test/form/samples/import-assertions/plugin-assertions-resolveid/main.js new file mode 100644 index 00000000000..7781ebfe7c6 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolveid/main.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'e'; + +console.log(a, b, d); +import('f', { assert: { type: 'f' } }); +import('g'); diff --git a/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js b/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js new file mode 100644 index 00000000000..b939795fd58 --- /dev/null +++ b/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js @@ -0,0 +1,28 @@ +module.exports = { + description: 'keep import assertions for dynamic imports', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: id => { + if (id === 'unresolved') return null; + return true; + }, + plugins: [ + { + resolveDynamicImport(specifier, importer) { + if (typeof specifier === 'object') { + if (specifier.type === 'TemplateLiteral') { + return "'resolvedString'"; + } + if (specifier.type === 'BinaryExpression') { + return { id: 'resolved-id', external: true }; + } + } else if (specifier === 'external-resolved') { + return { id: 'resolved-different', external: true }; + } + return null; + } + } + ], + output: { externalImportAssertions: false } + } +}; diff --git a/test/form/samples/import-assertions/removes-dynamic-assertions/_expected.js b/test/form/samples/import-assertions/removes-dynamic-assertions/_expected.js new file mode 100644 index 00000000000..2d417264620 --- /dev/null +++ b/test/form/samples/import-assertions/removes-dynamic-assertions/_expected.js @@ -0,0 +1,6 @@ +import('external'); +import(globalThis.unknown); +import('resolvedString'); +import('resolved-id'); +import('resolved-different'); +import('unresolved'); diff --git a/test/form/samples/import-assertions/removes-dynamic-assertions/main.js b/test/form/samples/import-assertions/removes-dynamic-assertions/main.js new file mode 100644 index 00000000000..38bfed638c0 --- /dev/null +++ b/test/form/samples/import-assertions/removes-dynamic-assertions/main.js @@ -0,0 +1,6 @@ +import('external', { assert: { type: 'special' } }); +import(globalThis.unknown, { assert: { type: 'special' } }); +import(`external-${globalThis.unknown}`, { assert: { type: 'special' } }); +import('external' + globalThis.unknown, { assert: { type: 'special' } }); +import('external-resolved', { assert: { type: 'special' } }); +import('unresolved', { assert: { type: 'special' } }); diff --git a/test/form/samples/import-assertions/removes-static-assertions/_config.js b/test/form/samples/import-assertions/removes-static-assertions/_config.js new file mode 100644 index 00000000000..30299daf04e --- /dev/null +++ b/test/form/samples/import-assertions/removes-static-assertions/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'keeps any import assertions on input', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: id => { + if (id === 'unresolved') return null; + return true; + }, + output: { name: 'bundle', externalImportAssertions: false } + } +}; diff --git a/test/form/samples/import-assertions/removes-static-assertions/_expected.js b/test/form/samples/import-assertions/removes-static-assertions/_expected.js new file mode 100644 index 00000000000..40f55f22388 --- /dev/null +++ b/test/form/samples/import-assertions/removes-static-assertions/_expected.js @@ -0,0 +1,7 @@ +import { a } from 'a'; +import * as b from 'b'; +export { c } from 'c'; +export * from 'd'; +import 'unresolved'; + +console.log(a, b, d); diff --git a/test/form/samples/import-assertions/removes-static-assertions/main.js b/test/form/samples/import-assertions/removes-static-assertions/main.js new file mode 100644 index 00000000000..02b154c863a --- /dev/null +++ b/test/form/samples/import-assertions/removes-static-assertions/main.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'unresolved' assert { type: 'e' }; + +console.log(a, b, d); + + diff --git a/test/function/samples/context-resolve/_config.js b/test/function/samples/context-resolve/_config.js index d2f132c360f..d41290cd853 100644 --- a/test/function/samples/context-resolve/_config.js +++ b/test/function/samples/context-resolve/_config.js @@ -6,6 +6,7 @@ const tests = [ source: './existing', expected: { id: path.join(__dirname, 'existing.js'), + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -24,6 +25,7 @@ const tests = [ source: './marked-directly-external-relative', expected: { id: path.join(__dirname, 'marked-directly-external-relative'), + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -34,6 +36,7 @@ const tests = [ source: './marked-external-relative', expected: { id: path.join(__dirname, 'marked-external-relative'), + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -44,6 +47,7 @@ const tests = [ source: 'marked-external-absolute', expected: { id: 'marked-external-absolute', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -54,6 +58,7 @@ const tests = [ source: 'resolved-name', expected: { id: 'resolved:resolved-name', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -64,6 +69,7 @@ const tests = [ source: 'resolved-false', expected: { id: 'resolved-false', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -74,6 +80,7 @@ const tests = [ source: 'resolved-object', expected: { id: 'resolved:resolved-object', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -84,6 +91,7 @@ const tests = [ source: 'resolved-object-non-external', expected: { id: 'resolved:resolved-object-non-external', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -94,6 +102,7 @@ const tests = [ source: 'resolved-object-external', expected: { id: 'resolved:resolved-object-external', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, diff --git a/test/function/samples/deprecated/manual-chunks-info/_config.js b/test/function/samples/deprecated/manual-chunks-info/_config.js index 44dcf747df5..c545b22c61c 100644 --- a/test/function/samples/deprecated/manual-chunks-info/_config.js +++ b/test/function/samples/deprecated/manual-chunks-info/_config.js @@ -20,6 +20,7 @@ module.exports = { { [getId('dynamic')]: { id: getId('dynamic'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -80,6 +81,7 @@ module.exports = { code: "export const promise = import('external');\nexport { default as internal } from './lib';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: true, id: 'external', meta: {}, @@ -95,6 +97,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -112,6 +115,7 @@ module.exports = { }, [getId('lib')]: { id: getId('lib'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -145,6 +149,7 @@ module.exports = { }, [getId('main')]: { id: getId('main'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -227,6 +232,7 @@ module.exports = { code: "export const promise = import('./dynamic');\nexport { default as value } from './lib';\nexport { external } from 'external';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: false, id: getId('dynamic'), meta: {}, @@ -242,6 +248,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -249,6 +256,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: true, id: 'external', meta: {}, @@ -266,6 +274,7 @@ module.exports = { }, external: { id: 'external', + assertions: {}, ast: null, code: null, dynamicallyImportedIdResolutions: [], diff --git a/test/function/samples/import-assertions/plugin-assertions-this-resolve/_config.js b/test/function/samples/import-assertions/plugin-assertions-this-resolve/_config.js new file mode 100644 index 00000000000..945e6f17f9f --- /dev/null +++ b/test/function/samples/import-assertions/plugin-assertions-this-resolve/_config.js @@ -0,0 +1,48 @@ +const assert = require('assert'); + +module.exports = { + description: 'allows plugins to provide assertions for this.resolve', + options: { + plugins: [ + { + name: 'first', + async resolveId(source, importer, { assertions }) { + assert.deepStrictEqual( + await this.resolve('external', undefined, { + skipSelf: true, + assertions: { a: 'c', b: 'd' } + }), + { + assertions: { a: 'changed', b: 'changed' }, + external: true, + id: 'external', + meta: {}, + moduleSideEffects: true, + syntheticNamedExports: false + } + ); + } + }, + { + name: 'second', + async resolveId(source, importer, { assertions }) { + if (source === 'external') { + return this.resolve(source, importer, { assertions, skipSelf: true }); + } + } + }, + { + name: 'third', + async resolveId(source, importer, { assertions }) { + if (source === 'external') { + return { + id: source, + external: true, + assertions: Object.fromEntries(Object.keys(assertions).map(key => [key, 'changed'])) + }; + } + } + } + ] + } +}; diff --git a/test/function/samples/import-assertions/plugin-assertions-this-resolve/main.js b/test/function/samples/import-assertions/plugin-assertions-this-resolve/main.js new file mode 100644 index 00000000000..cc1d88a24fa --- /dev/null +++ b/test/function/samples/import-assertions/plugin-assertions-this-resolve/main.js @@ -0,0 +1 @@ +assert.ok(true); diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/_config.js b/test/function/samples/import-assertions/warn-assertion-conflicts/_config.js new file mode 100644 index 00000000000..c51bf7d88a7 --- /dev/null +++ b/test/function/samples/import-assertions/warn-assertion-conflicts/_config.js @@ -0,0 +1,64 @@ +const path = require('path'); +const ID_MAIN = path.join(__dirname, 'main.js'); + +module.exports = { + description: 'warns for conflicting import assertions', + options: { + external: id => id.startsWith('external') + }, + warnings: [ + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + frame: ` +1: import './other.js'; +2: import 'external' assert { type: 'foo' }; +3: import 'external' assert { type: 'bar' }; + ^ +4: import 'external'; +5: import('external', { assert: { type: 'baz' } });`, + id: ID_MAIN, + loc: { + column: 0, + file: ID_MAIN, + line: 3 + }, + message: + 'Module "main.js" tried to import "external" with "type": "bar" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.', + pos: 63 + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + frame: ` +2: import 'external' assert { type: 'foo' }; +3: import 'external' assert { type: 'bar' }; +4: import 'external'; + ^ +5: import('external', { assert: { type: 'baz' } }); +6: import './dep.js' assert { type: 'foo' };`, + id: ID_MAIN, + loc: { + column: 0, + file: ID_MAIN, + line: 4 + }, + message: + 'Module "main.js" tried to import "external" with no assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.', + pos: 105 + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + message: + 'Module "main.js" tried to import "external" with "type": "baz" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.' + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + message: + 'Module "other.js" tried to import "external" with "type": "quuz" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.' + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + message: + 'Module "other.js" tried to import "dep.js" with "type": "bar" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.' + } + ] +}; diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/dep.js b/test/function/samples/import-assertions/warn-assertion-conflicts/dep.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/main.js b/test/function/samples/import-assertions/warn-assertion-conflicts/main.js new file mode 100644 index 00000000000..50f9cdb8b2b --- /dev/null +++ b/test/function/samples/import-assertions/warn-assertion-conflicts/main.js @@ -0,0 +1,6 @@ +import './other.js'; +import 'external' assert { type: 'foo' }; +import 'external' assert { type: 'bar' }; +import 'external'; +import('external', { assert: { type: 'baz' } }); +import './dep.js' assert { type: 'foo' }; diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/other.js b/test/function/samples/import-assertions/warn-assertion-conflicts/other.js new file mode 100644 index 00000000000..779091a9164 --- /dev/null +++ b/test/function/samples/import-assertions/warn-assertion-conflicts/other.js @@ -0,0 +1,2 @@ +import 'external' assert { type: 'quuz' }; +import './dep.js' assert { type: 'bar' }; diff --git a/test/function/samples/manual-chunks-info/_config.js b/test/function/samples/manual-chunks-info/_config.js index 9be80a1174b..f1d37e262ef 100644 --- a/test/function/samples/manual-chunks-info/_config.js +++ b/test/function/samples/manual-chunks-info/_config.js @@ -21,6 +21,7 @@ module.exports = { { [getId('dynamic')]: { id: getId('dynamic'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -81,6 +82,7 @@ module.exports = { code: "export const promise = import('external');\nexport { default as internal } from './lib';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: true, id: 'external', meta: {}, @@ -96,6 +98,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -113,6 +116,7 @@ module.exports = { }, [getId('lib')]: { id: getId('lib'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -146,6 +150,7 @@ module.exports = { }, [getId('main')]: { id: getId('main'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -228,6 +233,7 @@ module.exports = { code: "export const promise = import('./dynamic');\nexport { default as value } from './lib';\nexport { external } from 'external';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: false, id: getId('dynamic'), meta: {}, @@ -243,6 +249,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -250,6 +257,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: true, id: 'external', meta: {}, @@ -267,6 +275,7 @@ module.exports = { }, external: { id: 'external', + assertions: {}, ast: null, code: null, dynamicallyImportedIdResolutions: [], diff --git a/test/function/samples/module-parsed-hook/_config.js b/test/function/samples/module-parsed-hook/_config.js index 507e5d6d180..f2e8b02c49e 100644 --- a/test/function/samples/module-parsed-hook/_config.js +++ b/test/function/samples/module-parsed-hook/_config.js @@ -17,6 +17,8 @@ module.exports = { buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(parsedModules)), [ { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -53,11 +55,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_DEP, meta: {}, @@ -74,6 +76,8 @@ module.exports = { syntheticNamedExports: false }, { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -110,7 +114,6 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], diff --git a/test/function/samples/options-hook/_config.js b/test/function/samples/options-hook/_config.js index acf1441a057..7b24911685f 100644 --- a/test/function/samples/options-hook/_config.js +++ b/test/function/samples/options-hook/_config.js @@ -15,7 +15,7 @@ module.exports = { preserveParens: false, sourceType: 'module' }, - acornInjectPlugins: [], + acornInjectPlugins: [null], context: 'undefined', experimentalCacheExpiry: 10, input: ['used'], diff --git a/test/function/samples/output-options-hook/_config.js b/test/function/samples/output-options-hook/_config.js index ab64f6f952a..3c98cf8c830 100644 --- a/test/function/samples/output-options-hook/_config.js +++ b/test/function/samples/output-options-hook/_config.js @@ -29,6 +29,7 @@ module.exports = { esModule: 'if-default-prop', exports: 'auto', extend: false, + externalImportAssertions: true, externalLiveBindings: true, format: 'cjs', freeze: true, diff --git a/test/function/samples/plugin-module-information/_config.js b/test/function/samples/plugin-module-information/_config.js index 2312228e0db..59641559051 100644 --- a/test/function/samples/plugin-module-information/_config.js +++ b/test/function/samples/plugin-module-information/_config.js @@ -16,6 +16,7 @@ module.exports = { plugins: { load(id) { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(id))), { + assertions: {}, ast: null, code: null, dynamicImporters: [], @@ -47,6 +48,8 @@ module.exports = { ), { [ID_FOO]: { + id: ID_FOO, + assertions: {}, ast: { type: 'Program', start: 0, @@ -114,11 +117,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_FOO, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: true, id: ID_PATH, meta: {}, @@ -135,6 +138,8 @@ module.exports = { syntheticNamedExports: false }, [ID_MAIN]: { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -261,6 +266,7 @@ module.exports = { code: "export { foo } from './foo.js';\nexport const nested = import('./nested/nested');\nexport const path = import('path');\nexport const pathAgain = import(thePath);\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: false, id: ID_NESTED, meta: {}, @@ -268,6 +274,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: true, id: ID_PATH, meta: {}, @@ -279,11 +286,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_FOO, meta: {}, @@ -300,6 +307,8 @@ module.exports = { syntheticNamedExports: false }, [ID_NESTED]: { + id: ID_NESTED, + assertions: {}, ast: { type: 'Program', start: 0, @@ -370,11 +379,11 @@ module.exports = { dynamicImporters: [ID_MAIN], hasDefaultExport: false, moduleSideEffects: true, - id: ID_NESTED, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_FOO, meta: {}, @@ -391,6 +400,8 @@ module.exports = { syntheticNamedExports: false }, [ID_PATH]: { + id: ID_PATH, + assertions: {}, ast: null, code: null, dynamicallyImportedIdResolutions: [], @@ -398,7 +409,6 @@ module.exports = { dynamicImporters: [ID_MAIN], hasDefaultExport: null, moduleSideEffects: true, - id: ID_PATH, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], diff --git a/test/function/samples/preload-module/_config.js b/test/function/samples/preload-module/_config.js index cf38cada7af..5ab79b4ace3 100644 --- a/test/function/samples/preload-module/_config.js +++ b/test/function/samples/preload-module/_config.js @@ -31,13 +31,14 @@ module.exports = { meta: { testPlugin: 'first' } }); assert.deepStrictEqual(moduleInfo, { + id: ID_MAIN, + assertions: {}, code: "import './dep';\nassert.ok(true);\n", dynamicImporters: [], hasDefaultExport: false, dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], @@ -72,13 +73,14 @@ module.exports = { meta: { testPlugin: 'second' } }); assert.deepStrictEqual(moduleInfo, { + id: ID_DEP, + assertions: {}, code: 'assert.ok(true);\n', dynamicImporters: [], hasDefaultExport: false, dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], diff --git a/test/function/samples/resolve-relative-external-id/_config.js b/test/function/samples/resolve-relative-external-id/_config.js index f6a6d451524..bdff9370e82 100644 --- a/test/function/samples/resolve-relative-external-id/_config.js +++ b/test/function/samples/resolve-relative-external-id/_config.js @@ -8,6 +8,7 @@ module.exports = { plugins: { async buildStart() { assert.deepStrictEqual(await this.resolve('./external.js'), { + assertions: {}, external: true, id: path.join(__dirname, 'external.js'), meta: {}, @@ -17,6 +18,7 @@ module.exports = { assert.deepStrictEqual( await this.resolve('./external.js', path.join(__dirname, 'nested', 'some-file.js')), { + assertions: {}, external: true, id: path.join(__dirname, 'nested', 'external.js'), meta: {}, diff --git a/test/incremental/index.js b/test/incremental/index.js index 9ae4f56fcae..045063e7dd8 100644 --- a/test/incremental/index.js +++ b/test/incremental/index.js @@ -267,6 +267,7 @@ describe('incremental', () => { assert.deepEqual(bundle.cache.modules[1].resolvedIds, { foo: { id: 'foo', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -274,6 +275,7 @@ describe('incremental', () => { }, external: { id: 'external', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -360,6 +362,7 @@ describe('incremental', () => { assert.deepStrictEqual(resolvedSources, { __proto__: null, bar: { + assertions: {}, external: false, id: 'bar', meta: {}, @@ -378,6 +381,7 @@ describe('incremental', () => { assert.deepStrictEqual(resolvedSources, { __proto__: null, foo: { + assertions: {}, external: false, id: 'foo', meta: {}, diff --git a/test/misc/optionList.js b/test/misc/optionList.js index 88dfdea6e97..9cf18ae43ca 100644 --- a/test/misc/optionList.js +++ b/test/misc/optionList.js @@ -1,6 +1,6 @@ exports.input = 'acorn, acornInjectPlugins, cache, context, experimentalCacheExpiry, external, inlineDynamicImports, input, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, moduleContext, onwarn, perf, plugins, preserveEntrySignatures, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch'; exports.flags = - 'acorn, acornInjectPlugins, amd, assetFileNames, banner, bundleConfigAsCjs, c, cache, chunkFileNames, compact, config, configPlugin, context, d, dir, dynamicImportFunction, dynamicImportInCjs, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, generatedCode, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, sanitizeFileName, shimMissingExports, silent, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, validate, w, waitForBundleInput, watch'; + 'acorn, acornInjectPlugins, amd, assetFileNames, banner, bundleConfigAsCjs, c, cache, chunkFileNames, compact, config, configPlugin, context, d, dir, dynamicImportFunction, dynamicImportInCjs, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalImportAssertions, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, generatedCode, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, sanitizeFileName, shimMissingExports, silent, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, validate, w, waitForBundleInput, watch'; exports.output = - 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, dynamicImportInCjs, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, generatedCode, globals, hoistTransitiveImports, indent, inlineDynamicImports, interop, intro, manualChunks, minifyInternalExports, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, preserveModules, preserveModulesRoot, sanitizeFileName, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict, systemNullSetters, validate'; + 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, dynamicImportInCjs, entryFileNames, esModule, exports, extend, externalImportAssertions, externalLiveBindings, file, footer, format, freeze, generatedCode, globals, hoistTransitiveImports, indent, inlineDynamicImports, interop, intro, manualChunks, minifyInternalExports, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, preserveModules, preserveModulesRoot, sanitizeFileName, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict, systemNullSetters, validate'; diff --git a/typings/declarations.d.ts b/typings/declarations.d.ts index 51f94dc1e2c..8a46f5bdbfe 100644 --- a/typings/declarations.d.ts +++ b/typings/declarations.d.ts @@ -11,6 +11,10 @@ declare module 'rollup-plugin-string' { export const string: PluginImpl; } +declare module 'acorn-import-assertions' { + export const importAssertions: () => unknown; +} + declare module 'is-reference' { import type * as estree from 'estree';