Skip to content

Commit

Permalink
Deprecate hasModuleSideEffects in favor of moduleSideEffects and ensu…
Browse files Browse the repository at this point in the history
…re it is mutable on ModuleInfo
  • Loading branch information
lukastaegert committed Feb 1, 2022
1 parent 50f46bc commit 77d9b4a
Show file tree
Hide file tree
Showing 45 changed files with 644 additions and 128 deletions.
59 changes: 43 additions & 16 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 @@ -716,6 +741,8 @@ 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.
Note that 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.
#### `this.getWatchFiles`
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

0 comments on commit 77d9b4a

Please sign in to comment.