diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d44569d..f5e0ee657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # rollup changelog +## 4.57.0 + +_2026-01-27_ + +### Features + +- Add import attributes to all plugin hooks that did not provide them yet (#5700) +- Deprecate returning import attributes from `load` or `transform` hooks as that will no longer be supported with rollup 5 (#5700) + +### Pull Requests + +- [#5700](https://github.com/rollup/rollup/pull/5700): extend more hooks to include import attributes and add warnings (@TrickyPi, @lukastaegert) +- [#6243](https://github.com/rollup/rollup/pull/6243): fix(deps): update swc monorepo (major) (@renovate[bot], @lukastaegert) +- [#6244](https://github.com/rollup/rollup/pull/6244): fix(deps): lock file maintenance minor/patch updates (@renovate[bot], @lukastaegert) +- [#6245](https://github.com/rollup/rollup/pull/6245): chore(deps): lock file maintenance (@renovate[bot]) +- [#6246](https://github.com/rollup/rollup/pull/6246): Refactor to reduce Rollup 5 upgrade diff (@lukastaegert) + ## 4.56.0 _2026-01-22_ diff --git a/browser/package.json b/browser/package.json index b34ab0a2a..87ee7f690 100644 --- a/browser/package.json +++ b/browser/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/browser", - "version": "4.56.0", + "version": "4.57.0", "description": "Next-generation ES module bundler browser build", "main": "dist/rollup.browser.js", "module": "dist/es/rollup.browser.js", diff --git a/docs/plugin-development/index.md b/docs/plugin-development/index.md index cc6b5ff31..dfbda1a59 100644 --- a/docs/plugin-development/index.md +++ b/docs/plugin-development/index.md @@ -337,31 +337,32 @@ flowchart TB | | | | --: | :-- | -| 类型: | `(id: string) => LoadResult` | +| 类型: | `(id: string, options: Options) => LoadResult` | | 类别: | async, first | | 上一个钩子: | 已解析加载的 id 的 [`resolveId`](#resolveid) 或 [`resolveDynamicImport`](#resolvedynamicimport)。此外,此钩子可以通过调用 [`this.load`](#this-load) 来从插件钩子中的任何位置触发预加载与 id 对应的模块 | | 下一个钩子: | 如果未使用缓存,或者没有具有相同 `code` 的缓存副本,则为 [`transform`](#transform),否则为 [`shouldTransformCachedModule`](#shouldtransformcachedmodule) | ```typescript +type Options = { + attributes: Record; +}; + type LoadResult = string | null | SourceDescription; interface SourceDescription { code: string; map?: string | SourceMap; ast?: ESTree.Program; - attributes?: { [key: string]: string } | null; meta?: { [plugin: string]: any } | null; moduleSideEffects?: boolean | 'no-treeshake' | null; syntheticNamedExports?: boolean | string | null; } ``` -定义一个自定义加载器。返回 `null` 将推迟到其他 `load` 函数(最终是从文件系统加载的默认行为)。为了防止在某些情况下(例如,此钩子已经使用 [`this.parse`](#this-parse) 生成了 AST)产生额外的解析开销,此钩子可以选择返回一个 `{ code, ast, map }` 对象。`ast` 必须是一个标准的 ESTree AST,每个节点都有 `start` 和 `end` 属性。如果转换不移动代码,则可以通过将 `map` 设置为 `null` 来保留现有的源映射。否则,你可能需要生成源映射。请参阅 [源代码转换](#source-code-transformations) 部分。 +定义一个自定义加载器。`options.attributes` 包含导入此模块时使用的导入属性,由第一个解析此模块的 `resolveId` 钩子或第一个导入中存在的属性确定。返回 `null` 将推迟到其他 `load` 函数(最终是从文件系统加载的默认行为)。为了防止在某些情况下(例如,此钩子已经使用 [`this.parse`](#this-parse) 生成了 AST)产生额外的解析开销,此钩子可以选择返回一个 `{ code, ast, map }` 对象。`ast` 必须是一个标准的 ESTree AST,每个节点都有 `start` 和 `end` 属性。如果转换不移动代码,则可以通过将 `map` 设置为 `null` 来保留现有的源映射。否则,你可能需要生成源映射。请参阅 [源代码转换](#source-code-transformations) 部分。 如果 `moduleSideEffects` 返回 `false`,并且没有其他模块从该模块导入任何内容,则即使该模块具有副作用,该模块也不会包含在产物中。如果返回 `true`,则 Rollup 将使用其默认算法包含模块中具有副作用的所有语句(例如修改全局或导出变量)。如果返回 `"no-treeshake"`,则将关闭此模块的除屑优化,并且即使该模块为空,也将在生成的块之一中包含它。如果返回 `null` 或省略标志,则 `moduleSideEffects` 将由第一个解析此模块的 `resolveId` 钩子,[`treeshake.moduleSideEffects`](../configuration-options/index.md#treeshake-modulesideeffects) 选项或最终默认为 `true` 确定。`transform` 钩子可以覆盖此设置。 -`attributes` 包括导入此模块时使用的导入属性。目前,它们不会影响产物模块的呈现,而是用于文档目的。如果返回 `null` 或省略标志,则 `attributes` 将由第一个解析此模块的 `resolveId` 钩子或此模块的第一个导入中存在的断言确定。`transform` 钩子可以覆盖此设置。 - 有关 `syntheticNamedExports` 选项的影响,请参见 [合成命名导出](#synthetic-named-exports)。如果返回 `null` 或省略标志,则 `syntheticNamedExports` 将由第一个解析此模块的 `resolveId` 钩子确定,或者最终默认为 `false`。`transform` 钩子可以覆盖此设置。 有关如何使用 `meta` 选项的 [自定义模块元数据](#custom-module-meta-data)。如果此钩子返回 `meta` 对象,则该对象将与 `resolveId` 钩子返回的任何 `meta` 对象浅合并。如果没有钩子返回 `meta` 对象,则默认为一个空对象。`transform` 钩子可以进一步添加或替换该对象的属性。 @@ -470,7 +471,10 @@ function plugin2() { type ResolveDynamicImportHook = ( specifier: string | AstNode, importer: string, - options: { attributes: Record } + options: { + attributes: Record; + importerAttributes: Record; + } ) => ResolveIdResult; ``` @@ -484,6 +488,8 @@ type ResolveDynamicImportHook = ( `attributes` 告诉你导入中存在哪些导入属性。即 `import("foo", {assert: {type: "json"}})` 将传递 `attributes: {type: "json"}`。 +`importerAttributes` 表示导入模块的导入属性。 + 如果动态导入作为字符串参数传递,则从此钩子返回的字符串将被解释为现有模块 id,而返回 `null` 将推迟到其他解析器,最终到达 `resolveId`。 如果动态导入未传递字符串作为参数,则此钩子可以访问原始 AST 节点以进行分析,并以以下方式略有不同: @@ -508,6 +514,7 @@ type ResolveIdHook = ( source: string, importer: string | undefined, options: { + importerAttributes: Record | undefined; attributes: Record; custom?: { [plugin: string]: any }; isEntry: boolean; @@ -537,6 +544,8 @@ import { foo } from '../bar.js'; `importer` 是导入模块的解析完全后的 id。在解析入口点时,`importer` 通常为 `undefined`。这里的一个例外是通过 [`this.emitFile`](#this-emitfile) 生成的入口点,这里可以提供一个 `importer` 参数。 +`importerAttributes` 是导入模块的导入属性。在解析入口点时,`importerAttributes` 通常为 `undefined`。 + 对于这些情况,`isEntry` 选项将告诉你我们正在解析用户定义的入口点、已产出的块,还是是否为 [`this.resolve`](#this-resolve) 上下文函数提供了 `isEntry` 参数。 例如,你可以将其用作为入口点定义自定义代理模块的机制。以下插件将所有入口点代理到注入 polyfill 导入的模块中。 @@ -667,6 +676,7 @@ function externalizeDependencyPlugin() { ```typescript type ShouldTransformCachedModuleHook = (options: { ast: AstNode; + attributes: Record; code: string; id: string; meta: { [plugin: string]: any }; @@ -685,26 +695,29 @@ type ShouldTransformCachedModuleHook = (options: { | | | | --: | :-- | -| 类型: | `(code: string, id: string) => TransformResult` | +| 类型: | `(code: string, id: string, options: Options) => TransformResult` | | 类别: | async, sequential | | 上一个钩子: | [`load`](#load),用于加载当前处理的文件。如果使用缓存并且该模块有一个缓存副本,则为 [`shouldTransformCachedModule`](#shouldtransformcachedmodule),如果插件为该钩子返回了 `true` | | 下一个钩子: | [`moduleParsed`](#moduleparsed),一旦文件已被处理和解析 | ```typescript +type Options = { + attributes: Record; +}; + type TransformResult = string | null | Partial; interface SourceDescription { code: string; map?: string | SourceMap; ast?: ESTree.Program; - attributes?: { [key: string]: string } | null; meta?: { [plugin: string]: any } | null; moduleSideEffects?: boolean | 'no-treeshake' | null; syntheticNamedExports?: boolean | string | null; } ``` -可以被用来转换单个模块。为了防止额外的解析开销,例如,这个钩子已经使用 [`this.parse`](#this-parse) 生成了一个 AST,这个钩子可以选择返回一个 `{ code, ast, map }` 对象。`ast` 必须是一个标准的 ESTree AST,每个节点都有 `start` 和 `end` 属性。如果转换不移动代码,你可以通过将 `map` 设置为 `null` 来保留现有的 sourcemaps。否则,你可能需要生成源映射。请参阅 [源代码转换](#source-code-transformations) 部分。 +可以被用来转换单个模块。`options.attributes` 包含导入此模块时使用的导入属性,由第一个解析此模块的 `resolveId` 钩子或第一个导入中存在的属性确定。为了防止额外的解析开销,例如,这个钩子已经使用 [`this.parse`](#this-parse) 生成了一个 AST,这个钩子可以选择返回一个 `{ code, ast, map }` 对象。`ast` 必须是一个标准的 ESTree AST,每个节点都有 `start` 和 `end` 属性。如果转换不移动代码,你可以通过将 `map` 设置为 `null` 来保留现有的 sourcemaps。否则,你可能需要生成源映射。请参阅 [源代码转换](#source-code-transformations) 部分。 请注意,在观察模式下或明确使用缓存时,当重新构建时,此钩子的结果会被缓存,仅当模块的 `code` 发生更改或上次触发此钩子时添加了通过 `this.addWatchFile` 或 `this.emitFile` 添加的文件时,才会再次触发该模块的钩子。 @@ -720,8 +733,6 @@ interface SourceDescription { 如果返回 `null` 或省略标志,则 `moduleSideEffects` 将由加载此模块的 `load` 钩子、解析此模块的第一个 `resolveId` 钩子、[`treeshake.moduleSideEffects`](../configuration-options/index.md#treeshake-modulesideeffects) 选项或最终默认为 `true` 确定。 -`attributes` 包括了导入的属性,这些属性在导入此模块时使用。目前,它们不会影响产物模块的呈现,而是用于文档目的。如果返回 `null` 或省略标志,则 `attributes` 将由加载此模块的 `load` 钩子、解析此模块的第一个 `resolveId` 钩子或此模块的第一个导入中存在的属性确定。 - 有关 `syntheticNamedExports` 选项的影响,请参见 [合成命名导出](#synthetic-named-exports)。如果返回 `null` 或省略标志,则 `syntheticNamedExports` 将由加载此模块的 `load` 钩子、解析此模块的第一个 `resolveId` 钩子、[`treeshake.moduleSideEffects`](../configuration-options/index.md#treeshake-modulesideeffects) 选项或最终默认为 `false` 确定。 有关如何使用 `meta` 选项,请参见 [自定义模块元数据](#custom-module-meta-data)。如果返回 `null` 或省略选项,则 `meta` 将由加载此模块的 `load` 钩子、解析此模块的第一个 `resolveId` 钩子或最终默认为空对象确定。 @@ -1098,6 +1109,7 @@ type renderDynamicImportHook = (options: { chunk: PreRenderedChunkWithFileName; targetChunk: PreRenderedChunkWithFileName; getTargetChunkImports: () => DynamicImportTargetChunk[] | null; + targetModuleAttributes: Record; }) => { left: string; right: string } | null; type PreRenderedChunkWithFileName = PreRenderedChunk & { fileName: string }; @@ -1124,7 +1136,7 @@ interface ImportedExternalChunk { 这个钩子函数提供了对动态导入渲染的精细控制,它通过替换导入表达式参数左侧(`import(`)和右(`)`)侧的代码实现这一功能。如果返回 `null`,则会降级到其他同类型的钩子函数,最终渲染出特定格式的默认值。 -`format` 是渲染的输出格式,`moduleId` 是进行动态导入的模块的 id。如果导入可以被解析为内部或外部 id,那么 `targetModuleId` 将被设置为这个 id,否则它将是 `null`。如果动态导入包含了一个非字符串表达式,这个表达式被 [`resolveDynamicImport`](#resolvedynamicimport) 钩子函数解析为一个替换字符串,那么 `customResolution` 将包含那个字符串。`chunk` 和 `targetChunk` 分别提供了执行导入的块和被导入的块(目标块)的额外信息。`getTargetChunkImports` 返回一个数组,包含了被目标块导入的块。如果目标块未解析或是外部的,`targetChunk` 将为 null,`getTargetChunkImports` 也将返回 null。 +`format` 是渲染的输出格式,`moduleId` 是进行动态导入的模块的 id。如果导入可以被解析为内部或外部 id,那么 `targetModuleId` 将被设置为这个 id,`targetModuleAttributes` 将被设置为应用于此解析模块的导入属性,否则 `targetModuleId` 将是 `null`,`targetModuleAttributes` 将是一个空对象。如果动态导入包含了一个非字符串表达式,这个表达式被 [`resolveDynamicImport`](#resolvedynamicimport) 钩子函数解析为一个替换字符串,那么 `customResolution` 将包含那个字符串。`chunk` 和 `targetChunk` 分别提供了执行导入的块和被导入的块(目标块)的额外信息。`getTargetChunkImports` 返回一个数组,包含了被目标块导入的块。如果目标块未解析或是外部的,`targetChunk` 将为 null,`getTargetChunkImports` 也将返回 null。 `PreRenderedChunkWithFileName` 类型与 `PreRenderedChunk` 类型相同,只是多了一个 `fileName` 字段,这个字段包含了块的路径和文件名。如果块文件名格式包含了哈希,`fileName` 可能会包含一个占位符。 @@ -1258,6 +1270,7 @@ import('./lib.js'); ```typescript type ResolveFileUrlHook = (options: { + attributes: Record; chunkId: string; fileName: string; format: InternalModuleFormat; @@ -1271,6 +1284,7 @@ type ResolveFileUrlHook = (options: { 为此,除了 CommonJS 和 UMD 之外的所有格式都假定它们在浏览器环境中运行,其中 `URL` 和 `document` 可用。如果失败或要生成更优化的代码,则可以使用此钩子自定义此行为。为此,可以使用以下信息: +- `attributes`:引用此文件的模块的导入属性。 - `chunkId`:引用此文件的块的 ID。如果块文件名包含哈希,则此 ID 将包含占位符。如果最终出现在生成的代码中,Rollup 将使用实际文件名替换此占位符。 - `fileName`:产出文件的路径和文件名,相对于 `output.dir`,没有前导的 `./`。同样,如果这是一个将在其名称中具有哈希的块,则它将包含占位符。 - `format`:呈现的输出格式。 @@ -1298,7 +1312,7 @@ function resolveToDocumentPlugin() { | | | | --: | :-- | -| 类型: | `(property: string \| null, {chunkId: string, moduleId: string, format: string}) => string \| null` | +| 类型: | `(property: string \| null, {attributes: Record, chunkId: string, moduleId: string, format: string}) => string \| null` | | 类别: | sync, first | | 上一个钩子: | 当前块中每个动态导入表达式的 [`renderDynamicImport`](#renderdynamicimport) | | 下一个钩子: | 当前块的 [`banner`](#banner), [`footer`](#footer), [`intro`](#intro), [`outro`](#outro) 并行处理 | @@ -1307,7 +1321,7 @@ function resolveToDocumentPlugin() { 默认情况下,对于除 ES 模块以外的格式,Rollup 将 `import.meta.url` 替换为尝试匹配此行为的代码,返回当前块的动态 URL。请注意,除 CommonJS 和 UMD 之外的所有格式都假定它们在浏览器环境中运行,其中`URL`和`document`可用。对于其他属性,`import.meta.someProperty` -这种行为可以通过这个钩子进行更改,包括 ES 模块。对于每个 `import.meta<.someProperty>` 的出现,都会调用这个钩子,并传入属性的名称或者如果直接访问 `import.meta` 则为 `null`。例如,以下代码将使用原始模块相对路径到当前工作目录的解析 `import.meta.url`,并在运行时再次将此路径解析为当前文档的基本 URL: +这种行为可以通过这个钩子进行更改,包括 ES 模块。对于每个 `import.meta<.someProperty>` 的出现,都会调用这个钩子,并传入属性的名称或者如果直接访问 `import.meta` 则为 `null`。`attributes` 参数包含模块的导入属性。例如,以下代码将使用原始模块相对路径到当前工作目录的解析 `import.meta.url`,并在运行时再次将此路径解析为当前文档的基本 URL: ```js twoslash // ---cut-start--- @@ -1906,7 +1920,8 @@ type Resolve = ( options?: { skipSelf?: boolean; isEntry?: boolean; - attributes?: { [key: string]: string }; + importerAttributes?: Record; + attributes?: Record; custom?: { [plugin: string]: any }; } ) => Promise; @@ -1924,9 +1939,11 @@ type Resolve = ( 你还可以通过 `custom` 选项传递插件特定选项的对象,有关详细信息,请参见 [自定义解析器选项](#custom-resolver-options)。 -你在这里传递的 `isEntry` 值将传递给处理此调用的 [`resolveId`](#resolveid) 钩子,否则如果有导入器,则传递 `false`,如果没有,则传递 `true`。 +你在这里传递的 `isEntry` 值将传递给处理此调用的 [`resolveId`](#resolveid) 钩子,否则如果有导入模块,则传递 `false`,如果没有,则传递 `true`。 -如果为 `assertions` 传递对象,则它将模拟使用断言解析导入,例如 `assertions: {type: "json"}` 模拟解析 `import "foo" assert {type: "json"}`。这将传递给处理此调用的任何 [`resolveId`](#resolveid) 钩子,并最终成为返回的对象的一部分。 +如果传递 `importerAttributes` 对象,它将假设导入模块的导入属性是该对象中的属性。 + +如果为 `attributes` 传递对象,则它将模拟使用断言解析导入,例如 `attributes: {type: "json"}` 模拟解析 `import "foo" assert {type: "json"}`。这将传递给处理此调用的任何 [`resolveId`](#resolveid) 钩子,并最终成为返回的对象的一部分。 在从 `resolveId` 钩子调用此函数时,你应始终检查是否有意义将 `isEntry`、`custom` 和 `assertions` 选项传递下去。 @@ -2226,6 +2243,89 @@ console.log(__synthetic); 当用作入口点时,只有显式导出将被公开。对于 `syntheticNamedExports` 的字符串值,即使在示例中的合成回退导出 `__synthetic` 也不会被公开。但是,如果值为 `true`,则默认导出将被公开。这是 `syntheticNamedExports: true` 和 `syntheticNamedExports: 'default'` 之间唯一的显着区别。 +## 导入属性 {#import-attributes} + +ECMAScript 标准允许在导入上指定 [导入属性](https://github.com/tc39/proposal-import-attributes) 以自定义模块的加载方式。例如: + +```js +import data from './data.json' with { type: 'json' }; +import('./utils.js').then(utils => utils.process(data)); +``` + +Rollup 在插件钩子中提供对这些属性的访问,允许插件根据导入属性以不同的方式处理模块。 + +### 在插件中访问导入属性 {#accessing-import-attributes-in-plugins} + +当导入带有属性的模块时,这些属性会在多个钩子中提供给插件: + +- 在 [`resolveId`](#resolveid) 钩子中,`options.attributes` 包含导入属性,`options.importerAttributes` 包含导入模块的属性。 +- 在 [`load`](#load) 钩子中,`options.attributes` 包含首次导入模块时使用的导入属性。 +- 在 [`transform`](#transform) 钩子中,`options.attributes` 包含导入属性。 +- 在 [`shouldTransformCachedModule`](#shouldtransformcachedmodule) 钩子中,`attributes` 字段包含导入属性。 +- 在 [`resolveDynamicImport`](#resolvedynamicimport) 钩子中,`options.attributes` 包含动态导入的导入属性,`options.importerAttributes` 包含导入模块的属性。 +- 在 [`renderDynamicImport`](#renderdynamicimport) 钩子中,`targetModuleAttributes` 包含已解析动态导入目标的导入属性。 +- 在 [`resolveFileUrl`](#resolvefileurl) 钩子中,`options.attributes` 包含引用此文件的模块的导入属性。 +- 在 [`resolveImportMeta`](#resolveimportmeta) 钩子中,`options.attributes` 包含模块的导入属性。 +- 在 [`moduleParsed`](#moduleparsed) 钩子中,`moduleInfo.attributes` 包含导入属性。 + +### 使用导入属性自定义模块处理 {#using-import-attributes-to-customize-module-handling} + +一个常见的用例是根据导入属性以不同的方式处理 JSON 或样式表等资源: + +```js twoslash +import { resolve, dirname } from 'node:path'; +import { readFile } from 'node:fs/promises'; + +// ---cut-start--- +/** @returns {import('rollup').Plugin} */ +// ---cut-end--- +function customAssetsPlugin() { + return { + name: 'custom-assets', + resolveId(source, importer, options) { + if (options.attributes.type === 'json') { + return resolve(dirname(importer), source); + } + }, + async load(id, options) { + if (options.attributes.type === 'json') { + const content = await readFile(id, 'utf-8'); + return `export default ${content}`; + } + return null; + } + }; +} +``` + +### Passing attributes when resolving modules + +你可以在通过 [`this.resolve`](#this-resolve) 手动解析模块时传递 `attributes` 参数,以模拟解析带有特定属性的导入: + +```js twoslash +// ---cut-start--- +/** @returns {import('rollup').Plugin} */ +// ---cut-end--- +function proxyPlugin() { + return { + name: 'proxy', + async resolveId(source, importer, options) { + // 检查如何使用特定属性解析 + const jsonResolution = await this.resolve(source, importer, { + ...options, + attributes: { type: 'json' } + }); + + if (jsonResolution && !jsonResolution.external) { + // 特殊处理 JSON 导入 + return source + '?treated-as-json'; + } + return null; + } + }; +} +``` + ## 插件间通信 {#inter-plugin-communication} 在使用许多专用插件时,可能需要无关插件能够在构建期间交换信息。Rollup 通过几种机制使这成为可能。 diff --git a/package-lock.json b/package-lock.json index e628d1b31..715451314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rollup", - "version": "4.56.0", + "version": "4.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rollup", - "version": "4.56.0", + "version": "4.57.0", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" diff --git a/package.json b/package.json index 9cf3b49f1..f21b9d45a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "4.56.0", + "version": "4.57.0", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js", diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 907407d75..21be38040 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -30,7 +30,8 @@ import { logUnresolvedEntry, logUnresolvedImplicitDependant, logUnresolvedImport, - logUnresolvedImportTreatedAsExternal + logUnresolvedImportTreatedAsExternal, + warnDeprecation } from './utils/logs'; import { doAttributesDiffer, @@ -42,6 +43,7 @@ import relativeId from './utils/relativeId'; import { resolveId } from './utils/resolveId'; import stripBom from './utils/stripBom'; import transform from './utils/transform'; +import { URL_LOAD } from './utils/urls'; export interface UnresolvedModule { fileName: string | null; @@ -56,6 +58,7 @@ export type ModuleLoaderResolveId = ( customOptions: CustomPluginOptions | undefined, isEntry: boolean | undefined, attributes: Record, + importerAttributes: Record | undefined, skip?: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null ) => Promise; @@ -107,7 +110,7 @@ export class ModuleLoader { const result = this.extendLoadModulesPromise( Promise.all( unresolvedModules.map(id => - this.loadEntryModule(id, false, undefined, null, isAddForManualChunks) + this.loadEntryModule(id, false, undefined, null, isAddForManualChunks, undefined) ) ) ); @@ -130,7 +133,7 @@ export class ModuleLoader { const newEntryModules = await this.extendLoadModulesPromise( Promise.all( unresolvedEntryModules.map(({ id, importer }) => - this.loadEntryModule(id, true, importer, null) + this.loadEntryModule(id, true, importer, null, undefined, undefined) ) ).then(entryModules => { for (const [index, entryModule] of entryModules.entries()) { @@ -212,6 +215,7 @@ export class ModuleLoader { customOptions, isEntry, attributes, + importerAttributes, skip = null ) => this.getResolvedIdWithDefaults( @@ -228,6 +232,7 @@ export class ModuleLoader { customOptions, typeof isEntry === 'boolean' ? isEntry : !importer, attributes, + importerAttributes, this.options.fs ), importer, @@ -242,32 +247,44 @@ export class ModuleLoader { ): Promise { const chunkNamePriority = this.nextChunkNamePriority++; return this.extendLoadModulesPromise( - this.loadEntryModule(unresolvedModule.id, false, unresolvedModule.importer, null).then( - async entryModule => { - addChunkNamesToModule(entryModule, unresolvedModule, false, chunkNamePriority); - if (!entryModule.info.isEntry) { - const implicitlyLoadedAfterModules = await Promise.all( - implicitlyLoadedAfter.map(id => - this.loadEntryModule(id, false, unresolvedModule.importer, entryModule.id) + this.loadEntryModule( + unresolvedModule.id, + false, + unresolvedModule.importer, + null, + undefined, + undefined + ).then(async entryModule => { + addChunkNamesToModule(entryModule, unresolvedModule, false, chunkNamePriority); + if (!entryModule.info.isEntry) { + const implicitlyLoadedAfterModules = await Promise.all( + implicitlyLoadedAfter.map(id => + this.loadEntryModule( + id, + false, + unresolvedModule.importer, + entryModule.id, + undefined, + undefined ) - ); - // We need to check again if this is still an entry module as these - // changes need to be performed atomically to avoid race conditions - // if the same module is re-emitted as an entry module. - // The inverse changes happen in "handleExistingModule" - if (!entryModule.info.isEntry) { - this.implicitEntryModules.add(entryModule); - for (const module of implicitlyLoadedAfterModules) { - entryModule.implicitlyLoadedAfter.add(module); - } - for (const dependant of entryModule.implicitlyLoadedAfter) { - dependant.implicitlyLoadedBefore.add(entryModule); - } + ) + ); + // We need to check again if this is still an entry module as these + // changes need to be performed atomically to avoid race conditions + // if the same module is re-emitted as an entry module. + // The inverse changes happen in "handleExistingModule" + if (!entryModule.info.isEntry) { + this.implicitEntryModules.add(entryModule); + for (const module of implicitlyLoadedAfterModules) { + entryModule.implicitlyLoadedAfter.add(module); + } + for (const dependant of entryModule.implicitlyLoadedAfter) { + dependant.implicitlyLoadedBefore.add(entryModule); } } - return entryModule; } - ) + return entryModule; + }) ); } @@ -279,8 +296,21 @@ export class ModuleLoader { let source: LoadResult; try { source = await this.graph.fileOperationQueue.run(async () => { - const content = await this.pluginDriver.hookFirst('load', [id]); - if (content !== null) return content; + const content = await this.pluginDriver.hookFirst('load', [ + id, + { attributes: module.info.attributes } + ]); + if (content !== null) { + if (typeof content === 'object' && content.attributes) { + warnDeprecation( + 'Returning attributes from the "load" hook is forbidden.', + URL_LOAD, + false, + this.options + ); + } + return content; + } this.graph.watchFiles[id] = true; return (await this.options.fs.readFile(id, { encoding: 'utf8' })) as string; }); @@ -306,6 +336,7 @@ export class ModuleLoader { !(await this.pluginDriver.hookFirst('shouldTransformCachedModule', [ { ast: cachedModule.ast, + attributes: cachedModule.attributes, code: cachedModule.code, id: cachedModule.id, meta: cachedModule.meta, @@ -323,7 +354,7 @@ export class ModuleLoader { } else { module.updateOptions(sourceDescription); await module.setSource( - await transform(sourceDescription, module, this.pluginDriver, this.options.onLog) + await transform(sourceDescription, module, this.pluginDriver, this.options) ); } } @@ -586,7 +617,14 @@ export class ModuleLoader { (module.resolvedIds[source] = module.resolvedIds[source] || this.handleInvalidResolvedId( - await this.resolveId(source, module.id, EMPTY_OBJECT, false, attributes), + await this.resolveId( + source, + module.id, + EMPTY_OBJECT, + false, + attributes, + module.info.attributes + ), source, module.id, attributes @@ -666,7 +704,8 @@ export class ModuleLoader { isEntry: boolean, importer: string | undefined, implicitlyLoadedBefore: string | null, - isLoadForManualChunks = false + isLoadForManualChunks = false, + importerAttributes: Record | undefined ): Promise { const resolveIdResult = await resolveId( unresolvedId, @@ -678,6 +717,7 @@ export class ModuleLoader { EMPTY_OBJECT, true, EMPTY_OBJECT, + importerAttributes, this.options.fs ); if (resolveIdResult == null) { @@ -719,7 +759,7 @@ export class ModuleLoader { const resolution = await this.pluginDriver.hookFirst('resolveDynamicImport', [ specifier, importer, - { attributes } + { attributes, importerAttributes: module.info.attributes } ]); if (typeof specifier !== 'string') { if (typeof resolution === 'string') { @@ -750,7 +790,14 @@ export class ModuleLoader { return existingResolution; } return (module.resolvedIds[specifier] = this.handleInvalidResolvedId( - await this.resolveId(specifier, module.id, EMPTY_OBJECT, false, attributes), + await this.resolveId( + specifier, + module.id, + EMPTY_OBJECT, + false, + attributes, + module.info.attributes + ), specifier, module.id, attributes diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index bc2df215d..e89356eab 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -328,6 +328,8 @@ export default class ImportExpression extends NodeBase { }, moduleId: scope.context.module.id, targetChunk: targetChunk ? getChunkInfoWithPath(targetChunk) : null, + targetModuleAttributes: + resolution && typeof resolution !== 'string' ? resolution.info.attributes : {}, targetModuleId: resolution && typeof resolution !== 'string' ? resolution.id : null } ]); diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 5a4672e83..2a795f279 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -86,7 +86,10 @@ export default class MetaProperty extends NodeBase { start, end } = this; - const { id: moduleId } = module; + const { + id: moduleId, + info: { attributes } + } = module; if (name !== IMPORT) return; const chunkId = preliminaryChunkId!; @@ -97,7 +100,7 @@ export default class MetaProperty extends NodeBase { const isUrlObject = !!metaProperty?.startsWith(FILE_OBJ_PREFIX); const replacement = pluginDriver.hookFirstSync('resolveFileUrl', [ - { chunkId, fileName, format, moduleId, referenceId, relativePath } + { attributes, chunkId, fileName, format, moduleId, referenceId, relativePath } ]) || relativeUrlMechanisms[format](relativePath, isUrlObject); code.overwrite( @@ -111,7 +114,7 @@ export default class MetaProperty extends NodeBase { let replacement = pluginDriver.hookFirstSync('resolveImportMeta', [ metaProperty, - { chunkId, format, moduleId } + { attributes, chunkId, format, moduleId } ]); if (!replacement) { replacement = importMetaMechanisms[format]?.(metaProperty, { chunkId, snippets }); diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index e3c305f76..799df4a69 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -261,6 +261,7 @@ export interface PluginContext extends MinimalPluginContext { source: string, importer?: string, options?: { + importerAttributes?: Record; attributes?: Record; custom?: CustomPluginOptions; isEntry?: boolean; @@ -312,13 +313,19 @@ export type ResolveIdHook = ( this: PluginContext, source: string, importer: string | undefined, - options: { attributes: Record; custom?: CustomPluginOptions; isEntry: boolean } + options: { + attributes: Record; + custom?: CustomPluginOptions; + importerAttributes?: Record | undefined; + isEntry: boolean; + } ) => ResolveIdResult; export type ShouldTransformCachedModuleHook = ( this: PluginContext, options: { ast: ProgramNode; + attributes: Record; code: string; id: string; meta: CustomPluginOptions; @@ -338,7 +345,19 @@ export type HasModuleSideEffects = (id: string, external: boolean) => boolean; export type LoadResult = SourceDescription | string | NullValue; -export type LoadHook = (this: PluginContext, id: string) => LoadResult; +export type LoadHook = ( + this: PluginContext, + id: string, + // temporarily marked as optional for better Vite type-compatibility + options?: + | { + // unused, temporarily added for better Vite type-compatibility + ssr?: boolean | undefined; + // temporarily marked as optional for better Vite type-compatibility + attributes?: Record; + } + | undefined +) => LoadResult; export interface TransformPluginContext extends PluginContext { debug: LoggingFunctionWithPosition; @@ -353,7 +372,16 @@ export type TransformResult = string | NullValue | Partial; export type TransformHook = ( this: TransformPluginContext, code: string, - id: string + id: string, + // temporarily marked as optional for better Vite type-compatibility + options?: + | { + // unused, temporarily added for better Vite type-compatibility + ssr?: boolean | undefined; + // temporarily marked as optional for better Vite type-compatibility + attributes?: Record; + } + | undefined ) => TransformResult; export type ModuleParsedHook = (this: PluginContext, info: ModuleInfo) => void; @@ -370,18 +398,24 @@ export type ResolveDynamicImportHook = ( this: PluginContext, specifier: string | AstNode, importer: string, - options: { attributes: Record } + options: { attributes: Record; importerAttributes: Record } ) => ResolveIdResult; export type ResolveImportMetaHook = ( this: PluginContext, property: string | null, - options: { chunkId: string; format: InternalModuleFormat; moduleId: string } + options: { + attributes: Record; + chunkId: string; + format: InternalModuleFormat; + moduleId: string; + } ) => string | NullValue; export type ResolveFileUrlHook = ( this: PluginContext, options: { + attributes: Record; chunkId: string; fileName: string; format: InternalModuleFormat; @@ -463,6 +497,7 @@ export interface FunctionPluginHooks { chunk: PreRenderedChunkWithFileName; targetChunk: PreRenderedChunkWithFileName | null; getTargetChunkImports: () => DynamicImportTargetChunk[] | null; + targetModuleAttributes: Record; } ) => { left: string; right: string } | NullValue; renderError: (this: PluginContext, error?: Error) => void; diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 7590b89bf..5c64cbbd3 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -75,7 +75,11 @@ export function getPluginContext( watchMode: graph.watchMode }, parse: parseAst, - resolve(source, importer, { attributes, custom, isEntry, skipSelf } = BLANK) { + resolve( + source, + importer, + { attributes, custom, isEntry, skipSelf, importerAttributes } = BLANK + ) { skipSelf ??= true; return graph.moduleLoader.resolveId( source, @@ -83,6 +87,7 @@ export function getPluginContext( custom, isEntry, attributes || EMPTY_OBJECT, + importerAttributes, skipSelf ? [{ importer, plugin, source }] : null ); }, diff --git a/src/utils/resolveId.ts b/src/utils/resolveId.ts index 766da800c..a1e67acb3 100644 --- a/src/utils/resolveId.ts +++ b/src/utils/resolveId.ts @@ -14,6 +14,7 @@ export async function resolveId( customOptions: CustomPluginOptions | undefined, isEntry: boolean, attributes: Record, + importerAttributes: Record | undefined, fs: RollupFsModule ): Promise { const pluginResult = await resolveIdViaPlugins( @@ -24,7 +25,8 @@ export async function resolveId( skip, customOptions, isEntry, - attributes + attributes, + importerAttributes ); if (pluginResult != null) { diff --git a/src/utils/resolveIdViaPlugins.ts b/src/utils/resolveIdViaPlugins.ts index 0473a6a55..c3a2dc4e3 100644 --- a/src/utils/resolveIdViaPlugins.ts +++ b/src/utils/resolveIdViaPlugins.ts @@ -11,7 +11,8 @@ export function resolveIdViaPlugins( skip: readonly { importer: string | undefined; plugin: Plugin; source: string }[] | null, customOptions: CustomPluginOptions | undefined, isEntry: boolean, - attributes: Record + attributes: Record, + importerAttributes: Record | undefined ): Promise<[NonNullable, Plugin] | null> { let skipped: Set | null = null; let replaceContext: ReplaceContext | null = null; @@ -24,7 +25,11 @@ export function resolveIdViaPlugins( } replaceContext = (pluginContext, plugin): PluginContext => ({ ...pluginContext, - resolve: (source, importer, { attributes, custom, isEntry, skipSelf } = BLANK) => { + resolve: ( + source, + importer, + { attributes, custom, isEntry, skipSelf, importerAttributes } = BLANK + ) => { skipSelf ??= true; if ( skipSelf && @@ -46,6 +51,7 @@ export function resolveIdViaPlugins( custom, isEntry, attributes || EMPTY_OBJECT, + importerAttributes, skipSelf ? [...skip, { importer, plugin, source }] : skip ); } @@ -53,7 +59,7 @@ export function resolveIdViaPlugins( } return pluginDriver.hookFirstAndGetPlugin( 'resolveId', - [source, importer, { attributes, custom: customOptions, isEntry }], + [source, importer, { attributes, custom: customOptions, importerAttributes, isEntry }], replaceContext, skipped ); diff --git a/src/utils/transform.ts b/src/utils/transform.ts index c903a6135..4eac8b30d 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -5,7 +5,7 @@ import type { EmittedFile, ExistingRawSourceMap, LoggingFunctionWithPosition, - LogHandler, + NormalizedInputOptions, Plugin, PluginContext, RollupError, @@ -14,8 +14,6 @@ import type { TransformPluginContext, TransformResult } from '../rollup/types'; -import { getTrackedPluginCache } from './PluginCache'; -import type { PluginDriver } from './PluginDriver'; import { collapseSourcemap } from './collapseSourcemaps'; import { decodedSourcemap } from './decodedSourcemap'; import { LOGLEVEL_WARN } from './logging'; @@ -24,15 +22,19 @@ import { error, logInvalidSetAssetSourceCall, logNoTransformMapOrAstWithoutCode, - logPluginError + logPluginError, + warnDeprecation } from './logs'; import { normalizeLog } from './options/options'; +import { getTrackedPluginCache } from './PluginCache'; +import type { PluginDriver } from './PluginDriver'; +import { URL_TRANSFORM } from './urls'; export default async function transform( source: SourceDescription, module: Module, pluginDriver: PluginDriver, - log: LogHandler + options: NormalizedInputOptions ): Promise { const id = module.id; const sourcemapChain: DecodedSourceMapOrMissing[] = []; @@ -61,10 +63,18 @@ export default async function transform( module.updateOptions(result); if (result.code == null) { if (result.map || result.ast) { - log(LOGLEVEL_WARN, logNoTransformMapOrAstWithoutCode(plugin.name)); + options.onLog(LOGLEVEL_WARN, logNoTransformMapOrAstWithoutCode(plugin.name)); } return previousCode; } + if (result.attributes) { + warnDeprecation( + 'Returning attributes from the "transform" hook is forbidden.', + URL_TRANSFORM, + false, + options + ); + } ({ code, map, ast } = result); } else { return previousCode; @@ -101,7 +111,13 @@ export default async function transform( try { code = await pluginDriver.hookReduceArg0( 'transform', - [currentSource, id], + [ + currentSource, + id, + { + attributes: module.info.attributes + } + ], transformReducer, (pluginContext, plugin): TransformPluginContext => { pluginName = plugin.name; @@ -135,7 +151,7 @@ export default async function transform( originalCode, originalSourcemap, sourcemapChain, - log + options.onLog ); if (!combinedMap) { const magicString = new MagicString(originalCode); diff --git a/src/utils/urls.ts b/src/utils/urls.ts index 3b94e774b..b8e496359 100644 --- a/src/utils/urls.ts +++ b/src/utils/urls.ts @@ -46,3 +46,5 @@ export const URL_GENERATEBUNDLE = 'plugin-development/#generatebundle'; export const URL_RENDERDYNAMICIMPORT = 'plugin-development/#renderdynamicimport'; export const URL_THIS_GETMODULEIDS = 'plugin-development/#this-getmoduleids'; export const URL_THIS_GETMODULEINFO = 'plugin-development/#this-getmoduleinfo'; +export const URL_LOAD = 'plugin-development/#load'; +export const URL_TRANSFORM = 'plugin-development/#transform'; diff --git a/test/chunking-form/samples/resolve-file-url/_config.js b/test/chunking-form/samples/resolve-file-url/_config.js index eb4921765..a23db5196 100644 --- a/test/chunking-form/samples/resolve-file-url/_config.js +++ b/test/chunking-form/samples/resolve-file-url/_config.js @@ -1,3 +1,5 @@ +const assert = require('node:assert/strict'); + module.exports = defineTest({ description: 'allows to configure file urls', options: { @@ -25,7 +27,16 @@ module.exports = defineTest({ ); } }, - resolveFileUrl({ chunkId, fileName, format, moduleId, referenceId, relativePath }) { + resolveFileUrl({ + attributes, + chunkId, + fileName, + format, + moduleId, + referenceId, + relativePath + }) { + assert.deepEqual(attributes, {}); if (!moduleId.endsWith('resolved')) { return `'chunkId=${chunkId}:moduleId=${moduleId .replace(/\\/g, '/') @@ -39,7 +50,8 @@ module.exports = defineTest({ } }, { - resolveFileUrl({ moduleId }) { + resolveFileUrl({ attributes, moduleId }) { + assert.deepEqual(attributes, {}); if (moduleId === 'resolved') { return `'resolved'`; } diff --git a/test/form/samples/configure-file-url/_config.js b/test/form/samples/configure-file-url/_config.js index 9be0556b2..ba01108e1 100644 --- a/test/form/samples/configure-file-url/_config.js +++ b/test/form/samples/configure-file-url/_config.js @@ -1,3 +1,5 @@ +const assert = require('node:assert/strict'); + module.exports = defineTest({ description: 'allows to configure file urls', options: { @@ -19,7 +21,16 @@ module.exports = defineTest({ return `export default import.meta.ROLLUP_FILE_URL_${assetId};`; } }, - resolveFileUrl({ chunkId, fileName, format, moduleId, referenceId, relativePath }) { + resolveFileUrl({ + attributes, + chunkId, + fileName, + format, + moduleId, + referenceId, + relativePath + }) { + assert.deepEqual(attributes, {}); if (!moduleId.endsWith('resolved')) { return `'chunkId=${chunkId}:moduleId=${moduleId .replace(/\\/g, '/') @@ -34,7 +45,8 @@ module.exports = defineTest({ }, { name: 'second', - resolveFileUrl({ moduleId }) { + resolveFileUrl({ attributes, moduleId }) { + assert.deepEqual(attributes, {}); if (moduleId === 'resolved') { return `'resolved'`; } diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_config.js b/test/form/samples/resolve-file-url-import-meta-attributes/_config.js new file mode 100644 index 000000000..ba56bf71d --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_config.js @@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'adds attributes to file resolveFileUrl and resolveImportMeta hooks', + options: { + plugins: [ + { + name: 'first', + transform(code) { + return code.replace('PLACEHOLDER', () => { + const assetId = this.emitFile({ + type: 'asset', + name: 'my-asset', + source: 'Text content' + }); + return `import.meta.ROLLUP_FILE_URL_${assetId}`; + }); + }, + resolveFileUrl({ attributes }) { + return `'attributes=${JSON.stringify(attributes)}'`; + }, + resolveImportMeta(property, { attributes }) { + return `'attributes=${JSON.stringify(attributes)}'`; + } + } + ] + } +}); diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_expected/amd.js b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/amd.js new file mode 100644 index 000000000..894b44e6a --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/amd.js @@ -0,0 +1,9 @@ +define(['module', 'require'], (function (module, require) { 'use strict'; + + const url = 'attributes={"foo":"bar"}'; + const asset = 'attributes={"foo":"bar"}'; + + console.log('asset', asset); + console.log('url', url); + +})); diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_expected/assets/my-asset-Bx5J2bsd b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/assets/my-asset-Bx5J2bsd new file mode 100644 index 000000000..3e6860526 --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/assets/my-asset-Bx5J2bsd @@ -0,0 +1 @@ +Text content \ No newline at end of file diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_expected/cjs.js b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/cjs.js new file mode 100644 index 000000000..5c8c7418a --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/cjs.js @@ -0,0 +1,7 @@ +'use strict'; + +const url = 'attributes={"foo":"bar"}'; +const asset = 'attributes={"foo":"bar"}'; + +console.log('asset', asset); +console.log('url', url); diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_expected/es.js b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/es.js new file mode 100644 index 000000000..d61d60ba0 --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/es.js @@ -0,0 +1,5 @@ +const url = 'attributes={"foo":"bar"}'; +const asset = 'attributes={"foo":"bar"}'; + +console.log('asset', asset); +console.log('url', url); diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_expected/iife.js b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/iife.js new file mode 100644 index 000000000..3fec42aad --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/iife.js @@ -0,0 +1,10 @@ +(function () { + 'use strict'; + + const url = 'attributes={"foo":"bar"}'; + const asset = 'attributes={"foo":"bar"}'; + + console.log('asset', asset); + console.log('url', url); + +})(); diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_expected/system.js b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/system.js new file mode 100644 index 000000000..169df61ff --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/system.js @@ -0,0 +1,14 @@ +System.register([], (function (exports, module) { + 'use strict'; + return { + execute: (function () { + + const url = 'attributes={"foo":"bar"}'; + const asset = 'attributes={"foo":"bar"}'; + + console.log('asset', asset); + console.log('url', url); + + }) + }; +})); diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/_expected/umd.js b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/umd.js new file mode 100644 index 000000000..c4687cc51 --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/_expected/umd.js @@ -0,0 +1,12 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +})((function () { 'use strict'; + + const url = 'attributes={"foo":"bar"}'; + const asset = 'attributes={"foo":"bar"}'; + + console.log('asset', asset); + console.log('url', url); + +})); diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/dep.js b/test/form/samples/resolve-file-url-import-meta-attributes/dep.js new file mode 100644 index 000000000..bce123594 --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/dep.js @@ -0,0 +1,2 @@ +export const url = import.meta.url; +export const asset = PLACEHOLDER; diff --git a/test/form/samples/resolve-file-url-import-meta-attributes/main.js b/test/form/samples/resolve-file-url-import-meta-attributes/main.js new file mode 100644 index 000000000..722e65596 --- /dev/null +++ b/test/form/samples/resolve-file-url-import-meta-attributes/main.js @@ -0,0 +1,4 @@ +import { asset, url } from './dep.js' with { foo: 'bar' }; + +console.log('asset', asset); +console.log('url', url); diff --git a/test/function/samples/deprecated/load-attributes/_config.js b/test/function/samples/deprecated/load-attributes/_config.js new file mode 100644 index 000000000..178f57307 --- /dev/null +++ b/test/function/samples/deprecated/load-attributes/_config.js @@ -0,0 +1,36 @@ +const path = require('node:path'); + +const ID_MAIN = path.join(__dirname, 'main.js'); + +module.exports = defineTest({ + description: 'does not allow returning attributes from the "load" hook', + options: { + strictDeprecations: true, + plugins: [ + { + resolveId(source) { + if (source.endsWith('.json')) { + return { + id: source + }; + } + }, + load(id) { + if (id.endsWith('.json')) { + return { + code: 'export default {a:1}', + attributes: {} + }; + } + } + } + ] + }, + error: { + code: 'DEPRECATED_FEATURE', + message: + 'Could not load ./foo.json (imported by main.js): Returning attributes from the "load" hook is forbidden.', + url: 'https://rollupjs.org/plugin-development/#load', + watchFiles: [ID_MAIN] + } +}); diff --git a/test/function/samples/deprecated/load-attributes/main.js b/test/function/samples/deprecated/load-attributes/main.js new file mode 100644 index 000000000..9b3d6e673 --- /dev/null +++ b/test/function/samples/deprecated/load-attributes/main.js @@ -0,0 +1,2 @@ +import foo from './foo.json' with { type: 'json' }; +export default foo; diff --git a/test/function/samples/deprecated/transform-attributes/_config.js b/test/function/samples/deprecated/transform-attributes/_config.js new file mode 100644 index 000000000..2f74f9a8d --- /dev/null +++ b/test/function/samples/deprecated/transform-attributes/_config.js @@ -0,0 +1,43 @@ +const path = require('node:path'); + +const ID_MAIN = path.join(__dirname, 'main.js'); + +module.exports = defineTest({ + description: 'does not allow returning attributes from the "transform" hook', + options: { + strictDeprecations: true, + plugins: [ + { + resolveId(source) { + if (source.endsWith('.json')) { + return { + id: source + }; + } + }, + load(id) { + if (id.endsWith('.json')) { + return { + code: 'export default {a:1}' + }; + } + }, + transform(code, id) { + if (id.endsWith('.json')) { + return { code, attributes: {} }; + } + } + } + ] + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'transform', + id: './foo.json', + message: 'Returning attributes from the "transform" hook is forbidden.', + plugin: 'at position 1', + pluginCode: 'DEPRECATED_FEATURE', + url: 'https://rollupjs.org/plugin-development/#transform', + watchFiles: [ID_MAIN] + } +}); diff --git a/test/function/samples/deprecated/transform-attributes/main.js b/test/function/samples/deprecated/transform-attributes/main.js new file mode 100644 index 000000000..9b3d6e673 --- /dev/null +++ b/test/function/samples/deprecated/transform-attributes/main.js @@ -0,0 +1,2 @@ +import foo from './foo.json' with { type: 'json' }; +export default foo; diff --git a/test/function/samples/extend-more-hooks-to-include-import-attributes/_config.js b/test/function/samples/extend-more-hooks-to-include-import-attributes/_config.js new file mode 100644 index 000000000..4b2ab1fb7 --- /dev/null +++ b/test/function/samples/extend-more-hooks-to-include-import-attributes/_config.js @@ -0,0 +1,78 @@ +const assert = require('node:assert'); +const acorn = require('acorn'); + +const code = 'export default 42;\n'; + +module.exports = defineTest({ + description: 'extend load, transform and renderDynamicImport to include import attributes', + options: { + cache: { + modules: [ + { + id: './lib2.js', + ast: acorn.parse(code, { + ecmaVersion: 6, + sourceType: 'module' + }), + attributes: { type: 'javascript' }, + code, + dependencies: [], + customTransformCache: false, + originalCode: code, + originalSourcemap: null, + resolvedIds: {}, + sourcemapChain: [], + transformDependencies: [] + } + ] + }, + plugins: [ + { + resolveId(source, _importer, options) { + if (source.endsWith('.json')) { + assert.deepEqual(options.attributes, { type: 'json' }); + return { + id: source + }; + } + if (source.endsWith('lib2.js')) { + return { + id: source + }; + } + if (source.endsWith('lib4.js')) { + assert.deepEqual(options.importerAttributes, { type: 'javascript' }); + } + }, + resolveDynamicImport(specifier, _importer, options) { + if (specifier.endsWith('lib4.js')) { + assert.deepEqual(options.importerAttributes, { type: 'javascript' }); + } + }, + load(id, options) { + if (id.endsWith('.json')) { + assert.deepEqual(options.attributes, { type: 'json' }); + return 'export default {a:1}'; + } + if (id.endsWith('lib2.js')) { + return code; + } + }, + transform(code, id, options) { + if (id.endsWith('.json')) { + assert.deepEqual(options.attributes, { type: 'json' }); + return code; + } + }, + shouldTransformCachedModule({ attributes }) { + assert.deepEqual(attributes, { type: 'javascript' }); + }, + renderDynamicImport(options) { + if (options.targetModuleId.endsWith('lib3.js')) { + assert.deepEqual(options.targetModuleAttributes, { type: 'javascript' }); + } + } + } + ] + } +}); diff --git a/test/function/samples/extend-more-hooks-to-include-import-attributes/lib3.js b/test/function/samples/extend-more-hooks-to-include-import-attributes/lib3.js new file mode 100644 index 000000000..507abd07e --- /dev/null +++ b/test/function/samples/extend-more-hooks-to-include-import-attributes/lib3.js @@ -0,0 +1,2 @@ +export { default } from './lib4.js'; +import('./lib4.js'); diff --git a/test/function/samples/extend-more-hooks-to-include-import-attributes/lib4.js b/test/function/samples/extend-more-hooks-to-include-import-attributes/lib4.js new file mode 100644 index 000000000..aef22247d --- /dev/null +++ b/test/function/samples/extend-more-hooks-to-include-import-attributes/lib4.js @@ -0,0 +1 @@ +export default 1; diff --git a/test/function/samples/extend-more-hooks-to-include-import-attributes/main.js b/test/function/samples/extend-more-hooks-to-include-import-attributes/main.js new file mode 100644 index 000000000..2a76b4df1 --- /dev/null +++ b/test/function/samples/extend-more-hooks-to-include-import-attributes/main.js @@ -0,0 +1,4 @@ +import foo from './foo.json' with { type: 'json' }; +import('./lib2.js'); +import('./lib3.js', { with: { type: 'javascript' } }); +export default foo; diff --git a/test/incremental/index.js b/test/incremental/index.js index 0714db921..da07cec55 100644 --- a/test/incremental/index.js +++ b/test/incremental/index.js @@ -408,6 +408,7 @@ describe('incremental', () => { shouldTransformCachedModuleCalls++; assert.strictEqual(ast.type, 'Program'); assert.deepStrictEqual(other, { + attributes: {}, code: modules[id], moduleSideEffects: true, syntheticNamedExports: false