diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 89839ce7d8a..f99222892c7 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -784,6 +784,8 @@ type ModuleInfo = { importedIds: string[]; // the module ids statically imported by this module importedIdResolutions: ResolvedId[]; // how statically imported ids were resolved, for use with this.load importers: string[]; // the ids of all modules that statically import this module + exportedBindings: Record | null; // contains all exported variables associated with the path of `from`, `null` if external + exports: string[] | null; // all exported variables, `null` if external dynamicallyImportedIds: string[]; // the module ids imported by this module via dynamic import() dynamicallyImportedIdResolutions: ResolvedId[]; // how ids imported via dynamic import() were resolved dynamicImporters: string[]; // the ids of all modules that import this module via dynamic import() @@ -808,7 +810,7 @@ type ResolvedId = { During the build, this object represents currently available information about the module which may be inaccurate before the [`buildEnd`](guide/en/#buildend) hook: - `id` and `isExternal` will never change. -- `code`, `ast` and `hasDefaultExport` are only available after parsing, i.e. in the [`moduleParsed`](guide/en/#moduleparsed) hook or after awaiting [`this.load`](guide/en/#thisload). At that point, they will no longer change. +- `code`, `ast`, `hasDefaultExport`, `exports` and `exportedBindings` are only available after parsing, i.e. in the [`moduleParsed`](guide/en/#moduleparsed) hook or after awaiting [`this.load`](guide/en/#thisload). At that point, they will no longer change. - if `isEntry` is `true`, it will no longer change. It is however possible for modules to become entry points after they are parsed, either via [`this.emitFile`](guide/en/#thisemitfile) or because a plugin inspects a potential entry point via [`this.load`](guide/en/#thisload) in the [`resolveId`](guide/en/#resolveid) hook when resolving an entry point. Therefore, it is not recommended relying on this flag in the [`transform`](guide/en/#transform) hook. It will no longer change after `buildEnd`. - Similarly, `implicitlyLoadedAfterOneOf` can receive additional entries at any time before `buildEnd` via [`this.emitFile`](guide/en/#thisemitfile). - `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`. diff --git a/src/ExternalModule.ts b/src/ExternalModule.ts index b90c15f7b82..009d8079b2d 100644 --- a/src/ExternalModule.ts +++ b/src/ExternalModule.ts @@ -38,6 +38,8 @@ export default class ExternalModule { get dynamicImporters() { return dynamicImporters.sort(); }, + exportedBindings: null, + exports: null, hasDefaultExport: null, get hasModuleSideEffects() { warnDeprecation( diff --git a/src/Module.ts b/src/Module.ts index 0bda040dcc7..173ce7f4125 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -274,6 +274,8 @@ export default class Module { const { dynamicImports, dynamicImporters, + exportAllSources, + exports, implicitlyLoadedAfter, implicitlyLoadedBefore, importers, @@ -298,6 +300,26 @@ export default class Module { get dynamicImporters() { return dynamicImporters.sort(); }, + get exportedBindings() { + const exportBindings: Record = { '.': [...exports.keys()] }; + + for (const [name, { source }] of reexportDescriptions) { + (exportBindings[source] ??= []).push(name); + } + + for (const source of exportAllSources) { + (exportBindings[source] ??= []).push('*'); + } + + return exportBindings; + }, + get exports() { + return [ + ...exports.keys(), + ...reexportDescriptions.keys(), + ...[...exportAllSources].map(() => '*') + ]; + }, get hasDefaultExport() { // This information is only valid after parsing if (!module.ast) { diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index d44e797162f..1c35370a821 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -157,6 +157,8 @@ interface ModuleInfo extends ModuleOptions { dynamicImporters: readonly string[]; dynamicallyImportedIdResolutions: readonly ResolvedId[]; dynamicallyImportedIds: readonly string[]; + exportedBindings: Record | null; + exports: string[] | null; hasDefaultExport: boolean | null; /** @deprecated Use `moduleSideEffects` instead */ hasModuleSideEffects: boolean | 'no-treeshake'; 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 53d692f90d8..e2a9f924ad8 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 @@ -75,6 +75,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -148,6 +152,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], 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 157d42e036c..7f8db23b866 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 @@ -71,6 +71,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -144,6 +148,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exports: [], + exportedBindings: { + '.': [] + }, hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], 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 85e7dda5f51..ca1709a377e 100644 --- a/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js @@ -119,6 +119,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -243,6 +247,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -366,6 +374,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [ID_MAIN1, ID_MAIN2], 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 78299cef8a7..6b607a4bb57 100644 --- a/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js +++ b/test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js @@ -70,6 +70,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -143,6 +147,10 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [ID_MAIN], diff --git a/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/_config.js b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/_config.js new file mode 100644 index 00000000000..39fb90ab737 --- /dev/null +++ b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/_config.js @@ -0,0 +1,33 @@ +const assert = require('node:assert'); +const path = require('node:path'); + +const ID_MAIN = path.join(__dirname, 'main.js'); +const ID_MODULE = path.join(__dirname, 'module.js'); +const ID_MODULE_2 = path.join(__dirname, 'module2.js'); + +const expectedResult = { + [ID_MAIN]: { + exports: ['moduleAlias', '*'], + exportedBindings: { '.': [], './module.js': ['moduleAlias', '*'] } + }, + [ID_MODULE]: { + exports: ['default', 'module', '*'], + exportedBindings: { '.': ['default', 'module'], './module2.js': ['*'] } + }, + [ID_MODULE_2]: { + exports: ['module2'], + exportedBindings: { '.': ['module2'] } + } +}; + +module.exports = { + description: 'check exports and exportedBindings in moduleParsed as a supplementary test', + options: { + plugins: { + moduleParsed(moduleInfo) { + const { exports, exportedBindings, id } = moduleInfo; + assert.deepStrictEqual({ exports, exportedBindings }, expectedResult[id]); + } + } + } +}; diff --git a/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/main.js b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/main.js new file mode 100644 index 00000000000..7074f166f70 --- /dev/null +++ b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/main.js @@ -0,0 +1,3 @@ +export * from './module.js'; +export { module as moduleAlias } from './module.js'; +assert.ok(true); diff --git a/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/module.js b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/module.js new file mode 100644 index 00000000000..c2c66575c62 --- /dev/null +++ b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/module.js @@ -0,0 +1,3 @@ +export * from './module2.js'; +export default 1; +export const module = 1; diff --git a/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/module2.js b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/module2.js new file mode 100644 index 00000000000..e77bbfca10b --- /dev/null +++ b/test/function/samples/check-exports-exportedBindings-as-a-supplementary-test/module2.js @@ -0,0 +1 @@ +export const module2 = 1; diff --git a/test/function/samples/deprecated/manual-chunks-info/_config.js b/test/function/samples/deprecated/manual-chunks-info/_config.js index 95cc07c3300..27b20267963 100644 --- a/test/function/samples/deprecated/manual-chunks-info/_config.js +++ b/test/function/samples/deprecated/manual-chunks-info/_config.js @@ -91,6 +91,8 @@ module.exports = { ], dynamicallyImportedIds: ['external'], dynamicImporters: [getId('main')], + exports: ['promise', 'internal'], + exportedBindings: { '.': ['promise'], './lib': ['internal'] }, hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -134,6 +136,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { '.': ['default'] }, + exports: ['default'], hasDefaultExport: true, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -242,6 +246,8 @@ module.exports = { ], dynamicallyImportedIds: [getId('dynamic')], dynamicImporters: [], + exportedBindings: { '.': ['promise'], './lib': ['value'], external: ['external'] }, + exports: ['promise', 'value', 'external'], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -280,6 +286,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [getId('dynamic')], + exportedBindings: null, + exports: null, hasDefaultExport: null, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], diff --git a/test/function/samples/manual-chunks-info/_config.js b/test/function/samples/manual-chunks-info/_config.js index 26242b640a9..7f6b2b402c5 100644 --- a/test/function/samples/manual-chunks-info/_config.js +++ b/test/function/samples/manual-chunks-info/_config.js @@ -92,6 +92,8 @@ module.exports = { ], dynamicallyImportedIds: ['external'], dynamicImporters: [getId('main')], + exportedBindings: { '.': ['promise'], './lib': ['internal'] }, + exports: ['promise', 'internal'], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -135,6 +137,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { '.': ['default'] }, + exports: ['default'], hasDefaultExport: true, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -243,6 +247,12 @@ module.exports = { ], dynamicallyImportedIds: [getId('dynamic')], dynamicImporters: [], + exportedBindings: { + '.': ['promise'], + './lib': ['value'], + external: ['external'] + }, + exports: ['promise', 'value', 'external'], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -281,6 +291,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [getId('dynamic')], + exportedBindings: null, + exports: null, hasDefaultExport: null, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], diff --git a/test/function/samples/module-parsed-hook/_config.js b/test/function/samples/module-parsed-hook/_config.js index 30cf7d09fa6..41a10491355 100644 --- a/test/function/samples/module-parsed-hook/_config.js +++ b/test/function/samples/module-parsed-hook/_config.js @@ -53,6 +53,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { '.': [], './dep.js': ['value'] }, + exports: ['value'], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -112,6 +114,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [], + exportedBindings: { '.': ['value'] }, + exports: ['value'], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], diff --git a/test/function/samples/plugin-module-information/_config.js b/test/function/samples/plugin-module-information/_config.js index 74f6372ded6..e6f5e4d3de2 100644 --- a/test/function/samples/plugin-module-information/_config.js +++ b/test/function/samples/plugin-module-information/_config.js @@ -20,6 +20,10 @@ module.exports = { ast: null, code: null, dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: null, dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], @@ -113,6 +117,8 @@ module.exports = { }, code: "import path from 'path';\n\nexport const foo = path.resolve('foo');\n", dynamicallyImportedIdResolutions: [], + exportedBindings: { '.': ['foo'] }, + exports: ['foo'], dynamicallyImportedIds: [], dynamicImporters: [], hasDefaultExport: false, @@ -284,6 +290,11 @@ module.exports = { ], dynamicallyImportedIds: [ID_NESTED, ID_PATH], dynamicImporters: [], + exportedBindings: { + '.': ['nested', 'path', 'pathAgain'], + './foo.js': ['foo'] + }, + exports: ['nested', 'path', 'pathAgain', 'foo'], hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -377,6 +388,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [ID_MAIN], + exports: ['nested'], + exportedBindings: { '.': ['nested'] }, hasDefaultExport: false, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], @@ -407,6 +420,8 @@ module.exports = { dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], dynamicImporters: [ID_MAIN], + exportedBindings: null, + exports: null, hasDefaultExport: null, moduleSideEffects: true, implicitlyLoadedAfterOneOf: [], diff --git a/test/function/samples/preload-module/_config.js b/test/function/samples/preload-module/_config.js index 664c062e02a..1be7b15cdb9 100644 --- a/test/function/samples/preload-module/_config.js +++ b/test/function/samples/preload-module/_config.js @@ -35,6 +35,10 @@ module.exports = { assertions: {}, code: "import './dep';\nassert.ok(true);\n", dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [], @@ -74,6 +78,10 @@ module.exports = { assertions: {}, code: 'assert.ok(true);\n', dynamicImporters: [], + exportedBindings: { + '.': [] + }, + exports: [], hasDefaultExport: false, dynamicallyImportedIdResolutions: [], dynamicallyImportedIds: [],