[v3.0] Basic support for import assertions (#4646)

* Add acorn support for import assertions and extend AST

* Ignore pre-existing assertions on input files

* Naive support for JSON assertions in output

* Inject arbitrary import assertions

* Allows to disable import assertions altogether via `false`

* Support shorthand syntax for type assertions

* Keep assertions on fully dynamic imports

* Add documentation

* Add assertions to types

* Keep original assertions

* Make option a boolean

* Some extractions

* Allow plugins to add and change assertions

* Allow to pass assertions in this.resolve

* Warn for inconsistent import assertions

* Add new documentation

* Improve coverage
pull/4661/head
Lukas Taegert-Atkinson 1 year ago
parent 7c6bf53974
commit 33d8c4030d

@ -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

@ -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

@ -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<ResolvedId | null>,
moduleLoaderResolveId: ModuleLoaderResolveId,
skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null,
customOptions: CustomPluginOptions | undefined,
isEntry: boolean
isEntry: boolean,
assertions: Record<string, string>
): Promise<ResolveIdResult> {
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');

@ -36,6 +36,7 @@ Basic options:
--exports <mode> 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 <text> 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

@ -366,6 +366,7 @@ Many options have command line equivalents. In those cases, any arguments passed
--no-esModule Do not add __esModule property
--exports <mode> 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 <text> 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

@ -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}`<br> **Kind:** `async, first`<br> **Previous Hook:** [`resolveId`](guide/en/#resolveid) or [`resolveDynamicImport`](guide/en/#resolvedynamicimport) where the loaded id was resolved. Additionally, this hook can be triggered at any time from plugin hooks by calling [`this.load`](guide/en/#thisload) to preload the module corresponding to an id.<br> **Next Hook:** [`transform`](guide/en/#transform) to transform the loaded file 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}`<br> **Kind:** `async, first`<br> **Previous Hook:** [`resolveId`](guide/en/#resolveid) or [`resolveDynamicImport`](guide/en/#resolvedynamicimport) where the loaded id was resolved. Additionally, this hook can be triggered at any time from plugin hooks by calling [`this.load`](guide/en/#thisload) to preload the module corresponding to an id.<br> **Next Hook:** [`transform`](guide/en/#transform) to transform the loaded file 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}`<br> **Kind:** `async, first`<br> **Previous Hook:** [`moduleParsed`](guide/en/#moduleparsed) for the importing file.<br> **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}`<br> **Kind:** `async, first`<br> **Previous Hook:** [`moduleParsed`](guide/en/#moduleparsed) for the importing file.<br> **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}`<br> **Kind:** `async, first`<br> **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.<br> **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}`<br> **Kind:** `async, first`<br> **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.<br> **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`<br> **Kind:** `async, first`<br> **Previous Hook:** [`load`](guide/en/#load) where the cached file was loaded to compare its code with the cached version.<br> **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`<br> **Kind:** `async, first`<br> **Previous Hook:** [`load`](guide/en/#load) where the cached file was loaded to compare its code with the cached version.<br> **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}`<br> **Kind:** `async, sequential`<br> **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.<br> **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}`<br> **Kind:** `async, sequential`<br> **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.<br> **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<ModuleInfo>`
**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<ModuleInfo>`
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`

@ -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.

@ -532,6 +532,12 @@ Type: `boolean`<br> CLI: `--extend`/`--no-extend`<br> 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`<br> CLI: `--externalImportAssertions`/`--no-externalImportAssertions`<br> 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 }`<br> CLI: `--generatedCode <preset>`<br> Default: `"es5"`
@ -1800,7 +1806,7 @@ export default {
**treeshake.propertyReadSideEffects**<br> Type: `boolean | 'always'`<br> CLI: `--treeshake.propertyReadSideEffects`/`--no-treeshake.propertyReadSideEffects`<br> 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.

21
package-lock.json generated

@ -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",

@ -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",

@ -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
);
}
}

@ -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<string, string> | 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;
}

@ -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<string, string>
) {
this.suggestedVariableName = makeLegal(id.split(/[\\/]/).pop()!);
const { importers, dynamicImporters } = this;
const info: ModuleInfo = (this.info = {
assertions,
ast: null,
code: null,
dynamicallyImportedIdResolutions: EMPTY_ARRAY,

@ -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<Variable, Set<Module>>();
declare sourcemapChain: DecodedSourceMapOrMissing[];
readonly sources = new Set<string>();
readonly sourcesWithAssertions = new Map<string, Record<string, string>>();
declare transformFiles?: EmittedFile[];
private allExportNames: Set<string> | 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<string, string>
) {
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,

@ -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<string, string>,
skip?: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null
) => Promise<ResolvedId | null>;
type NormalizedResolveIdWithoutDefaults = Partial<PartialNull<ModuleOptions>> & {
external?: boolean | 'absolute';
id: string;
@ -174,7 +185,7 @@ export class ModuleLoader {
resolvedId: { id: string; resolveDependencies?: boolean } & Partial<PartialNull<ModuleOptions>>
): Promise<ModuleInfo> {
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<ResolvedId | null> => {
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<Module> {
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<Module | ExternalModule> {
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<string, string>
): 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<string, string>
): 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<string, string>
): Promise<ResolvedId | string | null> {
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
);
}
}

@ -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']
};

@ -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<string>;

@ -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<string> | null;

@ -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<string>;
declare type: NodeType.tImportAttribute;
declare value: Literal<string>;
}

