Skip to content

Commit

Permalink
Add hasDefaultExport to ModuleInfo (#4356)
Browse files Browse the repository at this point in the history
* Add hasDefaultExport to ModuleInfo

* Improve example

* Make export information available after loading, detect reexports

* Manually fix vulnerability
  • Loading branch information
lukastaegert committed Jan 22, 2022
1 parent bebc50d commit e88edfd
Show file tree
Hide file tree
Showing 22 changed files with 158 additions and 39 deletions.
56 changes: 40 additions & 16 deletions docs/05-plugin-development.md
Expand Up @@ -164,27 +164,42 @@ The `importer` is the fully resolved id of the importing module. When resolving

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.

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:
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.

```js
function onlyDefaultForEntriesPlugin() {
function injectPolyfillPlugin() {
return {
name: 'only-default-for-entries',
name: 'inject-polyfill',
async resolveId(source, importer, options) {
if (options.isEntry) {
// We need to skip this plugin to avoid an infinite loop
const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
// If it cannot be resolved, return `null` so that Rollup displays an error
if (!resolution) return null;
// If it cannot be resolved or is external, just return it so that
// Rollup can display an error
if (!resolution || resolution.external) return resolution;
// In the load hook of the proxy, we want to use this.load to find out
// if the entry has a default export. In the load hook, however, we no
// longer have the full "resolution" object that may contain meta-data
// from other plugins that is only added on first load. Therefore we
// trigger loading here without waiting for it.
this.load(resolution);
return `${resolution.id}?entry-proxy`;
}
return null;
},
load(id) {
async load(id) {
if (id.endsWith('?entry-proxy')) {
const importee = id.slice(0, -'?entry-proxy'.length);
// Note that this will throw if there is no default export
return `export {default} from '${importee}';`;
const entryId = id.slice(0, -'?entry-proxy'.length);
// We need to load and parse the original entry first because we need
// to know if it has a default export
const { hasDefaultExport } = await this.load({ id: entryId });
let code = `import 'polyfill';export * from ${JSON.stringify(entryId)};`;
// Namespace reexports do not reexport default, so we need special
// handling here
if (hasDefaultExport) {
code += `export { default } from ${JSON.stringify(entryId)};`;
}
return code;
}
return null;
}
Expand Down Expand Up @@ -673,6 +688,7 @@ type ModuleInfo = {
id: string; // the id of the module, for convenience
code: string | null; // the source code of the module, `null` if external or not yet available
ast: ESTree.Program; // the parsed abstract syntax tree if available
hasDefaultExport: boolean | null; // is there a default export, `null` if external or not yet available
isEntry: boolean; // is this a user- or plugin-defined entry point
isExternal: boolean; // for external modules that are referenced but not included in the graph
isIncluded: boolean | null; // is the module included after tree-shaking, `null` if external or not yet available
Expand Down Expand Up @@ -718,7 +734,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 should implement a `moduleParsed` hook.
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 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:
```js
export default function addProxyPlugin() {
Expand All @@ -744,7 +760,16 @@ export default function addProxyPlugin() {
load(id) {
if (id.endsWith('?proxy')) {
const importee = id.slice(0, -'?proxy'.length);
return `console.log('proxy for ${importee}'); export * from ${JSON.stringify(importee)};`;
// Note that namespace reexports do not reexport default exports
let code = `console.log('proxy for ${importee}'); export * from ${JSON.stringify(
importee
)};`;
// We know that while resolving the proxy, importee was already fully
// loaded and parsed, so we can rely on hasDefaultExport
if (this.getModuleInfo(importee).hasDefaultExport) {
code += `export { default } from ${JSON.stringify(importee)};`;
}
return code;
}
return null;
}
Expand Down Expand Up @@ -1142,7 +1167,7 @@ function parentPlugin() {
}
}
// ...plugin hooks
}
};
}

