From 10fb5566e71d24e268678cd8dd2376cafcdcb5dc Mon Sep 17 00:00:00 2001 From: Ryuya Date: Wed, 25 Jun 2025 03:21:53 -0700 Subject: [PATCH] test: add test case for circular dependency with externals (#19623) --- .../module/circular-externals/external-a.mjs | 10 +++ .../module/circular-externals/external-b.mjs | 10 +++ .../module/circular-externals/index.js | 51 +++++++++++++++ .../module/circular-externals/module-a.js | 12 ++++ .../module/circular-externals/module-b.js | 12 ++++ .../module/circular-externals/test.config.js | 5 ++ .../circular-externals/webpack.config.js | 65 +++++++++++++++++++ 7 files changed, 165 insertions(+) create mode 100644 test/configCases/module/circular-externals/external-a.mjs create mode 100644 test/configCases/module/circular-externals/external-b.mjs create mode 100644 test/configCases/module/circular-externals/index.js create mode 100644 test/configCases/module/circular-externals/module-a.js create mode 100644 test/configCases/module/circular-externals/module-b.js create mode 100644 test/configCases/module/circular-externals/test.config.js create mode 100644 test/configCases/module/circular-externals/webpack.config.js diff --git a/test/configCases/module/circular-externals/external-a.mjs b/test/configCases/module/circular-externals/external-a.mjs new file mode 100644 index 000000000..2785f5f3e --- /dev/null +++ b/test/configCases/module/circular-externals/external-a.mjs @@ -0,0 +1,10 @@ +import { externalValue as valueB, getOtherExternal as getB } from "./external-b.mjs"; + +export const externalValue = "external-A"; + +export function getOtherExternal() { + return valueB; +} + +// Re-export to test circular re-exports +export { getB as getOtherValue }; diff --git a/test/configCases/module/circular-externals/external-b.mjs b/test/configCases/module/circular-externals/external-b.mjs new file mode 100644 index 000000000..06ad1a9c1 --- /dev/null +++ b/test/configCases/module/circular-externals/external-b.mjs @@ -0,0 +1,10 @@ +import { externalValue as valueA, getOtherExternal as getA } from "./external-a.mjs"; + +export const externalValue = "external-B"; + +export function getOtherExternal() { + return valueA; +} + +// Re-export to test circular re-exports +export { getA as getOtherValue }; diff --git a/test/configCases/module/circular-externals/index.js b/test/configCases/module/circular-externals/index.js new file mode 100644 index 000000000..cf77fd392 --- /dev/null +++ b/test/configCases/module/circular-externals/index.js @@ -0,0 +1,51 @@ +import { valueA, getFromExternalA, callB } from "./module-a.js"; +import { valueB, getFromExternalB, callA } from "./module-b.js"; +import { externalValue as directExternalA } from "external-module-a"; +import { externalValue as directExternalB } from "external-module-b"; + +it("should handle circular dependencies between internal modules", () => { + expect(valueA).toBe("module-A"); + expect(valueB).toBe("module-B"); + expect(callB()).toBe("module-B"); + expect(callA()).toBe("module-A"); +}); + +it("should handle imports from external modules", () => { + expect(getFromExternalA()).toBe("external-A"); + expect(getFromExternalB()).toBe("external-B"); +}); + +it("should handle direct imports from external modules", () => { + expect(directExternalA).toBe("external-A"); + expect(directExternalB).toBe("external-B"); +}); + +// ESM external modules with circular dependencies +it("should maintain live bindings for ESM external modules", async () => { + // Import external modules that have circular dependencies + const moduleA = await import("external-module-a"); + const moduleB = await import("external-module-b"); + + // Verify that circular dependencies are resolved correctly + expect(moduleA.externalValue).toBe("external-A"); + expect(moduleB.externalValue).toBe("external-B"); + + // Verify that re-exports work correctly in circular scenarios + expect(moduleA.getOtherValue).toBeDefined(); + expect(moduleB.getOtherValue).toBeDefined(); + + // Test that the modules maintain their identity (live bindings) + expect(await import("external-module-a")).toBe(moduleA); + expect(await import("external-module-b")).toBe(moduleB); +}); + +// Edge case: Multiple imports of the same external module +it("should handle multiple imports of circular external modules", () => { + // This tests that the runtime module correctly caches external modules + const firstImportA = directExternalA; + const secondImportA = getFromExternalA(); + + // Both should reference the same value + expect(firstImportA).toBe(secondImportA); + expect(firstImportA).toBe("external-A"); +}); diff --git a/test/configCases/module/circular-externals/module-a.js b/test/configCases/module/circular-externals/module-a.js new file mode 100644 index 000000000..3d6241123 --- /dev/null +++ b/test/configCases/module/circular-externals/module-a.js @@ -0,0 +1,12 @@ +import { valueB } from "./module-b.js"; +import { externalValue } from "external-module-a"; + +export const valueA = "module-A"; + +export function getFromExternalA() { + return externalValue; +} + +export function callB() { + return valueB; +} diff --git a/test/configCases/module/circular-externals/module-b.js b/test/configCases/module/circular-externals/module-b.js new file mode 100644 index 000000000..8e68f5965 --- /dev/null +++ b/test/configCases/module/circular-externals/module-b.js @@ -0,0 +1,12 @@ +import { valueA } from "./module-a.js"; +import { externalValue } from "external-module-b"; + +export const valueB = "module-B"; + +export function getFromExternalB() { + return externalValue; +} + +export function callA() { + return valueA; +} diff --git a/test/configCases/module/circular-externals/test.config.js b/test/configCases/module/circular-externals/test.config.js new file mode 100644 index 000000000..1192a7afc --- /dev/null +++ b/test/configCases/module/circular-externals/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle() { + return "./main.mjs"; + } +}; diff --git a/test/configCases/module/circular-externals/webpack.config.js b/test/configCases/module/circular-externals/webpack.config.js new file mode 100644 index 000000000..4d111f588 --- /dev/null +++ b/test/configCases/module/circular-externals/webpack.config.js @@ -0,0 +1,65 @@ +const fs = require("fs"); +const path = require("path"); + +/** @type {import("../../../../types").Configuration} */ +module.exports = { + entry: "./index.js", + experiments: { + outputModule: true + }, + output: { + module: true, + library: { + type: "module" + }, + filename: "[name].mjs", + chunkFormat: "module" + }, + externals: { + "external-module-a": "module ./external-a.mjs", + "external-module-b": "module ./external-b.mjs" + }, + externalsType: "module", + optimization: { + concatenateModules: false + }, + plugins: [ + { + apply(compiler) { + compiler.hooks.thisCompilation.tap( + "copy-external-files", + compilation => { + compilation.hooks.processAssets.tap( + { + name: "copy-external-files", + stage: + compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL + }, + () => { + // Read the external module files + const externalA = fs.readFileSync( + path.join(__dirname, "external-a.mjs"), + "utf-8" + ); + const externalB = fs.readFileSync( + path.join(__dirname, "external-b.mjs"), + "utf-8" + ); + + // Emit them as assets + compilation.emitAsset( + "external-a.mjs", + new compiler.webpack.sources.RawSource(externalA) + ); + compilation.emitAsset( + "external-b.mjs", + new compiler.webpack.sources.RawSource(externalB) + ); + } + ); + } + ); + } + } + ] +};