From 2c15af485a6aab1547a95bb9166adc2ac7f5cc37 Mon Sep 17 00:00:00 2001 From: xusd320 Date: Fri, 6 Feb 2026 22:59:16 +0800 Subject: [PATCH 1/8] feat(utoo-web): improve fs error translation, support cjs extension, and standardize resolution --- crates/utoo-wasm/src/pack.rs | 15 --- packages/utoo-web/src/webpackLoaders/cjs.ts | 99 +++++++++++++------ .../polyfills/fsPolyfill/utils.ts | 59 ++++++++++- 3 files changed, 124 insertions(+), 49 deletions(-) diff --git a/crates/utoo-wasm/src/pack.rs b/crates/utoo-wasm/src/pack.rs index 8f6bf3e322..e513a23f80 100644 --- a/crates/utoo-wasm/src/pack.rs +++ b/crates/utoo-wasm/src/pack.rs @@ -352,21 +352,6 @@ pub async fn build() -> std::result::Result { tracing::info!("all project entrypoints wrote to disk."); - tracing::info!( - "pack tasks with {} apps {} libraries finished in {:?}", - entrypoints - .apps - .as_ref() - .map(|apps| apps.0.len()) - .unwrap_or_default(), - entrypoints - .libraries - .as_ref() - .map(|libraries| libraries.0.len()) - .unwrap_or_default(), - start.elapsed() - ); - let result = TurbopackResult { issues: issues.iter().map(|i| Issue::from(&**i)).collect(), diagnostics: diags.iter().map(|d| Diagnostic::from(d)).collect(), diff --git a/packages/utoo-web/src/webpackLoaders/cjs.ts b/packages/utoo-web/src/webpackLoaders/cjs.ts index c308f1e4f1..e07d232a15 100644 --- a/packages/utoo-web/src/webpackLoaders/cjs.ts +++ b/packages/utoo-web/src/webpackLoaders/cjs.ts @@ -11,6 +11,16 @@ const pkgJsonCache: Record = {}; const resolutionCache: Record = {}; const searchPathsCache: Record = {}; +const RESOLUTION_EXTENSIONS = [ + "", + ".js", + ".cjs", + ".json", + "/index.js", + "/index.cjs", + "/index.json", +]; + const executeModule = ( moduleCode: string, moduleId: string, @@ -105,8 +115,8 @@ const loadModule = ( try { const moduleCode = fs.readFileSync(cachedId, "utf8") as string; return executeModule(moduleCode, cachedId, id, importMaps, entrypoint); - } catch (e) { - // If read fails, fall back to full resolution + } catch { + // ignore } } @@ -139,6 +149,43 @@ const loadModule = ( let moduleCode = importMaps[resolvedId] || importMaps[id]; let moduleId = importMaps[resolvedId] ? resolvedId : id; + if (!moduleCode) { + // Try matching by stripping potential random prefix from absolute path + let longestKey = ""; + for (const key in importMaps) { + // Normalize key for comparison (ignore leading ./ or /) + let sanitizedKey = key; + if (sanitizedKey.startsWith("./")) sanitizedKey = sanitizedKey.slice(2); + if (sanitizedKey.startsWith("/")) sanitizedKey = sanitizedKey.slice(1); + if (!sanitizedKey) continue; + + const isSuffixOfId = id.startsWith("/") && id.endsWith(sanitizedKey); + const isSuffixOfResolvedId = + resolvedId.startsWith("/") && resolvedId.endsWith(sanitizedKey); + + if (isSuffixOfId || isSuffixOfResolvedId) { + const checkId = isSuffixOfId ? id : resolvedId; + const prefixEnd = checkId.length - sanitizedKey.length; + // Ensure it's a true path segment match + if (prefixEnd === 0 || checkId[prefixEnd - 1] === "/") { + if (key.length > longestKey.length) { + longestKey = key; + } + } + } + } + if (longestKey) { + moduleCode = importMaps[longestKey]; + // Important: Use the absolute path as moduleId so that context is correctly absolute for relative requires + let sanitizedKey = longestKey; + if (sanitizedKey.startsWith("./")) sanitizedKey = sanitizedKey.slice(2); + if (sanitizedKey.startsWith("/")) sanitizedKey = sanitizedKey.slice(1); + + moduleId = + id.startsWith("/") && id.endsWith(sanitizedKey) ? id : resolvedId; + } + } + // Fallback: Try resolving from node_modules if (!moduleCode && !id.startsWith(".") && !id.startsWith("/")) { let searchPaths = searchPathsCache[context]; @@ -186,17 +233,20 @@ const loadModule = ( const candidates = [ path.resolve(nodeModulesPath, mainField), path.resolve(nodeModulesPath, mainField) + ".js", + path.resolve(nodeModulesPath, mainField) + ".cjs", path.resolve(nodeModulesPath, mainField) + ".json", path.resolve(nodeModulesPath, mainField, "index.js"), + path.resolve(nodeModulesPath, mainField, "index.cjs"), + path.resolve(nodeModulesPath, mainField, "index.json"), ]; for (const candidate of candidates) { try { - // Try reading directly to bypass potential statSync issues - moduleCode = fs.readFileSync(candidate, "utf8") as string; + const content = fs.readFileSync(candidate, "utf8") as string; + moduleCode = content; resolvedId = candidate; moduleId = candidate; break; - } catch (e) { + } catch { // ignore } } @@ -207,16 +257,15 @@ const loadModule = ( } if (!moduleCode) { - const extensions = ["", ".js", ".json", "/index.js"]; - for (const ext of extensions) { + for (const ext of RESOLUTION_EXTENSIONS) { const p = nodeModulesPath + ext; try { - // Try reading directly - moduleCode = fs.readFileSync(p, "utf8") as string; + const content = fs.readFileSync(p, "utf8") as string; + moduleCode = content; resolvedId = p; moduleId = p; break; - } catch (e) { + } catch { // ignore } } @@ -226,28 +275,11 @@ const loadModule = ( } } - // Fallback: Try resolving absolute path - if (!moduleCode && id.startsWith("/")) { - const extensions = ["", ".js", ".json", "/index.js"]; - for (const ext of extensions) { - if (ext === "/index.js" && id.endsWith(".js")) continue; - const p = id + ext; - try { - moduleCode = fs.readFileSync(p, "utf8") as string; - resolvedId = p; - moduleId = id; - break; - } catch (e) { - // ignore - } - } - } - - if (!moduleCode) { - // Try extensions - const extensions = ["", ".js", ".json", "/index.js"]; - for (const ext of extensions) { + // Fallback: Try resolving absolute or relative path + if (!moduleCode && (id.startsWith("/") || id.startsWith("."))) { + for (const ext of RESOLUTION_EXTENSIONS) { if (ext === "/index.js" && id.endsWith(".js")) continue; + // Use resolvedId which handles both absolute paths and relative paths joined with context const p = resolvedId + ext; try { moduleCode = fs.readFileSync(p, "utf8") as string; @@ -267,7 +299,10 @@ const loadModule = ( } const error = new Error( - `Worker: Dependency ${id} (resolved: ${resolvedId}) not found. Context: ${context}`, + `Worker: Dependency ${id} (resolved: ${resolvedId}) not found. Context: ${context}. ` + + `ImportMaps count: ${Object.keys(importMaps).length}. ` + + `CWD: ${nodePolyFills.process.cwd()}. ` + + `Sample ImportMaps keys: ${Object.keys(importMaps).slice(0, 5).join(", ")}`, ); console.error(error); diff --git a/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts b/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts index 029c99f310..6481d4d077 100644 --- a/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts +++ b/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts @@ -17,8 +17,11 @@ export function getFs() { } export function translateError(error: any, path: string, syscall: string) { - const message = error.message || String(error); - if (message.includes("NotFound")) { + const message = (error.message || String(error)).toLowerCase(); + + // 1. NotFound (ENOENT) + // Mapping "NotFoundError" from tokio-fs-ext + if (message.includes("notfounderror") || message.includes("notfound")) { const e = new Error( `ENOENT: no such file or directory, ${syscall} '${path}'`, ); @@ -28,5 +31,57 @@ export function translateError(error: any, path: string, syscall: string) { (e as any).path = path; return e; } + + // 2. Directory error (Mapped to EISDIR) + // Mapping "TypeMismatchError" or "type mismatch" from tokio-fs-ext + if (message.includes("typemismatcherror") || message.includes("type mismatch")) { + const e = new Error( + `EISDIR: illegal operation on a directory, ${syscall} '${path}'`, + ); + (e as any).errno = -21; + (e as any).code = "EISDIR"; + (e as any).syscall = syscall; + (e as any).path = path; + return e; + } + + // 3. Locking/Concurrency (Mapped to EAGAIN/EBUSY) + // Mapping "NoModificationAllowedError" from tokio-fs-ext + if ( + message.includes("nomodificationallowederror") || + message.includes("wouldblock") + ) { + const e = new Error( + `EAGAIN: resource temporarily unavailable, ${syscall} '${path}'`, + ); + (e as any).errno = -11; + (e as any).code = "EAGAIN"; + (e as any).syscall = syscall; + (e as any).path = path; + return e; + } + + // 4. Permission Denied (EACCES) + // Mapping "NotAllowedError" or "SecurityError" from tokio-fs-ext + if (message.includes("notallowederror") || message.includes("securityerror")) { + const e = new Error(`EACCES: permission denied, ${syscall} '${path}'`); + (e as any).errno = -13; + (e as any).code = "EACCES"; + (e as any).syscall = syscall; + (e as any).path = path; + return e; + } + + // 5. Storage Full (ENOSPC) + // Mapping "QuotaExceededError" from tokio-fs-ext + if (message.includes("quotaexceedederror")) { + const e = new Error(`ENOSPC: no space left on device, ${syscall} '${path}'`); + (e as any).errno = -28; + (e as any).code = "ENOSPC"; + (e as any).syscall = syscall; + (e as any).path = path; + return e; + } + return error; } From a6380a565eff10de342fdd9e9cc66f0bb1382217 Mon Sep 17 00:00:00 2001 From: xusd320 Date: Mon, 9 Feb 2026 15:26:03 +0800 Subject: [PATCH 2/8] chore: bump tokio-fs-ext to 0.7.4 and update next.js submodule --- Cargo.lock | 4 +-- Cargo.toml | 2 +- next.js | 2 +- packages/utoo-web/src/utoo/index.d.ts | 40 +++++++++++++-------------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 758b93a0dd..7d12808851 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8312,9 +8312,9 @@ dependencies = [ [[package]] name = "tokio-fs-ext" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e76a6a499b9517434351b923ebe4d3326c383ad8f415e946488085c947a64b04" +checksum = "fd418cced85eea78f7e9b775fc3dc4a523d8e8102808697e7c7c3ce615f2d848" dependencies = [ "bitflags 2.9.4", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index dff84e3ea5..8c02da2930 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ serde_path_to_error = "0.1.16" simd-json = "0.17.0" thiserror = "1.0" tokio = { version = "1.47.1" } -tokio-fs-ext = "0.7.3" +tokio-fs-ext = "0.7.4" toml = "0.8" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] } diff --git a/next.js b/next.js index 73f4291249..38bfebf5ae 160000 --- a/next.js +++ b/next.js @@ -1 +1 @@ -Subproject commit 73f42912493c9cdad8968abe6981080c07679729 +Subproject commit 38bfebf5ae0bef77093541b69343f43118d8267e diff --git a/packages/utoo-web/src/utoo/index.d.ts b/packages/utoo-web/src/utoo/index.d.ts index 05767f4e42..f92e0bf0f3 100644 --- a/packages/utoo-web/src/utoo/index.d.ts +++ b/packages/utoo-web/src/utoo/index.d.ts @@ -178,13 +178,19 @@ export function workerCreated(worker_id: number): void; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { - readonly __wbg_roottask_free: (a: number, b: number) => void; - readonly registerWorkerScheduler: (a: any, b: any) => void; - readonly workerCreated: (a: number) => void; - readonly getWasmMemory: () => any; - readonly getWasmModule: () => any; - readonly initLogFilter: (a: number, b: number) => void; - readonly init_pack: () => void; + readonly __wbg_project_free: (a: number, b: number) => void; + readonly project_build: () => any; + readonly project_cwd: () => [number, number]; + readonly project_deps: (a: number, b: number, c: number) => any; + readonly project_entrypointsSubscribe: (a: any) => any; + readonly project_gzip: (a: any) => any; + readonly project_hmrEvents: (a: number, b: number, c: any) => any; + readonly project_init: (a: number, b: number) => void; + readonly project_install: (a: number, b: number, c: number) => any; + readonly project_setCwd: (a: number, b: number) => void; + readonly project_sigMd5: (a: any) => any; + readonly project_updateInfoSubscribe: (a: number, b: any) => void; + readonly project_writeAllToDisk: (a: any) => void; readonly __wbg_direntry_free: (a: number, b: number) => void; readonly __wbg_fs_free: (a: number, b: number) => void; readonly __wbg_get_direntry_name: (a: number) => [number, number]; @@ -216,19 +222,13 @@ export interface InitOutput { readonly fs_write: (a: number, b: number, c: any) => any; readonly fs_writeString: (a: number, b: number, c: number, d: number) => any; readonly fs_writeSync: (a: number, b: number, c: any) => [number, number]; - readonly __wbg_project_free: (a: number, b: number) => void; - readonly project_build: () => any; - readonly project_cwd: () => [number, number]; - readonly project_deps: (a: number, b: number, c: number) => any; - readonly project_entrypointsSubscribe: (a: any) => any; - readonly project_gzip: (a: any) => any; - readonly project_hmrEvents: (a: number, b: number, c: any) => any; - readonly project_init: (a: number, b: number) => void; - readonly project_install: (a: number, b: number, c: number) => any; - readonly project_setCwd: (a: number, b: number) => void; - readonly project_sigMd5: (a: any) => any; - readonly project_updateInfoSubscribe: (a: number, b: any) => void; - readonly project_writeAllToDisk: (a: any) => void; + readonly getWasmMemory: () => any; + readonly getWasmModule: () => any; + readonly initLogFilter: (a: number, b: number) => void; + readonly init_pack: () => void; + readonly __wbg_roottask_free: (a: number, b: number) => void; + readonly registerWorkerScheduler: (a: any, b: any) => void; + readonly workerCreated: (a: number) => void; readonly rust_mi_get_default_heap: () => number; readonly rust_mi_get_thread_id: () => number; readonly rust_mi_set_default_heap: (a: number) => void; From cce0b17a7adba3e2fd21618698eab9e2e72bace9 Mon Sep 17 00:00:00 2001 From: xusd320 Date: Mon, 9 Feb 2026 17:25:52 +0800 Subject: [PATCH 3/8] fix(wasm): export error constants from rust to ensure consistency with js polyfill --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/utoo-wasm/src/errors.rs | 49 ++++++++++++++- next.js | 2 +- packages/utoo-web/src/utoo/index.d.ts | 62 +++++++++++++------ .../polyfills/fsPolyfill/utils.ts | 52 ++++++++++++---- 6 files changed, 136 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d12808851..2662843148 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8312,9 +8312,9 @@ dependencies = [ [[package]] name = "tokio-fs-ext" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd418cced85eea78f7e9b775fc3dc4a523d8e8102808697e7c7c3ce615f2d848" +checksum = "b81c90b384ee8b63048b511a09468ee9ee1338042a94e4888105f67648b05ab2" dependencies = [ "bitflags 2.9.4", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index 8c02da2930..b82863d2c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ serde_path_to_error = "0.1.16" simd-json = "0.17.0" thiserror = "1.0" tokio = { version = "1.47.1" } -tokio-fs-ext = "0.7.4" +tokio-fs-ext = "0.7.5" toml = "0.8" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] } diff --git a/crates/utoo-wasm/src/errors.rs b/crates/utoo-wasm/src/errors.rs index 3ccc5b9f37..63e82d8e32 100644 --- a/crates/utoo-wasm/src/errors.rs +++ b/crates/utoo-wasm/src/errors.rs @@ -1,8 +1,55 @@ +use std::io; use wasm_bindgen::prelude::*; +// Helper macro to define constants and their JS getters (exported as functions) +macro_rules! define_error_consts { + ($($name:ident, $fn_name:ident = $val:expr),* $(,)?) => { + $( + // Internal Rust constant + pub const $name: &str = $val; + + // Exported JS function returning the string + #[wasm_bindgen(js_name = $name)] + pub fn $fn_name() -> String { + $name.to_string() + } + )* + }; +} + +define_error_consts! { + ERR_NOT_FOUND, err_not_found = "NotFoundError", + ERR_KEY_ALREADY_EXISTS, err_key_already_exists = "NoModificationAllowedError", + ERR_NOT_ALLOWED, err_not_allowed = "NotAllowedError", + ERR_NO_MODIFICATION_ALLOWED, err_no_modification_allowed = "NoModificationAllowedError", + ERR_TYPE_MISMATCH, err_type_mismatch = "TypeMismatchError", + ERR_QUOTA_EXCEEDED, err_quota_exceeded = "QuotaExceededError", + ERR_INVALID_STATE, err_invalid_state = "InvalidStateError", + ERR_ABORT, err_abort = "AbortError", +} + pub fn to_js_error(e: E) -> JsError where E: Into, { - JsError::new(&format!("{:#?}", e.into())) + let err = e.into(); + + if let Some(io_err) = err.downcast_ref::() { + let prefix = match io_err.kind() { + io::ErrorKind::NotFound => ERR_NOT_FOUND, + io::ErrorKind::PermissionDenied => ERR_NOT_ALLOWED, + io::ErrorKind::WouldBlock => ERR_NO_MODIFICATION_ALLOWED, + io::ErrorKind::InvalidData => ERR_TYPE_MISMATCH, + io::ErrorKind::StorageFull => ERR_QUOTA_EXCEEDED, + io::ErrorKind::InvalidInput => ERR_INVALID_STATE, + io::ErrorKind::Interrupted => ERR_ABORT, + _ => "", + }; + + if !prefix.is_empty() { + return JsError::new(&format!("{}: {}", prefix, io_err)); + } + } + + JsError::new(&format!("{:#?}", err)) } diff --git a/next.js b/next.js index 38bfebf5ae..7544553479 160000 --- a/next.js +++ b/next.js @@ -1 +1 @@ -Subproject commit 38bfebf5ae0bef77093541b69343f43118d8267e +Subproject commit 7544553479dd776124ff6778ac030c3d30a320a1 diff --git a/packages/utoo-web/src/utoo/index.d.ts b/packages/utoo-web/src/utoo/index.d.ts index f92e0bf0f3..81a6cbc3be 100644 --- a/packages/utoo-web/src/utoo/index.d.ts +++ b/packages/utoo-web/src/utoo/index.d.ts @@ -25,6 +25,22 @@ export class DirEntry { type: DirEntryType; } +export function ERR_ABORT(): string; + +export function ERR_INVALID_STATE(): string; + +export function ERR_KEY_ALREADY_EXISTS(): string; + +export function ERR_NOT_ALLOWED(): string; + +export function ERR_NOT_FOUND(): string; + +export function ERR_NO_MODIFICATION_ALLOWED(): string; + +export function ERR_QUOTA_EXCEEDED(): string; + +export function ERR_TYPE_MISMATCH(): string; + export class Fs { private constructor(); free(): void; @@ -178,19 +194,8 @@ export function workerCreated(worker_id: number): void; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { - readonly __wbg_project_free: (a: number, b: number) => void; - readonly project_build: () => any; - readonly project_cwd: () => [number, number]; - readonly project_deps: (a: number, b: number, c: number) => any; - readonly project_entrypointsSubscribe: (a: any) => any; - readonly project_gzip: (a: any) => any; - readonly project_hmrEvents: (a: number, b: number, c: any) => any; - readonly project_init: (a: number, b: number) => void; - readonly project_install: (a: number, b: number, c: number) => any; - readonly project_setCwd: (a: number, b: number) => void; - readonly project_sigMd5: (a: any) => any; - readonly project_updateInfoSubscribe: (a: number, b: any) => void; - readonly project_writeAllToDisk: (a: any) => void; + readonly initLogFilter: (a: number, b: number) => void; + readonly init_pack: () => void; readonly __wbg_direntry_free: (a: number, b: number) => void; readonly __wbg_fs_free: (a: number, b: number) => void; readonly __wbg_get_direntry_name: (a: number) => [number, number]; @@ -222,13 +227,32 @@ export interface InitOutput { readonly fs_write: (a: number, b: number, c: any) => any; readonly fs_writeString: (a: number, b: number, c: number, d: number) => any; readonly fs_writeSync: (a: number, b: number, c: any) => [number, number]; - readonly getWasmMemory: () => any; - readonly getWasmModule: () => any; - readonly initLogFilter: (a: number, b: number) => void; - readonly init_pack: () => void; readonly __wbg_roottask_free: (a: number, b: number) => void; readonly registerWorkerScheduler: (a: any, b: any) => void; readonly workerCreated: (a: number) => void; + readonly __wbg_project_free: (a: number, b: number) => void; + readonly project_build: () => any; + readonly project_cwd: () => [number, number]; + readonly project_deps: (a: number, b: number, c: number) => any; + readonly project_entrypointsSubscribe: (a: any) => any; + readonly project_gzip: (a: any) => any; + readonly project_hmrEvents: (a: number, b: number, c: any) => any; + readonly project_init: (a: number, b: number) => void; + readonly project_install: (a: number, b: number, c: number) => any; + readonly project_setCwd: (a: number, b: number) => void; + readonly project_sigMd5: (a: any) => any; + readonly project_updateInfoSubscribe: (a: number, b: any) => void; + readonly project_writeAllToDisk: (a: any) => void; + readonly getWasmMemory: () => any; + readonly getWasmModule: () => any; + readonly ERR_ABORT: () => [number, number]; + readonly ERR_INVALID_STATE: () => [number, number]; + readonly ERR_KEY_ALREADY_EXISTS: () => [number, number]; + readonly ERR_NOT_ALLOWED: () => [number, number]; + readonly ERR_NOT_FOUND: () => [number, number]; + readonly ERR_NO_MODIFICATION_ALLOWED: () => [number, number]; + readonly ERR_QUOTA_EXCEEDED: () => [number, number]; + readonly ERR_TYPE_MISMATCH: () => [number, number]; readonly rust_mi_get_default_heap: () => number; readonly rust_mi_get_thread_id: () => number; readonly rust_mi_set_default_heap: (a: number) => void; @@ -254,16 +278,16 @@ export interface InitOutput { readonly __wbg_createsyncaccesshandleoptions_free: (a: number, b: number) => void; readonly wasm_thread_entry_point: (a: number) => void; readonly wasm_bindgen_4243f1f505d3e2fc___closure__destroy___dyn_core_97b1fb32719f2dc8___ops__function__FnMut_____Output_______: (a: number, b: number) => void; + readonly wasm_bindgen_4243f1f505d3e2fc___closure__destroy___dyn_core_97b1fb32719f2dc8___ops__function__Fn__js_sys_8afca1662fec7f75___Array____Output_______: (a: number, b: number) => void; readonly wasm_bindgen_4243f1f505d3e2fc___closure__destroy___dyn_core_97b1fb32719f2dc8___ops__function__FnMut__wasm_bindgen_4243f1f505d3e2fc___JsValue____Output_______: (a: number, b: number) => void; readonly wasm_bindgen_4243f1f505d3e2fc___closure__destroy___dyn_core_97b1fb32719f2dc8___ops__function__FnMut__web_sys_95f4c484d1daab41___features__gen_MessageEvent__MessageEvent____Output_______: (a: number, b: number) => void; readonly wasm_bindgen_4243f1f505d3e2fc___closure__destroy___dyn_core_97b1fb32719f2dc8___ops__function__FnMut_____Output________1_: (a: number, b: number) => void; readonly wasm_bindgen_4243f1f505d3e2fc___closure__destroy___dyn_for__a__core_97b1fb32719f2dc8___ops__function__FnMut____a_web_sys_95f4c484d1daab41___features__gen_MessageEvent__MessageEvent____Output_______: (a: number, b: number) => void; - readonly wasm_bindgen_4243f1f505d3e2fc___closure__destroy___dyn_core_97b1fb32719f2dc8___ops__function__Fn__js_sys_8afca1662fec7f75___Array____Output_______: (a: number, b: number) => void; readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures_____invoke___js_sys_8afca1662fec7f75___Function__js_sys_8afca1662fec7f75___Function_____: (a: number, b: number, c: any, d: any) => void; + readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures_____invoke___js_sys_8afca1662fec7f75___Array_____: (a: number, b: number, c: any) => void; readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures_____invoke___wasm_bindgen_4243f1f505d3e2fc___JsValue_____: (a: number, b: number, c: any) => void; readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures_____invoke___web_sys_95f4c484d1daab41___features__gen_MessageEvent__MessageEvent_____: (a: number, b: number, c: any) => void; readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures________invoke___web_sys_95f4c484d1daab41___features__gen_MessageEvent__MessageEvent_____: (a: number, b: number, c: any) => void; - readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures_____invoke___js_sys_8afca1662fec7f75___Array_____: (a: number, b: number, c: any) => void; readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures_____invoke______: (a: number, b: number) => void; readonly wasm_bindgen_4243f1f505d3e2fc___convert__closures_____invoke_______1_: (a: number, b: number) => void; readonly memory: WebAssembly.Memory; diff --git a/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts b/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts index 6481d4d077..0d851ff773 100644 --- a/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts +++ b/packages/utoo-web/src/webpackLoaders/polyfills/fsPolyfill/utils.ts @@ -1,4 +1,13 @@ import path from "path"; +import { + ERR_ABORT, + ERR_INVALID_STATE, + ERR_NO_MODIFICATION_ALLOWED, + ERR_NOT_ALLOWED, + ERR_NOT_FOUND, + ERR_QUOTA_EXCEEDED, + ERR_TYPE_MISMATCH, +} from "../../../utoo"; import { workerData } from "../workerThreadsPolyfill"; export function resolvePath(p: string): string { @@ -17,11 +26,11 @@ export function getFs() { } export function translateError(error: any, path: string, syscall: string) { - const message = (error.message || String(error)).toLowerCase(); + const message = error.message || String(error); // 1. NotFound (ENOENT) // Mapping "NotFoundError" from tokio-fs-ext - if (message.includes("notfounderror") || message.includes("notfound")) { + if (message.includes(ERR_NOT_FOUND())) { const e = new Error( `ENOENT: no such file or directory, ${syscall} '${path}'`, ); @@ -34,7 +43,7 @@ export function translateError(error: any, path: string, syscall: string) { // 2. Directory error (Mapped to EISDIR) // Mapping "TypeMismatchError" or "type mismatch" from tokio-fs-ext - if (message.includes("typemismatcherror") || message.includes("type mismatch")) { + if (message.includes(ERR_TYPE_MISMATCH())) { const e = new Error( `EISDIR: illegal operation on a directory, ${syscall} '${path}'`, ); @@ -47,10 +56,7 @@ export function translateError(error: any, path: string, syscall: string) { // 3. Locking/Concurrency (Mapped to EAGAIN/EBUSY) // Mapping "NoModificationAllowedError" from tokio-fs-ext - if ( - message.includes("nomodificationallowederror") || - message.includes("wouldblock") - ) { + if (message.includes(ERR_NO_MODIFICATION_ALLOWED())) { const e = new Error( `EAGAIN: resource temporarily unavailable, ${syscall} '${path}'`, ); @@ -62,8 +68,8 @@ export function translateError(error: any, path: string, syscall: string) { } // 4. Permission Denied (EACCES) - // Mapping "NotAllowedError" or "SecurityError" from tokio-fs-ext - if (message.includes("notallowederror") || message.includes("securityerror")) { + // Mapping "NotAllowedError" from tokio-fs-ext + if (message.includes(ERR_NOT_ALLOWED())) { const e = new Error(`EACCES: permission denied, ${syscall} '${path}'`); (e as any).errno = -13; (e as any).code = "EACCES"; @@ -74,8 +80,10 @@ export function translateError(error: any, path: string, syscall: string) { // 5. Storage Full (ENOSPC) // Mapping "QuotaExceededError" from tokio-fs-ext - if (message.includes("quotaexceedederror")) { - const e = new Error(`ENOSPC: no space left on device, ${syscall} '${path}'`); + if (message.includes(ERR_QUOTA_EXCEEDED())) { + const e = new Error( + `ENOSPC: no space left on device, ${syscall} '${path}'`, + ); (e as any).errno = -28; (e as any).code = "ENOSPC"; (e as any).syscall = syscall; @@ -83,5 +91,27 @@ export function translateError(error: any, path: string, syscall: string) { return e; } + // 6. Invalid Argument (EINVAL) + // Mapping "InvalidStateError" from tokio-fs-ext + if (message.includes(ERR_INVALID_STATE())) { + const e = new Error(`EINVAL: invalid argument, ${syscall} '${path}'`); + (e as any).errno = -22; + (e as any).code = "EINVAL"; + (e as any).syscall = syscall; + (e as any).path = path; + return e; + } + + // 7. Interrupted (EINTR) + // Mapping "AbortError" from tokio-fs-ext + if (message.includes(ERR_ABORT())) { + const e = new Error(`EINTR: interrupted system call, ${syscall} '${path}'`); + (e as any).errno = -4; + (e as any).code = "EINTR"; + (e as any).syscall = syscall; + (e as any).path = path; + return e; + } + return error; } From a1b91f1b004943355f00918cc10bf9a9461d7d1c Mon Sep 17 00:00:00 2001 From: xusd320 Date: Mon, 9 Feb 2026 17:37:33 +0800 Subject: [PATCH 4/8] chore(wasm): add duration logging for build tasks --- crates/utoo-wasm/src/pack.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/utoo-wasm/src/pack.rs b/crates/utoo-wasm/src/pack.rs index e513a23f80..a9430017f7 100644 --- a/crates/utoo-wasm/src/pack.rs +++ b/crates/utoo-wasm/src/pack.rs @@ -350,7 +350,7 @@ pub async fn build() -> std::result::Result { }) .await?; - tracing::info!("all project entrypoints wrote to disk."); + tracing::info!("build finished in {:?}", start.elapsed()); let result = TurbopackResult { issues: issues.iter().map(|i| Issue::from(&**i)).collect(), @@ -402,7 +402,8 @@ pub async fn project_entrypoints_subscribe( turbo_tasks_clone.spawn_root_task(move || { let tx = tx.clone(); async move { - tracing::debug!("entrypointsSubscribe root task executing"); + let start = Instant::now(); + tracing::info!("dev build started..."); let entrypoints_with_issues_op = get_all_written_entrypoints_with_issues_operation(container); @@ -416,6 +417,8 @@ pub async fn project_entrypoints_subscribe( .await?; effects.apply().await?; + tracing::info!("dev build finished in {:?}", start.elapsed()); + let turbopack_result = TurbopackResult { issues: issues.iter().map(|i| Issue::from(&**i)).collect(), diagnostics: diagnostics.iter().map(|d| Diagnostic::from(d)).collect(), From 3bf88c31ce72f2fd9672cb47b6718f38807c2eba Mon Sep 17 00:00:00 2001 From: xusd320 Date: Mon, 9 Feb 2026 17:43:31 +0800 Subject: [PATCH 5/8] chore(wasm): remove AlreadyExists error mapping and dedupe cjs extensions --- packages/utoo-web/src/webpackLoaders/cjs.ts | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/utoo-web/src/webpackLoaders/cjs.ts b/packages/utoo-web/src/webpackLoaders/cjs.ts index e07d232a15..213cf16608 100644 --- a/packages/utoo-web/src/webpackLoaders/cjs.ts +++ b/packages/utoo-web/src/webpackLoaders/cjs.ts @@ -152,6 +152,8 @@ const loadModule = ( if (!moduleCode) { // Try matching by stripping potential random prefix from absolute path let longestKey = ""; + let longestSanitizedKey = ""; + for (const key in importMaps) { // Normalize key for comparison (ignore leading ./ or /) let sanitizedKey = key; @@ -170,6 +172,7 @@ const loadModule = ( if (prefixEnd === 0 || checkId[prefixEnd - 1] === "/") { if (key.length > longestKey.length) { longestKey = key; + longestSanitizedKey = sanitizedKey; } } } @@ -177,12 +180,10 @@ const loadModule = ( if (longestKey) { moduleCode = importMaps[longestKey]; // Important: Use the absolute path as moduleId so that context is correctly absolute for relative requires - let sanitizedKey = longestKey; - if (sanitizedKey.startsWith("./")) sanitizedKey = sanitizedKey.slice(2); - if (sanitizedKey.startsWith("/")) sanitizedKey = sanitizedKey.slice(1); - moduleId = - id.startsWith("/") && id.endsWith(sanitizedKey) ? id : resolvedId; + id.startsWith("/") && id.endsWith(longestSanitizedKey) + ? id + : resolvedId; } } @@ -230,16 +231,9 @@ const loadModule = ( try { const mainField = pkg.main; if (mainField) { - const candidates = [ - path.resolve(nodeModulesPath, mainField), - path.resolve(nodeModulesPath, mainField) + ".js", - path.resolve(nodeModulesPath, mainField) + ".cjs", - path.resolve(nodeModulesPath, mainField) + ".json", - path.resolve(nodeModulesPath, mainField, "index.js"), - path.resolve(nodeModulesPath, mainField, "index.cjs"), - path.resolve(nodeModulesPath, mainField, "index.json"), - ]; - for (const candidate of candidates) { + const resolvedMain = path.resolve(nodeModulesPath, mainField); + for (const ext of RESOLUTION_EXTENSIONS) { + const candidate = resolvedMain + ext; try { const content = fs.readFileSync(candidate, "utf8") as string; moduleCode = content; From 6755d32349e792441d73e87f6cc8be6e4778a6f4 Mon Sep 17 00:00:00 2001 From: xusd320 Date: Mon, 9 Feb 2026 19:11:12 +0800 Subject: [PATCH 6/8] feat(utoo-wasm): optimize tokio runtime usage --- Cargo.lock | 1 + crates/utoo-wasm/Cargo.toml | 1 + crates/utoo-wasm/src/tokio_runtime.rs | 50 ++++++++++++++++++++++----- packages/utoo-web/src/utoo/index.d.ts | 42 +++++++++++----------- 4 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2662843148..2557116392 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9796,6 +9796,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", + "swc_core 54.0.0", "tar", "tokio", "tokio-fs-ext", diff --git a/crates/utoo-wasm/Cargo.toml b/crates/utoo-wasm/Cargo.toml index 69daa97d39..53f4c15967 100644 --- a/crates/utoo-wasm/Cargo.toml +++ b/crates/utoo-wasm/Cargo.toml @@ -50,6 +50,7 @@ wasm-bindgen-futures = "0.4.58" pack-api = { workspace = true, optional = true } pack-core = { workspace = true, optional = true } parking_lot = { workspace = true, features = ["nightly"] } +swc_core = { workspace = true, features = ["base", "ecma_ast"] } turbo-rcstr = { workspace = true, optional = true } turbo-tasks = { workspace = true, optional = true } turbo-tasks-backend = { workspace = true, optional = true } diff --git a/crates/utoo-wasm/src/tokio_runtime.rs b/crates/utoo-wasm/src/tokio_runtime.rs index 4eaea643f7..3f38521c46 100644 --- a/crates/utoo-wasm/src/tokio_runtime.rs +++ b/crates/utoo-wasm/src/tokio_runtime.rs @@ -1,5 +1,5 @@ use std::{ - cell::OnceCell, + cell::{OnceCell, RefCell}, sync::{ atomic::{AtomicUsize, Ordering}, OnceLock, @@ -7,7 +7,15 @@ use std::{ time::Duration, }; -use tokio::runtime::{self, Runtime}; +use tokio::{ + runtime::{self, Runtime}, + time::Instant, +}; + +#[cfg(feature = "utoopack")] +thread_local! { + static LAST_SWC_ATOM_GC_TIME: RefCell> = const { RefCell::new(None) }; +} pub static TOKIO_RUNTIME: OnceLock = OnceLock::new(); @@ -20,14 +28,40 @@ pub fn runtime() -> &'static Runtime { pub fn init_tokio_runtime(worker_url: String) { TOKIO_RUNTIME.get_or_init(|| { - runtime::Builder::new_multi_thread() + let concurrency = (|| { + let global = js_sys::global(); + let navigator = + js_sys::Reflect::get(&global, &wasm_bindgen::JsValue::from_str("navigator")) + .ok()?; + let concurrency = js_sys::Reflect::get( + &navigator, + &wasm_bindgen::JsValue::from_str("hardwareConcurrency"), + ) + .ok()?; + concurrency.as_f64().map(|v| (v as usize).max(1)) + })() + .unwrap_or(1); + + let mut builder = runtime::Builder::new_multi_thread(); + builder .disable_lifo_slot() - .thread_name_fn(|| { - static ATOMIC_ID: AtomicUsize = AtomicUsize::new(1); - let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); - format!("tokio-runtime-worker-{id}") + .max_blocking_threads(concurrency); + + #[cfg(feature = "utoopack")] + builder + .on_thread_stop(|| { + turbo_tasks_malloc::TurboMalloc::thread_stop(); }) - .max_blocking_threads(1) + .on_thread_park(|| { + LAST_SWC_ATOM_GC_TIME.with_borrow_mut(|cell| { + if cell.is_none_or(|t| t.elapsed() > Duration::from_secs(2)) { + swc_core::ecma::atoms::hstr::global_atom_store_gc(); + *cell = Some(Instant::now()); + } + }); + }); + + builder .wasm_bindgen_shim_url(worker_url.clone()) .build() .expect("Failed to build tokio runtime") diff --git a/packages/utoo-web/src/utoo/index.d.ts b/packages/utoo-web/src/utoo/index.d.ts index 81a6cbc3be..fcf9f2b335 100644 --- a/packages/utoo-web/src/utoo/index.d.ts +++ b/packages/utoo-web/src/utoo/index.d.ts @@ -194,8 +194,29 @@ export function workerCreated(worker_id: number): void; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { + readonly __wbg_project_free: (a: number, b: number) => void; + readonly project_build: () => any; + readonly project_cwd: () => [number, number]; + readonly project_deps: (a: number, b: number, c: number) => any; + readonly project_entrypointsSubscribe: (a: any) => any; + readonly project_gzip: (a: any) => any; + readonly project_hmrEvents: (a: number, b: number, c: any) => any; + readonly project_init: (a: number, b: number) => void; + readonly project_install: (a: number, b: number, c: number) => any; + readonly project_setCwd: (a: number, b: number) => void; + readonly project_sigMd5: (a: any) => any; + readonly project_updateInfoSubscribe: (a: number, b: any) => void; + readonly project_writeAllToDisk: (a: any) => void; readonly initLogFilter: (a: number, b: number) => void; readonly init_pack: () => void; + readonly ERR_ABORT: () => [number, number]; + readonly ERR_INVALID_STATE: () => [number, number]; + readonly ERR_KEY_ALREADY_EXISTS: () => [number, number]; + readonly ERR_NOT_ALLOWED: () => [number, number]; + readonly ERR_NOT_FOUND: () => [number, number]; + readonly ERR_NO_MODIFICATION_ALLOWED: () => [number, number]; + readonly ERR_QUOTA_EXCEEDED: () => [number, number]; + readonly ERR_TYPE_MISMATCH: () => [number, number]; readonly __wbg_direntry_free: (a: number, b: number) => void; readonly __wbg_fs_free: (a: number, b: number) => void; readonly __wbg_get_direntry_name: (a: number) => [number, number]; @@ -230,29 +251,8 @@ export interface InitOutput { readonly __wbg_roottask_free: (a: number, b: number) => void; readonly registerWorkerScheduler: (a: any, b: any) => void; readonly workerCreated: (a: number) => void; - readonly __wbg_project_free: (a: number, b: number) => void; - readonly project_build: () => any; - readonly project_cwd: () => [number, number]; - readonly project_deps: (a: number, b: number, c: number) => any; - readonly project_entrypointsSubscribe: (a: any) => any; - readonly project_gzip: (a: any) => any; - readonly project_hmrEvents: (a: number, b: number, c: any) => any; - readonly project_init: (a: number, b: number) => void; - readonly project_install: (a: number, b: number, c: number) => any; - readonly project_setCwd: (a: number, b: number) => void; - readonly project_sigMd5: (a: any) => any; - readonly project_updateInfoSubscribe: (a: number, b: any) => void; - readonly project_writeAllToDisk: (a: any) => void; readonly getWasmMemory: () => any; readonly getWasmModule: () => any; - readonly ERR_ABORT: () => [number, number]; - readonly ERR_INVALID_STATE: () => [number, number]; - readonly ERR_KEY_ALREADY_EXISTS: () => [number, number]; - readonly ERR_NOT_ALLOWED: () => [number, number]; - readonly ERR_NOT_FOUND: () => [number, number]; - readonly ERR_NO_MODIFICATION_ALLOWED: () => [number, number]; - readonly ERR_QUOTA_EXCEEDED: () => [number, number]; - readonly ERR_TYPE_MISMATCH: () => [number, number]; readonly rust_mi_get_default_heap: () => number; readonly rust_mi_get_thread_id: () => number; readonly rust_mi_set_default_heap: (a: number) => void; From 8688954e48916c39a3b3d353a0516f0c7a3f18a2 Mon Sep 17 00:00:00 2001 From: xusd320 Date: Mon, 9 Feb 2026 22:37:52 +0800 Subject: [PATCH 7/8] chore(utoo-web): simplify cjs loader logic --- packages/utoo-web/src/webpackLoaders/cjs.ts | 234 ++++++++------------ 1 file changed, 91 insertions(+), 143 deletions(-) diff --git a/packages/utoo-web/src/webpackLoaders/cjs.ts b/packages/utoo-web/src/webpackLoaders/cjs.ts index 213cf16608..30848fdc6a 100644 --- a/packages/utoo-web/src/webpackLoaders/cjs.ts +++ b/packages/utoo-web/src/webpackLoaders/cjs.ts @@ -21,6 +21,27 @@ const RESOLUTION_EXTENSIONS = [ "/index.json", ]; +const tryReadFile = (p: string): string | null => { + try { + return fs.readFileSync(p, "utf8") as string; + } catch { + return null; + } +}; + +const resolveWithExtensions = ( + p: string, + skipIndexJsIfJs = false, +): { code: string; id: string } | null => { + for (const ext of RESOLUTION_EXTENSIONS) { + if (skipIndexJsIfJs && ext === "/index.js" && p.endsWith(".js")) continue; + const candidate = p + ext; + const code = tryReadFile(candidate); + if (code !== null) return { code, id: candidate }; + } + return null; +}; + const executeModule = ( moduleCode: string, moduleId: string, @@ -33,8 +54,8 @@ const executeModule = ( } const context = path.dirname(moduleId); - let finalExports = {}; + const moduleRequire = (childId: string) => loadModule(childId, context, importMaps, entrypoint); moduleRequire.resolve = (request: string) => request; @@ -44,7 +65,6 @@ const executeModule = ( installedModules[moduleId] = module; } - // Hack for entrypoint if (moduleId === entrypoint) { moduleCode = "self.Buffer = require('buffer').Buffer;" + moduleCode; } @@ -84,14 +104,14 @@ const executeModule = ( const originalWarn = console.warn; console.warn = (...args: any[]) => { const msg = args[0]?.toString() || ""; - if (msg.includes("(SystemJS Error#W3")) { - return; + if (!msg.includes("(SystemJS Error#W3")) { + originalWarn.apply(console, args); } - originalWarn.apply(console, args); }; + try { System.set(moduleId, { default: finalExports }); - } catch (e) { + } catch { // ignore } finally { console.warn = originalWarn; @@ -108,80 +128,52 @@ const loadModule = ( const cacheKey = `${context}:${id}`; if (resolutionCache[cacheKey]) { const cachedId = resolutionCache[cacheKey]; - if (installedModules[cachedId]) { - return installedModules[cachedId].exports; - } - // Fast path: if we know the resolved path, try to load it directly - try { - const moduleCode = fs.readFileSync(cachedId, "utf8") as string; - return executeModule(moduleCode, cachedId, id, importMaps, entrypoint); - } catch { - // ignore + if (installedModules[cachedId]) return installedModules[cachedId].exports; + + const code = tryReadFile(cachedId); + if (code !== null) { + return executeModule(code, cachedId, id, importMaps, entrypoint); } } // 1. Resolve - let resolvedId = id; - if (id.startsWith(".")) { - resolvedId = path.join(context, id); - } + let resolvedId = id.startsWith(".") ? path.join(context, id) : id; // 2. Check Cache (SystemJS) - let dependency = System.get(resolvedId); - if (dependency) return dependency.default; - - if (id !== resolvedId) { - dependency = System.get(id); - if (dependency) return dependency.default; - } + const sysDef = + System.get(resolvedId) || (id !== resolvedId && System.get(id)); + if (sysDef) return sysDef.default; // 3. Check Node Polyfills - if (id in nodePolyFills) { - // @ts-ignore - return nodePolyFills[id]; - } - if (resolvedId in nodePolyFills) { - // @ts-ignore - return nodePolyFills[resolvedId]; - } + if (id in nodePolyFills) return (nodePolyFills as any)[id]; + if (resolvedId in nodePolyFills) return (nodePolyFills as any)[resolvedId]; // 4. Check importMaps & FS let moduleCode = importMaps[resolvedId] || importMaps[id]; let moduleId = importMaps[resolvedId] ? resolvedId : id; if (!moduleCode) { - // Try matching by stripping potential random prefix from absolute path - let longestKey = ""; - let longestSanitizedKey = ""; - + let longestMatch: { key: string; sanitized: string } | null = null; for (const key in importMaps) { - // Normalize key for comparison (ignore leading ./ or /) - let sanitizedKey = key; - if (sanitizedKey.startsWith("./")) sanitizedKey = sanitizedKey.slice(2); - if (sanitizedKey.startsWith("/")) sanitizedKey = sanitizedKey.slice(1); - if (!sanitizedKey) continue; - - const isSuffixOfId = id.startsWith("/") && id.endsWith(sanitizedKey); - const isSuffixOfResolvedId = - resolvedId.startsWith("/") && resolvedId.endsWith(sanitizedKey); - - if (isSuffixOfId || isSuffixOfResolvedId) { - const checkId = isSuffixOfId ? id : resolvedId; - const prefixEnd = checkId.length - sanitizedKey.length; - // Ensure it's a true path segment match - if (prefixEnd === 0 || checkId[prefixEnd - 1] === "/") { - if (key.length > longestKey.length) { - longestKey = key; - longestSanitizedKey = sanitizedKey; - } - } + const sanitized = key.replace(/^\.?\//, ""); + if (!sanitized) continue; + + const isMatch = [id, resolvedId].some( + (p) => + p.startsWith("/") && + p.endsWith(sanitized) && + p[p.length - sanitized.length - 1] === "/", + ); + + if (isMatch && (!longestMatch || key.length > longestMatch.key.length)) { + longestMatch = { key, sanitized }; } } - if (longestKey) { - moduleCode = importMaps[longestKey]; - // Important: Use the absolute path as moduleId so that context is correctly absolute for relative requires + + if (longestMatch) { + moduleCode = importMaps[longestMatch.key]; moduleId = - id.startsWith("/") && id.endsWith(longestSanitizedKey) + id.startsWith("/") && id.endsWith(longestMatch.sanitized) ? id : resolvedId; } @@ -195,100 +187,63 @@ const loadModule = ( let currentDir = context; while (true) { if (path.basename(currentDir) !== "node_modules") { - const nodeModulesPath = path.join(currentDir, "node_modules"); - if (!searchPaths.includes(nodeModulesPath)) { - searchPaths.push(nodeModulesPath); - } + const nmPath = path.join(currentDir, "node_modules"); + if (!searchPaths.includes(nmPath)) searchPaths.push(nmPath); } const parent = path.dirname(currentDir); if (parent === currentDir) break; currentDir = parent; } - // Ensure cwd/node_modules is always included - const cwd = nodePolyFills.process.cwd(); - const cwdNodeModules = path.join(cwd, "node_modules"); - if (!searchPaths.includes(cwdNodeModules)) { - searchPaths.push(cwdNodeModules); - } + const cwdNm = path.join(nodePolyFills.process.cwd(), "node_modules"); + if (!searchPaths.includes(cwdNm)) searchPaths.push(cwdNm); searchPathsCache[context] = searchPaths; } for (const nodeModulesDir of searchPaths) { const nodeModulesPath = path.join(nodeModulesDir, id); + const pkgPath = path.join(nodeModulesPath, "package.json"); - // Check package.json first - const pkgJsonPath = path.join(nodeModulesPath, "package.json"); - let pkg; - try { - const content = fs.readFileSync(pkgJsonPath, "utf8") as string; - pkg = JSON.parse(content); - pkgJsonCache[pkgJsonPath] = pkg; - } catch (e) { - // ignore - } - - if (pkg) { - try { - const mainField = pkg.main; - if (mainField) { - const resolvedMain = path.resolve(nodeModulesPath, mainField); - for (const ext of RESOLUTION_EXTENSIONS) { - const candidate = resolvedMain + ext; - try { - const content = fs.readFileSync(candidate, "utf8") as string; - moduleCode = content; - resolvedId = candidate; - moduleId = candidate; - break; - } catch { - // ignore - } - } - } - } catch (e) { - // ignore + // Check package.json main + let pkgMain; + if (!pkgJsonCache[pkgPath]) { + const json = tryReadFile(pkgPath); + if (json) { + try { + pkgJsonCache[pkgPath] = JSON.parse(json); + } catch {} } } - - if (!moduleCode) { - for (const ext of RESOLUTION_EXTENSIONS) { - const p = nodeModulesPath + ext; - try { - const content = fs.readFileSync(p, "utf8") as string; - moduleCode = content; - resolvedId = p; - moduleId = p; - break; - } catch { - // ignore - } + if (pkgJsonCache[pkgPath]?.main) { + pkgMain = path.resolve(nodeModulesPath, pkgJsonCache[pkgPath].main); + const res = resolveWithExtensions(pkgMain); + if (res) { + moduleCode = res.code; + moduleId = resolvedId = res.id; + break; } } - if (moduleCode) break; + // Check direct file/directory + const res = resolveWithExtensions(nodeModulesPath); + if (res) { + moduleCode = res.code; + moduleId = resolvedId = res.id; + break; + } } } // Fallback: Try resolving absolute or relative path if (!moduleCode && (id.startsWith("/") || id.startsWith("."))) { - for (const ext of RESOLUTION_EXTENSIONS) { - if (ext === "/index.js" && id.endsWith(".js")) continue; - // Use resolvedId which handles both absolute paths and relative paths joined with context - const p = resolvedId + ext; - try { - moduleCode = fs.readFileSync(p, "utf8") as string; - resolvedId = p; - moduleId = p; - break; - } catch (e) { - // ignore - } + const res = resolveWithExtensions(resolvedId, id.endsWith(".js")); + if (res) { + moduleCode = res.code; + moduleId = resolvedId = res.id; } } if (moduleCode) { - const cacheKey = `${context}:${id}`; - resolutionCache[cacheKey] = moduleId; + resolutionCache[`${context}:${id}`] = moduleId; return executeModule(moduleCode, moduleId, id, importMaps, entrypoint); } @@ -298,9 +253,7 @@ const loadModule = ( `CWD: ${nodePolyFills.process.cwd()}. ` + `Sample ImportMaps keys: ${Object.keys(importMaps).slice(0, 5).join(", ")}`, ); - console.error(error); - throw error; }; @@ -308,11 +261,9 @@ export async function cjs( entrypoint: string, importMaps: Record, ) { - // Clear caches to avoid stale data across runs - for (const key in statCache) delete statCache[key]; - for (const key in pkgJsonCache) delete pkgJsonCache[key]; - for (const key in resolutionCache) delete resolutionCache[key]; - for (const key in searchPathsCache) delete searchPathsCache[key]; + [statCache, pkgJsonCache, resolutionCache, searchPathsCache].forEach((c) => { + for (const key in c) delete c[key]; + }); await Promise.all( Object.entries(importMaps).map(async ([k, v]) => { @@ -322,13 +273,11 @@ export async function cjs( if (response.ok) { importMaps[k] = await response.text(); } else { - console.error( - `Failed to fetch loader '${k}' from ${v}: ${response.status} ${response.statusText}`, - ); + console.error(`Failed to fetch loader '${k}': ${response.status}`); delete importMaps[k]; } } catch (error) { - console.error(`Error fetching loader '${k}' from ${v}:`, error); + console.error(`Error fetching loader '${k}':`, error); delete importMaps[k]; } } @@ -336,7 +285,6 @@ export async function cjs( ); // @ts-ignore - // a hack for loader-runner resolving self.__systemjs_require__ = (id: string) => loadModule(id, path.dirname(entrypoint), importMaps, entrypoint); // @ts-ignore From 09c8f978e8b60bd039e70ee5572f7cc15d4f1065 Mon Sep 17 00:00:00 2001 From: xusd320 Date: Tue, 10 Feb 2026 02:02:02 +0800 Subject: [PATCH 8/8] chore: update tokio-fs-ext to 0.7.7 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- next.js | 2 +- packages/utoo-web/src/utoo/index.d.ts | 30 +++++++++++++-------------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2557116392..31d066aa3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8312,9 +8312,9 @@ dependencies = [ [[package]] name = "tokio-fs-ext" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81c90b384ee8b63048b511a09468ee9ee1338042a94e4888105f67648b05ab2" +checksum = "912f3435ecb4330fed82d1b3a8cddf6354ea6472d40fadfb9063a3e9da1c6dec" dependencies = [ "bitflags 2.9.4", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index b82863d2c5..2996ff6075 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ serde_path_to_error = "0.1.16" simd-json = "0.17.0" thiserror = "1.0" tokio = { version = "1.47.1" } -tokio-fs-ext = "0.7.5" +tokio-fs-ext = "0.7.7" toml = "0.8" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] } diff --git a/next.js b/next.js index 7544553479..ae2d1afb23 160000 --- a/next.js +++ b/next.js @@ -1 +1 @@ -Subproject commit 7544553479dd776124ff6778ac030c3d30a320a1 +Subproject commit ae2d1afb231a599e0f6f2944528b0fd27991cc7d diff --git a/packages/utoo-web/src/utoo/index.d.ts b/packages/utoo-web/src/utoo/index.d.ts index fcf9f2b335..17ca4ac7d8 100644 --- a/packages/utoo-web/src/utoo/index.d.ts +++ b/packages/utoo-web/src/utoo/index.d.ts @@ -194,6 +194,21 @@ export function workerCreated(worker_id: number): void; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { + readonly __wbg_roottask_free: (a: number, b: number) => void; + readonly registerWorkerScheduler: (a: any, b: any) => void; + readonly workerCreated: (a: number) => void; + readonly getWasmMemory: () => any; + readonly getWasmModule: () => any; + readonly ERR_ABORT: () => [number, number]; + readonly ERR_INVALID_STATE: () => [number, number]; + readonly ERR_KEY_ALREADY_EXISTS: () => [number, number]; + readonly ERR_NOT_ALLOWED: () => [number, number]; + readonly ERR_NOT_FOUND: () => [number, number]; + readonly ERR_NO_MODIFICATION_ALLOWED: () => [number, number]; + readonly ERR_QUOTA_EXCEEDED: () => [number, number]; + readonly ERR_TYPE_MISMATCH: () => [number, number]; + readonly initLogFilter: (a: number, b: number) => void; + readonly init_pack: () => void; readonly __wbg_project_free: (a: number, b: number) => void; readonly project_build: () => any; readonly project_cwd: () => [number, number]; @@ -207,16 +222,6 @@ export interface InitOutput { readonly project_sigMd5: (a: any) => any; readonly project_updateInfoSubscribe: (a: number, b: any) => void; readonly project_writeAllToDisk: (a: any) => void; - readonly initLogFilter: (a: number, b: number) => void; - readonly init_pack: () => void; - readonly ERR_ABORT: () => [number, number]; - readonly ERR_INVALID_STATE: () => [number, number]; - readonly ERR_KEY_ALREADY_EXISTS: () => [number, number]; - readonly ERR_NOT_ALLOWED: () => [number, number]; - readonly ERR_NOT_FOUND: () => [number, number]; - readonly ERR_NO_MODIFICATION_ALLOWED: () => [number, number]; - readonly ERR_QUOTA_EXCEEDED: () => [number, number]; - readonly ERR_TYPE_MISMATCH: () => [number, number]; readonly __wbg_direntry_free: (a: number, b: number) => void; readonly __wbg_fs_free: (a: number, b: number) => void; readonly __wbg_get_direntry_name: (a: number) => [number, number]; @@ -248,11 +253,6 @@ export interface InitOutput { readonly fs_write: (a: number, b: number, c: any) => any; readonly fs_writeString: (a: number, b: number, c: number, d: number) => any; readonly fs_writeSync: (a: number, b: number, c: any) => [number, number]; - readonly __wbg_roottask_free: (a: number, b: number) => void; - readonly registerWorkerScheduler: (a: any, b: any) => void; - readonly workerCreated: (a: number) => void; - readonly getWasmMemory: () => any; - readonly getWasmModule: () => any; readonly rust_mi_get_default_heap: () => number; readonly rust_mi_get_thread_id: () => number; readonly rust_mi_set_default_heap: (a: number) => void;