Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 17 commits into from Oct 10, 2022

Conversation

lukastaegert
Copy link
Member

@lukastaegert lukastaegert commented Oct 3, 2022

BREAKING CHANGE: By default, this will add import assertions to all external .json file imports in the output.

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers:

Description

This PR adds basic support for import assertions to Rollup.

  • In input files, arbitrary import assertions are allowed. However, there will be a non-blocking warning if the same module is imported with conflicting assertions.
  • In ESM output by default for external modules, all assertions from the input are preserved. In case of conflicting assertions, the import assertions of the first import are used.
  • While there is also a warning if there are conflicting import assertions for bundled modules, they do not have any effect on the generated output. They can however be inspected by plugins via this.getModuleInfo.
  • Furthermore, there is a new output.externalImportAssertions option that allows to disabled assertions in the output.
  • For plugins, assertions are now passed in as a parameter to resolveId and resolveDynamicImport. If plugins do not return a value for assertions in their resolveId hook, the assertions from the actual import are used, otherwise they can override assertions at this point. For symmetry with other module options, they can also be changed in the load and transform hooks. In this.resolve, you can now also pass assertions that will be forwarded to resolveId.

output.externalImportAssertions

Type: boolean
CLI: --externalImportAssertions/--no-externalImportAssertions
Default: true

Whether to add import assertions to external imports in the output if the output format is es. By default, assertions are taken from the input files, but plugins can add or remove assertions later. E.g. import "foo" assert {type: "json"} will cause the same import to appear in the output unless the option is set to false. Note that all imports of a module need to have consistent assertions, otherwise a warning is emitted.

resolveId

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}

assertions tells you which import assertions were present in the import. I.e. import "foo" assert {type: "json"} will pass along assertions: {type: "json"}.

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.

resolveDynamicImport

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}

assertions tells you which import assertions were present in the import. I.e. import("foo", {assert: {type: "json"}}) will pass along assertions: {type: "json"}.

load

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}

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.

shouldTransformCachedModule

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

transform

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}

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.

this.getModuleInfo

Type: (moduleId: string) => (ModuleInfo | null)

Returns additional information about the module in question in the form

type ModuleInfo = {
  id: string; // the id of the module, for convenience
  code: string | null; // the source code of the module, `null` if external or not yet available
  ast: ESTree.Program; // the parsed abstract syntax tree if available
  hasDefaultExport: boolean | null; // is there a default export, `null` if external or not yet available
  isEntry: boolean; // is this a user- or plugin-defined entry point
  isExternal: boolean; // for external modules that are referenced but not included in the graph
  isIncluded: boolean | null; // is the module included after tree-shaking, `null` if external or not yet available
  importedIds: string[]; // the module ids statically imported by this module
  importedIdResolutions: ResolvedId[]; // how statically imported ids were resolved, for use with this.load
  importers: string[]; // the ids of all modules that statically import this module
  dynamicallyImportedIds: string[]; // the module ids imported by this module via dynamic import()
  dynamicallyImportedIdResolutions: ResolvedId[]; // how ids imported via dynamic import() were resolved
  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
};

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

this.resolve

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>

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 hooks handling this call and may ultimately become part of the returned object.

this.load

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>

@lukastaegert lukastaegert changed the base branch from master to release-3.0.0 October 3, 2022 19:27
@github-actions
Copy link

github-actions bot commented Oct 3, 2022

Thank you for your contribution! ❤️

You can try out this pull request locally by installing Rollup via

npm install rollup/rollup#rollup-3-import-assertions

or load it into the REPL:
https://rollupjs.org/repl/?pr=4646

@justinfagnani
Copy link

@lukastaegert how does this work for inputs that have assertions when there is no special config?

Say:

import './foo.css' assert { type: 'css' };

That's preserved in the output, right?

@lukastaegert
Copy link
Member Author

lukastaegert commented Oct 4, 2022

No it is not. Otherwise it would get really complicated. This solution just focuses on consistent output. And I am not aware that CSS is already standardized?
So without a config, JSON always gets assertions but nothing else.

@justinfagnani
Copy link

Hmm... I think this is the biggest use case for import assertions right now. A library may publish code that uses assertions and non-JS module types and by default they should continue to work after bundling as they did before.

Yes, CSS modules are standardized (In the HTML spec here) and shipping in Chrome and Edge since v93. Firefox and Webkit were involved in the development of import assertions as a result of working out CSS modules, and are both currently implementing the prerequisites for CSS modules (constructible stylesheets and adopedStyleSheets).

@lukastaegert
Copy link
Member Author

Yes, CSS modules are standardized (In the HTML spec here)

Fair enough, then we should also add them. Unfortunately I could not find an overview which assertions are supported where. Wasn't there also discussion about an html assertion?

@lukastaegert
Copy link
Member Author

The problem with keeping assertions is right now that it would blow up the feature with lots of edge cases to be considered. Assertions need to be tracked, stored on external module instances, we need to handle inconsistent assertions. At the same time it can be useful to add assertions even where there were no assertions before. Primary use case would be required JSON that is turned into an import by the commonjs plugin. Though one could argue that that could be the responsibility of the commonjs plugin. The beauty of the current version is that it is easy to turn off.
Note that import assertions are not even a stage 4 feature at this point https://github.com/tc39/proposal-import-assertions

