Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate hasModuleSideEffects in favor of moduleSideEffects and ensure it is mutable on ModuleInfo #4379

Merged
merged 2 commits into from Feb 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 51 additions & 17 deletions docs/05-plugin-development.md
Expand Up @@ -167,33 +167,58 @@ For those cases, the `isEntry` option will tell you if we are resolving a user d
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
// We prefix the polyfill id with \0 to tell other plugins not to try to load or
// transform it
const POLYFILL_ID = '\0polyfill';
const PROXY_SUFFIX = '?inject-polyfill-proxy';

function injectPolyfillPlugin() {
return {
name: 'inject-polyfill',
async resolveId(source, importer, options) {
if (source === POLYFILL_ID) {
// It is important that side effects are always respected for polyfills,
// otherwise using "treeshake.moduleSideEffects: false" may prevent the
// polyfill from being included.
return { id: POLYFILL_ID, moduleSideEffects: true };
}
if (options.isEntry) {
// We need to skip this plugin to avoid an infinite loop
// Determine what the actual entry would have been. We need "skipSelf"
// to avoid an infinite loop.
const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
// 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`;
// In the load hook of the proxy, we need to know if the entry has a
// default export. There, 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.
const moduleInfo = await this.load(resolution);
// We need to make sure side effects in the original entry point
// are respected even for treeshake.moduleSideEffects: false.
// "moduleSideEffects" is a writable property on ModuleInfo.
moduleInfo.moduleSideEffects = true;
// It is important that the new entry does not start with \0 and
// has the same directory as the original one to not mess up
// relative external import generation. Also keeping the name and
// just adding a "?query" to the end ensures that preserveModules
// will generate the original entry name for this entry.
return `${resolution.id}${PROXY_SUFFIX}`;
}
return null;
},
async load(id) {
if (id.endsWith('?entry-proxy')) {
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)};`;
load(id) {
if (id === POLYFILL_ID) {
// Replace with actual polyfill
return "console.log('polyfill');";
}
if (id.endsWith(PROXY_SUFFIX)) {
const entryId = id.slice(0, -PROXY_SUFFIX.length);
// We know ModuleInfo.hasDefaultExport is reliable because we awaited
// this.load in resolveId
const { hasDefaultExport } = this.getModuleInfo(entryId);
let code =
`import ${JSON.stringify(POLYFILL_ID)};` + `export * from ${JSON.stringify(entryId)};`;
// Namespace reexports do not reexport default, so we need special
// handling here
if (hasDefaultExport) {
Expand Down Expand Up @@ -700,8 +725,8 @@ 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
hasModuleSideEffects: boolean | 'no-treeshake'; // are imports of this module included if nothing is imported from it
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
};

Expand All @@ -714,7 +739,16 @@ type ResolvedId = {
};
```
During the build, this object represents currently available information about the module. Before the [`buildEnd`](guide/en/#buildend) hook, this information may be incomplete as e.g. the `importedIds` are not yet resolved or additional `importers` are discovered.
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.
- 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`.
- `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.
Returns `null` if the module id cannot be found.
Expand Down
2 changes: 1 addition & 1 deletion src/Chunk.ts
Expand Up @@ -241,7 +241,7 @@ export default class Chunk {
}
if (
!chunk.dependencies.has(chunkByModule.get(facadedModule)!) &&
facadedModule.info.hasModuleSideEffects &&
facadedModule.info.moduleSideEffects &&
facadedModule.hasEffects()
) {
chunk.dependencies.add(chunkByModule.get(facadedModule)!);
Expand Down
22 changes: 18 additions & 4 deletions src/ExternalModule.ts
Expand Up @@ -6,6 +6,7 @@ import type {
NormalizedOutputOptions
} from './rollup/types';
import { EMPTY_ARRAY } from './utils/blank';
import { warnDeprecation } from './utils/error';
import { makeLegal } from './utils/identifierHelpers';
import { normalize, relative } from './utils/path';
import { printQuotedStringList } from './utils/printStringList';
Expand All @@ -31,14 +32,14 @@ export default class ExternalModule {
constructor(
private readonly options: NormalizedInputOptions,
public readonly id: string,
hasModuleSideEffects: boolean | 'no-treeshake',
moduleSideEffects: boolean | 'no-treeshake',
meta: CustomPluginOptions,
public readonly renormalizeRenderPath: boolean
) {
this.suggestedVariableName = makeLegal(id.split(/[\\/]/).pop()!);

const { importers, dynamicImporters } = this;
this.info = {
const info: ModuleInfo = (this.info = {
ast: null,
code: null,
dynamicallyImportedIdResolutions: EMPTY_ARRAY,
Expand All @@ -47,7 +48,14 @@ export default class ExternalModule {
return dynamicImporters.sort();
},
hasDefaultExport: null,
hasModuleSideEffects,
get hasModuleSideEffects() {
warnDeprecation(
'Accessing ModuleInfo.hasModuleSideEffects from plugins is deprecated. Please use ModuleInfo.moduleSideEffects instead.',
false,
options
);
return info.moduleSideEffects;
},
id,
implicitlyLoadedAfterOneOf: EMPTY_ARRAY,
implicitlyLoadedBefore: EMPTY_ARRAY,
Expand All @@ -60,8 +68,14 @@ export default class ExternalModule {
isExternal: true,
isIncluded: null,
meta,
moduleSideEffects,
syntheticNamedExports: false
};
});
// Hide the deprecated key so that it only warns when accessed explicitly
Object.defineProperty(this.info, 'hasModuleSideEffects', {
...Object.getOwnPropertyDescriptor(this.info, 'hasModuleSideEffects'),
enumerable: false
});
}

getVariableForExportName(name: string): [variable: ExternalVariable] {
Expand Down
2 changes: 1 addition & 1 deletion src/Graph.ts
Expand Up @@ -189,7 +189,7 @@ export default class Graph {
this.needsTreeshakingPass = false;
for (const module of this.modules) {
if (module.isExecuted) {
if (module.info.hasModuleSideEffects === 'no-treeshake') {
if (module.info.moduleSideEffects === 'no-treeshake') {
module.includeAllInBundle();
} else {
module.include();
Expand Down
59 changes: 41 additions & 18 deletions src/Module.ts
Expand Up @@ -54,7 +54,8 @@ import {
errMissingExport,
errNamespaceConflict,
error,
errSyntheticNamedExportsNeedNamespaceExport
errSyntheticNamedExportsNeedNamespaceExport,
warnDeprecation
} from './utils/error';
import { getId } from './utils/getId';
import { getOrCreate } from './utils/getOrCreate';
Expand Down Expand Up @@ -247,7 +248,7 @@ export default class Module {
public readonly id: string,
private readonly options: NormalizedInputOptions,
isEntry: boolean,
hasModuleSideEffects: boolean | 'no-treeshake',
moduleSideEffects: boolean | 'no-treeshake',
syntheticNamedExports: boolean | string,
meta: CustomPluginOptions
) {
Expand All @@ -257,61 +258,83 @@ export default class Module {

// eslint-disable-next-line @typescript-eslint/no-this-alias
const module = this;
const {
dynamicImports,
dynamicImporters,
reexportDescriptions,
implicitlyLoadedAfter,
implicitlyLoadedBefore,
sources,
importers
} = this;
this.info = {
ast: null,
code: null,
get dynamicallyImportedIdResolutions() {
return module.dynamicImports
return dynamicImports
.map(({ argument }) => typeof argument === 'string' && module.resolvedIds[argument])
.filter(Boolean) as ResolvedId[];
},
get dynamicallyImportedIds() {
const dynamicallyImportedIds: string[] = [];
for (const { id } of module.dynamicImports) {
for (const { id } of dynamicImports) {
if (id) {
dynamicallyImportedIds.push(id);
}
}
return dynamicallyImportedIds;
},
get dynamicImporters() {
return module.dynamicImporters.sort();
return 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;
return 'default' in module.exports || 'default' in reexportDescriptions;
},
get hasModuleSideEffects() {
warnDeprecation(
'Accessing ModuleInfo.hasModuleSideEffects from plugins is deprecated. Please use ModuleInfo.moduleSideEffects instead.',
false,
options
);
return module.info.moduleSideEffects;
},
hasModuleSideEffects,
id,
get implicitlyLoadedAfterOneOf() {
return Array.from(module.implicitlyLoadedAfter, getId).sort();
return Array.from(implicitlyLoadedAfter, getId).sort();
},
get implicitlyLoadedBefore() {
return Array.from(module.implicitlyLoadedBefore, getId).sort();
return Array.from(implicitlyLoadedBefore, getId).sort();
},
get importedIdResolutions() {
return Array.from(module.sources, source => module.resolvedIds[source]).filter(Boolean);
return Array.from(sources, source => module.resolvedIds[source]).filter(Boolean);
},
get importedIds() {
return Array.from(module.sources, source => module.resolvedIds[source]?.id).filter(Boolean);
return Array.from(sources, source => module.resolvedIds[source]?.id).filter(Boolean);
},
get importers() {
return module.importers.sort();
return importers.sort();
},
isEntry,
isExternal: false,
get isIncluded() {
if (module.graph.phase !== BuildPhase.GENERATE) {
if (graph.phase !== BuildPhase.GENERATE) {
return null;
}
return module.isIncluded();
},
meta: { ...meta },
moduleSideEffects,
syntheticNamedExports
};
// Hide the deprecated key so that it only warns when accessed explicitly
Object.defineProperty(this.info, 'hasModuleSideEffects', {
...Object.getOwnPropertyDescriptor(this.info, 'hasModuleSideEffects'),
enumerable: false
});
}

basename(): string {
Expand Down Expand Up @@ -393,7 +416,7 @@ export default class Module {
}
necessaryDependencies.add(variable.module!);
}
if (!this.options.treeshake || this.info.hasModuleSideEffects === 'no-treeshake') {
if (!this.options.treeshake || this.info.moduleSideEffects === 'no-treeshake') {
for (const dependency of this.dependencies) {
relevantDependencies.add(dependency);
}
Expand Down Expand Up @@ -600,7 +623,7 @@ export default class Module {

hasEffects(): boolean {
return (
this.info.hasModuleSideEffects === 'no-treeshake' ||
this.info.moduleSideEffects === 'no-treeshake' ||
(this.ast!.included && this.ast!.hasEffects(createHasEffectsContext()))
);
}
Expand Down Expand Up @@ -766,7 +789,7 @@ export default class Module {
dependencies: Array.from(this.dependencies, getId),
id: this.id,
meta: this.info.meta,
moduleSideEffects: this.info.hasModuleSideEffects,
moduleSideEffects: this.info.moduleSideEffects,
originalCode: this.originalCode,
originalSourcemap: this.originalSourcemap,
resolvedIds: this.resolvedIds,
Expand Down Expand Up @@ -835,7 +858,7 @@ export default class Module {
syntheticNamedExports
}: Partial<PartialNull<ModuleOptions>>): void {
if (moduleSideEffects != null) {
this.info.hasModuleSideEffects = moduleSideEffects;
this.info.moduleSideEffects = moduleSideEffects;
}
if (syntheticNamedExports != null) {
this.info.syntheticNamedExports = syntheticNamedExports;
Expand Down Expand Up @@ -1008,7 +1031,7 @@ export default class Module {
relevantDependencies.add(dependency);
continue;
}
if (!(dependency.info.hasModuleSideEffects || alwaysCheckedDependencies.has(dependency))) {
if (!(dependency.info.moduleSideEffects || alwaysCheckedDependencies.has(dependency))) {
continue;
}
if (dependency instanceof ExternalModule || dependency.hasEffects()) {
Expand Down
2 changes: 1 addition & 1 deletion src/ModuleLoader.ts
Expand Up @@ -444,7 +444,7 @@ export class ModuleLoader {
module.dependencies.add(dependency);
dependency.importers.push(module.id);
}
if (!this.options.treeshake || module.info.hasModuleSideEffects === 'no-treeshake') {
if (!this.options.treeshake || module.info.moduleSideEffects === 'no-treeshake') {
for (const dependency of module.dependencies) {
if (dependency instanceof Module) {
dependency.importedFromNotTreeshaken = true;
Expand Down
5 changes: 2 additions & 3 deletions src/rollup/types.d.ts
Expand Up @@ -156,13 +156,14 @@ export type EmitChunk = (id: string, options?: { name?: string }) => string;

export type EmitFile = (emittedFile: EmittedFile) => string;

interface ModuleInfo {
interface ModuleInfo extends ModuleOptions {
ast: AcornNode | null;
code: string | null;
dynamicImporters: readonly string[];
dynamicallyImportedIdResolutions: readonly ResolvedId[];
dynamicallyImportedIds: readonly string[];
hasDefaultExport: boolean | null;
/** @deprecated Use `moduleSideEffects` instead */
hasModuleSideEffects: boolean | 'no-treeshake';
id: string;
implicitlyLoadedAfterOneOf: readonly string[];
Expand All @@ -173,8 +174,6 @@ interface ModuleInfo {
isEntry: boolean;
isExternal: boolean;
isIncluded: boolean | null;
meta: CustomPluginOptions;
syntheticNamedExports: boolean | string;
}

export type GetModuleInfo = (moduleId: string) => ModuleInfo | null;
Expand Down
2 changes: 1 addition & 1 deletion src/utils/traverseStaticDependencies.ts
Expand Up @@ -10,7 +10,7 @@ export function markModuleAndImpureDependenciesAsExecuted(baseModule: Module): v
if (
!(dependency instanceof ExternalModule) &&
!dependency.isExecuted &&
(dependency.info.hasModuleSideEffects || module.implicitlyLoadedBefore.has(dependency)) &&
(dependency.info.moduleSideEffects || module.implicitlyLoadedBefore.has(dependency)) &&
!visitedModules.has(dependency.id)
) {
dependency.isExecuted = true;
Expand Down