@ -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<string>;
declare specifiers: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[];
declare type: NodeType.tImportDeclaration;
// Do not bind specifiers
// Do not bind specifiers or assertions
bind(): void {}
hasEffects(): boolean {

@ -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<ChildScope, Set<string>>,
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(

@ -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';

@ -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,

@ -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();
}
}

@ -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}`
);
}
}

@ -83,6 +83,7 @@ type PartialNull<T> = {
};
interface ModuleOptions {
assertions: Record<string, string>;
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<string, string>;
custom?: CustomPluginOptions;
isEntry?: boolean;
skipSelf?: boolean;
}
) => Promise<ResolvedId | null>;
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<string, string>; 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<string, string> }
) => 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;

@ -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
);
},

@ -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<string, string>,
newAssertions: Record<string, string>,
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, string>): 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,

@ -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<keyof InputOptions> = {
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<keyof OutputOptions> = {
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'),

@ -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;

@ -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'),

@ -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<string, string> {
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<string, string>,
assertionB: Record<string, string>
): boolean {
const keysA = Object.keys(assertionA);
return (
keysA.length !== Object.keys(assertionB).length ||
keysA.some(key => assertionA[key] !== assertionB[key])
);
}

@ -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<ResolvedId | null>,
moduleLoaderResolveId: ModuleLoaderResolveId,
skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null,
customOptions: CustomPluginOptions | undefined,
isEntry: boolean
isEntry: boolean,
assertions: Record<string, string>
): Promise<ResolveIdResult> {
const pluginResult = await resolveIdViaPlugins(
source,
@ -27,7 +23,8 @@ export async function resolveId(
moduleLoaderResolveId,
skip,
customOptions,
isEntry
isEntry,
assertions
);
if (pluginResult != null) return pluginResult;

@ -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<ResolvedId | null>,
moduleLoaderResolveId: ModuleLoaderResolveId,
skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null,
customOptions: CustomPluginOptions | undefined,
isEntry: boolean
isEntry: boolean,
assertions: Record<string, string>
): Promise<ResolveIdResult> {
let skipped: Set<Plugin> | 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
);

@ -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: {},

@ -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: {},

@ -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: {},

@ -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: {},

@ -0,0 +1,7 @@
module.exports = {
description: 'handles special shapes of assertions',
expectedWarnings: 'UNRESOLVED_IMPORT',
options: {
external: () => true
}
};

@ -0,0 +1,4 @@
import('external-a', { assert: { type: 'json' } });
import('external-b');
import('external-c');
import('external-d');

@ -0,0 +1,4 @@
import('external-a', { 'assert': { 'type': 'json', foo: 1, ...{} } });
import('external-b', { assert: {} });
import('external-c', { ...{} });
import('external-d', {});

@ -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;
}
}
]
}
};

@ -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); });
}));

@ -0,0 +1,8 @@
'use strict';
import('external');
import(globalThis.unknown);
import('resolvedString');
import('resolved-id');
import('resolved-different');
import('unresolved');

@ -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' } });

@ -0,0 +1,11 @@
(function () {
'use strict';
import('external');
import(globalThis.unknown);
import('resolvedString');
import('resolved-id');
import('resolved-different');
import('unresolved');
})();

@ -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');
})
};
}));

@ -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');
}));

@ -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' } });

@ -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' }
}
};

@ -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]; }
});
});
}));

@ -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]; }
});
});

@ -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);

@ -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);

@ -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);
})
};
}));

@ -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]; }
});
});
}));

@ -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);

@ -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 };
}
}
]
}
};

@ -0,0 +1,4 @@
import('a', { assert: { type: 'changed' } });
import('resolved-b', { assert: { type: 'changed', extra: 'changed' } });
import('b');
import('resolved-a');

@ -0,0 +1,4 @@
import('a', { assert: { type: 'special' } });
import(globalThis.unknown, { assert: { type: 'special', extra: 'value' } });
import('b');
import(`external-${globalThis.unknown}`);

@ -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']))
};
}
}
]
}
};

@ -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');

@ -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');

@ -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 }
}
};

@ -0,0 +1,6 @@
import('external');
import(globalThis.unknown);
import('resolvedString');
import('resolved-id');
import('resolved-different');
import('unresolved');

@ -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' } });

@ -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 }
}
};

@ -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);

@ -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);

@ -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,

@ -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: [],

@ -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']))
};
}
}
}
]
}
};

@ -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.'
}
]
};

@ -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' };

@ -0,0 +1,2 @@
import 'external' assert { type: 'quuz' };
import './dep.js' assert { type: 'bar' };

@ -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: [],

@ -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: [],

@ -15,7 +15,7 @@ module.exports = {
preserveParens: false,
sourceType: 'module'
},
acornInjectPlugins: [],
acornInjectPlugins: [null],
context: 'undefined',
experimentalCacheExpiry: 10,
input: ['used'],

@ -29,6 +29,7 @@ module.exports = {
esModule: 'if-default-prop',
exports: 'auto',
extend: false,
externalImportAssertions: true,
externalLiveBindings: true,
format: 'cjs',
freeze: true,

@ -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: [],

@ -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: [],

@ -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: {},

@ -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: {},

@ -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';

@ -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';

Loading…
Cancel
Save