@thepassle
Copy link

This already looks really great. Thanks a lot for the time and effort you've put into this, and the quick response 🙂 However, as mentioned by Justin, my main usecase is this:

@lukastaegert how does this work for inputs that have assertions when there is no special config?

Say:

import './foo.css' assert { type: 'css' };

That's preserved in the output, right?

In case it helps, let me clarify my usecase again: Currently, I can use css import assertions in my code and it will work in browsers that support them, or via something like es-module-shims which also supports them. However, when I try to bundle my app with Rollup, it'll fail on the import assertion. So the issue is that I have valid browser code, which my bundler crashes on.

Ideally what I'd like is for rollup to leave the following import intact:
SOURCE CODE:

import './foo.css' assert { type: 'css' };

OUTPUT CODE:

import './foo.css' assert { type: 'css' };

@lukastaegert
Copy link
Member Author

However, when I try to bundle my app with Rollup, it'll fail on the import assertion

This issue is already fixed by this PR.

I will see if we can change the default to keep assertions instead in the next days, but as I said, quite a bit of work.

@codecov
Copy link

codecov bot commented Oct 8, 2022

Codecov Report

Merging #4646 (685d10c) into release-3.0.0 (abe30b8) will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@              Coverage Diff               @@
##           release-3.0.0    #4646   +/-   ##
==============================================
  Coverage          99.05%   99.06%           
==============================================
  Files                213      214    +1     
  Lines               7476     7554   +78     
  Branches            2069     2096   +27     
==============================================
+ Hits                7405     7483   +78     
  Misses                23       23           
  Partials              48       48           
Impacted Files Coverage Δ
src/ast/keys.ts 100.00% <ø> (ø)
src/ast/nodes/ExportAllDeclaration.ts 100.00% <ø> (ø)
src/ast/nodes/ExportNamedDeclaration.ts 100.00% <ø> (ø)
src/ast/nodes/ImportDeclaration.ts 100.00% <ø> (ø)
src/ast/nodes/NodeType.ts 100.00% <ø> (ø)
src/ast/nodes/index.ts 100.00% <ø> (ø)
src/utils/resolveId.ts 93.33% <ø> (ø)
src/Chunk.ts 100.00% <100.00%> (ø)
src/ExternalChunk.ts 100.00% <100.00%> (ø)
src/ExternalModule.ts 100.00% <100.00%> (ø)
... and 12 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@lukastaegert
Copy link
Member Author

lukastaegert commented Oct 10, 2022

I completely reworked the feature now. See the updated PR description. Basically this will now only take assertions that are already present in the input and warn about conflicting assertions. Plugins can however easily add or remove assertions at many points. There is only a simple new option that allows you to turn off assertions altogether, but for fine-grained control you need the plugin interface.

Unless there are heavy concerns, I would plan to release this feature tomorrow together with Rollup 3.

Incidentally, this makes the feature non-breaking 😄

@thepassle
Copy link

Judging from the updated description this indeed matches with how I'd expect this to work 🙂 Looking forward to trying this out. Thanks a lot for the discussion, and for the effort towards making this happen!

@justinfagnani
Copy link

Wow, this was quick work @lukastaegert! Agreed with @thepassle that this looks like what I need out of assertion support. This should allow for one build with no plugins for browsers with native CSS modules support and another build with a plugin to polyfill it, for instance. Thanks for adjusting the PR!!

@lukastaegert lukastaegert merged commit f74da45 into release-3.0.0 Oct 10, 2022
@lukastaegert lukastaegert deleted the rollup-3-import-assertions branch October 10, 2022 18:29
@LarsDenBakker
Copy link
Contributor

Thanks a lot for your efforts here @lukastaegert this really helps a lot to push standards-based development further!

lukastaegert added a commit that referenced this pull request Oct 10, 2022
* 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
@lukastaegert lukastaegert mentioned this pull request Oct 11, 2022
9 tasks
@rollup-bot
Copy link
Collaborator

This PR has been released as part of rollup@3.0.0-8. Note that this is a pre-release, so to test it, you need to install Rollup via npm install rollup@3.0.0-8 or npm install rollup@beta. It will likely become part of a regular release later.

lukastaegert added a commit that referenced this pull request Oct 11, 2022
* 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
@rollup-bot
Copy link
Collaborator

This PR has been released as part of rollup@3.0.0. You can test it via npm install rollup.

swiing added a commit to swiing/rollup-plugin-import-assertions that referenced this pull request May 12, 2023
Rollup v3 has introduced some support of import assertions in
rollup/rollup#4646. In particular, it uses
an `{ assertions: { type: <xxx> } }` object to track the assertions
linked to a module. However, this does not seem to be set in preload,
whereas it _is_ set afterwards. This results in a warning.

In order to silence the warning, we need to explicitly tell rollup
about the assertions type, so that this piece of information is
available even at preload time.

Note: this also makes the test 'handles garbage' pass. Without this
rollup issues 2 warnings instead of 1 expected (resulting in test failure).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants