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

Throw proper error when indirectly reexporting a recursive binding #4472

Merged
merged 3 commits into from Apr 30, 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
2 changes: 1 addition & 1 deletion docs/05-plugin-development.md
Expand Up @@ -905,7 +905,7 @@ Use Rollup's internal acorn instance to parse code to an AST.

#### `this.resolve`

**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, isEntry?: boolean, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, meta: {[plugin: string]: any}} | null>`
**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, meta: {[plugin: string]: any}} | null>`

Resolve imports to module ids (i.e. file names) using the same plugins that Rollup uses, and determine if an import should be external. If `null` is returned, the import could not be resolved by Rollup or any plugin but was not explicitly marked as external by the user. If an absolute external id is returned that should remain absolute in the output either via the [`makeAbsoluteExternalsRelative`](guide/en/#makeabsoluteexternalsrelative) option or by explicit plugin choice in the [`resolveId`](guide/en/#resolveid) hook, `external` will be `"absolute"` instead of `true`.

Expand Down
40 changes: 33 additions & 7 deletions src/Module.ts
Expand Up @@ -134,7 +134,7 @@ function getVariableForExportNameRecursive(
target: Module | ExternalModule,
name: string,
importerForSideEffects: Module | undefined,
isExportAllSearch: boolean,
isExportAllSearch: boolean | undefined,
searchedNamesAndModules = new Map<string, Set<Module | ExternalModule>>()
): [variable: Variable | null, indirectExternal?: boolean] {
const searchedModules = searchedNamesAndModules.get(name);
Expand Down Expand Up @@ -561,7 +561,10 @@ export default class Module {
return [this.exportShimVariable];
}
const name = exportDeclaration.localName;
const variable = this.traceVariable(name, importerForSideEffects)!;
const variable = this.traceVariable(name, {
importerForSideEffects,
searchedNamesAndModules
})!;
if (importerForSideEffects) {
getOrCreate(
importerForSideEffects.sideEffectDependenciesByVariable,
Expand Down Expand Up @@ -793,7 +796,18 @@ export default class Module {
};
}

traceVariable(name: string, importerForSideEffects?: Module): Variable | null {
traceVariable(
name: string,
{
importerForSideEffects,
isExportAllSearch,
searchedNamesAndModules
}: {
importerForSideEffects?: Module;
isExportAllSearch?: boolean;
searchedNamesAndModules?: Map<string, Set<Module | ExternalModule>>;
} = EMPTY_OBJECT
): Variable | null {
const localVariable = this.scope.variables.get(name);
if (localVariable) {
return localVariable;
Expand All @@ -807,9 +821,13 @@ export default class Module {
return otherModule.namespace;
}

const [declaration] = otherModule.getVariableForExportName(importDeclaration.name, {
importerForSideEffects: importerForSideEffects || this
});
const [declaration] = getVariableForExportNameRecursive(
otherModule,
importDeclaration.name,
importerForSideEffects || this,
isExportAllSearch,
searchedNamesAndModules
);

if (!declaration) {
return this.error(
Expand Down Expand Up @@ -1057,7 +1075,9 @@ export default class Module {
name,
importerForSideEffects,
true,
searchedNamesAndModules
// We are creating a copy to handle the case where the same binding is
// imported through different namespace reexports gracefully
copyNameToModulesMap(searchedNamesAndModules)
);

if (module instanceof ExternalModule || indirectExternal) {
Expand Down Expand Up @@ -1199,3 +1219,9 @@ function setAlternativeExporterIfCyclic(
}
}
}

const copyNameToModulesMap = (
searchedNamesAndModules?: Map<string, Set<Module | ExternalModule>>
): Map<string, Set<Module | ExternalModule>> | undefined =>
searchedNamesAndModules &&
new Map(Array.from(searchedNamesAndModules, ([name, modules]) => [name, new Set(modules)]));
12 changes: 12 additions & 0 deletions test/function/samples/circular-reexport/_config.js
@@ -0,0 +1,12 @@
const path = require('path');
const ID_MAIN = path.join(__dirname, 'main.js');

module.exports = {
description: 'throws proper error for circular reexports',
error: {
code: 'CIRCULAR_REEXPORT',
id: ID_MAIN,
message: '"foo" cannot be exported from main.js as it is a reexport that references itself.',
watchFiles: [ID_MAIN]
}
};
3 changes: 3 additions & 0 deletions test/function/samples/circular-reexport/main.js
@@ -0,0 +1,3 @@
import { foo } from './main.js';

export { foo };