Skip to content

Commit e88edfd

Browse files
authored
Add hasDefaultExport to ModuleInfo (#4356)
* Add hasDefaultExport to ModuleInfo * Improve example * Make export information available after loading, detect reexports * Manually fix vulnerability
1 parent bebc50d commit e88edfd

22 files changed

Lines changed: 158 additions & 39 deletions

File tree

docs/05-plugin-development.md

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -164,27 +164,42 @@ The `importer` is the fully resolved id of the importing module. When resolving
164164

165165
For those cases, the `isEntry` option will tell you if we are resolving a user defined entry point, an emitted chunk, or if the `isEntry` parameter was provided for the [`this.resolve`](guide/en/#thisresolve) context function.
166166

167-
You can use this for instance as a mechanism to define custom proxy modules for entry points. The following plugin will only expose the default export from entry points while still keeping named exports available for internal usage:
167+
You can use this for instance as a mechanism to define custom proxy modules for entry points. The following plugin will proxy all entry points to inject a polyfill import.
168168

169169
```js
170-
function onlyDefaultForEntriesPlugin() {
170+
function injectPolyfillPlugin() {
171171
return {
172-
name: 'only-default-for-entries',
172+
name: 'inject-polyfill',
173173
async resolveId(source, importer, options) {
174174
if (options.isEntry) {
175175
// We need to skip this plugin to avoid an infinite loop
176176
const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
177-
// If it cannot be resolved, return `null` so that Rollup displays an error
178-
if (!resolution) return null;
177+
// If it cannot be resolved or is external, just return it so that
178+
// Rollup can display an error
179+
if (!resolution || resolution.external) return resolution;
180+
// In the load hook of the proxy, we want to use this.load to find out
181+
// if the entry has a default export. In the load hook, however, we no
182+
// longer have the full "resolution" object that may contain meta-data
183+
// from other plugins that is only added on first load. Therefore we
184+
// trigger loading here without waiting for it.
185+
this.load(resolution);
179186
return `${resolution.id}?entry-proxy`;
180187
}
181188
return null;
182189
},
183-
load(id) {
190+
async load(id) {
184191
if (id.endsWith('?entry-proxy')) {
185-
const importee = id.slice(0, -'?entry-proxy'.length);
186-
// Note that this will throw if there is no default export
187-
return `export {default} from '${importee}';`;
192+
const entryId = id.slice(0, -'?entry-proxy'.length);
193+
// We need to load and parse the original entry first because we need
194+
// to know if it has a default export
195+
const { hasDefaultExport } = await this.load({ id: entryId });
196+
let code = `import 'polyfill';export * from ${JSON.stringify(entryId)};`;
197+
// Namespace reexports do not reexport default, so we need special
198+
// handling here
199+
if (hasDefaultExport) {
200+
code += `export { default } from ${JSON.stringify(entryId)};`;
201+
}
202+
return code;
188203
}
189204
return null;
190205
}
@@ -673,6 +688,7 @@ type ModuleInfo = {
673688
id: string; // the id of the module, for convenience
674689
code: string | null; // the source code of the module, `null` if external or not yet available
675690
ast: ESTree.Program; // the parsed abstract syntax tree if available
691+
hasDefaultExport: boolean | null; // is there a default export, `null` if external or not yet available
676692
isEntry: boolean; // is this a user- or plugin-defined entry point
677693
isExternal: boolean; // for external modules that are referenced but not included in the graph
678694
isIncluded: boolean | null; // is the module included after tree-shaking, `null` if external or not yet available
@@ -718,7 +734,7 @@ This allows you to inspect the final content of modules before deciding how to r
718734
719735
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 should implement a `moduleParsed` hook.
720736
721-
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:
737+
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:
722738
723739
```js
724740
export default function addProxyPlugin() {
@@ -744,7 +760,16 @@ export default function addProxyPlugin() {
744760
load(id) {
745761
if (id.endsWith('?proxy')) {
746762
const importee = id.slice(0, -'?proxy'.length);
747-
return `console.log('proxy for ${importee}'); export * from ${JSON.stringify(importee)};`;
763+
// Note that namespace reexports do not reexport default exports
764+
let code = `console.log('proxy for ${importee}'); export * from ${JSON.stringify(
765+
importee
766+
)};`;
767+
// We know that while resolving the proxy, importee was already fully
768+
// loaded and parsed, so we can rely on hasDefaultExport
769+
if (this.getModuleInfo(importee).hasDefaultExport) {
770+
code += `export { default } from ${JSON.stringify(importee)};`;
771+
}
772+
return code;
748773
}
749774
return null;
750775
}
@@ -1142,7 +1167,7 @@ function parentPlugin() {
11421167
}
11431168
}
11441169
// ...plugin hooks
1145-
}
1170+
};
11461171
}
11471172

11481173
function dependentPlugin() {
@@ -1151,20 +1176,19 @@ function dependentPlugin() {
11511176
name: 'dependent',
11521177
buildStart({ plugins }) {
11531178
const parentName = 'parent';
1154-
const parentPlugin = options.plugins
1155-
.find(plugin => plugin.name === parentName);
1179+
const parentPlugin = options.plugins.find(plugin => plugin.name === parentName);
11561180
if (!parentPlugin) {
11571181
// or handle this silently if it is optional
11581182
throw new Error(`This plugin depends on the "${parentName}" plugin.`);
11591183
}
11601184
// now you can access the API methods in subsequent hooks
11611185
parentApi = parentPlugin.api;
1162-
}
1186+
},
11631187
transform(code, id) {
11641188
if (thereIsAReasonToDoSomething(id)) {
11651189
parentApi.doSomething(id);
11661190
}
11671191
}
1168-
}
1192+
};
11691193
}
11701194
```

package-lock.json

Lines changed: 19 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"@rollup/plugin-json": "^4.1.0",
6565
"@rollup/plugin-node-resolve": "^13.1.3",
6666
"@rollup/plugin-replace": "^3.0.1",
67-
"@rollup/plugin-typescript": "^8.2.5",
67+
"@rollup/plugin-typescript": "^8.3.0",
6868
"@rollup/pluginutils": "^4.1.2",
6969
"@types/node": "^10.17.60",
7070
"@types/signal-exit": "^3.0.1",
@@ -100,7 +100,7 @@
100100
"pretty-bytes": "^5.6.0",
101101
"pretty-ms": "^7.0.1",
102102
"requirejs": "^2.3.6",
103-
"rollup": "^2.64.0",
103+
"rollup": "^2.65.0",
104104
"rollup-plugin-license": "^2.6.1",
105105
"rollup-plugin-string": "^3.0.0",
106106
"rollup-plugin-terser": "^7.0.2",
@@ -111,7 +111,7 @@
111111
"source-map": "^0.7.3",
112112
"source-map-support": "^0.5.21",
113113
"sourcemap-codec": "^1.4.8",
114-
"systemjs": "^6.11.0",
114+
"systemjs": "^6.12.1",
115115
"terser": "^5.10.0",
116116
"tslib": "^2.3.1",
117117
"typescript": "^4.5.5",

src/ExternalModule.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default class ExternalModule {
4646
get dynamicImporters() {
4747
return dynamicImporters.sort();
4848
},
49+
hasDefaultExport: null,
4950
hasModuleSideEffects,
5051
id,
5152
implicitlyLoadedAfterOneOf: EMPTY_ARRAY,

src/Module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ export default class Module {
277277
get dynamicImporters() {
278278
return module.dynamicImporters.sort();
279279
},
280+
get hasDefaultExport() {
281+
// This information is only valid after parsing
282+
if (!module.ast) {
283+
return null;
284+
}
285+
return 'default' in module.exports || 'default' in module.reexportDescriptions;
286+
},
280287
hasModuleSideEffects,
281288
id,
282289
get implicitlyLoadedAfterOneOf() {

src/rollup/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ interface ModuleInfo {
162162
dynamicImporters: readonly string[];
163163
dynamicallyImportedIdResolutions: readonly ResolvedId[];
164164
dynamicallyImportedIds: readonly string[];
165+
hasDefaultExport: boolean | null;
165166
hasModuleSideEffects: boolean | 'no-treeshake';
166167
id: string;
167168
implicitlyLoadedAfterOneOf: readonly string[];

test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = {
7373
dynamicallyImportedIdResolutions: [],
7474
dynamicallyImportedIds: [],
7575
dynamicImporters: [],
76+
hasDefaultExport: false,
7677
hasModuleSideEffects: true,
7778
id: ID_MAIN,
7879
implicitlyLoadedAfterOneOf: [],
@@ -143,6 +144,7 @@ module.exports = {
143144
dynamicallyImportedIdResolutions: [],
144145
dynamicallyImportedIds: [],
145146
dynamicImporters: [],
147+
hasDefaultExport: false,
146148
hasModuleSideEffects: true,
147149
id: ID_DEP,
148150
implicitlyLoadedAfterOneOf: [],

test/chunking-form/samples/implicit-dependencies/implicitly-dependent-entry/_config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ module.exports = {
6969
dynamicallyImportedIdResolutions: [],
7070
dynamicallyImportedIds: [],
7171
dynamicImporters: [],
72+
hasDefaultExport: false,
7273
hasModuleSideEffects: true,
7374
id: ID_MAIN,
7475
implicitlyLoadedAfterOneOf: [],
@@ -139,6 +140,7 @@ module.exports = {
139140
dynamicallyImportedIdResolutions: [],
140141
dynamicallyImportedIds: [],
141142
dynamicImporters: [],
143+
hasDefaultExport: false,
142144
hasModuleSideEffects: true,
143145
id: ID_DEP,
144146
implicitlyLoadedAfterOneOf: [],

test/chunking-form/samples/implicit-dependencies/multiple-dependencies/_config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ module.exports = {
117117
dynamicallyImportedIdResolutions: [],
118118
dynamicallyImportedIds: [],
119119
dynamicImporters: [],
120+
hasDefaultExport: false,
120121
hasModuleSideEffects: true,
121122
id: ID_MAIN1,
122123
implicitlyLoadedAfterOneOf: [],
@@ -236,6 +237,7 @@ module.exports = {
236237
dynamicallyImportedIdResolutions: [],
237238
dynamicallyImportedIds: [],
238239
dynamicImporters: [],
240+
hasDefaultExport: false,
239241
hasModuleSideEffects: true,
240242
id: ID_MAIN2,
241243
implicitlyLoadedAfterOneOf: [],
@@ -354,6 +356,7 @@ module.exports = {
354356
dynamicallyImportedIdResolutions: [],
355357
dynamicallyImportedIds: [],
356358
dynamicImporters: [],
359+
hasDefaultExport: false,
357360
hasModuleSideEffects: true,
358361
id: ID_DEP,
359362
implicitlyLoadedAfterOneOf: [ID_MAIN1, ID_MAIN2],

test/chunking-form/samples/implicit-dependencies/single-dependency/_config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ module.exports = {
6868
dynamicallyImportedIdResolutions: [],
6969
dynamicallyImportedIds: [],
7070
dynamicImporters: [],
71+
hasDefaultExport: false,
7172
hasModuleSideEffects: true,
7273
id: ID_MAIN,
7374
implicitlyLoadedAfterOneOf: [],
@@ -138,6 +139,7 @@ module.exports = {
138139
dynamicallyImportedIdResolutions: [],
139140
dynamicallyImportedIds: [],
140141
dynamicImporters: [],
142+
hasDefaultExport: false,
141143
hasModuleSideEffects: true,
142144
id: ID_DEP,
143145
implicitlyLoadedAfterOneOf: [ID_MAIN],

0 commit comments

Comments
 (0)