function dependentPlugin() {
Expand All @@ -1151,20 +1176,19 @@ function dependentPlugin() {
name: 'dependent',
buildStart({ plugins }) {
const parentName = 'parent';
const parentPlugin = options.plugins
.find(plugin => plugin.name === parentName);
const parentPlugin = options.plugins.find(plugin => plugin.name === parentName);
if (!parentPlugin) {
// or handle this silently if it is optional
throw new Error(`This plugin depends on the "${parentName}" plugin.`);
}
// now you can access the API methods in subsequent hooks
parentApi = parentPlugin.api;
}
},
transform(code, id) {
if (thereIsAReasonToDoSomething(id)) {
parentApi.doSomething(id);
}
}
}
};
}
```
39 changes: 19 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -64,7 +64,7 @@
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-typescript": "^8.2.5",
"@rollup/plugin-typescript": "^8.3.0",
"@rollup/pluginutils": "^4.1.2",
"@types/node": "^10.17.60",
"@types/signal-exit": "^3.0.1",
Expand Down Expand Up @@ -100,7 +100,7 @@
"pretty-bytes": "^5.6.0",
"pretty-ms": "^7.0.1",
"requirejs": "^2.3.6",
"rollup": "^2.64.0",
"rollup": "^2.65.0",
"rollup-plugin-license": "^2.6.1",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^7.0.2",
Expand All @@ -111,7 +111,7 @@
"source-map": "^0.7.3",
"source-map-support": "^0.5.21",
"sourcemap-codec": "^1.4.8",
"systemjs": "^6.11.0",
"systemjs": "^6.12.1",
"terser": "^5.10.0",
"tslib": "^2.3.1",
"typescript": "^4.5.5",
Expand Down
1 change: 1 addition & 0 deletions src/ExternalModule.ts
Expand Up @@ -46,6 +46,7 @@ export default class ExternalModule {
get dynamicImporters() {
return dynamicImporters.sort();
},
hasDefaultExport: null,
hasModuleSideEffects,
id,
implicitlyLoadedAfterOneOf: EMPTY_ARRAY,
Expand Down
7 changes: 7 additions & 0 deletions src/Module.ts
Expand Up @@ -277,6 +277,13 @@ export default class Module {
get dynamicImporters() {
return module.dynamicImporters.sort();
},
get hasDefaultExport() {
// This information is only valid after parsing
if (!module.ast) {
return null;
}
return 'default' in module.exports || 'default' in module.reexportDescriptions;
},
hasModuleSideEffects,
id,
get implicitlyLoadedAfterOneOf() {
Expand Down
1 change: 1 addition & 0 deletions src/rollup/types.d.ts
Expand Up @@ -162,6 +162,7 @@ interface ModuleInfo {
dynamicImporters: readonly string[];
dynamicallyImportedIdResolutions: readonly ResolvedId[];
dynamicallyImportedIds: readonly string[];
hasDefaultExport: boolean | null;
hasModuleSideEffects: boolean | 'no-treeshake';
id: string;
implicitlyLoadedAfterOneOf: readonly string[];
Expand Down
Expand Up @@ -73,6 +73,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_MAIN,
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -143,6 +144,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_DEP,
implicitlyLoadedAfterOneOf: [],
Expand Down
Expand Up @@ -69,6 +69,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_MAIN,
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -139,6 +140,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_DEP,
implicitlyLoadedAfterOneOf: [],
Expand Down
Expand Up @@ -117,6 +117,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_MAIN1,
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -236,6 +237,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_MAIN2,
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -354,6 +356,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_DEP,
implicitlyLoadedAfterOneOf: [ID_MAIN1, ID_MAIN2],
Expand Down
Expand Up @@ -68,6 +68,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_MAIN,
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -138,6 +139,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: ID_DEP,
implicitlyLoadedAfterOneOf: [ID_MAIN],
Expand Down
Expand Up @@ -111,6 +111,7 @@ module.exports = {
],
dynamicallyImportedIds: [getId('dynamic')],
dynamicImporters: [],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: getId('main'),
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -145,6 +146,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [getId('dynamic')],
hasDefaultExport: null,
hasModuleSideEffects: true,
id: 'external',
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -177,6 +179,7 @@ module.exports = {
dynamicallyImportedIdResolutions: [],
dynamicallyImportedIds: [],
dynamicImporters: [],
hasDefaultExport: true,
hasModuleSideEffects: true,
id: getId('lib'),
implicitlyLoadedAfterOneOf: [],
Expand Down Expand Up @@ -260,6 +263,7 @@ module.exports = {
],
dynamicallyImportedIds: ['external'],
dynamicImporters: [getId('main')],
hasDefaultExport: false,
hasModuleSideEffects: true,
id: getId('dynamic'),
implicitlyLoadedAfterOneOf: [],
Expand Down

0 comments on commit e88edfd

Please sign in to comment.