From 850ef7f16f7bab6f2ee03721b7280d6a03b8beb5 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Tue, 27 Oct 2026 17:17:17 +0800 Subject: [PATCH 1/2] fix(pack): honor externals for virtual requests --- .../src/shared/resolve/externals_plugin.rs | 67 +++++++------------ .../externals/virtual-alias/.gitignore | 1 + .../externals/virtual-alias/.snapshotignore | 2 + .../externals/virtual-alias/assert.js | 31 +++++++++ .../externals/virtual-alias/config.json | 33 +++++++++ .../externals/virtual-alias/input/fc.js | 1 + .../externals/virtual-alias/input/index.js | 13 ++++ .../_root-of-the-server___0z3rgi_jckydl.js | 44 ++++++++++++ .../externals/virtual-alias/output/main.js | 4 ++ .../externals/virtual-only/.gitignore | 1 + .../externals/virtual-only/.snapshotignore | 2 + .../snapshot/externals/virtual-only/assert.js | 27 ++++++++ .../externals/virtual-only/config.json | 21 ++++++ .../externals/virtual-only/input/fc.js | 1 + .../externals/virtual-only/input/index.js | 7 ++ .../_root-of-the-server___1uve16sck8pk_.js | 26 +++++++ .../externals/virtual-only/output/main.js | 4 ++ 17 files changed, 243 insertions(+), 42 deletions(-) create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/.gitignore create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/.snapshotignore create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/assert.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/config.json create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/input/fc.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/output/_root-of-the-server___0z3rgi_jckydl.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-alias/output/main.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/.gitignore create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/.snapshotignore create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/assert.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/config.json create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/input/fc.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/output/_root-of-the-server___1uve16sck8pk_.js create mode 100644 crates/pack-tests/tests/snapshot/externals/virtual-only/output/main.js diff --git a/crates/pack-core/src/shared/resolve/externals_plugin.rs b/crates/pack-core/src/shared/resolve/externals_plugin.rs index fab30dc97..777c712df 100644 --- a/crates/pack-core/src/shared/resolve/externals_plugin.rs +++ b/crates/pack-core/src/shared/resolve/externals_plugin.rs @@ -345,22 +345,27 @@ impl ExternalsPlugin { let before_condition = match externals_config_ref.is_empty() { true => BeforeResolvePluginCondition::Never.cell(), false => { - // Extract all possible module names from external keys - // For "react" -> ["react"] - // For "@ant/bigfish/antd" -> ["@ant/bigfish/antd", "@ant/bigfish"] - let mut modules = Vec::new(); - for key in externals_config_ref.keys() { - modules.push(key.clone()); - // Extract scoped package name if key contains additional path - if key.starts_with('@') - && let (Some(pos), Some(first_pos)) = (key.rfind('/'), key.find('/')) - && pos > first_pos - { - modules.push(key[..pos].into()); - } - } - - BeforeResolvePluginCondition::from_modules(Vc::cell(modules)) + // Match full requests, including URI schemes, before aliases are applied. + // Escape glob syntax so external keys remain literal request names. + let requests = externals_config_ref + .keys() + .map(|key| { + let mut escaped = String::with_capacity(key.len()); + for ch in key.chars() { + if matches!(ch, '\\' | '*' | '?' | '[' | ']' | '{' | '}' | ',') { + escaped.push('\\'); + } + escaped.push(ch); + } + escaped + }) + .collect::>() + .join(","); + + BeforeResolvePluginCondition::from_request_glob(Glob::new( + format!("{{{requests}}}").into(), + GlobOptions::default(), + )) } }; let before_condition = before_condition.to_resolved().await?; @@ -435,34 +440,12 @@ impl BeforeResolvePlugin for ExternalsPlugin { request: Vc, ) -> Result> { let externals_config = self.externals_config.await?; - let request_value = request.await?; - - let (module_name, subpath_str) = match &*request_value { - Request::Module { - module: Pattern::Constant(name), - path: Pattern::Constant(path), - .. - } => (name.as_str(), path.as_str()), - Request::Raw { - path: Pattern::Constant(name), - .. - } => (name.as_str(), ""), - _ => return Ok(ResolveResultOption::none()), - }; - - // Try full path first (e.g., "@ant/bigfish/antd" from module="@ant/bigfish" + path="/antd") - if !subpath_str.is_empty() { - let full_path = format!("{}{}", module_name, subpath_str); - if let Some(external_config) = externals_config.get(full_path.as_str()) { - return Ok(handle_external_config(external_config, &full_path)); - } - // If full path not in externals, skip to let after_resolve or normal resolution handle it + let Some(request) = request.await?.request() else { return Ok(ResolveResultOption::none()); - } + }; - // Try module name only (e.g., "react") - if let Some(external_config) = externals_config.get(module_name) { - return Ok(handle_external_config(external_config, module_name)); + if let Some(external_config) = externals_config.get(request.as_str()) { + return Ok(handle_external_config(external_config, &request)); } Ok(ResolveResultOption::none()) diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/.gitignore b/crates/pack-tests/tests/snapshot/externals/virtual-alias/.gitignore new file mode 100644 index 000000000..06d1c014c --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/.gitignore @@ -0,0 +1 @@ +/output/_turbopack__runtime.js diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/.snapshotignore b/crates/pack-tests/tests/snapshot/externals/virtual-alias/.snapshotignore new file mode 100644 index 000000000..8ff7c49a5 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/.snapshotignore @@ -0,0 +1,2 @@ +# The Node runtime is unrelated to external request matching. +_turbopack__runtime.js diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/assert.js b/crates/pack-tests/tests/snapshot/externals/virtual-alias/assert.js new file mode 100644 index 000000000..6298a10bc --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/assert.js @@ -0,0 +1,31 @@ +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); + +const result = spawnSync( + process.execPath, + [ + "-e", + ` + const assert = require("node:assert/strict"); + const result = require("./output/main.js"); + const { state } = require("./input/fc.js"); + + assert.equal(result.sameInstance, true); + assert.equal(result.requestInitialized, true); + assert.equal(result.sameUnscopedInstance, true); + assert.equal(result.unscopedInitialized, true); + assert.equal(result.sameLiteralInstance, true); + assert.equal(result.literalInitialized, true); + assert.equal(state.initialized, true); + `, + ], + { + cwd: __dirname, + env: { ...process.env, NODE_PATH: path.join(__dirname, "input") }, + encoding: "utf8", + }, +); + +assert.ifError(result.error); +assert.equal(result.status, 0, result.stderr || result.stdout); diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/config.json b/crates/pack-tests/tests/snapshot/externals/virtual-alias/config.json new file mode 100644 index 000000000..5619c0b72 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/config.json @@ -0,0 +1,33 @@ +{ + "runtimeType": "Production", + "config": { + "entry": [ + { + "import": "input/index.js", + "name": "main" + } + ], + "target": "node", + "resolve": { + "alias": { + "@mvp/request/h5": "@mvp/request/fc", + "plain-request/h5": "@mvp/request/fc", + "virtual:request/h5": "@mvp/request/fc", + "virtual:request/[h5]{test}*": "@mvp/request/fc", + "@mvp/request/fc": "./input/fc.js" + } + }, + "externals": { + "@mvp/request/h5": "commonjs fc", + "@mvp/request/fc": "commonjs fc", + "plain-request/h5": "commonjs fc", + "virtual:request/h5": "commonjs fc", + "virtual:request/[h5]{test}*": "commonjs fc" + }, + "optimization": { + "minify": false, + "moduleIds": "named" + }, + "sourceMaps": false + } +} diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/input/fc.js b/crates/pack-tests/tests/snapshot/externals/virtual-alias/input/fc.js new file mode 100644 index 000000000..86ad8ea76 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/input/fc.js @@ -0,0 +1 @@ +exports.state = { initialized: false }; diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/input/index.js b/crates/pack-tests/tests/snapshot/externals/virtual-alias/input/index.js new file mode 100644 index 000000000..a80a989d6 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/input/index.js @@ -0,0 +1,13 @@ +import { state as initialized } from "@mvp/request/h5"; +import { state as unscoped } from "plain-request/h5"; +import { state as requested } from "virtual:request/h5"; +import { state as literal } from "virtual:request/[h5]{test}*"; + +initialized.initialized = true; + +export const sameInstance = initialized === requested; +export const requestInitialized = requested.initialized; +export const sameUnscopedInstance = initialized === unscoped; +export const unscopedInitialized = unscoped.initialized; +export const sameLiteralInstance = initialized === literal; +export const literalInitialized = literal.initialized; diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/output/_root-of-the-server___0z3rgi_jckydl.js b/crates/pack-tests/tests/snapshot/externals/virtual-alias/output/_root-of-the-server___0z3rgi_jckydl.js new file mode 100644 index 000000000..ff6aeb43a --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/output/_root-of-the-server___0z3rgi_jckydl.js @@ -0,0 +1,44 @@ +module.exports = [ +"[externals]/fc [external] (fc, cjs)", ((__turbopack_context__, module, exports) => { + +var mod = __turbopack_context__.x("fc", () => require("fc")); + +module.exports = mod; +}), +"[project]/externals/virtual-alias/input/index.js [server] (ecmascript)", ((__turbopack_context__) => { +"use strict"; + +var __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__ = __turbopack_context__.i("[externals]/fc [external] (fc, cjs)"); +; +; +; +; +__TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"].initialized = true; +const sameInstance = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"] === __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"]; +const requestInitialized = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"].initialized; +const sameUnscopedInstance = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"] === __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"]; +const unscopedInitialized = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"].initialized; +const sameLiteralInstance = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"] === __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"]; +const literalInitialized = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"].initialized; +__turbopack_context__.s([ + "literalInitialized", + 0, + literalInitialized, + "requestInitialized", + 0, + requestInitialized, + "sameInstance", + 0, + sameInstance, + "sameLiteralInstance", + 0, + sameLiteralInstance, + "sameUnscopedInstance", + 0, + sameUnscopedInstance, + "unscopedInitialized", + 0, + unscopedInitialized +]); +}), +]; \ No newline at end of file diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-alias/output/main.js b/crates/pack-tests/tests/snapshot/externals/virtual-alias/output/main.js new file mode 100644 index 000000000..b5e810496 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-alias/output/main.js @@ -0,0 +1,4 @@ +var R=require("./_turbopack__runtime.js")("main.js") +R.c("_root-of-the-server___0z3rgi_jckydl.js") +R.m("[project]/externals/virtual-alias/input/index.js [server] (ecmascript)") +module.exports=R.m("[project]/externals/virtual-alias/input/index.js [server] (ecmascript)").exports \ No newline at end of file diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/.gitignore b/crates/pack-tests/tests/snapshot/externals/virtual-only/.gitignore new file mode 100644 index 000000000..06d1c014c --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/.gitignore @@ -0,0 +1 @@ +/output/_turbopack__runtime.js diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/.snapshotignore b/crates/pack-tests/tests/snapshot/externals/virtual-only/.snapshotignore new file mode 100644 index 000000000..8ff7c49a5 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/.snapshotignore @@ -0,0 +1,2 @@ +# The Node runtime is unrelated to external request matching. +_turbopack__runtime.js diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/assert.js b/crates/pack-tests/tests/snapshot/externals/virtual-only/assert.js new file mode 100644 index 000000000..f15cb18f9 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/assert.js @@ -0,0 +1,27 @@ +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); + +const result = spawnSync( + process.execPath, + [ + "-e", + ` + const assert = require("node:assert/strict"); + const result = require("./output/main.js"); + const { state } = require("./input/fc.js"); + + assert.equal(result.sameInstance, true); + assert.equal(result.requestInitialized, true); + assert.equal(state.initialized, true); + `, + ], + { + cwd: __dirname, + env: { ...process.env, NODE_PATH: path.join(__dirname, "input") }, + encoding: "utf8", + }, +); + +assert.ifError(result.error); +assert.equal(result.status, 0, result.stderr || result.stdout); diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/config.json b/crates/pack-tests/tests/snapshot/externals/virtual-only/config.json new file mode 100644 index 000000000..74c17e0ce --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/config.json @@ -0,0 +1,21 @@ +{ + "runtimeType": "Production", + "config": { + "entry": [ + { + "import": "input/index.js", + "name": "main" + } + ], + "target": "node", + "externals": { + "@mvp/request/h5": "commonjs fc", + "virtual:request/h5": "commonjs fc" + }, + "optimization": { + "minify": false, + "moduleIds": "named" + }, + "sourceMaps": false + } +} diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/input/fc.js b/crates/pack-tests/tests/snapshot/externals/virtual-only/input/fc.js new file mode 100644 index 000000000..86ad8ea76 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/input/fc.js @@ -0,0 +1 @@ +exports.state = { initialized: false }; diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/input/index.js b/crates/pack-tests/tests/snapshot/externals/virtual-only/input/index.js new file mode 100644 index 000000000..18e30f80a --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/input/index.js @@ -0,0 +1,7 @@ +import { state as initialized } from "@mvp/request/h5"; +import { state as requested } from "virtual:request/h5"; + +initialized.initialized = true; + +export const sameInstance = initialized === requested; +export const requestInitialized = requested.initialized; diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/output/_root-of-the-server___1uve16sck8pk_.js b/crates/pack-tests/tests/snapshot/externals/virtual-only/output/_root-of-the-server___1uve16sck8pk_.js new file mode 100644 index 000000000..003576729 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/output/_root-of-the-server___1uve16sck8pk_.js @@ -0,0 +1,26 @@ +module.exports = [ +"[externals]/fc [external] (fc, cjs)", ((__turbopack_context__, module, exports) => { + +var mod = __turbopack_context__.x("fc", () => require("fc")); + +module.exports = mod; +}), +"[project]/externals/virtual-only/input/index.js [server] (ecmascript)", ((__turbopack_context__) => { +"use strict"; + +var __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__ = __turbopack_context__.i("[externals]/fc [external] (fc, cjs)"); +; +; +__TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"].initialized = true; +const sameInstance = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"] === __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"]; +const requestInitialized = __TURBOPACK__imported__module__$5b$externals$5d2f$fc__$5b$external$5d$__$28$fc$2c$__cjs$29$__["state"].initialized; +__turbopack_context__.s([ + "requestInitialized", + 0, + requestInitialized, + "sameInstance", + 0, + sameInstance +]); +}), +]; \ No newline at end of file diff --git a/crates/pack-tests/tests/snapshot/externals/virtual-only/output/main.js b/crates/pack-tests/tests/snapshot/externals/virtual-only/output/main.js new file mode 100644 index 000000000..d7b7493a5 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/externals/virtual-only/output/main.js @@ -0,0 +1,4 @@ +var R=require("./_turbopack__runtime.js")("main.js") +R.c("_root-of-the-server___1uve16sck8pk_.js") +R.m("[project]/externals/virtual-only/input/index.js [server] (ecmascript)") +module.exports=R.m("[project]/externals/virtual-only/input/index.js [server] (ecmascript)").exports \ No newline at end of file From e36b4b8c90725cbcb933437aed9b8b67e69a6778 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Tue, 27 Oct 2026 17:47:24 +0800 Subject: [PATCH 2/2] ci: pin compatible Ubuntu runners for browser tests --- .github/workflows/pack-ecosystem.yml | 4 ++-- .github/workflows/utooweb-ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pack-ecosystem.yml b/.github/workflows/pack-ecosystem.yml index d29285d70..003eff1db 100644 --- a/.github/workflows/pack-ecosystem.yml +++ b/.github/workflows/pack-ecosystem.yml @@ -30,7 +30,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && contains(github.event.head_commit.message, '(pack)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pack)')) - uses: utooland/utoo-ecosystem-ci/.github/workflows/ecosystem-ci.yml@main + uses: utooland/utoo-ecosystem-ci/.github/workflows/ecosystem-ci.yml@e95e8271c1683b0c26329a40ed1ebcd69887b6c5 with: candidate_mode: source utoo_repository: >- @@ -40,4 +40,4 @@ jobs: suite: >- ${{ github.event_name == 'workflow_dispatch' && inputs.suite || 'all' }} harness_repository: utooland/utoo-ecosystem-ci - harness_ref: main + harness_ref: e95e8271c1683b0c26329a40ed1ebcd69887b6c5 diff --git a/.github/workflows/utooweb-ci.yml b/.github/workflows/utooweb-ci.yml index fc36456ab..1a7b784d7 100644 --- a/.github/workflows/utooweb-ci.yml +++ b/.github/workflows/utooweb-ci.yml @@ -16,7 +16,7 @@ permissions: jobs: build: name: utooweb-ci-build-and-e2e - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Init git submodules