From 29f3090f5e3cf3e4d0534ece8b587bc5799be638 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 2 Oct 2022 06:42:04 +0200 Subject: [PATCH 01/17] Add acorn support for import assertions and extend AST --- LICENSE.md | 7 +++++++ browser/LICENSE.md | 7 +++++++ package-lock.json | 21 +++++++++++++++++-- package.json | 1 + src/ast/keys.ts | 3 +++ src/ast/nodes/ExportAllDeclaration.ts | 2 ++ src/ast/nodes/ExportNamedDeclaration.ts | 2 ++ src/ast/nodes/ImportAttribute.ts | 10 +++++++++ src/ast/nodes/ImportDeclaration.ts | 4 +++- src/ast/nodes/ImportExpression.ts | 9 ++++++++ src/ast/nodes/NodeType.ts | 2 ++ src/ast/nodes/index.ts | 2 ++ src/ast/nodes/shared/Node.ts | 3 +-- src/utils/options/normalizeInputOptions.ts | 6 +++++- test/function/samples/options-hook/_config.js | 2 +- typings/declarations.d.ts | 4 ++++ 16 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 src/ast/nodes/ImportAttribute.ts diff --git a/LICENSE.md b/LICENSE.md index f173e24c97e..fd5c20a53a0 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -52,6 +52,13 @@ Repository: https://github.com/acornjs/acorn.git --------------------------------------- +## acorn-import-assertions +License: MIT +By: Sven Sauleau +Repository: https://github.com/xtuc/acorn-import-assertions + +--------------------------------------- + ## acorn-walk License: MIT By: Marijn Haverbeke, Ingvar Stepanyan, Adrian Heine diff --git a/browser/LICENSE.md b/browser/LICENSE.md index 8d9cc1268eb..c393ff66b59 100644 --- a/browser/LICENSE.md +++ b/browser/LICENSE.md @@ -52,6 +52,13 @@ Repository: https://github.com/acornjs/acorn.git --------------------------------------- +## acorn-import-assertions +License: MIT +By: Sven Sauleau +Repository: https://github.com/xtuc/acorn-import-assertions + +--------------------------------------- + ## acorn-walk License: MIT By: Marijn Haverbeke, Ingvar Stepanyan, Adrian Heine diff --git a/package-lock.json b/package-lock.json index 28b0dd0533d..9ecbf1ff6d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rollup", - "version": "3.0.0-4", + "version": "3.0.0-7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "rollup", - "version": "3.0.0-4", + "version": "3.0.0-7", "license": "MIT", "bin": { "rollup": "dist/bin/rollup" @@ -27,6 +27,7 @@ "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "acorn": "^8.8.0", + "acorn-import-assertions": "^1.8.0", "acorn-jsx": "^5.3.2", "acorn-walk": "^8.2.0", "buble": "^0.20.0", @@ -1327,6 +1328,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -8374,6 +8384,13 @@ "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", diff --git a/package.json b/package.json index 526a2fecbf4..9d72206c070 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "acorn": "^8.8.0", + "acorn-import-assertions": "^1.8.0", "acorn-jsx": "^5.3.2", "acorn-walk": "^8.2.0", "buble": "^0.20.0", diff --git a/src/ast/keys.ts b/src/ast/keys.ts index e9f9d437952..32764a3aa28 100644 --- a/src/ast/keys.ts +++ b/src/ast/keys.ts @@ -3,6 +3,9 @@ import type { GenericEsTreeNode } from './nodes/shared/Node'; export const keys: { [name: string]: string[]; } = { + // TODO this should be removed once ImportExpression follows official ESTree + // specs with "null" as default + ImportExpression: ['arguments'], Literal: [], Program: ['body'] }; diff --git a/src/ast/nodes/ExportAllDeclaration.ts b/src/ast/nodes/ExportAllDeclaration.ts index d4828620bf7..2768321eda6 100644 --- a/src/ast/nodes/ExportAllDeclaration.ts +++ b/src/ast/nodes/ExportAllDeclaration.ts @@ -1,11 +1,13 @@ import type MagicString from 'magic-string'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import type Identifier from './Identifier'; +import ImportAttribute from './ImportAttribute'; import type Literal from './Literal'; import type * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class ExportAllDeclaration extends NodeBase { + declare assertions: ImportAttribute[]; declare exported: Identifier | null; declare needsBoundaries: true; declare source: Literal; diff --git a/src/ast/nodes/ExportNamedDeclaration.ts b/src/ast/nodes/ExportNamedDeclaration.ts index 2b63e982618..f4257d43d6d 100644 --- a/src/ast/nodes/ExportNamedDeclaration.ts +++ b/src/ast/nodes/ExportNamedDeclaration.ts @@ -4,12 +4,14 @@ import type { HasEffectsContext } from '../ExecutionContext'; import type ClassDeclaration from './ClassDeclaration'; import type ExportSpecifier from './ExportSpecifier'; import type FunctionDeclaration from './FunctionDeclaration'; +import ImportAttribute from './ImportAttribute'; import type Literal from './Literal'; import type * as NodeType from './NodeType'; import type VariableDeclaration from './VariableDeclaration'; import { type Node, NodeBase } from './shared/Node'; export default class ExportNamedDeclaration extends NodeBase { + declare assertions: ImportAttribute[]; declare declaration: FunctionDeclaration | ClassDeclaration | VariableDeclaration | null; declare needsBoundaries: true; declare source: Literal | null; diff --git a/src/ast/nodes/ImportAttribute.ts b/src/ast/nodes/ImportAttribute.ts new file mode 100644 index 00000000000..a1e0c3bef49 --- /dev/null +++ b/src/ast/nodes/ImportAttribute.ts @@ -0,0 +1,10 @@ +import Identifier from './Identifier'; +import type Literal from './Literal'; +import type * as NodeType from './NodeType'; +import { NodeBase } from './shared/Node'; + +export default class ImportAttribute extends NodeBase { + declare key: Identifier | Literal; + declare type: NodeType.tImportAttribute; + declare value: Literal; +} diff --git a/src/ast/nodes/ImportDeclaration.ts b/src/ast/nodes/ImportDeclaration.ts index 9a4d98aff71..d642fa527eb 100644 --- a/src/ast/nodes/ImportDeclaration.ts +++ b/src/ast/nodes/ImportDeclaration.ts @@ -1,5 +1,6 @@ import type MagicString from 'magic-string'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; +import ImportAttribute from './ImportAttribute'; import type ImportDefaultSpecifier from './ImportDefaultSpecifier'; import type ImportNamespaceSpecifier from './ImportNamespaceSpecifier'; import type ImportSpecifier from './ImportSpecifier'; @@ -8,12 +9,13 @@ import type * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class ImportDeclaration extends NodeBase { + declare assertions: ImportAttribute[]; declare needsBoundaries: true; declare source: Literal; declare specifiers: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]; declare type: NodeType.tImportDeclaration; - // Do not bind specifiers + // Do not bind specifiers or assertions bind(): void {} hasEffects(): boolean { diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 10346a5651a..6880d2eb5ab 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -13,6 +13,7 @@ import type { InclusionContext } from '../ExecutionContext'; import type ChildScope from '../scopes/ChildScope'; import type NamespaceVariable from '../variables/NamespaceVariable'; import type * as NodeType from './NodeType'; +import ObjectExpression from './ObjectExpression'; import { type ExpressionNode, type IncludeChildren, NodeBase } from './shared/Node'; interface DynamicImportMechanism { @@ -20,7 +21,10 @@ 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; @@ -30,6 +34,11 @@ export default class ImportExpression extends NodeBase { 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; } diff --git a/src/ast/nodes/NodeType.ts b/src/ast/nodes/NodeType.ts index 6d75b6ac7c2..1fd93f3ca8f 100644 --- a/src/ast/nodes/NodeType.ts +++ b/src/ast/nodes/NodeType.ts @@ -30,6 +30,7 @@ export type tFunctionExpression = 'FunctionExpression'; export type tIdentifier = 'Identifier'; export type tIfStatement = 'IfStatement'; export type tImport = 'Import'; +export type tImportAttribute = 'ImportAttribute'; export type tImportDeclaration = 'ImportDeclaration'; export type tImportExpression = 'ImportExpression'; export type tImportDefaultSpecifier = 'ImportDefaultSpecifier'; @@ -101,6 +102,7 @@ export const FunctionExpression: tFunctionExpression = 'FunctionExpression'; export const Identifier: tIdentifier = 'Identifier'; export const IfStatement: tIfStatement = 'IfStatement'; export const Import: tImport = 'Import'; +export const ImportAttribute: tImportAttribute = 'ImportAttribute'; export const ImportDeclaration: tImportDeclaration = 'ImportDeclaration'; export const ImportExpression: tImportExpression = 'ImportExpression'; export const ImportDefaultSpecifier: tImportDefaultSpecifier = 'ImportDefaultSpecifier'; diff --git a/src/ast/nodes/index.ts b/src/ast/nodes/index.ts index 47f1ea5d09c..517ca63f8ba 100644 --- a/src/ast/nodes/index.ts +++ b/src/ast/nodes/index.ts @@ -29,6 +29,7 @@ import FunctionDeclaration from './FunctionDeclaration'; import FunctionExpression from './FunctionExpression'; import Identifier from './Identifier'; import IfStatement from './IfStatement'; +import ImportAttribute from './ImportAttribute'; import ImportDeclaration from './ImportDeclaration'; import ImportDefaultSpecifier from './ImportDefaultSpecifier'; import ImportExpression from './ImportExpression'; @@ -104,6 +105,7 @@ export const nodeConstructors: { FunctionExpression, Identifier, IfStatement, + ImportAttribute, ImportDeclaration, ImportDefaultSpecifier, ImportExpression, diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index cb5337284f5..a83a2606177 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -164,12 +164,11 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { bind(): void { for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; - if (value === null) continue; if (Array.isArray(value)) { for (const child of value) { child?.bind(); } - } else { + } else if (value) { value.bind(); } } diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index e357f6e89ae..267442f95ab 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -1,4 +1,5 @@ import * as acorn from 'acorn'; +import { importAssertions } from 'acorn-import-assertions'; import type { HasModuleSideEffects, InputOptions, @@ -101,7 +102,10 @@ const getAcorn = (config: InputOptions): acorn.Options => ({ const getAcornInjectPlugins = ( config: InputOptions -): NormalizedInputOptions['acornInjectPlugins'] => ensureArray(config.acornInjectPlugins); +): NormalizedInputOptions['acornInjectPlugins'] => [ + importAssertions, + ...ensureArray(config.acornInjectPlugins) +]; const getCache = (config: InputOptions): NormalizedInputOptions['cache'] => (config.cache as unknown as RollupBuild)?.cache || config.cache; diff --git a/test/function/samples/options-hook/_config.js b/test/function/samples/options-hook/_config.js index acf1441a057..7b24911685f 100644 --- a/test/function/samples/options-hook/_config.js +++ b/test/function/samples/options-hook/_config.js @@ -15,7 +15,7 @@ module.exports = { preserveParens: false, sourceType: 'module' }, - acornInjectPlugins: [], + acornInjectPlugins: [null], context: 'undefined', experimentalCacheExpiry: 10, input: ['used'], diff --git a/typings/declarations.d.ts b/typings/declarations.d.ts index 51f94dc1e2c..8a46f5bdbfe 100644 --- a/typings/declarations.d.ts +++ b/typings/declarations.d.ts @@ -11,6 +11,10 @@ declare module 'rollup-plugin-string' { export const string: PluginImpl; } +declare module 'acorn-import-assertions' { + export const importAssertions: () => unknown; +} + declare module 'is-reference' { import type * as estree from 'estree'; From 1cdbf2b0d55eebe8dbdfa10c9bec9e99854b1e32 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 2 Oct 2022 06:53:20 +0200 Subject: [PATCH 02/17] Ignore pre-existing assertions on input files --- src/ast/nodes/ImportExpression.ts | 3 ++ .../ignores-input-assertions/_config.js | 8 +++++ .../ignores-input-assertions/_expected/amd.js | 29 +++++++++++++++++ .../ignores-input-assertions/_expected/cjs.js | 31 +++++++++++++++++++ .../ignores-input-assertions/_expected/es.js | 7 +++++ .../_expected/iife.js | 15 +++++++++ .../_expected/system.js | 20 ++++++++++++ .../ignores-input-assertions/_expected/umd.js | 16 ++++++++++ .../ignores-input-assertions/main.js | 7 +++++ 9 files changed, 136 insertions(+) create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/_config.js create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/_expected/amd.js create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/_expected/cjs.js create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/_expected/es.js create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/_expected/iife.js create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/_expected/system.js create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/_expected/umd.js create mode 100644 test/form/samples/import-assertions/ignores-input-assertions/main.js diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 6880d2eb5ab..795506c6d89 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -75,6 +75,9 @@ export default class ImportExpression extends NodeBase { return; } + if (this.arguments) { + code.remove(this.source.end, this.end - 1); + } if (this.mechanism) { code.overwrite( this.start, diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_config.js b/test/form/samples/import-assertions/ignores-input-assertions/_config.js new file mode 100644 index 00000000000..5bd8f00faaa --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/_config.js @@ -0,0 +1,8 @@ +module.exports = { + solo: true, + description: 'ignores any import assertions on input', + options: { + external: () => true, + output: { name: 'bundle' } + } +}; diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/amd.js b/test/form/samples/import-assertions/ignores-input-assertions/_expected/amd.js new file mode 100644 index 00000000000..d0366d1af90 --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/_expected/amd.js @@ -0,0 +1,29 @@ +define(['require', 'exports', 'a', 'b', 'c'], (function (require, exports, a, b, c) { '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); + } + + console.log(a.a, b.b); + + new Promise(function (resolve, reject) { require(['d'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }).then(console.log); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + +})); diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/cjs.js b/test/form/samples/import-assertions/ignores-input-assertions/_expected/cjs.js new file mode 100644 index 00000000000..750fa4b890c --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/_expected/cjs.js @@ -0,0 +1,31 @@ +'use strict'; + +var a = require('a'); +var b = require('b'); +var c = require('c'); + +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); +} + +console.log(a.a, b.b); + +Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('d')); }).then(console.log); + +Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } +}); diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/es.js b/test/form/samples/import-assertions/ignores-input-assertions/_expected/es.js new file mode 100644 index 00000000000..53ebc4b190e --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/_expected/es.js @@ -0,0 +1,7 @@ +import { a } from 'a'; +import { b } from 'b'; +export { c } from 'c'; + +console.log(a, b); + +import('d').then(console.log); diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/iife.js b/test/form/samples/import-assertions/ignores-input-assertions/_expected/iife.js new file mode 100644 index 00000000000..6860373685c --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/_expected/iife.js @@ -0,0 +1,15 @@ +var bundle = (function (exports, a, b, c) { + 'use strict'; + + console.log(a.a, b.b); + + import('d').then(console.log); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + + return exports; + +})({}, a, b, c); diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/system.js b/test/form/samples/import-assertions/ignores-input-assertions/_expected/system.js new file mode 100644 index 00000000000..636fb7fe824 --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/_expected/system.js @@ -0,0 +1,20 @@ +System.register('bundle', ['a', 'b', 'c'], (function (exports, module) { + 'use strict'; + var a, b; + return { + setters: [function (module) { + a = module.a; + }, function (module) { + b = module.b; + }, function (module) { + exports('c', module.c); + }], + execute: (function () { + + console.log(a, b); + + module.import('d').then(console.log); + + }) + }; +})); diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/umd.js b/test/form/samples/import-assertions/ignores-input-assertions/_expected/umd.js new file mode 100644 index 00000000000..06d8bcc5fdc --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/_expected/umd.js @@ -0,0 +1,16 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('a'), require('b'), require('c')) : + typeof define === 'function' && define.amd ? define(['exports', 'a', 'b', 'c'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {}, global.a, global.b, global.c)); +})(this, (function (exports, a, b, c) { 'use strict'; + + console.log(a.a, b.b); + + import('d').then(console.log); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + +})); diff --git a/test/form/samples/import-assertions/ignores-input-assertions/main.js b/test/form/samples/import-assertions/ignores-input-assertions/main.js new file mode 100644 index 00000000000..50630c3143a --- /dev/null +++ b/test/form/samples/import-assertions/ignores-input-assertions/main.js @@ -0,0 +1,7 @@ +import { a } from 'a' assert { type: 'foo' }; +import { b } from 'b' assert { foo: 'bar', baz: 'quuz' }; +console.log(a, b); + +export { c } from 'c' assert { type: 'c' }; + +import('d', { assert: { type: 'd' } }).then(console.log); From d94cd7199a6aee9ee4a6b9c925f6631b50fb74e4 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 2 Oct 2022 07:00:22 +0200 Subject: [PATCH 03/17] Naive support for JSON assertions in output --- src/ast/nodes/ImportExpression.ts | 11 +++++++- src/finalisers/es.ts | 27 ++++++++++++------- .../ignores-input-assertions/_config.js | 1 - .../json-import-assertions/_config.js | 7 +++++ .../json-import-assertions/_expected/amd.js | 26 ++++++++++++++++++ .../json-import-assertions/_expected/cjs.js | 26 ++++++++++++++++++ .../json-import-assertions/_expected/es.js | 6 +++++ .../json-import-assertions/_expected/iife.js | 12 +++++++++ .../_expected/system.js | 17 ++++++++++++ .../json-import-assertions/_expected/umd.js | 13 +++++++++ .../json-import-assertions/main.js | 6 +++++ 11 files changed, 140 insertions(+), 12 deletions(-) create mode 100644 test/form/samples/import-assertions/json-import-assertions/_config.js create mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/amd.js create mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js create mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/es.js create mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/iife.js create mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/system.js create mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/umd.js create mode 100644 test/form/samples/import-assertions/json-import-assertions/main.js diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 795506c6d89..47b38d5f834 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -58,7 +58,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([], { @@ -89,6 +89,15 @@ export default class ImportExpression extends NodeBase { } if (this.resolutionString) { code.overwrite(this.source.start, this.source.end, this.resolutionString); + if (this.resolutionString.endsWith(".json'")) { + code.appendLeft( + this.end - 1, + `,${_}${getObject( + [['assert', getObject([['type', "'json'"]], { lineBreakIndent: null })]], + { lineBreakIndent: null } + )}` + ); + } if (this.namespaceExportName) { const [left, right] = getDirectReturnFunction(['n'], { functionReturn: true, diff --git a/src/finalisers/es.ts b/src/finalisers/es.ts index 668f2000322..5a30b023bfe 100644 --- a/src/finalisers/es.ts +++ b/src/finalisers/es.ts @@ -10,9 +10,9 @@ export default function es( { accessedGlobals, indent: t, intro, outro, dependencies, exports, snippets }: FinaliserOptions, { externalLiveBindings, freeze, namespaceToStringTag }: NormalizedOutputOptions ): void { - const { _, n } = snippets; + const { n } = snippets; - const importBlock = getImportBlock(dependencies, _); + const importBlock = getImportBlock(dependencies, snippets); if (importBlock.length > 0) intro += importBlock.join(n) + n + n; intro += getHelpersBlock( null, @@ -32,11 +32,18 @@ export default function es( magicString.trim(); } -function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { +function getImportBlock( + dependencies: ChunkDependency[], + { _, getObject }: GenerateCodeSnippets +): string[] { const importBlock: string[] = []; for (const { importPath, reexports, imports, name } of dependencies) { + const assertion = importPath.endsWith('.json') + ? `${_}assert${_}${getObject([['type', "'json'"]], { lineBreakIndent: null })}` + : ''; + const pathWithAssertion = `'${importPath}'${assertion};`; if (!reexports && !imports) { - importBlock.push(`import${_}'${importPath}';`); + importBlock.push(`import${_}${pathWithAssertion}`); continue; } if (imports) { @@ -53,10 +60,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 +74,7 @@ function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { return `${specifier.imported} as ${specifier.local}`; } }) - .join(`,${_}`)}${_}}${_}from${_}'${importPath}';` + .join(`,${_}`)}${_}}${_}from${_}${pathWithAssertion}` ); } } @@ -85,14 +92,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 +119,7 @@ function getImportBlock(dependencies: ChunkDependency[], _: string): string[] { return `${specifier.imported} as ${specifier.reexported}`; } }) - .join(`,${_}`)}${_}}${_}from${_}'${importPath}';` + .join(`,${_}`)}${_}}${_}from${_}${pathWithAssertion}` ); } } diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_config.js b/test/form/samples/import-assertions/ignores-input-assertions/_config.js index 5bd8f00faaa..02fe9ff6617 100644 --- a/test/form/samples/import-assertions/ignores-input-assertions/_config.js +++ b/test/form/samples/import-assertions/ignores-input-assertions/_config.js @@ -1,5 +1,4 @@ module.exports = { - solo: true, description: 'ignores any import assertions on input', options: { external: () => true, diff --git a/test/form/samples/import-assertions/json-import-assertions/_config.js b/test/form/samples/import-assertions/json-import-assertions/_config.js new file mode 100644 index 00000000000..dc865ea6e18 --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/_config.js @@ -0,0 +1,7 @@ +module.exports = { + description: 'adds import assertions for external JSON files', + options: { + external: ['./foo.json'], + output: { name: 'bundle' } + } +}; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js b/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js new file mode 100644 index 00000000000..a41800f1b75 --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js @@ -0,0 +1,26 @@ +define(['require', 'exports', './foo.json'], (function (require, exports, json) { '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); + } + + console.log(json); + + new Promise(function (resolve, reject) { require(['./foo.json', { assert: { type: 'json' } }], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }).then(console.log); + + exports.json = json; + +})); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js b/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js new file mode 100644 index 00000000000..4d635c17e3b --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js @@ -0,0 +1,26 @@ +'use strict'; + +var json = require('./foo.json'); + +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); +} + +console.log(json); + +Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('./foo.json', { assert: { type: 'json' } })); }).then(console.log); + +exports.json = json; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/es.js b/test/form/samples/import-assertions/json-import-assertions/_expected/es.js new file mode 100644 index 00000000000..97f9536e767 --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/es.js @@ -0,0 +1,6 @@ +import json from './foo.json' assert { type: 'json' }; +export { default as json } from './foo.json' assert { type: 'json' }; + +console.log(json); + +import('./foo.json', { assert: { type: 'json' } }).then(console.log); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js b/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js new file mode 100644 index 00000000000..00ae1d3e0b6 --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js @@ -0,0 +1,12 @@ +var bundle = (function (exports, json) { + 'use strict'; + + console.log(json); + + import('./foo.json', { assert: { type: 'json' } }).then(console.log); + + exports.json = json; + + return exports; + +})({}, json); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/system.js b/test/form/samples/import-assertions/json-import-assertions/_expected/system.js new file mode 100644 index 00000000000..07ef84c33ec --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/system.js @@ -0,0 +1,17 @@ +System.register('bundle', ['./foo.json'], (function (exports, module) { + 'use strict'; + var json; + return { + setters: [function (module) { + json = module.default; + exports('json', module.default); + }], + execute: (function () { + + console.log(json); + + module.import('./foo.json', { assert: { type: 'json' } }).then(console.log); + + }) + }; +})); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js b/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js new file mode 100644 index 00000000000..3074d49e793 --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js @@ -0,0 +1,13 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./foo.json')) : + typeof define === 'function' && define.amd ? define(['exports', './foo.json'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {}, global.json)); +})(this, (function (exports, json) { 'use strict'; + + console.log(json); + + import('./foo.json', { assert: { type: 'json' } }).then(console.log); + + exports.json = json; + +})); diff --git a/test/form/samples/import-assertions/json-import-assertions/main.js b/test/form/samples/import-assertions/json-import-assertions/main.js new file mode 100644 index 00000000000..7e39c113278 --- /dev/null +++ b/test/form/samples/import-assertions/json-import-assertions/main.js @@ -0,0 +1,6 @@ +import json from './foo.json'; +console.log(json); + +export { default as json } from './foo.json'; + +import('./foo.json').then(console.log); From 20f83f3ebf2635c68c095adda04ba513e277704c Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 3 Oct 2022 06:41:08 +0200 Subject: [PATCH 04/17] Inject arbitrary import assertions --- src/Chunk.ts | 27 +++++++++++++---- src/ExternalChunk.ts | 29 ++++++++++++++++++- src/ast/nodes/ImportExpression.ts | 14 +++++---- src/finalisers/es.ts | 11 ++----- src/rollup/types.d.ts | 4 +++ src/utils/options/mergeOptions.ts | 5 ++-- src/utils/options/normalizeOutputOptions.ts | 12 ++++++++ .../import-assertions-function/_config.js | 15 ++++++++++ .../import-assertions-function/_expected.js | 7 +++++ .../import-assertions-function/main.js | 7 +++++ .../json-import-assertions/_expected/amd.js | 2 +- .../json-import-assertions/_expected/cjs.js | 2 +- .../json-import-assertions/_expected/iife.js | 2 +- .../_expected/system.js | 2 +- .../json-import-assertions/_expected/umd.js | 2 +- 15 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 test/form/samples/import-assertions/import-assertions-function/_config.js create mode 100644 test/form/samples/import-assertions/import-assertions-function/_expected.js create mode 100644 test/form/samples/import-assertions/import-assertions-function/main.js diff --git a/src/Chunk.ts b/src/Chunk.ts index d45880e854d..aa5b0c1efd8 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -89,6 +89,7 @@ export type ResolvedDynamicImport = ( ) & { node: ImportExpression }; export interface ChunkDependency { + assertions: string | null; defaultVariableName: string | undefined; globalName: string | false | undefined; importPath: string; @@ -853,6 +854,17 @@ export default class Chunk { ); } + private getDynamicImportStringAndAssertions( + resolution: ExternalModule | string | null, + fileName: string + ): [importPath: string, assertions: string | null] { + if (resolution instanceof ExternalModule) { + const chunk = this.externalChunkByModule.get(resolution)!; + return [`'${chunk.getImportPath(fileName)}'`, chunk.getImportAssertions(this.snippets)]; + } + return [resolution || '', null]; + } + private getFallbackChunkName(): string { if (this.manualChunkAlias) { return this.manualChunkAlias; @@ -1032,6 +1044,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 +1194,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 +1211,9 @@ export default class Chunk { snippets, pluginDriver, accessedGlobalsByScope, - resolution instanceof ExternalModule - ? `'${this.externalChunkByModule.get(resolution)!.getImportPath(fileName)}'` - : resolution || '', - false + resolutionString, + false, + assertions ); } } diff --git a/src/ExternalChunk.ts b/src/ExternalChunk.ts index 2e76832b61e..b875a86422c 100644 --- a/src/ExternalChunk.ts +++ b/src/ExternalChunk.ts @@ -1,6 +1,7 @@ import ExternalModule from './ExternalModule'; -import { NormalizedOutputOptions } from './rollup/types'; +import { ModuleInfo, NormalizedOutputOptions } from './rollup/types'; import { escapeId } from './utils/escapeId'; +import { GenerateCodeSnippets } from './utils/generateCodeSnippets'; import { normalize, relative } from './utils/path'; import { getImportPath } from './utils/relativeId'; @@ -12,6 +13,8 @@ export default class ExternalChunk { variableName = ''; private fileName: string | null = null; + private importAssertions: string | null = null; + private moduleInfo: ModuleInfo; private renormalizeRenderPath: boolean; constructor( @@ -20,6 +23,7 @@ export default class ExternalChunk { private inputBase: string ) { this.id = module.id; + this.moduleInfo = module.info; this.renormalizeRenderPath = module.renormalizeRenderPath; this.suggestedVariableName = module.suggestedVariableName; } @@ -34,6 +38,13 @@ 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), + snippets + )); + } + getImportPath(importer: string): string { return escapeId( this.renormalizeRenderPath @@ -42,3 +53,19 @@ export default class ExternalChunk { ); } } + +function formatAssertions( + assertions: Record | null | void | false, + { getObject }: GenerateCodeSnippets +): string | null { + if (!assertions) { + return null; + } + const assertionEntries: [key: string, value: string][] = Object.entries(assertions).map( + ([key, value]) => [key, `'${value}'`] + ); + if (assertionEntries.length) { + return getObject(assertionEntries, { lineBreakIndent: null }); + } + return null; +} diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 47b38d5f834..e8148c69c51 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -29,6 +29,7 @@ export default class ImportExpression extends NodeBase { declare source: ExpressionNode; declare type: NodeType.tImportExpression; + private assertions: string | null = null; private mechanism: DynamicImportMechanism | null = null; private namespaceExportName: string | false | undefined = undefined; private resolution: Module | ExternalModule | string | null = null; @@ -89,13 +90,12 @@ export default class ImportExpression extends NodeBase { } if (this.resolutionString) { code.overwrite(this.source.start, this.source.end, this.resolutionString); - if (this.resolutionString.endsWith(".json'")) { + if (this.assertions) { code.appendLeft( this.end - 1, - `,${_}${getObject( - [['assert', getObject([['type', "'json'"]], { lineBreakIndent: null })]], - { lineBreakIndent: null } - )}` + `,${_}${getObject([['assert', this.assertions]], { + lineBreakIndent: null + })}` ); } if (this.namespaceExportName) { @@ -119,13 +119,15 @@ export default class ImportExpression extends NodeBase { pluginDriver: PluginDriver, accessedGlobalsByScope: Map>, resolutionString: string, - namespaceExportName: string | false | undefined + namespaceExportName: string | false | undefined, + assertions: string | null ): void { const { format } = options; this.inlineNamespace = null; this.resolution = resolution; this.resolutionString = resolutionString; this.namespaceExportName = namespaceExportName; + this.assertions = assertions; const accessedGlobals = [...(accessedImportGlobals[format] || [])]; let helper: string | null; ({ helper, mechanism: this.mechanism } = this.getDynamicImportMechanismAndHelper( diff --git a/src/finalisers/es.ts b/src/finalisers/es.ts index 5a30b023bfe..51eaa1a8b5d 100644 --- a/src/finalisers/es.ts +++ b/src/finalisers/es.ts @@ -32,15 +32,10 @@ export default function es( magicString.trim(); } -function getImportBlock( - dependencies: ChunkDependency[], - { _, getObject }: GenerateCodeSnippets -): string[] { +function getImportBlock(dependencies: ChunkDependency[], { _ }: GenerateCodeSnippets): string[] { const importBlock: string[] = []; - for (const { importPath, reexports, imports, name } of dependencies) { - const assertion = importPath.endsWith('.json') - ? `${_}assert${_}${getObject([['type', "'json'"]], { lineBreakIndent: null })}` - : ''; + for (const { importPath, reexports, imports, name, assertions } of dependencies) { + const assertion = assertions ? `${_}assert${_}${assertions}` : ''; const pathWithAssertion = `'${importPath}'${assertion};`; if (!reexports && !imports) { importBlock.push(`import${_}${pathWithAssertion}`); diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 1086dd6c900..4b95a23bcfd 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -564,6 +564,8 @@ interface GeneratedCodeOptions extends Partial { export type OptionsPaths = Record | ((id: string) => string); +type GetExternalImportAssertion = (moduleInfo: ModuleInfo) => Record | null | void; + export type InteropType = 'compat' | 'auto' | 'esModule' | 'default' | 'defaultOnly'; export type GetInterop = (id: string | null) => InteropType; @@ -618,6 +620,7 @@ export interface OutputOptions { esModule?: boolean | 'if-default-prop'; exports?: 'default' | 'named' | 'none' | 'auto'; extend?: boolean; + externalImportAssertions?: GetExternalImportAssertion; externalLiveBindings?: boolean; // only required for bundle.write file?: string; @@ -669,6 +672,7 @@ export interface NormalizedOutputOptions { esModule: boolean | 'if-default-prop'; exports: 'default' | 'named' | 'none' | 'auto'; extend: boolean; + externalImportAssertions: GetExternalImportAssertion; externalLiveBindings: boolean; file: string | undefined; footer: AddonFunction; diff --git a/src/utils/options/mergeOptions.ts b/src/utils/options/mergeOptions.ts index ba4e129b722..bf3ba84824b 100644 --- a/src/utils/options/mergeOptions.ts +++ b/src/utils/options/mergeOptions.ts @@ -106,7 +106,7 @@ function mergeInputOptions( overrides: CommandConfigObject, defaultOnWarnHandler: WarningHandler ): InputOptions { - const getOption = (name: string): any => overrides[name] ?? config[name]; + const getOption = (name: keyof InputOptions): any => overrides[name] ?? config[name]; const inputOptions: CompleteInputOptions = { acorn: getOption('acorn'), acornInjectPlugins: config.acornInjectPlugins as @@ -222,7 +222,7 @@ function mergeOutputOptions( overrides: GenericConfigObject, warn: WarningHandler ): OutputOptions { - const getOption = (name: string): any => overrides[name] ?? config[name]; + const getOption = (name: keyof OutputOptions): any => overrides[name] ?? config[name]; const outputOptions: CompleteOutputOptions = { amd: getObjectOption(config, overrides, 'amd'), assetFileNames: getOption('assetFileNames'), @@ -236,6 +236,7 @@ function mergeOutputOptions( esModule: getOption('esModule'), exports: getOption('exports'), extend: getOption('extend'), + externalImportAssertions: getOption('externalImportAssertions'), externalLiveBindings: getOption('externalLiveBindings'), file: getOption('file'), footer: getOption('footer'), diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index b9ad0b5b3d2..d1822112adb 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -6,6 +6,7 @@ import type { OutputOptions, SourcemapPathTransformOption } from '../../rollup/types'; +import { EMPTY_OBJECT } from '../blank'; import { ensureArray } from '../ensureArray'; import { errInvalidExportOptionValue, errInvalidOption, error, warnDeprecation } from '../error'; import { resolve } from '../path'; @@ -48,6 +49,7 @@ export function normalizeOutputOptions( esModule: config.esModule ?? 'if-default-prop', exports: getExports(config, unsetOptions), extend: config.extend || false, + externalImportAssertions: getExternalImportAssertions(config), externalLiveBindings: config.externalLiveBindings ?? true, file, footer: getAddon(config, 'footer'), @@ -355,6 +357,16 @@ function getExports( return configExports || 'auto'; } +function getExternalImportAssertions( + config: OutputOptions +): NormalizedOutputOptions['externalImportAssertions'] { + const configExternalImportAssertions = config.externalImportAssertions; + if (typeof configExternalImportAssertions === 'function') { + return configExternalImportAssertions; + } + return ({ id }) => (id.endsWith('.json') ? { type: 'json' } : EMPTY_OBJECT); +} + const getGeneratedCode = ( config: OutputOptions, preferConst: boolean diff --git a/test/form/samples/import-assertions/import-assertions-function/_config.js b/test/form/samples/import-assertions/import-assertions-function/_config.js new file mode 100644 index 00000000000..a329fb8f391 --- /dev/null +++ b/test/form/samples/import-assertions/import-assertions-function/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'adds import assertions for external JSON files', + options: { + external: true, + output: { + name: 'bundle', + externalImportAssertions({ id, ...other }) { + const [, assertions] = id.split('?'); + if (assertions) { + return Object.fromEntries(assertions.split('&').map(assertion => assertion.split('='))); + } + } + } + } +}; diff --git a/test/form/samples/import-assertions/import-assertions-function/_expected.js b/test/form/samples/import-assertions/import-assertions-function/_expected.js new file mode 100644 index 00000000000..8da792d4bdd --- /dev/null +++ b/test/form/samples/import-assertions/import-assertions-function/_expected.js @@ -0,0 +1,7 @@ +import 'external'; +import 'external?type=foo' assert { type: 'foo' }; +import 'external?type=bar&foo=baz' assert { type: 'bar', foo: 'baz' }; + +import('external'); +import('external?type=foo', { assert: { type: 'foo' } }); +import('external?type=bar&foo=baz', { assert: { type: 'bar', foo: 'baz' } }); diff --git a/test/form/samples/import-assertions/import-assertions-function/main.js b/test/form/samples/import-assertions/import-assertions-function/main.js new file mode 100644 index 00000000000..e2156a99127 --- /dev/null +++ b/test/form/samples/import-assertions/import-assertions-function/main.js @@ -0,0 +1,7 @@ +import 'external'; +import 'external?type=foo'; +import 'external?type=bar&foo=baz'; + +import('external'); +import('external?type=foo'); +import('external?type=bar&foo=baz'); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js b/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js index a41800f1b75..e4f1e18a2c3 100644 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js @@ -19,7 +19,7 @@ define(['require', 'exports', './foo.json'], (function (require, exports, json) console.log(json); - new Promise(function (resolve, reject) { require(['./foo.json', { assert: { type: 'json' } }], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }).then(console.log); + new Promise(function (resolve, reject) { require(['./foo.json'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }).then(console.log); exports.json = json; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js b/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js index 4d635c17e3b..b76edb2d727 100644 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js @@ -21,6 +21,6 @@ function _interopNamespaceDefault(e) { console.log(json); -Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('./foo.json', { assert: { type: 'json' } })); }).then(console.log); +Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('./foo.json')); }).then(console.log); exports.json = json; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js b/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js index 00ae1d3e0b6..878816f533b 100644 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js @@ -3,7 +3,7 @@ var bundle = (function (exports, json) { console.log(json); - import('./foo.json', { assert: { type: 'json' } }).then(console.log); + import('./foo.json').then(console.log); exports.json = json; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/system.js b/test/form/samples/import-assertions/json-import-assertions/_expected/system.js index 07ef84c33ec..1802824a6a9 100644 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/system.js +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/system.js @@ -10,7 +10,7 @@ System.register('bundle', ['./foo.json'], (function (exports, module) { console.log(json); - module.import('./foo.json', { assert: { type: 'json' } }).then(console.log); + module.import('./foo.json').then(console.log); }) }; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js b/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js index 3074d49e793..812cbae8249 100644 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js +++ b/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js @@ -6,7 +6,7 @@ console.log(json); - import('./foo.json', { assert: { type: 'json' } }).then(console.log); + import('./foo.json').then(console.log); exports.json = json; From 4c6f5b0bc2507a8313736c27bc132a743eb7c586 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 3 Oct 2022 06:55:48 +0200 Subject: [PATCH 05/17] Allows to disable import assertions altogether via `false` --- src/rollup/types.d.ts | 8 +++++--- src/utils/options/normalizeOutputOptions.ts | 6 ++++-- .../import-assertions/import-assertions-false/_config.js | 8 ++++++++ .../import-assertions-false/_expected.js | 6 ++++++ .../import-assertions/import-assertions-false/main.js | 6 ++++++ 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 test/form/samples/import-assertions/import-assertions-false/_config.js create mode 100644 test/form/samples/import-assertions/import-assertions-false/_expected.js create mode 100644 test/form/samples/import-assertions/import-assertions-false/main.js diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 4b95a23bcfd..f8286b68412 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -564,7 +564,9 @@ interface GeneratedCodeOptions extends Partial { export type OptionsPaths = Record | ((id: string) => string); -type GetExternalImportAssertion = (moduleInfo: ModuleInfo) => Record | null | void; +type GetExternalImportAssertions = (moduleInfo: ModuleInfo) => Record | null | void; + +type ExternalImportAssertionsOption = GetExternalImportAssertions | false; export type InteropType = 'compat' | 'auto' | 'esModule' | 'default' | 'defaultOnly'; @@ -620,7 +622,7 @@ export interface OutputOptions { esModule?: boolean | 'if-default-prop'; exports?: 'default' | 'named' | 'none' | 'auto'; extend?: boolean; - externalImportAssertions?: GetExternalImportAssertion; + externalImportAssertions?: ExternalImportAssertionsOption; externalLiveBindings?: boolean; // only required for bundle.write file?: string; @@ -672,7 +674,7 @@ export interface NormalizedOutputOptions { esModule: boolean | 'if-default-prop'; exports: 'default' | 'named' | 'none' | 'auto'; extend: boolean; - externalImportAssertions: GetExternalImportAssertion; + externalImportAssertions: GetExternalImportAssertions; externalLiveBindings: boolean; file: string | undefined; footer: AddonFunction; diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index d1822112adb..1d48b3b3528 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -6,7 +6,6 @@ import type { OutputOptions, SourcemapPathTransformOption } from '../../rollup/types'; -import { EMPTY_OBJECT } from '../blank'; import { ensureArray } from '../ensureArray'; import { errInvalidExportOptionValue, errInvalidOption, error, warnDeprecation } from '../error'; import { resolve } from '../path'; @@ -364,7 +363,10 @@ function getExternalImportAssertions( if (typeof configExternalImportAssertions === 'function') { return configExternalImportAssertions; } - return ({ id }) => (id.endsWith('.json') ? { type: 'json' } : EMPTY_OBJECT); + if (configExternalImportAssertions === false) { + return () => null; + } + return ({ id }) => (id.endsWith('.json') ? { type: 'json' } : null); } const getGeneratedCode = ( diff --git a/test/form/samples/import-assertions/import-assertions-false/_config.js b/test/form/samples/import-assertions/import-assertions-false/_config.js new file mode 100644 index 00000000000..cd993537de9 --- /dev/null +++ b/test/form/samples/import-assertions/import-assertions-false/_config.js @@ -0,0 +1,8 @@ +module.exports = { + description: + 'does not add import assertions for external JSON files when assertions are disabled', + options: { + external: ['./foo.json'], + output: { name: 'bundle', externalImportAssertions: false } + } +}; diff --git a/test/form/samples/import-assertions/import-assertions-false/_expected.js b/test/form/samples/import-assertions/import-assertions-false/_expected.js new file mode 100644 index 00000000000..15269e6d861 --- /dev/null +++ b/test/form/samples/import-assertions/import-assertions-false/_expected.js @@ -0,0 +1,6 @@ +import json from './foo.json'; +export { default as json } from './foo.json'; + +console.log(json); + +import('./foo.json').then(console.log); diff --git a/test/form/samples/import-assertions/import-assertions-false/main.js b/test/form/samples/import-assertions/import-assertions-false/main.js new file mode 100644 index 00000000000..7e39c113278 --- /dev/null +++ b/test/form/samples/import-assertions/import-assertions-false/main.js @@ -0,0 +1,6 @@ +import json from './foo.json'; +console.log(json); + +export { default as json } from './foo.json'; + +import('./foo.json').then(console.log); From a4469ccefc9093341a0904b955e83824dfe0f5d3 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 3 Oct 2022 07:09:41 +0200 Subject: [PATCH 06/17] Support shorthand syntax for type assertions --- src/rollup/types.d.ts | 5 ++++- src/utils/options/normalizeOutputOptions.ts | 10 ++++++++-- .../assertion-type-via-extension/_config.js | 15 +++++++++++++++ .../assertion-type-via-extension/_expected.js | 11 +++++++++++ .../assertion-type-via-extension/main.js | 11 +++++++++++ .../_config.js | 0 .../_expected/amd.js | 0 .../_expected/cjs.js | 0 .../_expected/es.js | 0 .../_expected/iife.js | 0 .../_expected/system.js | 0 .../_expected/umd.js | 0 .../main.js | 0 test/misc/optionList.js | 4 ++-- 14 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 test/form/samples/import-assertions/assertion-type-via-extension/_config.js create mode 100644 test/form/samples/import-assertions/assertion-type-via-extension/_expected.js create mode 100644 test/form/samples/import-assertions/assertion-type-via-extension/main.js rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/_config.js (100%) rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/_expected/amd.js (100%) rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/_expected/cjs.js (100%) rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/_expected/es.js (100%) rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/_expected/iife.js (100%) rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/_expected/system.js (100%) rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/_expected/umd.js (100%) rename test/form/samples/import-assertions/{ignores-input-assertions => ignores-assertions}/main.js (100%) diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index f8286b68412..5078445030e 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -566,7 +566,10 @@ export type OptionsPaths = Record | ((id: string) => string); type GetExternalImportAssertions = (moduleInfo: ModuleInfo) => Record | null | void; -type ExternalImportAssertionsOption = GetExternalImportAssertions | false; +type ExternalImportAssertionsOption = + | GetExternalImportAssertions + | false + | Record; export type InteropType = 'compat' | 'auto' | 'esModule' | 'default' | 'defaultOnly'; diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index 1d48b3b3528..6c8cf082710 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -8,7 +8,7 @@ import type { } from '../../rollup/types'; import { ensureArray } from '../ensureArray'; import { errInvalidExportOptionValue, errInvalidOption, error, warnDeprecation } from '../error'; -import { resolve } from '../path'; +import { extname, resolve } from '../path'; import { sanitizeFileName as defaultSanitizeFileName } from '../sanitizeFileName'; import { isValidUrl } from '../url'; import { @@ -356,6 +356,8 @@ function getExports( return configExports || 'auto'; } +const defaultAssertions = { '.json': 'json' }; + function getExternalImportAssertions( config: OutputOptions ): NormalizedOutputOptions['externalImportAssertions'] { @@ -366,7 +368,11 @@ function getExternalImportAssertions( if (configExternalImportAssertions === false) { return () => null; } - return ({ id }) => (id.endsWith('.json') ? { type: 'json' } : null); + const typeAssertions = configExternalImportAssertions || defaultAssertions; + return ({ id }) => { + const type = typeAssertions[extname(id)]; + return type ? { type } : null; + }; } const getGeneratedCode = ( diff --git a/test/form/samples/import-assertions/assertion-type-via-extension/_config.js b/test/form/samples/import-assertions/assertion-type-via-extension/_config.js new file mode 100644 index 00000000000..b87e99d61ad --- /dev/null +++ b/test/form/samples/import-assertions/assertion-type-via-extension/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'allows to configure the "type" assertion via a shorthand', + options: { + external: true, + output: { + name: 'bundle', + externalImportAssertions: { + '.json': null, + '.css': 'css', + '.foo': 'special', + '': 'empty' + } + } + } +}; diff --git a/test/form/samples/import-assertions/assertion-type-via-extension/_expected.js b/test/form/samples/import-assertions/assertion-type-via-extension/_expected.js new file mode 100644 index 00000000000..17833f57d42 --- /dev/null +++ b/test/form/samples/import-assertions/assertion-type-via-extension/_expected.js @@ -0,0 +1,11 @@ +import 'external.json'; +import 'external.css' assert { type: 'css' }; +import 'external.foo' assert { type: 'special' }; +import 'external' assert { type: 'empty' }; +import 'external.ignored'; + +import('external.json'); +import('external.css', { assert: { type: 'css' } }); +import('external.foo', { assert: { type: 'special' } }); +import('external', { assert: { type: 'empty' } }); +import('external.ignored'); diff --git a/test/form/samples/import-assertions/assertion-type-via-extension/main.js b/test/form/samples/import-assertions/assertion-type-via-extension/main.js new file mode 100644 index 00000000000..f41f906c4e6 --- /dev/null +++ b/test/form/samples/import-assertions/assertion-type-via-extension/main.js @@ -0,0 +1,11 @@ +import 'external.json'; +import 'external.css'; +import 'external.foo'; +import 'external'; +import 'external.ignored'; + +import('external.json'); +import('external.css'); +import('external.foo'); +import('external'); +import('external.ignored'); diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_config.js b/test/form/samples/import-assertions/ignores-assertions/_config.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/_config.js rename to test/form/samples/import-assertions/ignores-assertions/_config.js diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/amd.js b/test/form/samples/import-assertions/ignores-assertions/_expected/amd.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/_expected/amd.js rename to test/form/samples/import-assertions/ignores-assertions/_expected/amd.js diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/cjs.js b/test/form/samples/import-assertions/ignores-assertions/_expected/cjs.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/_expected/cjs.js rename to test/form/samples/import-assertions/ignores-assertions/_expected/cjs.js diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/es.js b/test/form/samples/import-assertions/ignores-assertions/_expected/es.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/_expected/es.js rename to test/form/samples/import-assertions/ignores-assertions/_expected/es.js diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/iife.js b/test/form/samples/import-assertions/ignores-assertions/_expected/iife.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/_expected/iife.js rename to test/form/samples/import-assertions/ignores-assertions/_expected/iife.js diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/system.js b/test/form/samples/import-assertions/ignores-assertions/_expected/system.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/_expected/system.js rename to test/form/samples/import-assertions/ignores-assertions/_expected/system.js diff --git a/test/form/samples/import-assertions/ignores-input-assertions/_expected/umd.js b/test/form/samples/import-assertions/ignores-assertions/_expected/umd.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/_expected/umd.js rename to test/form/samples/import-assertions/ignores-assertions/_expected/umd.js diff --git a/test/form/samples/import-assertions/ignores-input-assertions/main.js b/test/form/samples/import-assertions/ignores-assertions/main.js similarity index 100% rename from test/form/samples/import-assertions/ignores-input-assertions/main.js rename to test/form/samples/import-assertions/ignores-assertions/main.js diff --git a/test/misc/optionList.js b/test/misc/optionList.js index 88dfdea6e97..9cf18ae43ca 100644 --- a/test/misc/optionList.js +++ b/test/misc/optionList.js @@ -1,6 +1,6 @@ exports.input = 'acorn, acornInjectPlugins, cache, context, experimentalCacheExpiry, external, inlineDynamicImports, input, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, moduleContext, onwarn, perf, plugins, preserveEntrySignatures, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch'; exports.flags = - 'acorn, acornInjectPlugins, amd, assetFileNames, banner, bundleConfigAsCjs, c, cache, chunkFileNames, compact, config, configPlugin, context, d, dir, dynamicImportFunction, dynamicImportInCjs, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, generatedCode, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, sanitizeFileName, shimMissingExports, silent, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, validate, w, waitForBundleInput, watch'; + 'acorn, acornInjectPlugins, amd, assetFileNames, banner, bundleConfigAsCjs, c, cache, chunkFileNames, compact, config, configPlugin, context, d, dir, dynamicImportFunction, dynamicImportInCjs, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalImportAssertions, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, generatedCode, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, sanitizeFileName, shimMissingExports, silent, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, validate, w, waitForBundleInput, watch'; exports.output = - 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, dynamicImportInCjs, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, generatedCode, globals, hoistTransitiveImports, indent, inlineDynamicImports, interop, intro, manualChunks, minifyInternalExports, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, preserveModules, preserveModulesRoot, sanitizeFileName, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict, systemNullSetters, validate'; + 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, dynamicImportInCjs, entryFileNames, esModule, exports, extend, externalImportAssertions, externalLiveBindings, file, footer, format, freeze, generatedCode, globals, hoistTransitiveImports, indent, inlineDynamicImports, interop, intro, manualChunks, minifyInternalExports, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, preserveModules, preserveModulesRoot, sanitizeFileName, sourcemap, sourcemapBaseUrl, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict, systemNullSetters, validate'; From a2e406b720f0e1338aafe4877dae7cd225c24dcd Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 3 Oct 2022 17:14:23 +0200 Subject: [PATCH 07/17] Keep assertions on fully dynamic imports --- src/Chunk.ts | 4 +- src/ast/nodes/ImportExpression.ts | 37 +++++++++---------- .../keep-dynamic-assertions/_config.js | 6 +++ .../keep-dynamic-assertions/_expected/amd.js | 22 +++++++++++ .../keep-dynamic-assertions/_expected/cjs.js | 20 ++++++++++ .../keep-dynamic-assertions/_expected/es.js | 1 + .../keep-dynamic-assertions/_expected/iife.js | 6 +++ .../_expected/system.js | 10 +++++ .../keep-dynamic-assertions/_expected/umd.js | 8 ++++ .../keep-dynamic-assertions/main.js | 1 + 10 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/_config.js create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js create mode 100644 test/form/samples/import-assertions/keep-dynamic-assertions/main.js diff --git a/src/Chunk.ts b/src/Chunk.ts index aa5b0c1efd8..0c18fa732b6 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -857,12 +857,12 @@ export default class Chunk { private getDynamicImportStringAndAssertions( resolution: ExternalModule | string | null, fileName: string - ): [importPath: string, assertions: string | null] { + ): [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 || '', null]; + return [resolution || '', this.outputOptions.format === 'es' || null]; } private getFallbackChunkName(): string { diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index e8148c69c51..c0a379afc74 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -29,7 +29,7 @@ export default class ImportExpression extends NodeBase { declare source: ExpressionNode; declare type: NodeType.tImportExpression; - private assertions: string | null = null; + private assertions: string | null | true = null; private mechanism: DynamicImportMechanism | null = null; private namespaceExportName: string | false | undefined = undefined; private resolution: Module | ExternalModule | string | null = null; @@ -70,34 +70,20 @@ 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.arguments) { - code.remove(this.source.end, this.end - 1); - } 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); - if (this.assertions) { - code.appendLeft( - this.end - 1, - `,${_}${getObject([['assert', this.assertions]], { - lineBreakIndent: null - })}` - ); - } if (this.namespaceExportName) { const [left, right] = getDirectReturnFunction(['n'], { functionReturn: true, @@ -109,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( @@ -120,7 +119,7 @@ export default class ImportExpression extends NodeBase { accessedGlobalsByScope: Map>, resolutionString: string, namespaceExportName: string | false | undefined, - assertions: string | null + assertions: string | null | true ): void { const { format } = options; this.inlineNamespace = null; diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js new file mode 100644 index 00000000000..14e4b6662e6 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js @@ -0,0 +1,6 @@ +module.exports = { + description: 'keep import assertions for non-resolvable dynamic imports', + options: { + external: true + } +}; diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js new file mode 100644 index 00000000000..622ca8b31b8 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js @@ -0,0 +1,22 @@ +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); + } + + (function (t) { return new Promise(function (resolve, reject) { require([t], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); })(globalThis.unknown); + +})); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js new file mode 100644 index 00000000000..440ea28ac47 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js @@ -0,0 +1,20 @@ +'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); +} + +(function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require(t)); }); })(globalThis.unknown); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js new file mode 100644 index 00000000000..63795c9009a --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js @@ -0,0 +1 @@ +import(globalThis.unknown, { assert: { type: 'special' } }); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js new file mode 100644 index 00000000000..da9cafa781a --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + import(globalThis.unknown); + +})(); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js new file mode 100644 index 00000000000..264d8ebb609 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js @@ -0,0 +1,10 @@ +System.register([], (function (exports, module) { + 'use strict'; + return { + execute: (function () { + + module.import(globalThis.unknown); + + }) + }; +})); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js new file mode 100644 index 00000000000..bc60999ca93 --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js @@ -0,0 +1,8 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +})((function () { 'use strict'; + + import(globalThis.unknown); + +})); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/main.js b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js new file mode 100644 index 00000000000..63795c9009a --- /dev/null +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js @@ -0,0 +1 @@ +import(globalThis.unknown, { assert: { type: 'special' } }); From 301aae0e4dce7b0baf4fd1c753a91860414c5917 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 3 Oct 2022 21:19:12 +0200 Subject: [PATCH 08/17] Add documentation --- docs/999-big-list-of-options.md | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 3d74dfecba3..4a02fbe64d3 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -532,6 +532,65 @@ Type: `boolean`
CLI: `--extend`/`--no-extend`
Default: `false` Whether to extend the global variable defined by the `name` option in `umd` or `iife` formats. When `true`, the global variable will be defined as `(global.name = global.name || {})`. When false, the global defined by `name` will be overwritten like `(global.name = {})`. +#### output.externalImportAssertions + +Type: `{[extName: string]: string | null} | false | (moduleInfo: ModulInfo) => ({[key: string]: string} | null)`
Default: `{".json": "json"}` + +Which import assertions to add to external imports in the output if the output format is `es`. By default, only external `.json` files will receive a `type: 'json'` assertion. + +- If an object is provided, then the keys are considered to be file extensions while the values add a corresponding `type` assertion to files of that type. + + ```js + // config + export default { + external: ['./foo.css', './bar.json'], + // ... + output: { + externalImportAssertions: { + '.css': 'css' + } + // ... + } + }; + + // input + import './foo.css'; + import './bar.json'; + + // output + import './foo.css' assert { type: 'css' }; + import './bar.json'; + ``` + +- If a function is provided, it will be called once for each external dependency with information about the external module. If it returns an object, its key-value pairs will be added as arbitrary assertions to all imports of that dependency. + + ```js + // config + export default { + external: ['./foo.css', './bar.json'], + // ... + output: { + externalImportAssertions({ id }) { + if (id.endsWith('.css')) { + return { type: 'css', special: 'attribute' }; + } + return null; + } + // ... + } + }; + + // input + import './foo.css'; + import './bar.json'; + + // output + import './foo.css' assert { type: 'css', special: 'attribute' }; + import './bar.json'; + ``` + +- `false` will disable all assertions. + #### output.generatedCode Type: `"es5" | "es2015" | { arrowFunctions?: boolean, constBindings?: boolean, objectShorthand?: boolean, preset?: "es5" | "es2015", reservedNamesAsProps?: boolean, symbols?: boolean }`
CLI: `--generatedCode `
Default: `"es5"` From 538de2cf17d9a268e83156800d47d582f759a458 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 5 Oct 2022 07:02:36 +0200 Subject: [PATCH 09/17] Add assertions to types --- src/ExternalModule.ts | 4 +++- src/Module.ts | 3 +++ src/ModuleLoader.ts | 4 ++++ src/rollup/types.d.ts | 1 + .../_config.js | 8 ++++++-- .../implicitly-dependent-entry/_config.js | 8 ++++++-- .../multiple-dependencies/_config.js | 18 +++++++++++++++--- .../single-dependency/_config.js | 8 ++++++-- .../samples/context-resolve/_config.js | 9 +++++++++ .../deprecated/manual-chunks-info/_config.js | 9 +++++++++ .../samples/manual-chunks-info/_config.js | 9 +++++++++ .../samples/module-parsed-hook/_config.js | 7 +++++-- .../plugin-module-information/_config.js | 18 ++++++++++++++---- .../function/samples/preload-module/_config.js | 6 ++++-- .../resolve-relative-external-id/_config.js | 2 ++ test/incremental/index.js | 4 ++++ 16 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/ExternalModule.ts b/src/ExternalModule.ts index b80a1aac79a..891bd1e5adc 100644 --- a/src/ExternalModule.ts +++ b/src/ExternalModule.ts @@ -1,6 +1,6 @@ import ExternalVariable from './ast/variables/ExternalVariable'; import type { CustomPluginOptions, ModuleInfo, NormalizedInputOptions } from './rollup/types'; -import { EMPTY_ARRAY } from './utils/blank'; +import { EMPTY_ARRAY, EMPTY_OBJECT } from './utils/blank'; import { errUnusedExternalImports, warnDeprecation } from './utils/error'; import { makeLegal } from './utils/identifierHelpers'; @@ -29,6 +29,8 @@ export default class ExternalModule { const { importers, dynamicImporters } = this; const info: ModuleInfo = (this.info = { + // TODO Lukas use correct assertions + assertions: EMPTY_OBJECT, ast: null, code: null, dynamicallyImportedIdResolutions: EMPTY_ARRAY, diff --git a/src/Module.ts b/src/Module.ts index 12da5bbdd11..df0a2a83f55 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -274,6 +274,8 @@ export default class Module { } = this; this.info = { + // TODO Lukas use correct assertions + assertions: EMPTY_OBJECT, ast: null, code: null, get dynamicallyImportedIdResolutions() { @@ -784,6 +786,7 @@ export default class Module { toJSON(): ModuleJSON { return { + assertions: this.info.assertions, ast: this.ast!.esTreeNode, code: this.info.code!, customTransformCache: this.customTransformCache, diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 8546bfa62f0..4113123bde5 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -546,6 +546,8 @@ export class ModuleLoader { } const external = resolvedId.external || false; return { + // TODO Lukas use correct assertions from import + assertions: EMPTY_OBJECT, external, id: resolvedId.id, meta: resolvedId.meta || {}, @@ -584,6 +586,8 @@ export class ModuleLoader { } this.options.onwarn(errUnresolvedImportTreatedAsExternal(source, importer)); return { + // TODO Lukas use correct assertions from import + assertions: EMPTY_OBJECT, external: true, id: source, meta: {}, diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 5078445030e..1c229f76610 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -83,6 +83,7 @@ type PartialNull = { }; interface ModuleOptions { + assertions: Record; meta: CustomPluginOptions; moduleSideEffects: boolean | 'no-treeshake'; syntheticNamedExports: boolean | string; diff --git a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js index 7d43493ea6d..939da5f5eaa 100644 --- a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js @@ -25,6 +25,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN))), { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -75,11 +77,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, @@ -96,6 +98,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -146,11 +150,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, diff --git a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js index 3c0a5fd5345..682022286ab 100644 --- a/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js @@ -21,6 +21,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN))), { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -71,11 +73,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, @@ -92,6 +94,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -142,11 +146,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, diff --git a/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js b/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js index 6abadc7fdce..0682a960244 100644 --- a/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js @@ -34,6 +34,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN1))), { + id: ID_MAIN1, + assertions: {}, ast: { type: 'Program', start: 0, @@ -119,11 +121,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN1, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [ID_DEP], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB1, meta: {}, @@ -131,6 +133,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB1B, meta: {}, @@ -138,6 +141,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB2, meta: {}, @@ -154,6 +158,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN2))), { + id: ID_MAIN2, + assertions: {}, ast: { type: 'Program', start: 0, @@ -239,11 +245,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN2, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [ID_DEP], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB1, meta: {}, @@ -251,6 +257,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB1B, meta: {}, @@ -258,6 +265,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB3, meta: {}, @@ -274,6 +282,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -358,11 +368,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [ID_MAIN1, ID_MAIN2], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB1, meta: {}, @@ -370,6 +380,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB2, meta: {}, @@ -377,6 +388,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: false, id: ID_LIB3, meta: {}, diff --git a/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js b/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js index 0a1b56b3f85..5569a48cb99 100644 --- a/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js @@ -20,6 +20,8 @@ module.exports = { }, buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_MAIN))), { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -70,11 +72,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [ID_DEP], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, @@ -91,6 +93,8 @@ module.exports = { syntheticNamedExports: false }); assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(ID_DEP))), { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -141,11 +145,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [ID_MAIN], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_LIB, meta: {}, diff --git a/test/function/samples/context-resolve/_config.js b/test/function/samples/context-resolve/_config.js index d2f132c360f..d41290cd853 100644 --- a/test/function/samples/context-resolve/_config.js +++ b/test/function/samples/context-resolve/_config.js @@ -6,6 +6,7 @@ const tests = [ source: './existing', expected: { id: path.join(__dirname, 'existing.js'), + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -24,6 +25,7 @@ const tests = [ source: './marked-directly-external-relative', expected: { id: path.join(__dirname, 'marked-directly-external-relative'), + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -34,6 +36,7 @@ const tests = [ source: './marked-external-relative', expected: { id: path.join(__dirname, 'marked-external-relative'), + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -44,6 +47,7 @@ const tests = [ source: 'marked-external-absolute', expected: { id: 'marked-external-absolute', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -54,6 +58,7 @@ const tests = [ source: 'resolved-name', expected: { id: 'resolved:resolved-name', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -64,6 +69,7 @@ const tests = [ source: 'resolved-false', expected: { id: 'resolved-false', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -74,6 +80,7 @@ const tests = [ source: 'resolved-object', expected: { id: 'resolved:resolved-object', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -84,6 +91,7 @@ const tests = [ source: 'resolved-object-non-external', expected: { id: 'resolved:resolved-object-non-external', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -94,6 +102,7 @@ const tests = [ source: 'resolved-object-external', expected: { id: 'resolved:resolved-object-external', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, diff --git a/test/function/samples/deprecated/manual-chunks-info/_config.js b/test/function/samples/deprecated/manual-chunks-info/_config.js index 44dcf747df5..c545b22c61c 100644 --- a/test/function/samples/deprecated/manual-chunks-info/_config.js +++ b/test/function/samples/deprecated/manual-chunks-info/_config.js @@ -20,6 +20,7 @@ module.exports = { { [getId('dynamic')]: { id: getId('dynamic'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -80,6 +81,7 @@ module.exports = { code: "export const promise = import('external');\nexport { default as internal } from './lib';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: true, id: 'external', meta: {}, @@ -95,6 +97,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -112,6 +115,7 @@ module.exports = { }, [getId('lib')]: { id: getId('lib'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -145,6 +149,7 @@ module.exports = { }, [getId('main')]: { id: getId('main'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -227,6 +232,7 @@ module.exports = { code: "export const promise = import('./dynamic');\nexport { default as value } from './lib';\nexport { external } from 'external';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: false, id: getId('dynamic'), meta: {}, @@ -242,6 +248,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -249,6 +256,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: true, id: 'external', meta: {}, @@ -266,6 +274,7 @@ module.exports = { }, external: { id: 'external', + assertions: {}, ast: null, code: null, dynamicallyImportedIdResolutions: [], diff --git a/test/function/samples/manual-chunks-info/_config.js b/test/function/samples/manual-chunks-info/_config.js index 9be80a1174b..f1d37e262ef 100644 --- a/test/function/samples/manual-chunks-info/_config.js +++ b/test/function/samples/manual-chunks-info/_config.js @@ -21,6 +21,7 @@ module.exports = { { [getId('dynamic')]: { id: getId('dynamic'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -81,6 +82,7 @@ module.exports = { code: "export const promise = import('external');\nexport { default as internal } from './lib';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: true, id: 'external', meta: {}, @@ -96,6 +98,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -113,6 +116,7 @@ module.exports = { }, [getId('lib')]: { id: getId('lib'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -146,6 +150,7 @@ module.exports = { }, [getId('main')]: { id: getId('main'), + assertions: {}, ast: { type: 'Program', start: 0, @@ -228,6 +233,7 @@ module.exports = { code: "export const promise = import('./dynamic');\nexport { default as value } from './lib';\nexport { external } from 'external';\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: false, id: getId('dynamic'), meta: {}, @@ -243,6 +249,7 @@ module.exports = { implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: getId('lib'), meta: {}, @@ -250,6 +257,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: true, id: 'external', meta: {}, @@ -267,6 +275,7 @@ module.exports = { }, external: { id: 'external', + assertions: {}, ast: null, code: null, dynamicallyImportedIdResolutions: [], diff --git a/test/function/samples/module-parsed-hook/_config.js b/test/function/samples/module-parsed-hook/_config.js index 507e5d6d180..f2e8b02c49e 100644 --- a/test/function/samples/module-parsed-hook/_config.js +++ b/test/function/samples/module-parsed-hook/_config.js @@ -17,6 +17,8 @@ module.exports = { buildEnd() { assert.deepStrictEqual(JSON.parse(JSON.stringify(parsedModules)), [ { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -53,11 +55,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_DEP, meta: {}, @@ -74,6 +76,8 @@ module.exports = { syntheticNamedExports: false }, { + id: ID_DEP, + assertions: {}, ast: { type: 'Program', start: 0, @@ -110,7 +114,6 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], diff --git a/test/function/samples/plugin-module-information/_config.js b/test/function/samples/plugin-module-information/_config.js index 2312228e0db..59641559051 100644 --- a/test/function/samples/plugin-module-information/_config.js +++ b/test/function/samples/plugin-module-information/_config.js @@ -16,6 +16,7 @@ module.exports = { plugins: { load(id) { assert.deepStrictEqual(JSON.parse(JSON.stringify(this.getModuleInfo(id))), { + assertions: {}, ast: null, code: null, dynamicImporters: [], @@ -47,6 +48,8 @@ module.exports = { ), { [ID_FOO]: { + id: ID_FOO, + assertions: {}, ast: { type: 'Program', start: 0, @@ -114,11 +117,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_FOO, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: true, id: ID_PATH, meta: {}, @@ -135,6 +138,8 @@ module.exports = { syntheticNamedExports: false }, [ID_MAIN]: { + id: ID_MAIN, + assertions: {}, ast: { type: 'Program', start: 0, @@ -261,6 +266,7 @@ module.exports = { code: "export { foo } from './foo.js';\nexport const nested = import('./nested/nested');\nexport const path = import('path');\nexport const pathAgain = import(thePath);\n", dynamicallyImportedIdResolutions: [ { + assertions: {}, external: false, id: ID_NESTED, meta: {}, @@ -268,6 +274,7 @@ module.exports = { syntheticNamedExports: false }, { + assertions: {}, external: true, id: ID_PATH, meta: {}, @@ -279,11 +286,11 @@ module.exports = { dynamicImporters: [], hasDefaultExport: false, moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_FOO, meta: {}, @@ -300,6 +307,8 @@ module.exports = { syntheticNamedExports: false }, [ID_NESTED]: { + id: ID_NESTED, + assertions: {}, ast: { type: 'Program', start: 0, @@ -370,11 +379,11 @@ module.exports = { dynamicImporters: [ID_MAIN], hasDefaultExport: false, moduleSideEffects: true, - id: ID_NESTED, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [ { + assertions: {}, external: false, id: ID_FOO, meta: {}, @@ -391,6 +400,8 @@ module.exports = { syntheticNamedExports: false }, [ID_PATH]: { + id: ID_PATH, + assertions: {}, ast: null, code: null, dynamicallyImportedIdResolutions: [], @@ -398,7 +409,6 @@ module.exports = { dynamicImporters: [ID_MAIN], hasDefaultExport: null, moduleSideEffects: true, - id: ID_PATH, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], diff --git a/test/function/samples/preload-module/_config.js b/test/function/samples/preload-module/_config.js index cf38cada7af..5ab79b4ace3 100644 --- a/test/function/samples/preload-module/_config.js +++ b/test/function/samples/preload-module/_config.js @@ -31,13 +31,14 @@ module.exports = { meta: { testPlugin: 'first' } }); assert.deepStrictEqual(moduleInfo, { + id: ID_MAIN, + assertions: {}, code: "import './dep';\nassert.ok(true);\n", dynamicImporters: [], hasDefaultExport: false, dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], moduleSideEffects: true, - id: ID_MAIN, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], @@ -72,13 +73,14 @@ module.exports = { meta: { testPlugin: 'second' } }); assert.deepStrictEqual(moduleInfo, { + id: ID_DEP, + assertions: {}, code: 'assert.ok(true);\n', dynamicImporters: [], hasDefaultExport: false, dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], moduleSideEffects: true, - id: ID_DEP, implicitlyLoadedAfterOneOf: [], implicitlyLoadedBefore: [], importedIdResolutions: [], diff --git a/test/function/samples/resolve-relative-external-id/_config.js b/test/function/samples/resolve-relative-external-id/_config.js index f6a6d451524..bdff9370e82 100644 --- a/test/function/samples/resolve-relative-external-id/_config.js +++ b/test/function/samples/resolve-relative-external-id/_config.js @@ -8,6 +8,7 @@ module.exports = { plugins: { async buildStart() { assert.deepStrictEqual(await this.resolve('./external.js'), { + assertions: {}, external: true, id: path.join(__dirname, 'external.js'), meta: {}, @@ -17,6 +18,7 @@ module.exports = { assert.deepStrictEqual( await this.resolve('./external.js', path.join(__dirname, 'nested', 'some-file.js')), { + assertions: {}, external: true, id: path.join(__dirname, 'nested', 'external.js'), meta: {}, diff --git a/test/incremental/index.js b/test/incremental/index.js index 9ae4f56fcae..045063e7dd8 100644 --- a/test/incremental/index.js +++ b/test/incremental/index.js @@ -267,6 +267,7 @@ describe('incremental', () => { assert.deepEqual(bundle.cache.modules[1].resolvedIds, { foo: { id: 'foo', + assertions: {}, external: false, meta: {}, moduleSideEffects: true, @@ -274,6 +275,7 @@ describe('incremental', () => { }, external: { id: 'external', + assertions: {}, external: true, meta: {}, moduleSideEffects: true, @@ -360,6 +362,7 @@ describe('incremental', () => { assert.deepStrictEqual(resolvedSources, { __proto__: null, bar: { + assertions: {}, external: false, id: 'bar', meta: {}, @@ -378,6 +381,7 @@ describe('incremental', () => { assert.deepStrictEqual(resolvedSources, { __proto__: null, foo: { + assertions: {}, external: false, id: 'foo', meta: {}, From 2b03ca921ffb44933b6a6945f4050dedf04ae791 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 7 Oct 2022 19:33:49 +0200 Subject: [PATCH 10/17] Keep original assertions --- browser/src/resolveId.ts | 1 + src/ExternalModule.ts | 8 +-- src/Module.ts | 27 +++++++--- src/ModuleLoader.ts | 52 ++++++++++++------- src/ast/nodes/ImportDeclaration.ts | 2 +- src/utils/PluginContext.ts | 4 +- src/utils/options/normalizeOutputOptions.ts | 17 +++--- src/utils/parseAssertions.ts | 49 +++++++++++++++++ src/utils/resolveId.ts | 2 + src/utils/resolveIdViaPlugins.ts | 6 ++- .../assertion-type-via-extension/_config.js | 15 ------ .../assertion-type-via-extension/_expected.js | 11 ---- .../assertion-type-via-extension/main.js | 11 ---- .../ignores-assertions/_expected/es.js | 7 --- .../import-assertions-function/_config.js | 15 ------ .../import-assertions-function/_expected.js | 7 --- .../import-assertions-function/main.js | 7 --- .../json-import-assertions/_config.js | 7 --- .../json-import-assertions/_expected/amd.js | 26 ---------- .../json-import-assertions/_expected/cjs.js | 26 ---------- .../json-import-assertions/_expected/es.js | 6 --- .../json-import-assertions/_expected/iife.js | 12 ----- .../_expected/system.js | 17 ------ .../json-import-assertions/_expected/umd.js | 13 ----- .../json-import-assertions/main.js | 6 --- .../keep-dynamic-assertions/_config.js | 21 +++++++- .../keep-dynamic-assertions/_expected/amd.js | 4 ++ .../keep-dynamic-assertions/_expected/cjs.js | 4 ++ .../keep-dynamic-assertions/_expected/es.js | 4 ++ .../keep-dynamic-assertions/_expected/iife.js | 4 ++ .../_expected/system.js | 4 ++ .../keep-dynamic-assertions/_expected/umd.js | 4 ++ .../keep-dynamic-assertions/main.js | 4 ++ .../_config.js | 2 +- .../_expected/amd.js | 0 .../_expected/cjs.js | 0 .../keeps-assertions/_expected/es.js | 7 +++ .../_expected/iife.js | 0 .../_expected/system.js | 0 .../_expected/umd.js | 0 .../main.js | 2 +- 41 files changed, 184 insertions(+), 230 deletions(-) create mode 100644 src/utils/parseAssertions.ts delete mode 100644 test/form/samples/import-assertions/assertion-type-via-extension/_config.js delete mode 100644 test/form/samples/import-assertions/assertion-type-via-extension/_expected.js delete mode 100644 test/form/samples/import-assertions/assertion-type-via-extension/main.js delete mode 100644 test/form/samples/import-assertions/ignores-assertions/_expected/es.js delete mode 100644 test/form/samples/import-assertions/import-assertions-function/_config.js delete mode 100644 test/form/samples/import-assertions/import-assertions-function/_expected.js delete mode 100644 test/form/samples/import-assertions/import-assertions-function/main.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/_config.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/amd.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/es.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/iife.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/system.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/_expected/umd.js delete mode 100644 test/form/samples/import-assertions/json-import-assertions/main.js rename test/form/samples/import-assertions/{ignores-assertions => keeps-assertions}/_config.js (61%) rename test/form/samples/import-assertions/{ignores-assertions => keeps-assertions}/_expected/amd.js (100%) rename test/form/samples/import-assertions/{ignores-assertions => keeps-assertions}/_expected/cjs.js (100%) create mode 100644 test/form/samples/import-assertions/keeps-assertions/_expected/es.js rename test/form/samples/import-assertions/{ignores-assertions => keeps-assertions}/_expected/iife.js (100%) rename test/form/samples/import-assertions/{ignores-assertions => keeps-assertions}/_expected/system.js (100%) rename test/form/samples/import-assertions/{ignores-assertions => keeps-assertions}/_expected/umd.js (100%) rename test/form/samples/import-assertions/{ignores-assertions => keeps-assertions}/main.js (79%) diff --git a/browser/src/resolveId.ts b/browser/src/resolveId.ts index 494a3792941..c36b095ced1 100644 --- a/browser/src/resolveId.ts +++ b/browser/src/resolveId.ts @@ -18,6 +18,7 @@ export async function resolveId( importer: string | undefined, customOptions: CustomPluginOptions | undefined, isEntry: boolean | undefined, + assertions: Record, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null ) => Promise, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, diff --git a/src/ExternalModule.ts b/src/ExternalModule.ts index 891bd1e5adc..78d8a3e2e92 100644 --- a/src/ExternalModule.ts +++ b/src/ExternalModule.ts @@ -1,6 +1,6 @@ import ExternalVariable from './ast/variables/ExternalVariable'; import type { CustomPluginOptions, ModuleInfo, NormalizedInputOptions } from './rollup/types'; -import { EMPTY_ARRAY, EMPTY_OBJECT } from './utils/blank'; +import { EMPTY_ARRAY } from './utils/blank'; import { errUnusedExternalImports, warnDeprecation } from './utils/error'; import { makeLegal } from './utils/identifierHelpers'; @@ -23,14 +23,14 @@ export default class ExternalModule { public readonly id: string, moduleSideEffects: boolean | 'no-treeshake', meta: CustomPluginOptions, - public readonly renormalizeRenderPath: boolean + public readonly renormalizeRenderPath: boolean, + assertions: Record ) { this.suggestedVariableName = makeLegal(id.split(/[\\/]/).pop()!); const { importers, dynamicImporters } = this; const info: ModuleInfo = (this.info = { - // TODO Lukas use correct assertions - assertions: EMPTY_OBJECT, + assertions, ast: null, code: null, dynamicallyImportedIdResolutions: EMPTY_ARRAY, diff --git a/src/Module.ts b/src/Module.ts index df0a2a83f55..8ec1d2543f9 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -10,6 +10,7 @@ import ExportAllDeclaration from './ast/nodes/ExportAllDeclaration'; import ExportDefaultDeclaration from './ast/nodes/ExportDefaultDeclaration'; import type ExportNamedDeclaration from './ast/nodes/ExportNamedDeclaration'; import type Identifier from './ast/nodes/Identifier'; +import ImportAttribute from './ast/nodes/ImportAttribute'; import type ImportDeclaration from './ast/nodes/ImportDeclaration'; import type ImportExpression from './ast/nodes/ImportExpression'; import Literal from './ast/nodes/Literal'; @@ -65,6 +66,7 @@ import { getId } from './utils/getId'; import { getOrCreate } from './utils/getOrCreate'; import { getOriginalLocation } from './utils/getOriginalLocation'; import { makeLegal } from './utils/identifierHelpers'; +import { getAssertionsFromImportExportDeclaration } from './utils/parseAssertions'; import { basename, extname } from './utils/path'; import type { RenderOptions } from './utils/renderHelpers'; import { timeEnd, timeStart } from './utils/timers'; @@ -223,7 +225,7 @@ export default class Module { declare scope: ModuleScope; readonly sideEffectDependenciesByVariable = new Map>(); declare sourcemapChain: DecodedSourceMapOrMissing[]; - readonly sources = new Set(); + readonly sourcesWithAssertions = new Map>(); declare transformFiles?: EmittedFile[]; private allExportNames: Set | null = null; @@ -270,7 +272,7 @@ export default class Module { implicitlyLoadedBefore, importers, reexportDescriptions, - sources + sourcesWithAssertions } = this; this.info = { @@ -314,12 +316,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(); @@ -903,7 +911,7 @@ export default class Module { }); } else if (node instanceof ExportAllDeclaration) { const source = node.source.value; - this.sources.add(source); + this.addSource(source, node.assertions); if (node.exported) { // export * as name from './other' @@ -923,7 +931,7 @@ export default class Module { // export { name } from './other' const source = node.source.value; - this.sources.add(source); + this.addSource(source, node.assertions); for (const specifier of node.specifiers) { const name = specifier.exported.name; this.reexportDescriptions.set(name, { @@ -963,7 +971,7 @@ export default class Module { private addImport(node: ImportDeclaration): void { const source = node.source.value; - this.sources.add(source); + this.addSource(source, node.assertions); for (const specifier of node.specifiers) { const isDefault = specifier.type === NodeType.ImportDefaultSpecifier; const isNamespace = specifier.type === NodeType.ImportNamespaceSpecifier; @@ -1042,6 +1050,11 @@ export default class Module { addSideEffectDependencies(alwaysCheckedDependencies); } + private addSource(source: string, assertions: ImportAttribute[] | undefined) { + // TODO Lukas handle existing and conflicting sources + this.sourcesWithAssertions.set(source, getAssertionsFromImportExportDeclaration(assertions)); + } + private getVariableFromNamespaceReexports( name: string, importerForSideEffects?: Module, diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 4113123bde5..8a7c411a44d 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -30,6 +30,7 @@ import { errUnresolvedImportTreatedAsExternal } from './utils/error'; import { promises as fs } from './utils/fs'; +import { getAssertionsFromImportExpression } from './utils/parseAssertions'; import { isAbsolute, isRelative, resolve } from './utils/path'; import relativeId from './utils/relativeId'; import { resolveId } from './utils/resolveId'; @@ -174,7 +175,8 @@ export class ModuleLoader { resolvedId: { id: string; resolveDependencies?: boolean } & Partial> ): Promise { const module = await this.fetchModule( - this.getResolvedIdWithDefaults(resolvedId)!, + // TODO Lukas use correct assertions from resolvedId + this.getResolvedIdWithDefaults(resolvedId, EMPTY_OBJECT)!, undefined, false, resolvedId.resolveDependencies ? RESOLVE_DEPENDENCIES : true @@ -187,9 +189,10 @@ export class ModuleLoader { importer: string | undefined, customOptions: CustomPluginOptions | undefined, isEntry: boolean | undefined, + assertions: Record, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null = null - ): Promise => { - return this.getResolvedIdWithDefaults( + ): Promise => + this.getResolvedIdWithDefaults( this.getNormalizedResolvedIdWithoutDefaults( this.options.external(source, importer, false) ? false @@ -203,12 +206,11 @@ export class ModuleLoader { customOptions, typeof isEntry === 'boolean' ? isEntry : !importer ), - importer, source - ) + ), + assertions ); - }; private addEntryWithImplicitDependants( unresolvedModule: UnresolvedModule, @@ -411,8 +413,10 @@ export class ModuleLoader { importer: string, resolvedId: ResolvedId ): Promise { + // TODO Lukas handle internal if (resolvedId.external) { - const { external, id, moduleSideEffects, meta } = resolvedId; + const { assertions, external, id, moduleSideEffects, meta } = resolvedId; + // TODO Lukas check assert for existing module if (!this.modulesById.has(id)) { this.modulesById.set( id, @@ -421,7 +425,8 @@ export class ModuleLoader { id, moduleSideEffects, meta, - external !== 'absolute' && isAbsolute(id) + external !== 'absolute' && isAbsolute(id), + assertions ) ); } @@ -512,7 +517,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,14 +529,14 @@ 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), + await this.resolveId(source, module.id, EMPTY_OBJECT, false, assertions), source, module.id )) @@ -539,15 +545,16 @@ export class ModuleLoader { } private getResolvedIdWithDefaults( - resolvedId: NormalizedResolveIdWithoutDefaults | null + resolvedId: NormalizedResolveIdWithoutDefaults | null, + assertions: Record ): ResolvedId | null { if (!resolvedId) { return null; } const external = resolvedId.external || false; + // TODO Lukas also consider assertions from resolvedId return { - // TODO Lukas use correct assertions from import - assertions: EMPTY_OBJECT, + assertions, external, id: resolvedId.id, meta: resolvedId.meta || {}, @@ -634,10 +641,12 @@ export class ModuleLoader { ); } return this.fetchModule( + // TODO Lukas use correct assertions from input this.getResolvedIdWithDefaults( typeof resolveIdResult === 'object' ? (resolveIdResult as NormalizedResolveIdWithoutDefaults) - : { id: resolveIdResult } + : { id: resolveIdResult }, + EMPTY_OBJECT )!, undefined, isEntry, @@ -648,7 +657,8 @@ export class ModuleLoader { private async resolveDynamicImport( module: Module, specifier: string | acorn.Node, - importer: string + importer: string, + assertions: Record ): Promise { const resolution = await this.pluginDriver.hookFirst('resolveDynamicImport', [ specifier, @@ -661,22 +671,26 @@ export class ModuleLoader { if (!resolution) { return null; } + // TODO Lukas use withDefaults logic instead? Merge with other resolution case? return { + assertions, external: false, moduleSideEffects: true, ...resolution } as ResolvedId; } if (resolution == null) { + // TODO Lukas handle existing resolved id conflicts return (module.resolvedIds[specifier] ??= this.handleResolveId( - await this.resolveId(specifier, module.id, EMPTY_OBJECT, false), + await this.resolveId(specifier, module.id, EMPTY_OBJECT, false, assertions), specifier, module.id )); } return this.handleResolveId( this.getResolvedIdWithDefaults( - this.getNormalizedResolvedIdWithoutDefaults(resolution, importer, specifier) + this.getNormalizedResolvedIdWithoutDefaults(resolution, importer, specifier), + assertions ), specifier, importer diff --git a/src/ast/nodes/ImportDeclaration.ts b/src/ast/nodes/ImportDeclaration.ts index d642fa527eb..249271d0aed 100644 --- a/src/ast/nodes/ImportDeclaration.ts +++ b/src/ast/nodes/ImportDeclaration.ts @@ -9,7 +9,7 @@ import type * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class ImportDeclaration extends NodeBase { - declare assertions: ImportAttribute[]; + declare assertions?: ImportAttribute[]; declare needsBoundaries: true; declare source: Literal; declare specifiers: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]; diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 8a871c4e734..45bb21c9253 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -9,7 +9,7 @@ import type { } from '../rollup/types'; import type { FileEmitter } from './FileEmitter'; import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; -import { BLANK } from './blank'; +import { BLANK, EMPTY_OBJECT } from './blank'; import { BuildPhase } from './buildPhase'; import { errInvalidRollupPhaseForAddWatchFile, @@ -99,6 +99,8 @@ export function getPluginContext( importer, custom, isEntry, + // TODO Lukas use correct assertions + EMPTY_OBJECT, skipSelf ? [{ importer, plugin, source }] : null ); }, diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index 6c8cf082710..183d33c7d99 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -356,8 +356,9 @@ function getExports( return configExports || 'auto'; } -const defaultAssertions = { '.json': 'json' }; - +// TODO Lukas for truly dynamic imports, setting `false` should still prevent assertions +// How about a special format, e.g. id: null, specifier: AcornNode, importer: string +// TODO Lukas the default is now `true` function getExternalImportAssertions( config: OutputOptions ): NormalizedOutputOptions['externalImportAssertions'] { @@ -368,11 +369,13 @@ function getExternalImportAssertions( if (configExternalImportAssertions === false) { return () => null; } - const typeAssertions = configExternalImportAssertions || defaultAssertions; - return ({ id }) => { - const type = typeAssertions[extname(id)]; - return type ? { type } : null; - }; + if (configExternalImportAssertions) { + return ({ id }) => { + const type = configExternalImportAssertions[extname(id)]; + return type ? { type } : null; + }; + } + return ({ assertions }) => assertions; } const getGeneratedCode = ( diff --git a/src/utils/parseAssertions.ts b/src/utils/parseAssertions.ts new file mode 100644 index 00000000000..98c95272bf6 --- /dev/null +++ b/src/utils/parseAssertions.ts @@ -0,0 +1,49 @@ +import Identifier from '../ast/nodes/Identifier'; +import ImportAttribute from '../ast/nodes/ImportAttribute'; +import ImportExpression from '../ast/nodes/ImportExpression'; +import Literal, { LiteralValue } from '../ast/nodes/Literal'; +import ObjectExpression from '../ast/nodes/ObjectExpression'; +import Property from '../ast/nodes/Property'; +import SpreadElement from '../ast/nodes/SpreadElement'; +import { EMPTY_OBJECT } from './blank'; + +export function getAssertionsFromImportExpression(node: ImportExpression): Record { + const assertProperty = node.arguments?.[0]?.properties.find( + (property): property is Property => getPropertyKey(property) === 'assert' + )?.value; + if (!assertProperty) { + return EMPTY_OBJECT; + } + const assertFields = (assertProperty as ObjectExpression).properties + .map(property => { + const key = getPropertyKey(property); + if ( + typeof key === 'string' && + typeof ((property as Property).value as Literal)?.value === 'string' + ) { + return [key, ((property as Property).value as Literal).value] as [string, string]; + } + return null; + }) + .filter((property): property is [string, string] => !!property); + if (assertFields.length > 0) { + return Object.fromEntries(assertFields); + } + return EMPTY_OBJECT; +} + +const getPropertyKey = ( + property: Property | SpreadElement | ImportAttribute +): LiteralValue | undefined => + ((property as Property | ImportAttribute).key as Identifier).name || + ((property as Property | ImportAttribute).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; +} diff --git a/src/utils/resolveId.ts b/src/utils/resolveId.ts index d2848d13acd..1695342189e 100644 --- a/src/utils/resolveId.ts +++ b/src/utils/resolveId.ts @@ -9,11 +9,13 @@ export async function resolveId( importer: string | undefined, preserveSymlinks: boolean, pluginDriver: PluginDriver, + // TODO Lukas extract/reuse type moduleLoaderResolveId: ( source: string, importer: string | undefined, customOptions: CustomPluginOptions | undefined, isEntry: boolean | undefined, + assertions: Record, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null ) => Promise, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, diff --git a/src/utils/resolveIdViaPlugins.ts b/src/utils/resolveIdViaPlugins.ts index 02a4975a8ca..a4b77c14691 100644 --- a/src/utils/resolveIdViaPlugins.ts +++ b/src/utils/resolveIdViaPlugins.ts @@ -6,17 +6,19 @@ import type { 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, + // TODO Lukas extract/reuse type moduleLoaderResolveId: ( source: string, importer: string | undefined, customOptions: CustomPluginOptions | undefined, isEntry: boolean | undefined, + assertions: Record, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null ) => Promise, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, @@ -40,6 +42,8 @@ export function resolveIdViaPlugins( importer, custom, isEntry, + // TODO Lukas use assertions provided via this.resolve + EMPTY_OBJECT, skipSelf ? [...skip, { importer, plugin, source }] : skip ); } diff --git a/test/form/samples/import-assertions/assertion-type-via-extension/_config.js b/test/form/samples/import-assertions/assertion-type-via-extension/_config.js deleted file mode 100644 index b87e99d61ad..00000000000 --- a/test/form/samples/import-assertions/assertion-type-via-extension/_config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - description: 'allows to configure the "type" assertion via a shorthand', - options: { - external: true, - output: { - name: 'bundle', - externalImportAssertions: { - '.json': null, - '.css': 'css', - '.foo': 'special', - '': 'empty' - } - } - } -}; diff --git a/test/form/samples/import-assertions/assertion-type-via-extension/_expected.js b/test/form/samples/import-assertions/assertion-type-via-extension/_expected.js deleted file mode 100644 index 17833f57d42..00000000000 --- a/test/form/samples/import-assertions/assertion-type-via-extension/_expected.js +++ /dev/null @@ -1,11 +0,0 @@ -import 'external.json'; -import 'external.css' assert { type: 'css' }; -import 'external.foo' assert { type: 'special' }; -import 'external' assert { type: 'empty' }; -import 'external.ignored'; - -import('external.json'); -import('external.css', { assert: { type: 'css' } }); -import('external.foo', { assert: { type: 'special' } }); -import('external', { assert: { type: 'empty' } }); -import('external.ignored'); diff --git a/test/form/samples/import-assertions/assertion-type-via-extension/main.js b/test/form/samples/import-assertions/assertion-type-via-extension/main.js deleted file mode 100644 index f41f906c4e6..00000000000 --- a/test/form/samples/import-assertions/assertion-type-via-extension/main.js +++ /dev/null @@ -1,11 +0,0 @@ -import 'external.json'; -import 'external.css'; -import 'external.foo'; -import 'external'; -import 'external.ignored'; - -import('external.json'); -import('external.css'); -import('external.foo'); -import('external'); -import('external.ignored'); diff --git a/test/form/samples/import-assertions/ignores-assertions/_expected/es.js b/test/form/samples/import-assertions/ignores-assertions/_expected/es.js deleted file mode 100644 index 53ebc4b190e..00000000000 --- a/test/form/samples/import-assertions/ignores-assertions/_expected/es.js +++ /dev/null @@ -1,7 +0,0 @@ -import { a } from 'a'; -import { b } from 'b'; -export { c } from 'c'; - -console.log(a, b); - -import('d').then(console.log); diff --git a/test/form/samples/import-assertions/import-assertions-function/_config.js b/test/form/samples/import-assertions/import-assertions-function/_config.js deleted file mode 100644 index a329fb8f391..00000000000 --- a/test/form/samples/import-assertions/import-assertions-function/_config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - description: 'adds import assertions for external JSON files', - options: { - external: true, - output: { - name: 'bundle', - externalImportAssertions({ id, ...other }) { - const [, assertions] = id.split('?'); - if (assertions) { - return Object.fromEntries(assertions.split('&').map(assertion => assertion.split('='))); - } - } - } - } -}; diff --git a/test/form/samples/import-assertions/import-assertions-function/_expected.js b/test/form/samples/import-assertions/import-assertions-function/_expected.js deleted file mode 100644 index 8da792d4bdd..00000000000 --- a/test/form/samples/import-assertions/import-assertions-function/_expected.js +++ /dev/null @@ -1,7 +0,0 @@ -import 'external'; -import 'external?type=foo' assert { type: 'foo' }; -import 'external?type=bar&foo=baz' assert { type: 'bar', foo: 'baz' }; - -import('external'); -import('external?type=foo', { assert: { type: 'foo' } }); -import('external?type=bar&foo=baz', { assert: { type: 'bar', foo: 'baz' } }); diff --git a/test/form/samples/import-assertions/import-assertions-function/main.js b/test/form/samples/import-assertions/import-assertions-function/main.js deleted file mode 100644 index e2156a99127..00000000000 --- a/test/form/samples/import-assertions/import-assertions-function/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import 'external'; -import 'external?type=foo'; -import 'external?type=bar&foo=baz'; - -import('external'); -import('external?type=foo'); -import('external?type=bar&foo=baz'); diff --git a/test/form/samples/import-assertions/json-import-assertions/_config.js b/test/form/samples/import-assertions/json-import-assertions/_config.js deleted file mode 100644 index dc865ea6e18..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/_config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - description: 'adds import assertions for external JSON files', - options: { - external: ['./foo.json'], - output: { name: 'bundle' } - } -}; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js b/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js deleted file mode 100644 index e4f1e18a2c3..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/amd.js +++ /dev/null @@ -1,26 +0,0 @@ -define(['require', 'exports', './foo.json'], (function (require, exports, json) { '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); - } - - console.log(json); - - new Promise(function (resolve, reject) { require(['./foo.json'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }).then(console.log); - - exports.json = json; - -})); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js b/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js deleted file mode 100644 index b76edb2d727..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/cjs.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -var json = require('./foo.json'); - -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); -} - -console.log(json); - -Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('./foo.json')); }).then(console.log); - -exports.json = json; diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/es.js b/test/form/samples/import-assertions/json-import-assertions/_expected/es.js deleted file mode 100644 index 97f9536e767..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/es.js +++ /dev/null @@ -1,6 +0,0 @@ -import json from './foo.json' assert { type: 'json' }; -export { default as json } from './foo.json' assert { type: 'json' }; - -console.log(json); - -import('./foo.json', { assert: { type: 'json' } }).then(console.log); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js b/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js deleted file mode 100644 index 878816f533b..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/iife.js +++ /dev/null @@ -1,12 +0,0 @@ -var bundle = (function (exports, json) { - 'use strict'; - - console.log(json); - - import('./foo.json').then(console.log); - - exports.json = json; - - return exports; - -})({}, json); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/system.js b/test/form/samples/import-assertions/json-import-assertions/_expected/system.js deleted file mode 100644 index 1802824a6a9..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/system.js +++ /dev/null @@ -1,17 +0,0 @@ -System.register('bundle', ['./foo.json'], (function (exports, module) { - 'use strict'; - var json; - return { - setters: [function (module) { - json = module.default; - exports('json', module.default); - }], - execute: (function () { - - console.log(json); - - module.import('./foo.json').then(console.log); - - }) - }; -})); diff --git a/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js b/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js deleted file mode 100644 index 812cbae8249..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/_expected/umd.js +++ /dev/null @@ -1,13 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./foo.json')) : - typeof define === 'function' && define.amd ? define(['exports', './foo.json'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {}, global.json)); -})(this, (function (exports, json) { 'use strict'; - - console.log(json); - - import('./foo.json').then(console.log); - - exports.json = json; - -})); diff --git a/test/form/samples/import-assertions/json-import-assertions/main.js b/test/form/samples/import-assertions/json-import-assertions/main.js deleted file mode 100644 index 7e39c113278..00000000000 --- a/test/form/samples/import-assertions/json-import-assertions/main.js +++ /dev/null @@ -1,6 +0,0 @@ -import json from './foo.json'; -console.log(json); - -export { default as json } from './foo.json'; - -import('./foo.json').then(console.log); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js index 14e4b6662e6..61c736507ee 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js @@ -1,6 +1,23 @@ module.exports = { - description: 'keep import assertions for non-resolvable dynamic imports', + description: 'keep import assertions for dynamic imports', options: { - external: true + external: true, + plugins: [ + { + resolveDynamicImport(specifier, importer) { + if (typeof specifier === 'object') { + if (specifier.type === 'TemplateLiteral') { + return "'resolvedString'"; + } + if (specifier.type === 'BinaryExpression') { + return { id: 'resolved-id', external: true }; + } + } else if (specifier === 'external-resolved') { + return { id: 'resolved-different', external: true }; + } + return null; + } + } + ] } }; diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js index 622ca8b31b8..66e0a84ccc0 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js @@ -17,6 +17,10 @@ define(['require'], (function (require) { 'use strict'; 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); }); })); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js index 440ea28ac47..10dbb214e3a 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js @@ -17,4 +17,8 @@ function _interopNamespaceDefault(e) { return Object.freeze(n); } +Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('external')); }); (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require(t)); }); })(globalThis.unknown); +(function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require(t)); }); })('resolvedString'); +Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('resolved-id')); }); +Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('resolved-different')); }); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js index 63795c9009a..3fc4b2567af 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js @@ -1 +1,5 @@ +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' } }); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js index da9cafa781a..eec48d152c6 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js @@ -1,6 +1,10 @@ (function () { 'use strict'; + import('external'); import(globalThis.unknown); + import('resolvedString'); + import('resolved-id'); + import('resolved-different'); })(); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js index 264d8ebb609..b6bafaa6fab 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js @@ -3,7 +3,11 @@ System.register([], (function (exports, module) { return { execute: (function () { + module.import('external'); module.import(globalThis.unknown); + module.import('resolvedString'); + module.import('resolved-id'); + module.import('resolved-different'); }) }; diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js index bc60999ca93..1bf0a9af4dd 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js @@ -3,6 +3,10 @@ factory(); })((function () { 'use strict'; + import('external'); import(globalThis.unknown); + import('resolvedString'); + import('resolved-id'); + import('resolved-different'); })); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/main.js b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js index 63795c9009a..a60049128b5 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/main.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js @@ -1 +1,5 @@ +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' } }); diff --git a/test/form/samples/import-assertions/ignores-assertions/_config.js b/test/form/samples/import-assertions/keeps-assertions/_config.js similarity index 61% rename from test/form/samples/import-assertions/ignores-assertions/_config.js rename to test/form/samples/import-assertions/keeps-assertions/_config.js index 02fe9ff6617..c796a54e4a0 100644 --- a/test/form/samples/import-assertions/ignores-assertions/_config.js +++ b/test/form/samples/import-assertions/keeps-assertions/_config.js @@ -1,5 +1,5 @@ module.exports = { - description: 'ignores any import assertions on input', + description: 'keeps any import assertions on input', options: { external: () => true, output: { name: 'bundle' } diff --git a/test/form/samples/import-assertions/ignores-assertions/_expected/amd.js b/test/form/samples/import-assertions/keeps-assertions/_expected/amd.js similarity index 100% rename from test/form/samples/import-assertions/ignores-assertions/_expected/amd.js rename to test/form/samples/import-assertions/keeps-assertions/_expected/amd.js diff --git a/test/form/samples/import-assertions/ignores-assertions/_expected/cjs.js b/test/form/samples/import-assertions/keeps-assertions/_expected/cjs.js similarity index 100% rename from test/form/samples/import-assertions/ignores-assertions/_expected/cjs.js rename to test/form/samples/import-assertions/keeps-assertions/_expected/cjs.js diff --git a/test/form/samples/import-assertions/keeps-assertions/_expected/es.js b/test/form/samples/import-assertions/keeps-assertions/_expected/es.js new file mode 100644 index 00000000000..a986aea2757 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-assertions/_expected/es.js @@ -0,0 +1,7 @@ +import { a } from 'a' assert { type: 'a' }; +import { b } from 'b' assert { foo: 'bar', baz: 'quuz' }; +export { c } from 'c' assert { type: 'c' }; + +console.log(a, b); + +import('d', { assert: { type: 'd' } }).then(console.log); diff --git a/test/form/samples/import-assertions/ignores-assertions/_expected/iife.js b/test/form/samples/import-assertions/keeps-assertions/_expected/iife.js similarity index 100% rename from test/form/samples/import-assertions/ignores-assertions/_expected/iife.js rename to test/form/samples/import-assertions/keeps-assertions/_expected/iife.js diff --git a/test/form/samples/import-assertions/ignores-assertions/_expected/system.js b/test/form/samples/import-assertions/keeps-assertions/_expected/system.js similarity index 100% rename from test/form/samples/import-assertions/ignores-assertions/_expected/system.js rename to test/form/samples/import-assertions/keeps-assertions/_expected/system.js diff --git a/test/form/samples/import-assertions/ignores-assertions/_expected/umd.js b/test/form/samples/import-assertions/keeps-assertions/_expected/umd.js similarity index 100% rename from test/form/samples/import-assertions/ignores-assertions/_expected/umd.js rename to test/form/samples/import-assertions/keeps-assertions/_expected/umd.js diff --git a/test/form/samples/import-assertions/ignores-assertions/main.js b/test/form/samples/import-assertions/keeps-assertions/main.js similarity index 79% rename from test/form/samples/import-assertions/ignores-assertions/main.js rename to test/form/samples/import-assertions/keeps-assertions/main.js index 50630c3143a..018ccbd2b06 100644 --- a/test/form/samples/import-assertions/ignores-assertions/main.js +++ b/test/form/samples/import-assertions/keeps-assertions/main.js @@ -1,4 +1,4 @@ -import { a } from 'a' assert { type: 'foo' }; +import { a } from 'a' assert { type: 'a' }; import { b } from 'b' assert { foo: 'bar', baz: 'quuz' }; console.log(a, b); From 8cfa724ef8801ac74b04aaf2b8e9c71e2ae46775 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 8 Oct 2022 06:49:38 +0200 Subject: [PATCH 11/17] Make option a boolean --- src/Chunk.ts | 5 ++- src/ExternalChunk.ts | 4 +- src/ModuleLoader.ts | 23 ++++++----- src/rollup/types.d.ts | 11 +----- src/utils/options/normalizeOutputOptions.ts | 27 ++----------- .../import-assertions-false/_config.js | 8 ---- .../import-assertions-false/_expected.js | 6 --- .../import-assertions-false/main.js | 6 --- .../keep-dynamic-assertions/_config.js | 7 +++- .../keep-dynamic-assertions/_expected/amd.js | 1 + .../keep-dynamic-assertions/_expected/cjs.js | 28 +++---------- .../keep-dynamic-assertions/_expected/es.js | 1 + .../keep-dynamic-assertions/_expected/iife.js | 1 + .../_expected/system.js | 1 + .../keep-dynamic-assertions/_expected/umd.js | 1 + .../keep-dynamic-assertions/main.js | 1 + .../keeps-assertions/_config.js | 7 ---- .../keeps-assertions/_expected/es.js | 7 ---- .../keeps-assertions/_expected/iife.js | 15 ------- .../keeps-assertions/_expected/system.js | 20 ---------- .../keeps-assertions/_expected/umd.js | 16 -------- .../keeps-assertions/main.js | 7 ---- .../keeps-static-assertions/_config.js | 12 ++++++ .../keeps-static-assertions/_expected/amd.js | 35 +++++++++++++++++ .../_expected/cjs.js | 12 +++++- .../keeps-static-assertions/_expected/es.js | 7 ++++ .../_expected/iife.js} | 17 ++++++-- .../_expected/system.js | 28 +++++++++++++ .../keeps-static-assertions/_expected/umd.js | 39 +++++++++++++++++++ .../keeps-static-assertions/main.js | 9 +++++ .../removes-dynamic-assertions/_config.js | 29 ++++++++++++++ .../removes-dynamic-assertions/_expected.js | 6 +++ .../removes-dynamic-assertions/main.js | 6 +++ .../removes-static-assertions/_config.js | 12 ++++++ .../removes-static-assertions/_expected.js | 7 ++++ .../removes-static-assertions/main.js | 9 +++++ 36 files changed, 265 insertions(+), 166 deletions(-) delete mode 100644 test/form/samples/import-assertions/import-assertions-false/_config.js delete mode 100644 test/form/samples/import-assertions/import-assertions-false/_expected.js delete mode 100644 test/form/samples/import-assertions/import-assertions-false/main.js delete mode 100644 test/form/samples/import-assertions/keeps-assertions/_config.js delete mode 100644 test/form/samples/import-assertions/keeps-assertions/_expected/es.js delete mode 100644 test/form/samples/import-assertions/keeps-assertions/_expected/iife.js delete mode 100644 test/form/samples/import-assertions/keeps-assertions/_expected/system.js delete mode 100644 test/form/samples/import-assertions/keeps-assertions/_expected/umd.js delete mode 100644 test/form/samples/import-assertions/keeps-assertions/main.js create mode 100644 test/form/samples/import-assertions/keeps-static-assertions/_config.js create mode 100644 test/form/samples/import-assertions/keeps-static-assertions/_expected/amd.js rename test/form/samples/import-assertions/{keeps-assertions => keeps-static-assertions}/_expected/cjs.js (61%) create mode 100644 test/form/samples/import-assertions/keeps-static-assertions/_expected/es.js rename test/form/samples/import-assertions/{keeps-assertions/_expected/amd.js => keeps-static-assertions/_expected/iife.js} (54%) create mode 100644 test/form/samples/import-assertions/keeps-static-assertions/_expected/system.js create mode 100644 test/form/samples/import-assertions/keeps-static-assertions/_expected/umd.js create mode 100644 test/form/samples/import-assertions/keeps-static-assertions/main.js create mode 100644 test/form/samples/import-assertions/removes-dynamic-assertions/_config.js create mode 100644 test/form/samples/import-assertions/removes-dynamic-assertions/_expected.js create mode 100644 test/form/samples/import-assertions/removes-dynamic-assertions/main.js create mode 100644 test/form/samples/import-assertions/removes-static-assertions/_config.js create mode 100644 test/form/samples/import-assertions/removes-static-assertions/_expected.js create mode 100644 test/form/samples/import-assertions/removes-static-assertions/main.js diff --git a/src/Chunk.ts b/src/Chunk.ts index 0c18fa732b6..2f8182455b2 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -862,7 +862,10 @@ export default class Chunk { const chunk = this.externalChunkByModule.get(resolution)!; return [`'${chunk.getImportPath(fileName)}'`, chunk.getImportAssertions(this.snippets)]; } - return [resolution || '', this.outputOptions.format === 'es' || null]; + return [ + resolution || '', + (this.outputOptions.format === 'es' && this.outputOptions.externalImportAssertions) || null + ]; } private getFallbackChunkName(): string { diff --git a/src/ExternalChunk.ts b/src/ExternalChunk.ts index b875a86422c..43a07d913ac 100644 --- a/src/ExternalChunk.ts +++ b/src/ExternalChunk.ts @@ -40,7 +40,9 @@ export default class ExternalChunk { getImportAssertions(snippets: GenerateCodeSnippets): string | null { return (this.importAssertions ||= formatAssertions( - this.options.format === 'es' && this.options.externalImportAssertions(this.moduleInfo), + this.options.format === 'es' && + this.options.externalImportAssertions && + this.moduleInfo.assertions, snippets )); } diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 8a7c411a44d..526f66053a6 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -535,10 +535,11 @@ export class ModuleLoader { source, (module.resolvedIds[source] = module.resolvedIds[source] || - this.handleResolveId( + this.handleInvalidResolvedId( await this.resolveId(source, module.id, EMPTY_OBJECT, false, assertions), source, - module.id + module.id, + assertions )) ] as [string, ResolvedId] ); @@ -582,10 +583,11 @@ export class ModuleLoader { return this.fetchModuleDependencies(module, ...(await loadPromise)); } - private handleResolveId( + private handleInvalidResolvedId( resolvedId: ResolvedId | null, source: string, - importer: string + importer: string, + assertions: Record ): ResolvedId { if (resolvedId === null) { if (isRelative(source)) { @@ -593,8 +595,7 @@ export class ModuleLoader { } this.options.onwarn(errUnresolvedImportTreatedAsExternal(source, importer)); return { - // TODO Lukas use correct assertions from import - assertions: EMPTY_OBJECT, + assertions, external: true, id: source, meta: {}, @@ -681,19 +682,21 @@ export class ModuleLoader { } if (resolution == null) { // TODO Lukas handle existing resolved id conflicts - return (module.resolvedIds[specifier] ??= this.handleResolveId( + 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), assertions ), specifier, - importer + importer, + assertions ); } } diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 1c229f76610..8a461bdddd6 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -565,13 +565,6 @@ interface GeneratedCodeOptions extends Partial { export type OptionsPaths = Record | ((id: string) => string); -type GetExternalImportAssertions = (moduleInfo: ModuleInfo) => Record | null | void; - -type ExternalImportAssertionsOption = - | GetExternalImportAssertions - | false - | Record; - export type InteropType = 'compat' | 'auto' | 'esModule' | 'default' | 'defaultOnly'; export type GetInterop = (id: string | null) => InteropType; @@ -626,7 +619,7 @@ export interface OutputOptions { esModule?: boolean | 'if-default-prop'; exports?: 'default' | 'named' | 'none' | 'auto'; extend?: boolean; - externalImportAssertions?: ExternalImportAssertionsOption; + externalImportAssertions?: boolean; externalLiveBindings?: boolean; // only required for bundle.write file?: string; @@ -678,7 +671,7 @@ export interface NormalizedOutputOptions { esModule: boolean | 'if-default-prop'; exports: 'default' | 'named' | 'none' | 'auto'; extend: boolean; - externalImportAssertions: GetExternalImportAssertions; + externalImportAssertions: boolean; externalLiveBindings: boolean; file: string | undefined; footer: AddonFunction; diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index 183d33c7d99..ded7a7d6709 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -8,7 +8,7 @@ import type { } from '../../rollup/types'; import { ensureArray } from '../ensureArray'; import { errInvalidExportOptionValue, errInvalidOption, error, warnDeprecation } from '../error'; -import { extname, resolve } from '../path'; +import { resolve } from '../path'; import { sanitizeFileName as defaultSanitizeFileName } from '../sanitizeFileName'; import { isValidUrl } from '../url'; import { @@ -48,7 +48,8 @@ export function normalizeOutputOptions( esModule: config.esModule ?? 'if-default-prop', exports: getExports(config, unsetOptions), extend: config.extend || false, - externalImportAssertions: getExternalImportAssertions(config), + // TODO Lukas for truly dynamic imports, setting `false` should still prevent assertions + externalImportAssertions: config.externalImportAssertions ?? true, externalLiveBindings: config.externalLiveBindings ?? true, file, footer: getAddon(config, 'footer'), @@ -356,28 +357,6 @@ function getExports( return configExports || 'auto'; } -// TODO Lukas for truly dynamic imports, setting `false` should still prevent assertions -// How about a special format, e.g. id: null, specifier: AcornNode, importer: string -// TODO Lukas the default is now `true` -function getExternalImportAssertions( - config: OutputOptions -): NormalizedOutputOptions['externalImportAssertions'] { - const configExternalImportAssertions = config.externalImportAssertions; - if (typeof configExternalImportAssertions === 'function') { - return configExternalImportAssertions; - } - if (configExternalImportAssertions === false) { - return () => null; - } - if (configExternalImportAssertions) { - return ({ id }) => { - const type = configExternalImportAssertions[extname(id)]; - return type ? { type } : null; - }; - } - return ({ assertions }) => assertions; -} - const getGeneratedCode = ( config: OutputOptions, preferConst: boolean diff --git a/test/form/samples/import-assertions/import-assertions-false/_config.js b/test/form/samples/import-assertions/import-assertions-false/_config.js deleted file mode 100644 index cd993537de9..00000000000 --- a/test/form/samples/import-assertions/import-assertions-false/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - description: - 'does not add import assertions for external JSON files when assertions are disabled', - options: { - external: ['./foo.json'], - output: { name: 'bundle', externalImportAssertions: false } - } -}; diff --git a/test/form/samples/import-assertions/import-assertions-false/_expected.js b/test/form/samples/import-assertions/import-assertions-false/_expected.js deleted file mode 100644 index 15269e6d861..00000000000 --- a/test/form/samples/import-assertions/import-assertions-false/_expected.js +++ /dev/null @@ -1,6 +0,0 @@ -import json from './foo.json'; -export { default as json } from './foo.json'; - -console.log(json); - -import('./foo.json').then(console.log); diff --git a/test/form/samples/import-assertions/import-assertions-false/main.js b/test/form/samples/import-assertions/import-assertions-false/main.js deleted file mode 100644 index 7e39c113278..00000000000 --- a/test/form/samples/import-assertions/import-assertions-false/main.js +++ /dev/null @@ -1,6 +0,0 @@ -import json from './foo.json'; -console.log(json); - -export { default as json } from './foo.json'; - -import('./foo.json').then(console.log); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js index 61c736507ee..ff6fa2a5fe3 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js @@ -1,7 +1,12 @@ module.exports = { + // solo: true, description: 'keep import assertions for dynamic imports', + expectedWarnings: 'UNRESOLVED_IMPORT', options: { - external: true, + external: id => { + if (id === 'unresolved') return null; + return true; + }, plugins: [ { resolveDynamicImport(specifier, importer) { diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js index 66e0a84ccc0..e9478c8fc48 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/amd.js @@ -22,5 +22,6 @@ define(['require'], (function (require) { 'use strict'; (function (t) { return new Promise(function (resolve, reject) { require([t], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); })('resolvedString'); new Promise(function (resolve, reject) { require(['resolved-id'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); new Promise(function (resolve, reject) { require(['resolved-different'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); + new Promise(function (resolve, reject) { require(['unresolved'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); })); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js index 10dbb214e3a..e1cd2b6d0da 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/cjs.js @@ -1,24 +1,8 @@ '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); -} - -Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('external')); }); -(function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require(t)); }); })(globalThis.unknown); -(function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require(t)); }); })('resolvedString'); -Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('resolved-id')); }); -Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('resolved-different')); }); +import('external'); +import(globalThis.unknown); +import('resolvedString'); +import('resolved-id'); +import('resolved-different'); +import('unresolved'); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js index 3fc4b2567af..87164b7871f 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/es.js @@ -3,3 +3,4 @@ import(globalThis.unknown, { assert: { type: 'special' } }); import('resolvedString', { assert: { type: 'special' } }); import('resolved-id', { assert: { type: 'special' } }); import('resolved-different', { assert: { type: 'special' } }); +import('unresolved', { assert: { type: 'special' } }); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js index eec48d152c6..0da8002824f 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/iife.js @@ -6,5 +6,6 @@ import('resolvedString'); import('resolved-id'); import('resolved-different'); + import('unresolved'); })(); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js index b6bafaa6fab..44c758c2912 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/system.js @@ -8,6 +8,7 @@ System.register([], (function (exports, module) { module.import('resolvedString'); module.import('resolved-id'); module.import('resolved-different'); + module.import('unresolved'); }) }; diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js index 1bf0a9af4dd..cd9d7642deb 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_expected/umd.js @@ -8,5 +8,6 @@ import('resolvedString'); import('resolved-id'); import('resolved-different'); + import('unresolved'); })); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/main.js b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js index a60049128b5..38bfed638c0 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/main.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/main.js @@ -3,3 +3,4 @@ import(globalThis.unknown, { assert: { type: 'special' } }); import(`external-${globalThis.unknown}`, { assert: { type: 'special' } }); import('external' + globalThis.unknown, { assert: { type: 'special' } }); import('external-resolved', { assert: { type: 'special' } }); +import('unresolved', { assert: { type: 'special' } }); diff --git a/test/form/samples/import-assertions/keeps-assertions/_config.js b/test/form/samples/import-assertions/keeps-assertions/_config.js deleted file mode 100644 index c796a54e4a0..00000000000 --- a/test/form/samples/import-assertions/keeps-assertions/_config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - description: 'keeps any import assertions on input', - options: { - external: () => true, - output: { name: 'bundle' } - } -}; diff --git a/test/form/samples/import-assertions/keeps-assertions/_expected/es.js b/test/form/samples/import-assertions/keeps-assertions/_expected/es.js deleted file mode 100644 index a986aea2757..00000000000 --- a/test/form/samples/import-assertions/keeps-assertions/_expected/es.js +++ /dev/null @@ -1,7 +0,0 @@ -import { a } from 'a' assert { type: 'a' }; -import { b } from 'b' assert { foo: 'bar', baz: 'quuz' }; -export { c } from 'c' assert { type: 'c' }; - -console.log(a, b); - -import('d', { assert: { type: 'd' } }).then(console.log); diff --git a/test/form/samples/import-assertions/keeps-assertions/_expected/iife.js b/test/form/samples/import-assertions/keeps-assertions/_expected/iife.js deleted file mode 100644 index 6860373685c..00000000000 --- a/test/form/samples/import-assertions/keeps-assertions/_expected/iife.js +++ /dev/null @@ -1,15 +0,0 @@ -var bundle = (function (exports, a, b, c) { - 'use strict'; - - console.log(a.a, b.b); - - import('d').then(console.log); - - Object.defineProperty(exports, 'c', { - enumerable: true, - get: function () { return c.c; } - }); - - return exports; - -})({}, a, b, c); diff --git a/test/form/samples/import-assertions/keeps-assertions/_expected/system.js b/test/form/samples/import-assertions/keeps-assertions/_expected/system.js deleted file mode 100644 index 636fb7fe824..00000000000 --- a/test/form/samples/import-assertions/keeps-assertions/_expected/system.js +++ /dev/null @@ -1,20 +0,0 @@ -System.register('bundle', ['a', 'b', 'c'], (function (exports, module) { - 'use strict'; - var a, b; - return { - setters: [function (module) { - a = module.a; - }, function (module) { - b = module.b; - }, function (module) { - exports('c', module.c); - }], - execute: (function () { - - console.log(a, b); - - module.import('d').then(console.log); - - }) - }; -})); diff --git a/test/form/samples/import-assertions/keeps-assertions/_expected/umd.js b/test/form/samples/import-assertions/keeps-assertions/_expected/umd.js deleted file mode 100644 index 06d8bcc5fdc..00000000000 --- a/test/form/samples/import-assertions/keeps-assertions/_expected/umd.js +++ /dev/null @@ -1,16 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('a'), require('b'), require('c')) : - typeof define === 'function' && define.amd ? define(['exports', 'a', 'b', 'c'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {}, global.a, global.b, global.c)); -})(this, (function (exports, a, b, c) { 'use strict'; - - console.log(a.a, b.b); - - import('d').then(console.log); - - Object.defineProperty(exports, 'c', { - enumerable: true, - get: function () { return c.c; } - }); - -})); diff --git a/test/form/samples/import-assertions/keeps-assertions/main.js b/test/form/samples/import-assertions/keeps-assertions/main.js deleted file mode 100644 index 018ccbd2b06..00000000000 --- a/test/form/samples/import-assertions/keeps-assertions/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import { a } from 'a' assert { type: 'a' }; -import { b } from 'b' assert { foo: 'bar', baz: 'quuz' }; -console.log(a, b); - -export { c } from 'c' assert { type: 'c' }; - -import('d', { assert: { type: 'd' } }).then(console.log); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_config.js b/test/form/samples/import-assertions/keeps-static-assertions/_config.js new file mode 100644 index 00000000000..32f833d24cc --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_config.js @@ -0,0 +1,12 @@ +module.exports = { + // solo: true, + description: 'keeps any import assertions on input', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: id => { + if (id === 'unresolved') return null; + return true; + }, + output: { name: 'bundle' } + } +}; diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/amd.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/amd.js new file mode 100644 index 00000000000..418dca68f57 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/amd.js @@ -0,0 +1,35 @@ +define(['exports', 'a', 'b', 'c', 'd', 'unresolved'], (function (exports, a, b, c, d$1, unresolved) { 'use strict'; + + function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); + } + + var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); + + console.log(a.a, b__namespace, d); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); + }); + +})); diff --git a/test/form/samples/import-assertions/keeps-assertions/_expected/cjs.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/cjs.js similarity index 61% rename from test/form/samples/import-assertions/keeps-assertions/_expected/cjs.js rename to test/form/samples/import-assertions/keeps-static-assertions/_expected/cjs.js index 750fa4b890c..cb59fa6ed49 100644 --- a/test/form/samples/import-assertions/keeps-assertions/_expected/cjs.js +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/cjs.js @@ -3,6 +3,8 @@ 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); @@ -21,11 +23,17 @@ function _interopNamespaceDefault(e) { return Object.freeze(n); } -console.log(a.a, b.b); +var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); -Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespaceDefault(require('d')); }).then(console.log); +console.log(a.a, b__namespace, d); Object.defineProperty(exports, 'c', { enumerable: true, get: function () { return c.c; } }); +Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); +}); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/es.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/es.js new file mode 100644 index 00000000000..ba6c7ba4532 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/es.js @@ -0,0 +1,7 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'unresolved' assert { type: 'e' }; + +console.log(a, b, d); diff --git a/test/form/samples/import-assertions/keeps-assertions/_expected/amd.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/iife.js similarity index 54% rename from test/form/samples/import-assertions/keeps-assertions/_expected/amd.js rename to test/form/samples/import-assertions/keeps-static-assertions/_expected/iife.js index d0366d1af90..8398b9e18ab 100644 --- a/test/form/samples/import-assertions/keeps-assertions/_expected/amd.js +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/iife.js @@ -1,4 +1,5 @@ -define(['require', 'exports', 'a', 'b', 'c'], (function (require, exports, a, b, c) { 'use strict'; +var bundle = (function (exports, a, b, c, d$1) { + 'use strict'; function _interopNamespaceDefault(e) { var n = Object.create(null); @@ -17,13 +18,21 @@ define(['require', 'exports', 'a', 'b', 'c'], (function (require, exports, a, b, return Object.freeze(n); } - console.log(a.a, b.b); + var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); - new Promise(function (resolve, reject) { require(['d'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }).then(console.log); + console.log(a.a, b__namespace, d); Object.defineProperty(exports, 'c', { enumerable: true, get: function () { return c.c; } }); + Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); + }); + + return exports; -})); +})({}, a, b, c, d$1); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/system.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/system.js new file mode 100644 index 00000000000..e1e3a6ae431 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/system.js @@ -0,0 +1,28 @@ +System.register('bundle', ['a', 'b', 'c', 'd', 'unresolved'], (function (exports) { + 'use strict'; + var _starExcludes = { + default: 1, + c: 1 + }; + var a, b; + return { + setters: [function (module) { + a = module.a; + }, function (module) { + b = module; + }, function (module) { + exports('c', module.c); + }, function (module) { + var setter = {}; + for (var name in module) { + if (!_starExcludes[name]) setter[name] = module[name]; + } + exports(setter); + }, null], + execute: (function () { + + console.log(a, b, d); + + }) + }; +})); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_expected/umd.js b/test/form/samples/import-assertions/keeps-static-assertions/_expected/umd.js new file mode 100644 index 00000000000..f30436f32f2 --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/_expected/umd.js @@ -0,0 +1,39 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('a'), require('b'), require('c'), require('d'), require('unresolved')) : + typeof define === 'function' && define.amd ? define(['exports', 'a', 'b', 'c', 'd', 'unresolved'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {}, global.a, global.b, global.c, global.d$1)); +})(this, (function (exports, a, b, c, d$1) { 'use strict'; + + function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); + } + + var b__namespace = /*#__PURE__*/_interopNamespaceDefault(b); + + console.log(a.a, b__namespace, d); + + Object.defineProperty(exports, 'c', { + enumerable: true, + get: function () { return c.c; } + }); + Object.keys(d$1).forEach(function (k) { + if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { + enumerable: true, + get: function () { return d$1[k]; } + }); + }); + +})); diff --git a/test/form/samples/import-assertions/keeps-static-assertions/main.js b/test/form/samples/import-assertions/keeps-static-assertions/main.js new file mode 100644 index 00000000000..02b154c863a --- /dev/null +++ b/test/form/samples/import-assertions/keeps-static-assertions/main.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'unresolved' assert { type: 'e' }; + +console.log(a, b, d); + + diff --git a/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js b/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js new file mode 100644 index 00000000000..fedd5e69506 --- /dev/null +++ b/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js @@ -0,0 +1,29 @@ +module.exports = { + // solo: true, + description: 'keep import assertions for dynamic imports', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: id => { + if (id === 'unresolved') return null; + return true; + }, + plugins: [ + { + resolveDynamicImport(specifier, importer) { + if (typeof specifier === 'object') { + if (specifier.type === 'TemplateLiteral') { + return "'resolvedString'"; + } + if (specifier.type === 'BinaryExpression') { + return { id: 'resolved-id', external: true }; + } + } else if (specifier === 'external-resolved') { + return { id: 'resolved-different', external: true }; + } + return null; + } + } + ], + output: { externalImportAssertions: false } + } +}; diff --git a/test/form/samples/import-assertions/removes-dynamic-assertions/_expected.js b/test/form/samples/import-assertions/removes-dynamic-assertions/_expected.js new file mode 100644 index 00000000000..2d417264620 --- /dev/null +++ b/test/form/samples/import-assertions/removes-dynamic-assertions/_expected.js @@ -0,0 +1,6 @@ +import('external'); +import(globalThis.unknown); +import('resolvedString'); +import('resolved-id'); +import('resolved-different'); +import('unresolved'); diff --git a/test/form/samples/import-assertions/removes-dynamic-assertions/main.js b/test/form/samples/import-assertions/removes-dynamic-assertions/main.js new file mode 100644 index 00000000000..38bfed638c0 --- /dev/null +++ b/test/form/samples/import-assertions/removes-dynamic-assertions/main.js @@ -0,0 +1,6 @@ +import('external', { assert: { type: 'special' } }); +import(globalThis.unknown, { assert: { type: 'special' } }); +import(`external-${globalThis.unknown}`, { assert: { type: 'special' } }); +import('external' + globalThis.unknown, { assert: { type: 'special' } }); +import('external-resolved', { assert: { type: 'special' } }); +import('unresolved', { assert: { type: 'special' } }); diff --git a/test/form/samples/import-assertions/removes-static-assertions/_config.js b/test/form/samples/import-assertions/removes-static-assertions/_config.js new file mode 100644 index 00000000000..dfae32a63c0 --- /dev/null +++ b/test/form/samples/import-assertions/removes-static-assertions/_config.js @@ -0,0 +1,12 @@ +module.exports = { + // solo: true, + description: 'keeps any import assertions on input', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: id => { + if (id === 'unresolved') return null; + return true; + }, + output: { name: 'bundle', externalImportAssertions: false } + } +}; diff --git a/test/form/samples/import-assertions/removes-static-assertions/_expected.js b/test/form/samples/import-assertions/removes-static-assertions/_expected.js new file mode 100644 index 00000000000..40f55f22388 --- /dev/null +++ b/test/form/samples/import-assertions/removes-static-assertions/_expected.js @@ -0,0 +1,7 @@ +import { a } from 'a'; +import * as b from 'b'; +export { c } from 'c'; +export * from 'd'; +import 'unresolved'; + +console.log(a, b, d); diff --git a/test/form/samples/import-assertions/removes-static-assertions/main.js b/test/form/samples/import-assertions/removes-static-assertions/main.js new file mode 100644 index 00000000000..02b154c863a --- /dev/null +++ b/test/form/samples/import-assertions/removes-static-assertions/main.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'unresolved' assert { type: 'e' }; + +console.log(a, b, d); + + From a36935fdf981b55413eb1c6e8eeb806ff759d959 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 8 Oct 2022 07:15:48 +0200 Subject: [PATCH 12/17] Some extractions --- browser/src/resolveId.ts | 17 ++------- src/ModuleLoader.ts | 36 +++++++++++-------- src/utils/PluginContext.ts | 2 +- src/utils/options/normalizeOutputOptions.ts | 1 - src/utils/resolveId.ts | 13 ++----- src/utils/resolveIdViaPlugins.ts | 19 ++-------- .../samples/output-options-hook/_config.js | 1 + 7 files changed, 32 insertions(+), 57 deletions(-) diff --git a/browser/src/resolveId.ts b/browser/src/resolveId.ts index c36b095ced1..47772a9a981 100644 --- a/browser/src/resolveId.ts +++ b/browser/src/resolveId.ts @@ -1,9 +1,5 @@ -import type { - CustomPluginOptions, - Plugin, - ResolvedId, - ResolveIdResult -} from '../../src/rollup/types'; +import { ModuleLoaderResolveId } from '../../src/ModuleLoader'; +import type { CustomPluginOptions, Plugin, ResolveIdResult } from '../../src/rollup/types'; import type { PluginDriver } from '../../src/utils/PluginDriver'; import { resolveIdViaPlugins } from '../../src/utils/resolveIdViaPlugins'; import { throwNoFileSystem } from './error'; @@ -13,14 +9,7 @@ export async function resolveId( importer: string | undefined, _preserveSymlinks: boolean, pluginDriver: PluginDriver, - moduleLoaderResolveId: ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - assertions: Record, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null - ) => Promise, + moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, isEntry: boolean diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 526f66053a6..d70b9193ed0 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -43,6 +43,15 @@ export interface UnresolvedModule { name: string | null; } +export type ModuleLoaderResolveId = ( + source: string, + importer: string | undefined, + customOptions: CustomPluginOptions | undefined, + isEntry: boolean | undefined, + assertions: Record, + skip?: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null +) => Promise; + type NormalizedResolveIdWithoutDefaults = Partial> & { external?: boolean | 'absolute'; id: string; @@ -184,14 +193,14 @@ export class ModuleLoader { return module.info; } - resolveId = async ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - assertions: Record, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null = null - ): Promise => + resolveId: ModuleLoaderResolveId = async ( + source, + importer, + customOptions, + isEntry, + assertions, + skip = null + ) => this.getResolvedIdWithDefaults( this.getNormalizedResolvedIdWithoutDefaults( this.options.external(source, importer, false) @@ -672,13 +681,10 @@ export class ModuleLoader { if (!resolution) { return null; } - // TODO Lukas use withDefaults logic instead? Merge with other resolution case? - return { - assertions, - external: false, - moduleSideEffects: true, - ...resolution - } as ResolvedId; + return this.getResolvedIdWithDefaults( + resolution as NormalizedResolveIdWithoutDefaults, + assertions + ); } if (resolution == null) { // TODO Lukas handle existing resolved id conflicts diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 45bb21c9253..686feb6d2e4 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -99,7 +99,7 @@ export function getPluginContext( importer, custom, isEntry, - // TODO Lukas use correct assertions + // TODO Lukas allow to provide assertions EMPTY_OBJECT, skipSelf ? [{ importer, plugin, source }] : null ); diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index ded7a7d6709..6451375503f 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -48,7 +48,6 @@ export function normalizeOutputOptions( esModule: config.esModule ?? 'if-default-prop', exports: getExports(config, unsetOptions), extend: config.extend || false, - // TODO Lukas for truly dynamic imports, setting `false` should still prevent assertions externalImportAssertions: config.externalImportAssertions ?? true, externalLiveBindings: config.externalLiveBindings ?? true, file, diff --git a/src/utils/resolveId.ts b/src/utils/resolveId.ts index 1695342189e..19505e3d88a 100644 --- a/src/utils/resolveId.ts +++ b/src/utils/resolveId.ts @@ -1,4 +1,5 @@ -import type { CustomPluginOptions, Plugin, ResolvedId, ResolveIdResult } from '../rollup/types'; +import { ModuleLoaderResolveId } from '../ModuleLoader'; +import type { CustomPluginOptions, Plugin, ResolveIdResult } from '../rollup/types'; import type { PluginDriver } from './PluginDriver'; import { promises as fs } from './fs'; import { basename, dirname, isAbsolute, resolve } from './path'; @@ -9,15 +10,7 @@ export async function resolveId( importer: string | undefined, preserveSymlinks: boolean, pluginDriver: PluginDriver, - // TODO Lukas extract/reuse type - moduleLoaderResolveId: ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - assertions: Record, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null - ) => Promise, + moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, isEntry: boolean diff --git a/src/utils/resolveIdViaPlugins.ts b/src/utils/resolveIdViaPlugins.ts index a4b77c14691..85e580185a7 100644 --- a/src/utils/resolveIdViaPlugins.ts +++ b/src/utils/resolveIdViaPlugins.ts @@ -1,10 +1,5 @@ -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, EMPTY_OBJECT } from './blank'; @@ -12,15 +7,7 @@ export function resolveIdViaPlugins( source: string, importer: string | undefined, pluginDriver: PluginDriver, - // TODO Lukas extract/reuse type - moduleLoaderResolveId: ( - source: string, - importer: string | undefined, - customOptions: CustomPluginOptions | undefined, - isEntry: boolean | undefined, - assertions: Record, - skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null - ) => Promise, + moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, isEntry: boolean diff --git a/test/function/samples/output-options-hook/_config.js b/test/function/samples/output-options-hook/_config.js index ab64f6f952a..3c98cf8c830 100644 --- a/test/function/samples/output-options-hook/_config.js +++ b/test/function/samples/output-options-hook/_config.js @@ -29,6 +29,7 @@ module.exports = { esModule: 'if-default-prop', exports: 'auto', extend: false, + externalImportAssertions: true, externalLiveBindings: true, format: 'cjs', freeze: true, From f88abf40e1c20b096e8f6e04a7b008c4e679bb2a Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 9 Oct 2022 06:28:47 +0200 Subject: [PATCH 13/17] Allow plugins to add and change assertions --- browser/src/resolveId.ts | 6 +++-- src/ModuleLoader.ts | 13 ++++++----- src/rollup/types.d.ts | 5 +++-- src/utils/resolveId.ts | 6 +++-- src/utils/resolveIdViaPlugins.ts | 5 +++-- .../keep-dynamic-assertions/_config.js | 1 - .../keeps-static-assertions/_config.js | 1 - .../_config.js | 22 +++++++++++++++++++ .../_expected.js | 4 ++++ .../main.js | 4 ++++ .../plugin-assertions-resolveid/_config.js | 17 ++++++++++++++ .../plugin-assertions-resolveid/_expected.js | 9 ++++++++ .../plugin-assertions-resolveid/main.js | 9 ++++++++ .../removes-dynamic-assertions/_config.js | 1 - .../removes-static-assertions/_config.js | 1 - 15 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_config.js create mode 100644 test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_expected.js create mode 100644 test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/main.js create mode 100644 test/form/samples/import-assertions/plugin-assertions-resolveid/_config.js create mode 100644 test/form/samples/import-assertions/plugin-assertions-resolveid/_expected.js create mode 100644 test/form/samples/import-assertions/plugin-assertions-resolveid/main.js diff --git a/browser/src/resolveId.ts b/browser/src/resolveId.ts index 47772a9a981..da1803f5632 100644 --- a/browser/src/resolveId.ts +++ b/browser/src/resolveId.ts @@ -12,7 +12,8 @@ export async function resolveId( moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, - isEntry: boolean + isEntry: boolean, + assertions: Record ): Promise { const pluginResult = await resolveIdViaPlugins( source, @@ -21,7 +22,8 @@ export async function resolveId( moduleLoaderResolveId, skip, customOptions, - isEntry + isEntry, + assertions ); if (pluginResult == null) { throwNoFileSystem('path.resolve'); diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index d70b9193ed0..9c06969d072 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -213,7 +213,8 @@ export class ModuleLoader { this.resolveId, skip, customOptions, - typeof isEntry === 'boolean' ? isEntry : !importer + typeof isEntry === 'boolean' ? isEntry : !importer, + assertions ), importer, source @@ -562,9 +563,8 @@ export class ModuleLoader { return null; } const external = resolvedId.external || false; - // TODO Lukas also consider assertions from resolvedId return { - assertions, + assertions: resolvedId.assertions || assertions, external, id: resolvedId.id, meta: resolvedId.meta || {}, @@ -631,7 +631,8 @@ export class ModuleLoader { this.resolveId, null, EMPTY_OBJECT, - true + true, + EMPTY_OBJECT ); if (resolveIdResult == null) { return error( @@ -651,7 +652,6 @@ export class ModuleLoader { ); } return this.fetchModule( - // TODO Lukas use correct assertions from input this.getResolvedIdWithDefaults( typeof resolveIdResult === 'object' ? (resolveIdResult as NormalizedResolveIdWithoutDefaults) @@ -672,7 +672,8 @@ export class ModuleLoader { ): Promise { const resolution = await this.pluginDriver.hookFirst('resolveDynamicImport', [ specifier, - importer + importer, + { assertions } ]); if (typeof specifier !== 'string') { if (typeof resolution === 'string') { diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 8a461bdddd6..961ffcc59d4 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -221,7 +221,7 @@ export type ResolveIdHook = ( this: PluginContext, source: string, importer: string | undefined, - options: { custom?: CustomPluginOptions; isEntry: boolean } + options: { assertions: Record; custom?: CustomPluginOptions; isEntry: boolean } ) => ResolveIdResult; export type ShouldTransformCachedModuleHook = ( @@ -276,7 +276,8 @@ export type RenderChunkHook = ( export type ResolveDynamicImportHook = ( this: PluginContext, specifier: string | AcornNode, - importer: string + importer: string, + options: { assertions: Record } ) => ResolveIdResult; export type ResolveImportMetaHook = ( diff --git a/src/utils/resolveId.ts b/src/utils/resolveId.ts index 19505e3d88a..b0edba5e68a 100644 --- a/src/utils/resolveId.ts +++ b/src/utils/resolveId.ts @@ -13,7 +13,8 @@ export async function resolveId( moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, - isEntry: boolean + isEntry: boolean, + assertions: Record ): Promise { const pluginResult = await resolveIdViaPlugins( source, @@ -22,7 +23,8 @@ export async function resolveId( moduleLoaderResolveId, skip, customOptions, - isEntry + isEntry, + assertions ); if (pluginResult != null) return pluginResult; diff --git a/src/utils/resolveIdViaPlugins.ts b/src/utils/resolveIdViaPlugins.ts index 85e580185a7..a60db118643 100644 --- a/src/utils/resolveIdViaPlugins.ts +++ b/src/utils/resolveIdViaPlugins.ts @@ -10,7 +10,8 @@ export function resolveIdViaPlugins( moduleLoaderResolveId: ModuleLoaderResolveId, skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, - isEntry: boolean + isEntry: boolean, + assertions: Record ): Promise { let skipped: Set | null = null; let replaceContext: ReplaceContext | null = null; @@ -38,7 +39,7 @@ export function resolveIdViaPlugins( } return pluginDriver.hookFirst( 'resolveId', - [source, importer, { custom: customOptions, isEntry }], + [source, importer, { assertions, custom: customOptions, isEntry }], replaceContext, skipped ); diff --git a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js index ff6fa2a5fe3..e2571b057c7 100644 --- a/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js +++ b/test/form/samples/import-assertions/keep-dynamic-assertions/_config.js @@ -1,5 +1,4 @@ module.exports = { - // solo: true, description: 'keep import assertions for dynamic imports', expectedWarnings: 'UNRESOLVED_IMPORT', options: { diff --git a/test/form/samples/import-assertions/keeps-static-assertions/_config.js b/test/form/samples/import-assertions/keeps-static-assertions/_config.js index 32f833d24cc..7940684a958 100644 --- a/test/form/samples/import-assertions/keeps-static-assertions/_config.js +++ b/test/form/samples/import-assertions/keeps-static-assertions/_config.js @@ -1,5 +1,4 @@ module.exports = { - // solo: true, description: 'keeps any import assertions on input', expectedWarnings: 'UNRESOLVED_IMPORT', options: { diff --git a/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_config.js b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_config.js new file mode 100644 index 00000000000..efbc46143d0 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_config.js @@ -0,0 +1,22 @@ +module.exports = { + description: 'allows plugins to read and write import assertions in resolveDynamicImport', + options: { + plugins: [ + { + resolveDynamicImport(specifier, importer, { assertions }) { + const resolutionOptions = { + external: true, + assertions: Object.fromEntries(Object.keys(assertions).map(key => [key, 'changed'])) + }; + if (typeof specifier === 'object') { + if (specifier.type === 'TemplateLiteral') { + return { id: 'resolved-a', ...resolutionOptions }; + } + return { id: 'resolved-b', ...resolutionOptions }; + } + return { id: specifier, ...resolutionOptions }; + } + } + ] + } +}; diff --git a/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_expected.js b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_expected.js new file mode 100644 index 00000000000..279b0c9a4f0 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/_expected.js @@ -0,0 +1,4 @@ +import('a', { assert: { type: 'changed' } }); +import('resolved-b', { assert: { type: 'changed', extra: 'changed' } }); +import('b'); +import('resolved-a'); diff --git a/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/main.js b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/main.js new file mode 100644 index 00000000000..6d8a892a6d4 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolvedynamicimport/main.js @@ -0,0 +1,4 @@ +import('a', { assert: { type: 'special' } }); +import(globalThis.unknown, { assert: { type: 'special', extra: 'value' } }); +import('b'); +import(`external-${globalThis.unknown}`); diff --git a/test/form/samples/import-assertions/plugin-assertions-resolveid/_config.js b/test/form/samples/import-assertions/plugin-assertions-resolveid/_config.js new file mode 100644 index 00000000000..77c68a879e1 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolveid/_config.js @@ -0,0 +1,17 @@ +module.exports = { + description: 'allows plugins to read and write import assertions in resolveId', + options: { + output: { name: 'bundle' }, + plugins: [ + { + resolveId(source, importer, { assertions, isEntry }) { + return { + id: source, + external: !isEntry, + assertions: Object.fromEntries(Object.keys(assertions).map(key => [key, 'changed'])) + }; + } + } + ] + } +}; diff --git a/test/form/samples/import-assertions/plugin-assertions-resolveid/_expected.js b/test/form/samples/import-assertions/plugin-assertions-resolveid/_expected.js new file mode 100644 index 00000000000..200aa397375 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolveid/_expected.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'changed', extra: 'changed' }; +import * as b from 'b' assert { type: 'changed' }; +export { c } from 'c' assert { type: 'changed' }; +export * from 'd' assert { type: 'changed' }; +import 'e'; + +console.log(a, b, d); +import('f', { assert: { type: 'changed' } }); +import('g'); diff --git a/test/form/samples/import-assertions/plugin-assertions-resolveid/main.js b/test/form/samples/import-assertions/plugin-assertions-resolveid/main.js new file mode 100644 index 00000000000..7781ebfe7c6 --- /dev/null +++ b/test/form/samples/import-assertions/plugin-assertions-resolveid/main.js @@ -0,0 +1,9 @@ +import { a } from 'a' assert { type: 'a', extra: 'extra' }; +import * as b from 'b' assert { type: 'b' }; +export { c } from 'c' assert { type: 'c' }; +export * from 'd' assert { type: 'd' }; +import 'e'; + +console.log(a, b, d); +import('f', { assert: { type: 'f' } }); +import('g'); diff --git a/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js b/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js index fedd5e69506..b939795fd58 100644 --- a/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js +++ b/test/form/samples/import-assertions/removes-dynamic-assertions/_config.js @@ -1,5 +1,4 @@ module.exports = { - // solo: true, description: 'keep import assertions for dynamic imports', expectedWarnings: 'UNRESOLVED_IMPORT', options: { diff --git a/test/form/samples/import-assertions/removes-static-assertions/_config.js b/test/form/samples/import-assertions/removes-static-assertions/_config.js index dfae32a63c0..30299daf04e 100644 --- a/test/form/samples/import-assertions/removes-static-assertions/_config.js +++ b/test/form/samples/import-assertions/removes-static-assertions/_config.js @@ -1,5 +1,4 @@ module.exports = { - // solo: true, description: 'keeps any import assertions on input', expectedWarnings: 'UNRESOLVED_IMPORT', options: { From 12f9954d0afe840569431da3f0977e1724303aa2 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 9 Oct 2022 07:08:15 +0200 Subject: [PATCH 14/17] Allow to pass assertions in this.resolve --- src/rollup/types.d.ts | 7 ++- src/utils/PluginContext.ts | 5 +- src/utils/resolveIdViaPlugins.ts | 5 +- .../plugin-assertions-this-resolve/_config.js | 48 +++++++++++++++++++ .../plugin-assertions-this-resolve/main.js | 1 + 5 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 test/function/samples/import-assertions/plugin-assertions-this-resolve/_config.js create mode 100644 test/function/samples/import-assertions/plugin-assertions-this-resolve/main.js diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 961ffcc59d4..18dcf4311a4 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -190,7 +190,12 @@ export interface PluginContext extends MinimalPluginContext { resolve: ( source: string, importer?: string, - options?: { custom?: CustomPluginOptions; isEntry?: boolean; skipSelf?: boolean } + options?: { + assertions?: Record; + custom?: CustomPluginOptions; + isEntry?: boolean; + skipSelf?: boolean; + } ) => Promise; setAssetSource: (assetReferenceId: string, source: string | Uint8Array) => void; warn: (warning: RollupWarning | string, pos?: number | { column: number; line: number }) => void; diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 686feb6d2e4..64f866a2836 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -93,14 +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, - // TODO Lukas allow to provide assertions - EMPTY_OBJECT, + assertions || EMPTY_OBJECT, skipSelf ? [{ importer, plugin, source }] : null ); }, diff --git a/src/utils/resolveIdViaPlugins.ts b/src/utils/resolveIdViaPlugins.ts index a60db118643..7f65cc2dabb 100644 --- a/src/utils/resolveIdViaPlugins.ts +++ b/src/utils/resolveIdViaPlugins.ts @@ -24,14 +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, - // TODO Lukas use assertions provided via this.resolve - EMPTY_OBJECT, + assertions || EMPTY_OBJECT, skipSelf ? [...skip, { importer, plugin, source }] : skip ); } diff --git a/test/function/samples/import-assertions/plugin-assertions-this-resolve/_config.js b/test/function/samples/import-assertions/plugin-assertions-this-resolve/_config.js new file mode 100644 index 00000000000..945e6f17f9f --- /dev/null +++ b/test/function/samples/import-assertions/plugin-assertions-this-resolve/_config.js @@ -0,0 +1,48 @@ +const assert = require('assert'); + +module.exports = { + description: 'allows plugins to provide assertions for this.resolve', + options: { + plugins: [ + { + name: 'first', + async resolveId(source, importer, { assertions }) { + assert.deepStrictEqual( + await this.resolve('external', undefined, { + skipSelf: true, + assertions: { a: 'c', b: 'd' } + }), + { + assertions: { a: 'changed', b: 'changed' }, + external: true, + id: 'external', + meta: {}, + moduleSideEffects: true, + syntheticNamedExports: false + } + ); + } + }, + { + name: 'second', + async resolveId(source, importer, { assertions }) { + if (source === 'external') { + return this.resolve(source, importer, { assertions, skipSelf: true }); + } + } + }, + { + name: 'third', + async resolveId(source, importer, { assertions }) { + if (source === 'external') { + return { + id: source, + external: true, + assertions: Object.fromEntries(Object.keys(assertions).map(key => [key, 'changed'])) + }; + } + } + } + ] + } +}; diff --git a/test/function/samples/import-assertions/plugin-assertions-this-resolve/main.js b/test/function/samples/import-assertions/plugin-assertions-this-resolve/main.js new file mode 100644 index 00000000000..cc1d88a24fa --- /dev/null +++ b/test/function/samples/import-assertions/plugin-assertions-this-resolve/main.js @@ -0,0 +1 @@ +assert.ok(true); From 2eb1d08920e4a5c227fc9bb63cd5ea25c062f3b5 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 9 Oct 2022 21:50:17 +0200 Subject: [PATCH 15/17] Warn for inconsistent import assertions --- src/Module.ts | 38 +++++++--- src/ModuleLoader.ts | 74 ++++++++++++------- src/utils/error.ts | 25 +++++++ src/utils/parseAssertions.ts | 11 +++ .../warn-assertion-conflicts/_config.js | 64 ++++++++++++++++ .../warn-assertion-conflicts/dep.js | 0 .../warn-assertion-conflicts/main.js | 6 ++ .../warn-assertion-conflicts/other.js | 2 + 8 files changed, 184 insertions(+), 36 deletions(-) create mode 100644 test/function/samples/import-assertions/warn-assertion-conflicts/_config.js create mode 100644 test/function/samples/import-assertions/warn-assertion-conflicts/dep.js create mode 100644 test/function/samples/import-assertions/warn-assertion-conflicts/main.js create mode 100644 test/function/samples/import-assertions/warn-assertion-conflicts/other.js diff --git a/src/Module.ts b/src/Module.ts index 8ec1d2543f9..2fbbe653d6f 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -10,7 +10,6 @@ import ExportAllDeclaration from './ast/nodes/ExportAllDeclaration'; import ExportDefaultDeclaration from './ast/nodes/ExportDefaultDeclaration'; import type ExportNamedDeclaration from './ast/nodes/ExportNamedDeclaration'; import type Identifier from './ast/nodes/Identifier'; -import ImportAttribute from './ast/nodes/ImportAttribute'; import type ImportDeclaration from './ast/nodes/ImportDeclaration'; import type ImportExpression from './ast/nodes/ImportExpression'; import Literal from './ast/nodes/Literal'; @@ -52,6 +51,7 @@ import { augmentCodeLocation, errAmbiguousExternalNamespaces, errCircularReexport, + errInconsistentImportAssertions, errInvalidFormatForTopLevelAwait, errInvalidSourcemapForError, errMissingExport, @@ -66,7 +66,10 @@ import { getId } from './utils/getId'; import { getOrCreate } from './utils/getOrCreate'; import { getOriginalLocation } from './utils/getOriginalLocation'; import { makeLegal } from './utils/identifierHelpers'; -import { getAssertionsFromImportExportDeclaration } from './utils/parseAssertions'; +import { + doAssertionsDiffer, + getAssertionsFromImportExportDeclaration +} from './utils/parseAssertions'; import { basename, extname } from './utils/path'; import type { RenderOptions } from './utils/renderHelpers'; import { timeEnd, timeStart } from './utils/timers'; @@ -257,7 +260,8 @@ export default class Module { isEntry: boolean, moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, - meta: CustomPluginOptions + meta: CustomPluginOptions, + assertions: Record ) { this.excludeFromSourcemap = /\0/.test(id); this.context = options.moduleContext(id); @@ -276,8 +280,7 @@ export default class Module { } = this; this.info = { - // TODO Lukas use correct assertions - assertions: EMPTY_OBJECT, + assertions, ast: null, code: null, get dynamicallyImportedIdResolutions() { @@ -911,7 +914,7 @@ export default class Module { }); } else if (node instanceof ExportAllDeclaration) { const source = node.source.value; - this.addSource(source, node.assertions); + this.addSource(source, node); if (node.exported) { // export * as name from './other' @@ -931,7 +934,7 @@ export default class Module { // export { name } from './other' const source = node.source.value; - this.addSource(source, node.assertions); + this.addSource(source, node); for (const specifier of node.specifiers) { const name = specifier.exported.name; this.reexportDescriptions.set(name, { @@ -971,7 +974,7 @@ export default class Module { private addImport(node: ImportDeclaration): void { const source = node.source.value; - this.addSource(source, node.assertions); + this.addSource(source, node); for (const specifier of node.specifiers) { const isDefault = specifier.type === NodeType.ImportDefaultSpecifier; const isNamespace = specifier.type === NodeType.ImportNamespaceSpecifier; @@ -1050,9 +1053,22 @@ export default class Module { addSideEffectDependencies(alwaysCheckedDependencies); } - private addSource(source: string, assertions: ImportAttribute[] | undefined) { - // TODO Lukas handle existing and conflicting sources - this.sourcesWithAssertions.set(source, getAssertionsFromImportExportDeclaration(assertions)); + 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( diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 9c06969d072..00dab89391a 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -22,6 +22,7 @@ import { errEntryCannotBeExternal, errExternalSyntheticExports, errImplicitDependantCannotBeExternal, + errInconsistentImportAssertions, errInternalIdCannotBeExternal, error, errUnresolvedEntry, @@ -30,7 +31,7 @@ import { errUnresolvedImportTreatedAsExternal } from './utils/error'; import { promises as fs } from './utils/fs'; -import { getAssertionsFromImportExpression } from './utils/parseAssertions'; +import { doAssertionsDiffer, getAssertionsFromImportExpression } from './utils/parseAssertions'; import { isAbsolute, isRelative, resolve } from './utils/path'; import relativeId from './utils/relativeId'; import { resolveId } from './utils/resolveId'; @@ -184,7 +185,6 @@ export class ModuleLoader { resolvedId: { id: string; resolveDependencies?: boolean } & Partial> ): Promise { const module = await this.fetchModule( - // TODO Lukas use correct assertions from resolvedId this.getResolvedIdWithDefaults(resolvedId, EMPTY_OBJECT)!, undefined, false, @@ -352,17 +352,24 @@ export class ModuleLoader { } } - // If this is a preload, then this method always waits for the dependencies of the module to be resolved. - // Otherwise if the module does not exist, it waits for the module and all its dependencies to be loaded. - // Otherwise it returns immediately. + // If this is a preload, then this method always waits for the dependencies of + // the module to be resolved. + // Otherwise, if the module does not exist, it waits for the module and all + // its dependencies to be loaded. + // Otherwise, it returns immediately. private async fetchModule( - { id, meta, moduleSideEffects, syntheticNamedExports }: ResolvedId, + { assertions, id, meta, moduleSideEffects, syntheticNamedExports }: ResolvedId, importer: string | undefined, isEntry: boolean, isPreload: PreloadType ): Promise { const existingModule = this.modulesById.get(id); if (existingModule instanceof Module) { + if (importer && doAssertionsDiffer(assertions, existingModule.info.assertions)) { + this.options.onwarn( + errInconsistentImportAssertions(existingModule.info.assertions, assertions, id, importer) + ); + } await this.handleExistingModule(existingModule, isEntry, isPreload); return existingModule; } @@ -374,7 +381,8 @@ export class ModuleLoader { isEntry, moduleSideEffects, syntheticNamedExports, - meta + meta, + assertions ); this.modulesById.set(id, module); this.graph.watchFiles[id] = true; @@ -423,27 +431,30 @@ export class ModuleLoader { importer: string, resolvedId: ResolvedId ): Promise { - // TODO Lukas handle internal if (resolvedId.external) { const { assertions, external, id, moduleSideEffects, meta } = resolvedId; - // TODO Lukas check assert for existing module - if (!this.modulesById.has(id)) { - this.modulesById.set( + 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), - assertions - ) + 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); } @@ -688,8 +699,21 @@ export class ModuleLoader { ); } if (resolution == null) { - // TODO Lukas handle existing resolved id conflicts - return (module.resolvedIds[specifier] ??= this.handleInvalidResolvedId( + 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, diff --git a/src/utils/error.ts b/src/utils/error.ts index 925f1a7eeba..d5feabb5113 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -69,6 +69,7 @@ const ADDON_ERROR = 'ADDON_ERROR', FILE_NOT_FOUND = 'FILE_NOT_FOUND', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', ILLEGAL_REASSIGNMENT = 'ILLEGAL_REASSIGNMENT', + INCONSISTENT_IMPORT_ASSERTIONS = 'INCONSISTENT_IMPORT_ASSERTIONS', INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_CONFIG_MODULE_FORMAT = 'INVALID_CONFIG_MODULE_FORMAT', @@ -344,6 +345,30 @@ export function errIllegalImportReassignment(name: string, importingId: string): }; } +export function errInconsistentImportAssertions( + existingAssertions: Record, + newAssertions: Record, + source: string, + importer: string +): RollupLog { + return { + code: INCONSISTENT_IMPORT_ASSERTIONS, + message: `Module "${relativeId(importer)}" tried to import "${relativeId( + source + )}" with ${formatAssertions( + newAssertions + )} assertions, but it was already imported elsewhere with ${formatAssertions( + existingAssertions + )} assertions. Please ensure that import assertions for the same module are always consistent.` + }; +} + +const formatAssertions = (assertions: Record): string => { + const entries = Object.entries(assertions); + if (entries.length === 0) return 'no'; + return entries.map(([key, value]) => `"${key}": "${value}"`).join(', '); +}; + export function errInputHookInOutputPlugin(pluginName: string, hookName: string): RollupLog { return { code: INPUT_HOOK_IN_OUTPUT_PLUGIN, diff --git a/src/utils/parseAssertions.ts b/src/utils/parseAssertions.ts index 98c95272bf6..7a54a172777 100644 --- a/src/utils/parseAssertions.ts +++ b/src/utils/parseAssertions.ts @@ -47,3 +47,14 @@ export function getAssertionsFromImportExportDeclaration( ) : EMPTY_OBJECT; } + +export function doAssertionsDiffer( + assertionA: Record, + assertionB: Record +): boolean { + const keysA = Object.keys(assertionA); + return ( + keysA.length !== Object.keys(assertionB).length || + keysA.some(key => assertionA[key] !== assertionB[key]) + ); +} diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/_config.js b/test/function/samples/import-assertions/warn-assertion-conflicts/_config.js new file mode 100644 index 00000000000..c51bf7d88a7 --- /dev/null +++ b/test/function/samples/import-assertions/warn-assertion-conflicts/_config.js @@ -0,0 +1,64 @@ +const path = require('path'); +const ID_MAIN = path.join(__dirname, 'main.js'); + +module.exports = { + description: 'warns for conflicting import assertions', + options: { + external: id => id.startsWith('external') + }, + warnings: [ + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + frame: ` +1: import './other.js'; +2: import 'external' assert { type: 'foo' }; +3: import 'external' assert { type: 'bar' }; + ^ +4: import 'external'; +5: import('external', { assert: { type: 'baz' } });`, + id: ID_MAIN, + loc: { + column: 0, + file: ID_MAIN, + line: 3 + }, + message: + 'Module "main.js" tried to import "external" with "type": "bar" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.', + pos: 63 + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + frame: ` +2: import 'external' assert { type: 'foo' }; +3: import 'external' assert { type: 'bar' }; +4: import 'external'; + ^ +5: import('external', { assert: { type: 'baz' } }); +6: import './dep.js' assert { type: 'foo' };`, + id: ID_MAIN, + loc: { + column: 0, + file: ID_MAIN, + line: 4 + }, + message: + 'Module "main.js" tried to import "external" with no assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.', + pos: 105 + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + message: + 'Module "main.js" tried to import "external" with "type": "baz" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.' + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + message: + 'Module "other.js" tried to import "external" with "type": "quuz" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.' + }, + { + code: 'INCONSISTENT_IMPORT_ASSERTIONS', + message: + 'Module "other.js" tried to import "dep.js" with "type": "bar" assertions, but it was already imported elsewhere with "type": "foo" assertions. Please ensure that import assertions for the same module are always consistent.' + } + ] +}; diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/dep.js b/test/function/samples/import-assertions/warn-assertion-conflicts/dep.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/main.js b/test/function/samples/import-assertions/warn-assertion-conflicts/main.js new file mode 100644 index 00000000000..50f9cdb8b2b --- /dev/null +++ b/test/function/samples/import-assertions/warn-assertion-conflicts/main.js @@ -0,0 +1,6 @@ +import './other.js'; +import 'external' assert { type: 'foo' }; +import 'external' assert { type: 'bar' }; +import 'external'; +import('external', { assert: { type: 'baz' } }); +import './dep.js' assert { type: 'foo' }; diff --git a/test/function/samples/import-assertions/warn-assertion-conflicts/other.js b/test/function/samples/import-assertions/warn-assertion-conflicts/other.js new file mode 100644 index 00000000000..779091a9164 --- /dev/null +++ b/test/function/samples/import-assertions/warn-assertion-conflicts/other.js @@ -0,0 +1,2 @@ +import 'external' assert { type: 'quuz' }; +import './dep.js' assert { type: 'bar' }; From a83ab2cc3cb88a1e6d9773132e97caa3bf90ceea Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 10 Oct 2022 07:04:42 +0200 Subject: [PATCH 16/17] Add new documentation --- cli/help.md | 5 +-- docs/01-command-line-reference.md | 5 +-- docs/05-plugin-development.md | 50 ++++++++++++++++---------- docs/08-troubleshooting.md | 4 +-- docs/999-big-list-of-options.md | 59 ++----------------------------- 5 files changed, 43 insertions(+), 80 deletions(-) diff --git a/cli/help.md b/cli/help.md index eeea7cce45e..069d4b91d71 100644 --- a/cli/help.md +++ b/cli/help.md @@ -36,6 +36,7 @@ Basic options: --exports Specify export mode (auto, default, named, none) --extend Extend global variable defined by --name --no-externalLiveBindings Do not generate code to support live bindings +--no-externalImportAssertions Omit import assertions in "es" output --failAfterWarnings Exit with an error if the build produced warnings --footer Code to insert at end of bundle (outside wrapper) --no-freeze Do not freeze namespace objects @@ -68,8 +69,8 @@ Basic options: --no-systemNullSetters Do not replace empty SystemJS setters with `null` --no-treeshake Disable tree-shaking optimisations --no-treeshake.annotations Ignore pure call annotations ---no-treeshake.moduleSideEffects Assume modules have no side-effects ---no-treeshake.propertyReadSideEffects Ignore property access side-effects +--no-treeshake.moduleSideEffects Assume modules have no side effects +--no-treeshake.propertyReadSideEffects Ignore property access side effects --no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking --no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw --waitForBundleInput Wait for bundle input files diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index 7690bf07965..5df499cd3c6 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -366,6 +366,7 @@ Many options have command line equivalents. In those cases, any arguments passed --no-esModule Do not add __esModule property --exports Specify export mode (auto, default, named, none) --extend Extend global variable defined by --name +--no-externalImportAssertions Omit import assertions in "es" output --no-externalLiveBindings Do not generate code to support live bindings --failAfterWarnings Exit with an error if the build produced warnings --footer Code to insert at end of bundle (outside wrapper) @@ -399,8 +400,8 @@ Many options have command line equivalents. In those cases, any arguments passed --no-systemNullSetters Do not replace empty SystemJS setters with `null` --no-treeshake Disable tree-shaking optimisations --no-treeshake.annotations Ignore pure call annotations ---no-treeshake.moduleSideEffects Assume modules have no side-effects ---no-treeshake.propertyReadSideEffects Ignore property access side-effects +--no-treeshake.moduleSideEffects Assume modules have no side effects +--no-treeshake.propertyReadSideEffects Ignore property access side effects --no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking --no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw --waitForBundleInput Wait for bundle input files diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index e927847fe84..a0fc1da4174 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -148,17 +148,19 @@ Notifies a plugin when the watcher process will close so that all open resources #### `load` -**Type:** `(id: string) => string | null | {code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`
**Kind:** `async, first`
**Previous Hook:** [`resolveId`](guide/en/#resolveid) or [`resolveDynamicImport`](guide/en/#resolvedynamicimport) where the loaded id was resolved. Additionally, this hook can be triggered at any time from plugin hooks by calling [`this.load`](guide/en/#thisload) to preload the module corresponding to an id.
**Next Hook:** [`transform`](guide/en/#transform) to transform the loaded file if no cache was used, or there was no cached copy with the same `code`, otherwise [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule). +**Type:** `(id: string) => string | null | {code: string, map?: string | SourceMap, ast? : ESTree.Program, assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`
**Kind:** `async, first`
**Previous Hook:** [`resolveId`](guide/en/#resolveid) or [`resolveDynamicImport`](guide/en/#resolvedynamicimport) where the loaded id was resolved. Additionally, this hook can be triggered at any time from plugin hooks by calling [`this.load`](guide/en/#thisload) to preload the module corresponding to an id.
**Next Hook:** [`transform`](guide/en/#transform) to transform the loaded file if no cache was used, or there was no cached copy with the same `code`, otherwise [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule). Defines a custom loader. Returning `null` defers to other `load` functions (and eventually the default behavior of loading from the file system). To prevent additional parsing overhead in case e.g. this hook already used `this.parse` to generate an AST for some reason, this hook can optionally return a `{ code, ast, map }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node. If the transformation does not move code, you can preserve existing sourcemaps by setting `map` to `null`. Otherwise you might need to generate the source map. See [the section on source code transformations](#source-code-transformations). -If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included in the bundle even if the module would have side-effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. The `transform` hook can override this. +If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included in the bundle even if the module would have side effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. The `transform` hook can override this. + +`assertions` contain the import assertions that were used when this module was imported. At the moment, they do not influence rendering for bundled modules but rather serve documentation purposes. If `null` is returned or the flag is omitted, then `assertions` will be determined by the first `resolveId` hook that resolved this module, or the assertions present in the first import of this module. The `transform` hook can override this. See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect of the `syntheticNamedExports` option. If `null` is returned or the flag is omitted, then `syntheticNamedExports` will be determined by the first `resolveId` hook that resolved this module or eventually default to `false`. The `transform` hook can override this. See [custom module meta-data](guide/en/#custom-module-meta-data) for how to use the `meta` option. If a `meta` object is returned by this hook, it will be merged shallowly with any `meta` object returned by the resolveId hook. If no hook returns a `meta` object it will default to an empty object. The `transform` hook can further add or replace properties of this object. -You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `moduleSideEffects`, `syntheticNamedExports` and `meta` inside this hook. +You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` inside this hook. #### `moduleParsed` @@ -180,10 +182,12 @@ This is the only hook that does not have access to most [plugin context](guide/e #### `resolveDynamicImport` -**Type:** `(specifier: string | ESTree.Node, importer: string) => string | false | null | {id: string, external?: boolean}`
**Kind:** `async, first`
**Previous Hook:** [`moduleParsed`](guide/en/#moduleparsed) for the importing file.
**Next Hook:** [`load`](guide/en/#load) if the hook resolved with an id that has not yet been loaded, [`resolveId`](guide/en/#resolveid) if the dynamic import contains a string and was not resolved by the hook, otherwise [`buildEnd`](guide/en/#buildend). +**Type:** `(specifier: string | ESTree.Node, importer: string, {assertions: {[key: string]: string}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`
**Kind:** `async, first`
**Previous Hook:** [`moduleParsed`](guide/en/#moduleparsed) for the importing file.
**Next Hook:** [`load`](guide/en/#load) if the hook resolved with an id that has not yet been loaded, [`resolveId`](guide/en/#resolveid) if the dynamic import contains a string and was not resolved by the hook, otherwise [`buildEnd`](guide/en/#buildend). Defines a custom resolver for dynamic imports. Returning `false` signals that the import should be kept as it is and not be passed to other resolvers thus making it external. Similar to the [`resolveId`](guide/en/#resolveid) hook, you can also return an object to resolve the import to a different id while marking it as external at the same time. +`assertions` tells you which import assertions were present in the import. I.e. `import("foo", {assert: {type: "json"}})` will pass along `assertions: {type: "json"}`. + In case a dynamic import is passed a string as argument, a string returned from this hook will be interpreted as an existing module id while returning `null` will defer to other resolvers and eventually to `resolveId` . In case a dynamic import is not passed a string as argument, this hook gets access to the raw AST nodes to analyze and behaves slightly different in the following ways: @@ -196,7 +200,7 @@ Note that the return value of this hook will not be passed to `resolveId` afterw #### `resolveId` -**Type:** `(source: string, importer: string | undefined, options: {isEntry: boolean, custom?: {[plugin: string]: any}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`
**Kind:** `async, first`
**Previous Hook:** [`buildStart`](guide/en/#buildstart) if we are resolving an entry point, [`moduleParsed`](guide/en/#moduleparsed) if we are resolving an import, or as fallback for [`resolveDynamicImport`](guide/en/#resolvedynamicimport). Additionally, this hook can be triggered during the build phase from plugin hooks by calling [`this.emitFile`](guide/en/#thisemitfile) to emit an entry point or at any time by calling [`this.resolve`](guide/en/#thisresolve) to manually resolve an id.
**Next Hook:** [`load`](guide/en/#load) if the resolved id that has not yet been loaded, otherwise [`buildEnd`](guide/en/#buildend). +**Type:** `(source: string, importer: string | undefined, options: {isEntry: boolean, assertions: {[key: string]: string}, custom?: {[plugin: string]: any}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`
**Kind:** `async, first`
**Previous Hook:** [`buildStart`](guide/en/#buildstart) if we are resolving an entry point, [`moduleParsed`](guide/en/#moduleparsed) if we are resolving an import, or as fallback for [`resolveDynamicImport`](guide/en/#resolvedynamicimport). Additionally, this hook can be triggered during the build phase from plugin hooks by calling [`this.emitFile`](guide/en/#thisemitfile) to emit an entry point or at any time by calling [`this.resolve`](guide/en/#thisresolve) to manually resolve an id.
**Next Hook:** [`load`](guide/en/#load) if the resolved id that has not yet been loaded, otherwise [`buildEnd`](guide/en/#buildend). Defines a custom resolver. A resolver can be useful for e.g. locating third-party dependencies. Here `source` is the importee exactly as it is written in the import statement, i.e. for @@ -278,6 +282,8 @@ function injectPolyfillPlugin() { } ``` +`assertions` tells you which import assertions were present in the import. I.e. `import "foo" assert {type: "json"}` will pass along `assertions: {type: "json"}`. + Returning `null` defers to other `resolveId` functions and eventually the default resolution behavior. Returning `false` signals that `source` should be treated as an external module and not included in the bundle. If this happens for a relative import, the id will be renormalized the same way as when the `external` option is used. If you return an object, then it is possible to resolve an import to a different id while excluding it from the bundle at the same time. This allows you to replace dependencies with external dependencies without the need for the user to mark them as "external" manually via the `external` option: @@ -298,13 +304,15 @@ function externalizeDependencyPlugin() { If `external` is `true`, then absolute ids will be converted to relative ids based on the user's choice for the [`makeAbsoluteExternalsRelative`](guide/en/#makeabsoluteexternalsrelative) option. This choice can be overridden by passing either `external: "relative"` to always convert an absolute id to a relative id or `external: "absolute"` to keep it as an absolute id. When returning an object, relative external ids, i.e. ids starting with `./` or `../`, will _not_ be internally converted to an absolute id and converted back to a relative id in the output, but are instead included in the output unchanged. If you want relative ids to be renormalised and deduplicated instead, return an absolute file system location as `id` and choose `external: "relative"`. -If `false` is returned for `moduleSideEffects` in the first hook that resolves a module id and no other module imports anything from this module, then this module will not be included even if the module would have side-effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `treeshake.moduleSideEffects` option or default to `true`. The `load` and `transform` hooks can override this. +If `false` is returned for `moduleSideEffects` in the first hook that resolves a module id and no other module imports anything from this module, then this module will not be included even if the module would have side effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `treeshake.moduleSideEffects` option or default to `true`. The `load` and `transform` hooks can override this. + +If you return a value for `assertions` for an external module, this will determine how imports of this module will be rendered when generating `"es"` output. E.g. `{id: "foo", external: true, assertions: {type: "json"}}` will cause imports of this module appear as `import "foo" assert {type: "json"}`. If you do not pass a value, the value of the `assertions` input parameter will be used. Pass an empty object to remove any assertions. While `assertions` do not influence rendering for bundled modules, they still need to be consistent across all imports of a module, otherwise a warning is emitted. The `load` and `transform` hooks can override this. See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect of the `syntheticNamedExports` option. If `null` is returned or the flag is omitted, then `syntheticNamedExports` will default to `false`. The `load` and `transform` hooks can override this. See [custom module meta-data](guide/en/#custom-module-meta-data) for how to use the `meta` option. If `null` is returned or the option is omitted, then `meta` will default to an empty object. The `load` and `transform` hooks can add or replace properties of this object. -Note that while `resolveId` will be called for each import of a module and can therefore resolve to the same `id` many times, values for `external`, `moduleSideEffects`, `syntheticNamedExports` or `meta` can only be set once before the module is loaded. The reason is that after this call, Rollup will continue with the [`load`](guide/en/#load) and [`transform`](guide/en/#transform) hooks for that module that may override these values and should take precedence if they do so. +Note that while `resolveId` will be called for each import of a module and can therefore resolve to the same `id` many times, values for `external`, `assertions`, `meta`, `moduleSideEffects` or `syntheticNamedExports` can only be set once before the module is loaded. The reason is that after this call, Rollup will continue with the [`load`](guide/en/#load) and [`transform`](guide/en/#transform) hooks for that module that may override these values and should take precedence if they do so. When triggering this hook from a plugin via [`this.resolve`](guide/en/#thisresolve), it is possible to pass a custom options object to this hook. While this object will be passed unmodified, plugins should follow the convention of adding a `custom` property with an object where the keys correspond to the names of the plugins that the options are intended for. For details see [custom resolver options](guide/en/#custom-resolver-options). @@ -312,7 +320,7 @@ In watch mode or when using the cache explicitly, the resolved imports of a cach #### `shouldTransformCachedModule` -**Type:** `({id: string, code: string, ast: ESTree.Program, resoledSources: {[source: string]: ResolvedId}, meta: {[plugin: string]: any}, moduleSideEffects: boolean | "no-treeshake", syntheticNamedExports: string | boolean}) => boolean`
**Kind:** `async, first`
**Previous Hook:** [`load`](guide/en/#load) where the cached file was loaded to compare its code with the cached version.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) if no plugin returns `true`, otherwise [`transform`](guide/en/#transform). +**Type:** `({id: string, code: string, ast: ESTree.Program, resolvedSources: {[source: string]: ResolvedId}, assertions: {[key: string]: string}, meta: {[plugin: string]: any}, moduleSideEffects: boolean | "no-treeshake", syntheticNamedExports: boolean | string}) => boolean`
**Kind:** `async, first`
**Previous Hook:** [`load`](guide/en/#load) where the cached file was loaded to compare its code with the cached version.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) if no plugin returns `true`, otherwise [`transform`](guide/en/#transform). If the Rollup cache is used (e.g. in watch mode or explicitly via the JavaScript API), Rollup will skip the [`transform`](guide/en/#transform) hook of a module if after the [`load`](guide/en/#transform) hook, the loaded `code` is identical to the code of the cached copy. To prevent this, discard the cached copy and instead transform a module, plugins can implement this hook and return `true`. @@ -322,7 +330,7 @@ If a plugin does not return `true`, Rollup will trigger this hook for other plug #### `transform` -**Type:** `(code: string, id: string) => string | null | {code?: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`
**Kind:** `async, sequential`
**Previous Hook:** [`load`](guide/en/#load) where the currently handled file was loaded. If caching is used and there was a cached copy of that module, [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule) if a plugin returned `true` for that hook.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) once the file has been processed and parsed. +**Type:** `(code: string, id: string) => string | null | {code?: string, map?: string | SourceMap, ast? : ESTree.Program, assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`
**Kind:** `async, sequential`
**Previous Hook:** [`load`](guide/en/#load) where the currently handled file was loaded. If caching is used and there was a cached copy of that module, [`shouldTransformCachedModule`](guide/en/#shouldtransformcachedmodule) if a plugin returned `true` for that hook.
**Next Hook:** [`moduleParsed`](guide/en/#moduleparsed) once the file has been processed and parsed. Can be used to transform individual modules. To prevent additional parsing overhead in case e.g. this hook already used `this.parse` to generate an AST for some reason, this hook can optionally return a `{ code, ast, map }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node. If the transformation does not move code, you can preserve existing sourcemaps by setting `map` to `null`. Otherwise you might need to generate the source map. See [the section on source code transformations](#source-code-transformations). @@ -332,19 +340,21 @@ In all other cases, the [`shouldTransformCachedModule`](guide/en/#shouldtransfor You can also use the object form of the return value to configure additional properties of the module. Note that it's possible to return only properties and no code transformations. -If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included even if the module would have side-effects. +If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included even if the module would have side effects. -If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). +If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. +`assertions` contain the import assertions that were used when this module was imported. At the moment, they do not influence rendering for bundled modules but rather serve documentation purposes. If `null` is returned or the flag is omitted, then `assertions` will be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module, or the assertions present in the first import of this module. + See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect of the `syntheticNamedExports` option. If `null` is returned or the flag is omitted, then `syntheticNamedExports` will be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `false`. See [custom module meta-data](guide/en/#custom-module-meta-data) for how to use the `meta` option. If `null` is returned or the option is omitted, then `meta` will be determined by the `load` hook that loaded this module, the first `resolveId` hook that resolved this module or eventually default to an empty object. -You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `moduleSideEffects`, `syntheticNamedExports` and `meta` inside this hook. +You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the previous values of `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` inside this hook. #### `watchChange` @@ -779,6 +789,7 @@ type ModuleInfo = { dynamicImporters: string[]; // the ids of all modules that import this module via dynamic import() implicitlyLoadedAfterOneOf: string[]; // implicit relationships, declared via this.emitFile implicitlyLoadedBefore: string[]; // implicit relationships, declared via this.emitFile + assertions: { [key: string]: string }; // import assertions for this module meta: { [plugin: string]: any }; // custom module meta-data moduleSideEffects: boolean | 'no-treeshake'; // are imports of this module included if nothing is imported from it syntheticNamedExports: boolean | string; // final value of synthetic named exports @@ -787,9 +798,10 @@ type ModuleInfo = { type ResolvedId = { id: string; // the id of the imported module external: boolean | 'absolute'; // is this module external, "absolute" means it will not be rendered as relative in the module + assertions: { [key: string]: string }; // import assertions for this import + meta: { [plugin: string]: any }; // custom module meta-data when resolving the module moduleSideEffects: boolean | 'no-treeshake'; // are side effects of the module observed, is tree-shaking enabled syntheticNamedExports: boolean | string; // does the module allow importing non-existing named exports - meta: { [plugin: string]: any }; // custom module meta-data when resolving the module }; ``` @@ -802,7 +814,7 @@ During the build, this object represents currently available information about t - `importers`, `dynamicImporters` and `implicitlyLoadedBefore` will start as empty arrays, which receive additional entries as new importers and implicit dependents are discovered. They will no longer change after `buildEnd`. - `isIncluded` is only available after `buildEnd`, at which point it will no longer change. - `importedIds`, `importedIdResolutions`, `dynamicallyImportedIds` and `dynamicallyImportedIdResolutions` are available when a module has been parsed and its dependencies have been resolved. This is the case in the `moduleParsed` hook or after awaiting [`this.load`](guide/en/#thisload) with the `resolveDependencies` flag. At that point, they will no longer change. -- `meta`, `moduleSideEffects` and `syntheticNamedExports` can be changed by [`load`](guide/en/#load) and [`transform`](guide/en/#transform) hooks. Moreover, while most properties are read-only, `moduleSideEffects` is writable and changes will be picked up if they occur before the `buildEnd` hook is triggered. `meta` should not be overwritten, but it is ok to mutate its properties at any time to store meta information about a module. The advantage of doing this instead of keeping state in a plugin is that `meta` is persisted to and restored from the cache if it is used, e.g. when using watch mode from the CLI. +- `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` can be changed by [`load`](guide/en/#load) and [`transform`](guide/en/#transform) hooks. Moreover, while most properties are read-only, these properties are writable and changes will be picked up if they occur before the `buildEnd` hook is triggered. `meta` itself should not be overwritten, but it is ok to mutate its properties at any time to store meta information about a module. The advantage of doing this instead of keeping state in a plugin is that `meta` is persisted to and restored from the cache if it is used, e.g. when using watch mode from the CLI. Returns `null` if the module id cannot be found. @@ -814,7 +826,7 @@ Get ids of the files which has been watched previously. Include both files added #### `this.load` -**Type:** `({id: string, moduleSideEffects?: boolean | 'no-treeshake' | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null, resolveDependencies?: boolean}) => Promise` +**Type:** `({id: string, resolveDependencies?: boolean, assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}) => Promise` Loads and parses the module corresponding to the given id, attaching additional meta information to the module if provided. This will trigger the same [`load`](guide/en/#load), [`transform`](guide/en/#transform) and [`moduleParsed`](guide/en/#moduleparsed) hooks that would be triggered if the module were imported by another module. @@ -822,7 +834,7 @@ This allows you to inspect the final content of modules before deciding how to r The returned Promise will resolve once the module has been fully transformed and parsed but before any imports have been resolved. That means that the resulting `ModuleInfo` will have empty `importedIds`, `dynamicallyImportedIds`, `importedIdResolutions` and `dynamicallyImportedIdResolutions`. This helps to avoid deadlock situations when awaiting `this.load` in a `resolveId` hook. If you are interested in `importedIds` and `dynamicallyImportedIds`, you can either implement a `moduleParsed` hook or pass the `resolveDependencies` flag, which will make the Promise returned by `this.load` wait until all dependency ids have been resolved. -Note that with regard to the `moduleSideEffects`, `syntheticNamedExports` and `meta` options, the same restrictions apply as for the `resolveId` hook: Their values only have an effect if the module has not been loaded yet. Thus, it is very important to use `this.resolve` first to find out if any plugins want to set special values for these options in their `resolveId` hook, and pass these options on to `this.load` if appropriate. The example below showcases how this can be handled to add a proxy module for modules containing a special code comment. Note the special handling for re-exporting the default export: +Note that with regard to the `assertions`, `meta`, `moduleSideEffects` and `syntheticNamedExports` options, the same restrictions apply as for the `resolveId` hook: Their values only have an effect if the module has not been loaded yet. Thus, it is very important to use `this.resolve` first to find out if any plugins want to set special values for these options in their `resolveId` hook, and pass these options on to `this.load` if appropriate. The example below showcases how this can be handled to add a proxy module for modules containing a special code comment. Note the special handling for re-exporting the default export: ```js export default function addProxyPlugin() { @@ -955,7 +967,7 @@ Use Rollup's internal acorn instance to parse code to an AST. #### `this.resolve` -**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, meta: {[plugin: string]: any}} | null>` +**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, assertions?: {[key: string]: string}, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", assertions: {[key: string]: string}, meta: {[plugin: string]: any} | null, moduleSideEffects: boolean | "no-treeshake", syntheticNamedExports: boolean | string>` Resolve imports to module ids (i.e. file names) using the same plugins that Rollup uses, and determine if an import should be external. If `null` is returned, the import could not be resolved by Rollup or any plugin but was not explicitly marked as external by the user. If an absolute external id is returned that should remain absolute in the output either via the [`makeAbsoluteExternalsRelative`](guide/en/#makeabsoluteexternalsrelative) option or by explicit plugin choice in the [`resolveId`](guide/en/#resolveid) hook, `external` will be `"absolute"` instead of `true`. @@ -965,7 +977,9 @@ You can also pass an object of plugin-specific options via the `custom` option, The value for `isEntry` you pass here will be passed along to the [`resolveId`](guide/en/#resolveid) hooks handling this call, otherwise `false` will be passed if there is an importer and `true` if there is not. -When calling this function from a `resolveId` hook, you should always check if it makes sense for you to pass along the `isEntry` and `custom` options. +If you pass an object for `assertions`, it will simulate resolving an import with an assertion, e.g. `assertions: {type: "json"}` simulates resolving `import "foo" assert {type: "json"}`. This will be passed to any [`resolveId`](guide/en/#resolveid) hooks handling this call and may ultimately become part of the returned object. + +When calling this function from a `resolveId` hook, you should always check if it makes sense for you to pass along the `isEntry`, `custom` and `assertions` options. #### `this.setAssetSource` diff --git a/docs/08-troubleshooting.md b/docs/08-troubleshooting.md index 7ed5f3e30c1..8f4e12f4de2 100644 --- a/docs/08-troubleshooting.md +++ b/docs/08-troubleshooting.md @@ -36,9 +36,9 @@ Using the [Function constructor](https://developer.mozilla.org/en-US/docs/Web/Ja Sometimes, you'll end up with code in your bundle that doesn't seem like it should be there. For example, if you import a utility from `lodash-es`, you might expect that you'll get the bare minimum of code necessary for that utility to work. -But Rollup has to be conservative about what code it removes in order to guarantee that the end result will run correctly. If an imported module appears to have _side-effects_, either on bits of the module that you're using or on the global environment, Rollup plays it safe and includes those side-effects. +But Rollup has to be conservative about what code it removes in order to guarantee that the end result will run correctly. If an imported module appears to have _side effects_, either on bits of the module that you're using or on the global environment, Rollup plays it safe and includes those side effects. -Because static analysis in a dynamic language like JavaScript is hard, there will occasionally be false positives. Lodash is a good example of a module that _looks_ like it has lots of side-effects, even in places that it doesn't. You can often mitigate those false positives by importing submodules (e.g. `import map from 'lodash-es/map'` rather than `import { map } from 'lodash-es'`). +Because static analysis in a dynamic language like JavaScript is hard, there will occasionally be false positives. Lodash is a good example of a module that _looks_ like it has lots of side effects, even in places that it doesn't. You can often mitigate those false positives by importing submodules (e.g. `import map from 'lodash-es/map'` rather than `import { map } from 'lodash-es'`). Rollup's static analysis will improve over time, but it will never be perfect in all cases – that's just JavaScript. diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 4a02fbe64d3..16514383d8e 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -534,62 +534,9 @@ Whether to extend the global variable defined by the `name` option in `umd` or ` #### output.externalImportAssertions -Type: `{[extName: string]: string | null} | false | (moduleInfo: ModulInfo) => ({[key: string]: string} | null)`
Default: `{".json": "json"}` +Type: `boolean`
CLI: `--externalImportAssertions`/`--no-externalImportAssertions`
Default: `true` -Which import assertions to add to external imports in the output if the output format is `es`. By default, only external `.json` files will receive a `type: 'json'` assertion. - -- If an object is provided, then the keys are considered to be file extensions while the values add a corresponding `type` assertion to files of that type. - - ```js - // config - export default { - external: ['./foo.css', './bar.json'], - // ... - output: { - externalImportAssertions: { - '.css': 'css' - } - // ... - } - }; - - // input - import './foo.css'; - import './bar.json'; - - // output - import './foo.css' assert { type: 'css' }; - import './bar.json'; - ``` - -- If a function is provided, it will be called once for each external dependency with information about the external module. If it returns an object, its key-value pairs will be added as arbitrary assertions to all imports of that dependency. - - ```js - // config - export default { - external: ['./foo.css', './bar.json'], - // ... - output: { - externalImportAssertions({ id }) { - if (id.endsWith('.css')) { - return { type: 'css', special: 'attribute' }; - } - return null; - } - // ... - } - }; - - // input - import './foo.css'; - import './bar.json'; - - // output - import './foo.css' assert { type: 'css', special: 'attribute' }; - import './bar.json'; - ``` - -- `false` will disable all assertions. +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 @@ -1859,7 +1806,7 @@ export default { **treeshake.propertyReadSideEffects**
Type: `boolean | 'always'`
CLI: `--treeshake.propertyReadSideEffects`/`--no-treeshake.propertyReadSideEffects`
Default: `true` -If `true`, retain unused property reads that Rollup can determine to have side-effects. This includes accessing properties of `null` or `undefined` or triggering explicit getters via property access. Note that this does not cover destructuring assignment or getters on objects passed as function parameters. +If `true`, retain unused property reads that Rollup can determine to have side effects. This includes accessing properties of `null` or `undefined` or triggering explicit getters via property access. Note that this does not cover destructuring assignment or getters on objects passed as function parameters. If `false`, assume reading a property of an object never has side effects. Depending on your code, disabling this option can significantly reduce bundle size but can potentially break functionality if you rely on getters or errors from illegal property access. From 685d10c63d3b434aeb4969cd558de0ad2bb01f9e Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 10 Oct 2022 10:33:19 +0200 Subject: [PATCH 17/17] Improve coverage --- src/utils/parseAssertions.ts | 9 +++++---- .../import-assertions/assertion-shapes/_config.js | 7 +++++++ .../import-assertions/assertion-shapes/_expected.js | 4 ++++ .../samples/import-assertions/assertion-shapes/main.js | 4 ++++ 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 test/form/samples/import-assertions/assertion-shapes/_config.js create mode 100644 test/form/samples/import-assertions/assertion-shapes/_expected.js create mode 100644 test/form/samples/import-assertions/assertion-shapes/main.js diff --git a/src/utils/parseAssertions.ts b/src/utils/parseAssertions.ts index 7a54a172777..4a0f535477a 100644 --- a/src/utils/parseAssertions.ts +++ b/src/utils/parseAssertions.ts @@ -19,7 +19,7 @@ export function getAssertionsFromImportExpression(node: ImportExpression): Recor const key = getPropertyKey(property); if ( typeof key === 'string' && - typeof ((property as Property).value as Literal)?.value === 'string' + typeof ((property as Property).value as Literal).value === 'string' ) { return [key, ((property as Property).value as Literal).value] as [string, string]; } @@ -34,9 +34,10 @@ export function getAssertionsFromImportExpression(node: ImportExpression): Recor const getPropertyKey = ( property: Property | SpreadElement | ImportAttribute -): LiteralValue | undefined => - ((property as Property | ImportAttribute).key as Identifier).name || - ((property as Property | ImportAttribute).key as Literal).value; +): 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 diff --git a/test/form/samples/import-assertions/assertion-shapes/_config.js b/test/form/samples/import-assertions/assertion-shapes/_config.js new file mode 100644 index 00000000000..3686c79e002 --- /dev/null +++ b/test/form/samples/import-assertions/assertion-shapes/_config.js @@ -0,0 +1,7 @@ +module.exports = { + description: 'handles special shapes of assertions', + expectedWarnings: 'UNRESOLVED_IMPORT', + options: { + external: () => true + } +}; diff --git a/test/form/samples/import-assertions/assertion-shapes/_expected.js b/test/form/samples/import-assertions/assertion-shapes/_expected.js new file mode 100644 index 00000000000..b5d8cbaa42d --- /dev/null +++ b/test/form/samples/import-assertions/assertion-shapes/_expected.js @@ -0,0 +1,4 @@ +import('external-a', { assert: { type: 'json' } }); +import('external-b'); +import('external-c'); +import('external-d'); diff --git a/test/form/samples/import-assertions/assertion-shapes/main.js b/test/form/samples/import-assertions/assertion-shapes/main.js new file mode 100644 index 00000000000..a82cad812bc --- /dev/null +++ b/test/form/samples/import-assertions/assertion-shapes/main.js @@ -0,0 +1,4 @@ +import('external-a', { 'assert': { 'type': 'json', foo: 1, ...{} } }); +import('external-b', { assert: {} }); +import('external-c', { ...{} }); +import('external-d', {});