From f56460fd1ddc28f3370e49228ab8ac693375fbbe Mon Sep 17 00:00:00 2001 From: lbb00 Date: Tue, 8 Sep 2026 11:37:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20=E5=8D=87=E7=BA=A7=20dimina=20subm?= =?UTF-8?q?odule=20=E4=B8=8E=E7=9B=B8=E5=85=B3=E4=BE=9D=E8=B5=96=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=8A=8A=20devtools=20e2e=20=E4=BB=8E=2045=20?= =?UTF-8?q?=E5=88=86=E9=92=9F=E5=8E=8B=E5=88=B0=2026=20=E5=88=86=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit submodule 从 d7f6f702 升到 a66e5b7b。这次升级挪了两棵目录树:example 从 dimina/fe/example 搬到 dimina/examples/miniprogram,service 从 dimina/fe/service 搬到 dimina/fe/packages/service,kit 这边所有指向旧路径的地方都跟着改了。 上游 #320 让 getFileSystemManager() 把每个原型方法都绑成了实例自己的属性。实例属性会盖住原型,我们原本替换"不支持的接口"的做法就失效了——小程序调用一个我们没实现的文件接口时,拿到的是上游的真实实现而不是我们的报错。改成同时遍历实例属性和原型属性。 依赖和 pnpm 版本一并升级。 e2e 提速做了两件事: 把五个 native-host 用例文件合成一个,共用一次 Electron 启动。断言逐条搬过来,26 条 expect 与合并前逐字相同,用例数不变。白屏用例结束时显式把设备型号复位,会改源文件的重新编译用例排在最后并在 afterAll 写回。 playwright 配置改成三个 project。native-host 单独一组、独占运行,跑完之后另外两组并行。这里不能只靠给 native-host 设 workers:1:那只挡住它自己不并行,另外两组照样和它抢资源,实测 home-button 的模拟器冷启动会超过 30 秒等待上限,而同一个文件串行只要 20.5 秒。总并发上限定在 2 也是实测的——6 和 3 都会把这台 16GB 机器的内存吃爆,被系统杀在跑完之前。 顺带修掉一个测试自己的问题:standalone-devtools-window-churn 遍历窗口找浮动调试窗时没有跳过已销毁的窗口,项目关闭再打开的间隙里对已销毁窗口调 getTitle() 会抛 "Object has been destroyed"。同一个文件遍历 webContents 时本来就有这个过滤,补齐窗口这一侧。 验证:devtools e2e 261 通过 1 跳过 0 失败(26.2 分钟,此前 44.8 分钟);electron-runtime e2e 73 通过;compiler 那 22 个没被 pnpm run test 串起来的 test:* 脚本手动逐个跑过,全部退出码 0;gate.sh 五步全绿。 Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 3 + .npmrc | 3 +- dimina | 2 +- package.json | 9 +- packages/compiler/package.json | 26 +- packages/compiler/scripts/build-compiler.js | 31 +- packages/compiler/scripts/build-types.js | 38 + .../compiler/scripts/check-wasm-alignment.js | 48 +- .../compiler/scripts/gen-bench-fixture.js | 2 +- .../scripts/resolve-installed-version.js | 38 +- .../scripts/snapshot-upstream-versions.js | 21 +- .../compiler/scripts/test-binary-outputs.js | 2 +- packages/compiler/scripts/test-decompose.js | 2 +- packages/compiler/scripts/test-idle-shrink.js | 2 +- .../compiler/scripts/test-lazy-toolchain.js | 20 +- packages/compiler/scripts/test-node.js | 2 +- .../compiler/scripts/test-pool-hardening.js | 2 +- packages/compiler/scripts/test-pool-node.js | 23 +- .../compiler/scripts/test-pool-scopehash.js | 2 +- .../scripts/test-pool-toolchain-death.js | 2 +- packages/compiler/scripts/test-realm-reuse.js | 2 +- .../test-stage-worker-message-order.js | 2 +- .../compiler/scripts/test-warm-default.js | 2 +- .../compiler/scripts/transitive-dep-pairs.js | 27 + packages/compiler/src/compile-core.js | 33 +- packages/compiler/src/pool-node.js | 12 +- packages/compiler/src/shims/module.js | 17 + packages/compiler/src/stage-worker-node.js | 4 +- packages/compiler/tsconfig.types.json | 11 +- .../compiler/upstream-lockfile-snapshot.json | 21 +- packages/devtools/e2e/dock-real-drag.spec.ts | 19 +- .../e2e/dock-resize-sync-regressions.spec.ts | 7 +- .../e2e/dock-resize-sync-regressions2.spec.ts | 5 +- .../devtools/e2e/dock-resize-sync.spec.ts | 7 +- .../devtools/e2e/dock-separator-drag.spec.ts | 5 +- .../devtools/e2e/dock-tab-reorder.spec.ts | 22 +- packages/devtools/e2e/helpers.ts | 75 ++ .../e2e/home-button-no-tabbar.spec.ts | 13 +- packages/devtools/e2e/minigame-launch.spec.ts | 8 +- ...ve-host-devtools-elements-navigate.spec.ts | 11 +- .../e2e/native-host-devtools-elements.spec.ts | 306 ----- ...ative-host-navigate-after-relaunch.spec.ts | 5 +- packages/devtools/e2e/native-host-nh1.spec.ts | 708 ++++++++++ .../e2e/native-host-thumbnail-capture.spec.ts | 229 ---- .../native-host-websocket-contract.spec.ts | 2 +- ...native-host-white-screen-reconcile.spec.ts | 149 -- .../e2e/native-host-wx-method.spec.ts | 187 --- .../e2e/project-list-category.spec.ts | 9 +- .../e2e/recompile-button-recompiles.spec.ts | 276 ---- .../standalone-devtools-window-churn.spec.ts | 11 +- packages/devtools/playwright.config.ts | 159 ++- .../projects/builtin-templates.test.ts | 2 +- .../src/simulator/service-apis/file/index.js | 25 +- .../simulator/service-apis/file/index.test.ts | 31 + .../service-apis/file/upstream-impl.js | 53 +- .../dimina-electron-runtime/build-assets.mjs | 26 +- .../e2e/electron-entry.js | 48 +- .../dimina-electron-runtime/e2e/helpers.ts | 35 + .../native-host-switchtab-rerender.spec.ts | 4 + packages/workbench/src/dimina-json-schemas.ts | 2 +- pnpm-lock.yaml | 1195 +++++++++-------- pnpm-workspace.yaml | 36 + scripts/gate.sh | 10 +- 63 files changed, 2217 insertions(+), 1872 deletions(-) create mode 100644 packages/compiler/scripts/build-types.js create mode 100644 packages/compiler/scripts/transitive-dep-pairs.js create mode 100644 packages/compiler/src/shims/module.js delete mode 100644 packages/devtools/e2e/native-host-devtools-elements.spec.ts create mode 100644 packages/devtools/e2e/native-host-nh1.spec.ts delete mode 100644 packages/devtools/e2e/native-host-thumbnail-capture.spec.ts delete mode 100644 packages/devtools/e2e/native-host-white-screen-reconcile.spec.ts delete mode 100644 packages/devtools/e2e/native-host-wx-method.spec.ts delete mode 100644 packages/devtools/e2e/recompile-button-recompiles.spec.ts diff --git a/.gitignore b/.gitignore index b86db38a..d670969a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ coverage-detail/ # scripts/patrol.sh 的只读巡检输出 patrol-reports/ + +# .agents/test_generate_claude.py 运行时产生 +.agents/__pycache__/ diff --git a/.npmrc b/.npmrc index bf2e7648..7539d0ef 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -shamefully-hoist=true +# pnpm 11+ only reads auth/registry settings from .npmrc. Everything else +# (shamefullyHoist included) lives in pnpm-workspace.yaml now. diff --git a/dimina b/dimina index d7f6f702..a66e5b7b 160000 --- a/dimina +++ b/dimina @@ -1 +1 @@ -Subproject commit d7f6f702b19b79b118552af34d9f910bfe435e5a +Subproject commit a66e5b7b4a1b207950477610d56d799a6c9213e6 diff --git a/package.json b/package.json index 6933820e..277d69c0 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,5 @@ "typescript": "5.9.2", "typescript-eslint": "^8.58.2" }, - "pnpm": { - "overrides": { - "html-encoding-sniffer": "4.0.0", - "jsdom": "26.1.0", - "minimist@<1.2.8": "^1.2.8" - } - }, - "packageManager": "pnpm@9.15.9" + "packageManager": "pnpm@12.3.4" } diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 41e9fab6..d00893d8 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -65,7 +65,7 @@ "snapshot:upstream-versions": "node scripts/snapshot-upstream-versions.js", "build:node": "node scripts/build-compiler.js node", "build:browser": "node scripts/build-compiler.js browser", - "build:types": "tsc -p tsconfig.types.json", + "build:types": "node scripts/build-types.js", "build": "pnpm run build:node && pnpm run build:browser && pnpm run build:types", "check-types": "pnpm run build:types && node scripts/test-types.js", "test:node": "node scripts/build-compiler.js node && node --import ./scripts/register-kit.js scripts/test-node.js", @@ -100,32 +100,32 @@ }, "dependencies": { "@babel/parser": "^7.29.7", - "@vue/compiler-sfc": "^3.5.41", + "@vue/compiler-sfc": "^3.5.42", "assert": "^2.1.0", - "autoprefixer": "^10.5.3", + "autoprefixer": "^10.5.4", "buffer": "^6.0.3", "cheerio": "^1.2.0", - "cssnano": "8.0.6", - "esbuild": "^0.28.1", + "cssnano": "9.0.3", + "esbuild": "^0.28.2", "estree-walker": "^3.0.3", "events": "^3.3.0", "htmlparser2": "^12.0.0", - "less": "^4.9.0", + "less": "^4.9.1", "magic-string": "1.2.3", "memfs": "^4.57.8", - "oxc-parser": "^0.145.0", + "oxc-parser": "^0.148.0", "oxc-walker": "^1.1.1", "path-browserify": "^1.0.1", - "postcss": "^8.5.26", - "postcss-selector-parser": "^7.1.5", - "sass": "1.102.0", + "postcss": "^8.5.28", + "postcss-selector-parser": "^7.1.6", + "sass": "1.104.0", "source-map-js": "^1.2.1", "stream-browserify": "^3.0.0", "util": "^0.12.5" }, "peerDependencies": { - "@oxc-parser/binding-wasm32-wasi": "^0.145.0", - "esbuild-wasm": "^0.28.1" + "@oxc-parser/binding-wasm32-wasi": "^0.148.0", + "esbuild-wasm": "^0.28.2" }, "peerDependenciesMeta": { "@oxc-parser/binding-wasm32-wasi": { @@ -136,7 +136,7 @@ } }, "devDependencies": { - "esbuild-wasm": "^0.28.1", + "esbuild-wasm": "^0.28.2", "typescript": "5.9.2" } } diff --git a/packages/compiler/scripts/build-compiler.js b/packages/compiler/scripts/build-compiler.js index 705ab119..66c90079 100644 --- a/packages/compiler/scripts/build-compiler.js +++ b/packages/compiler/scripts/build-compiler.js @@ -38,9 +38,29 @@ const exportAppend = { name: 'export-append', setup(build) { const appends = { - 'logic-compiler.js': '\nexport { writeCompileRes }\nexport function __resetLogicState() { processedModules.clear() }\nexport function __setEnableSourcemap(v) { enableSourcemap = !!v }\n', - 'style-compiler.js': '\nexport function __resetStyleState() { compileRes.clear() }\n', - 'view-compiler.js': '\nexport function __resetViewState() { compileResCache.clear(); wxsModuleRegistry.clear(); wxsFilePathMap.clear() }\nexport function __setEnableSourcemap(v) { enableSourcemap = !!v }\n', + // `sourcemapTargetPath` is dmcc's own worker-message state (set from + // `sourcemapTargetPath: targetPath` in the message that spawns a build) — + // this driver never sends that message, so writeCompileRes's sourcemap + // branch would dereference it as null. The driver (pool-node.js's runAttempt) + // now threads the caller-known FINAL output dir through runStage's own opts; + // `targetPath` here is that value. Falling back to getTargetPath() (the + // staging dir) when no explicit value is given mirrors dmcc's own + // `targetPath || getTargetPath()`, and runs on every enable/disable — sourcemap + // off always clears it to null — so it can never carry over stale state from a + // prior build sharing this pooled realm. + 'logic-compiler.js': '\nexport { writeCompileRes }\nexport function __resetLogicState() { processedModules.clear() }\nexport function __setEnableSourcemap(v, targetPath) { enableSourcemap = !!v; sourcemapTargetPath = enableSourcemap ? (targetPath || getTargetPath()) : null }\n', + // loadSass is otherwise only reachable through the `.scss`/`.sass` branch inside + // compileSS — a style compile with no such file never calls it. Exposing it lets + // compile-core.js's preloadStage warm dart-sass at worker spawn time, by stage + // identity alone, instead of waiting for a matching file to show up. + 'style-compiler.js': '\nexport function __resetStyleState() { compileRes.clear() }\nexport function __preloadStyleToolchain() { return loadSass() }\n', + // templateRenderCache/optionalChainingCache are dmcc's own view-compile + // perf caches (named-template render output, optional-chaining rewrites); + // dmcc's own worker-message handler clears them alongside the older + // caches at end-of-task, but this driver calls compileML directly and + // never goes through that handler, so they must be cleared here too or + // a pooled worker realm leaks them across unrelated builds indefinitely. + 'view-compiler.js': '\nexport function __resetViewState() { compileResCache.clear(); wxsModuleRegistry.clear(); wxsFilePathMap.clear(); templateRenderCache.clear(); optionalChainingCache.clear() }\nexport function __setEnableSourcemap(v) { enableSourcemap = !!v }\n', 'utils.js': '\nexport function __resetAssets() { for (const k of Object.keys(assetsMap)) delete assetsMap[k] }\n', } build.onLoad({ filter: /(core[\\/](logic|style|view)-compiler|common[\\/]utils)\.js$/ }, async (args) => { @@ -126,6 +146,11 @@ if (MODE === 'node') { 'crypto': shim('crypto.js'), 'node:async_hooks': shim('async_hooks.js'), 'async_hooks': shim('async_hooks.js'), + // cssnano 9.x uses createRequire(import.meta.url) to dynamically load a + // user-supplied preset/plugin/configFile by string; kit's fixed cssnano() + // call never exercises that path (see src/shims/module.js). + 'node:module': shim('module.js'), + 'module': shim('module.js'), 'node:path': 'path-browserify', 'path': 'path-browserify', 'node:events': 'events', diff --git a/packages/compiler/scripts/build-types.js b/packages/compiler/scripts/build-types.js new file mode 100644 index 00000000..97ee9b08 --- /dev/null +++ b/packages/compiler/scripts/build-types.js @@ -0,0 +1,38 @@ +// Wraps `tsc -p tsconfig.types.json` because that config's rootDir has to +// cover both packages/compiler/src AND the dimina/fe submodule source it +// imports by relative path (see the comment in tsconfig.types.json) — so tsc +// necessarily also emits declarations for the submodule branch into the +// scratch outDir. This script keeps only the packages/compiler/src branch, +// flattened to the same dist/types/.d.ts layout package.json's +// `exports[*].types` already points at, and discards the rest. +import { createRequire } from 'node:module' +import { spawnSync } from 'node:child_process' +import { cpSync, existsSync, rmSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const require = createRequire(import.meta.url) +const pkgRoot = path.dirname(fileURLToPath(new URL('../package.json', import.meta.url))) +const rawDir = path.join(pkgRoot, 'dist/types/.raw') +const kitRawDir = path.join(rawDir, 'packages/compiler/src') +const outDir = path.join(pkgRoot, 'dist/types') + +rmSync(rawDir, { recursive: true, force: true }) + +const tscBin = path.join(path.dirname(require.resolve('typescript/package.json')), 'bin', 'tsc') +const tsc = spawnSync(process.execPath, [tscBin, '-p', 'tsconfig.types.json', '--pretty', 'false'], { + cwd: pkgRoot, + encoding: 'utf8', +}) +if (tsc.status !== 0) { + console.error(tsc.stdout || '') + console.error(tsc.stderr || '') + process.exit(tsc.status ?? 1) +} + +if (!existsSync(kitRawDir)) { + console.error(`build-types: expected tsc output at ${path.relative(pkgRoot, kitRawDir)}, found nothing`) + process.exit(1) +} +cpSync(kitRawDir, outDir, { recursive: true }) +rmSync(rawDir, { recursive: true, force: true }) diff --git a/packages/compiler/scripts/check-wasm-alignment.js b/packages/compiler/scripts/check-wasm-alignment.js index bfb262be..10a760ec 100644 --- a/packages/compiler/scripts/check-wasm-alignment.js +++ b/packages/compiler/scripts/check-wasm-alignment.js @@ -2,7 +2,8 @@ import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { execFileSync } from 'node:child_process' import path from 'node:path' -import { resolveInstalledVersion } from './resolve-installed-version.js' +import { resolveInstalledVersion, resolveTransitiveVersion } from './resolve-installed-version.js' +import { TRANSITIVE_DEP_PATHS } from './transitive-dep-pairs.js' function printUsage() { console.log( @@ -126,6 +127,51 @@ for (const [nativeName, wasmName] of wasmPairs) { } } +// Neither side declares these — kit and dmcc each install their own +// `autoprefixer`, which independently pulls in `browserslist`/`caniuse-lite` +// (and `caniuse-lite` again, on a SEPARATE edge, via `browserslist`'s own +// dependency on it). A patch-level caniuse-lite drift alone can flip which +// vendor prefix `overrideBrowserslist: ['cover 99.5%']` resolves to, so this +// has to be checked even though it's invisible to the kitDeps loop above +// (kit's own package.json never lists these names directly). Resolved by +// walking each full path from `autoprefixer`'s own install dir +// (resolveTransitiveVersion), not kit's package root — pnpm's isolated +// node_modules means resolving straight from the root can land in an +// unrelated hoisted copy and misreport it as aligned. +for (const depPath of TRANSITIVE_DEP_PATHS) { + const key = depPath.join('>') + const upstreamVersion = snapshot.transitiveVersions?.[key] + if (!upstreamVersion) { + unresolved.push(` ${key}: snapshot has no recorded version — run scripts/snapshot-upstream-versions.js`) + continue + } + + const kitVersion = await resolveTransitiveVersion(kitPkgPath, depPath) + if (!kitVersion) { + unresolved.push(` ${key}: kit=unresolved upstream(snapshot)=${upstreamVersion}`) + continue + } + if (kitVersion !== upstreamVersion) { + // pnpm@9.15.9 (the version this repo is pinned to) only reliably honors a + // 2-segment override key. A literal join of a 3+-segment path (e.g. + // "autoprefixer>browserslist>caniuse-lite") either throws + // ERR_PNPM_INVALID_SELECTOR (unversioned) or silently no-ops (version- + // qualified on every segment) — the only form that actually takes effect + // is a 2-segment key with a VERSION-QUALIFIED PARENT, anchored on the + // second-to-last package in the chain (e.g. "browserslist@4.28.7>caniuse-lite"). + // The suggested key below is built that way so copy-pasting it works. + const parentPathKey = depPath.slice(0, -1).join('>') + const parentVersion = snapshot.transitiveVersions?.[parentPathKey] + const parentSpec = depPath.length <= 2 ? depPath[depPath.length - 2] : `${depPath[depPath.length - 2]}@${parentVersion ?? ''}` + const suggestedKey = `${parentSpec}>${depPath[depPath.length - 1]}` + mismatches.push( + ` ${key}: kit resolves ${kitVersion}, upstream snapshot resolves ${upstreamVersion} — ` + + 'pin the kit-side version via pnpm.overrides in the root package.json ' + + `("${suggestedKey}": "${upstreamVersion}"), then pnpm install.`, + ) + } +} + if (unresolved.length > 0) { console.error( 'check-wasm-alignment: could not resolve installed versions for some shared deps — ' + diff --git a/packages/compiler/scripts/gen-bench-fixture.js b/packages/compiler/scripts/gen-bench-fixture.js index 10cdddb8..55c7ffdc 100644 --- a/packages/compiler/scripts/gen-bench-fixture.js +++ b/packages/compiler/scripts/gen-bench-fixture.js @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url' import path from 'node:path' const EXAMPLE = process.argv[3] || 'base' -const APP = fileURLToPath(new URL(`../../../dimina/fe/example/${EXAMPLE}`, import.meta.url)) +const APP = fileURLToPath(new URL(`../../../dimina/examples/miniprogram/${EXAMPLE}`, import.meta.url)) const TEXT = new Set(['.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less', '.scss', '.sass', '.wxs', '.dds', '.css']) const out = {} ;(function rd(d, b) { diff --git a/packages/compiler/scripts/resolve-installed-version.js b/packages/compiler/scripts/resolve-installed-version.js index e9699584..ff55a3db 100644 --- a/packages/compiler/scripts/resolve-installed-version.js +++ b/packages/compiler/scripts/resolve-installed-version.js @@ -29,7 +29,11 @@ async function exists(p) { // in an unrelated hoisted node_modules — "resolved" would then misreport an // unrelated install as the answer. Reject any resolution that doesn't land // under requiredPrefix instead of treating it as valid. -export async function resolveInstalledVersion(anchorPackageJsonPath, depName, requiredPrefix) { +// Resolves `depName` as `anchorPackageJsonPath`'s own directory would resolve +// it, and returns the directory of ITS package.json (not the version) — the +// shared primitive both `resolveInstalledVersion` and +// `resolveTransitiveVersion` walk from. +async function resolvePackageDir(anchorPackageJsonPath, depName, requiredPrefix) { // Real ESM resolution (`import.meta.resolve`), not CJS `require.resolve`: // an ESM-only dep whose `exports` map lists only an `import` condition (no // `require`/`default` fallback) throws ERR_PACKAGE_PATH_NOT_EXPORTED under @@ -63,7 +67,7 @@ export async function resolveInstalledVersion(anchorPackageJsonPath, depName, re const candidate = path.join(dir, 'package.json') if (await exists(candidate)) { const pkg = await readJson(candidate) - if (pkg.name === depName) return pkg.version + if (pkg.name === depName) return dir } const parent = path.dirname(dir) if (parent === dir) break @@ -71,3 +75,33 @@ export async function resolveInstalledVersion(anchorPackageJsonPath, depName, re } return null } + +export async function resolveInstalledVersion(anchorPackageJsonPath, depName, requiredPrefix) { + const dir = await resolvePackageDir(anchorPackageJsonPath, depName, requiredPrefix) + if (!dir) return null + const pkg = await readJson(path.join(dir, 'package.json')) + return pkg.version +} + +// Resolves a TRANSITIVE dependency the way the LAST name in `pathNames` would +// actually be required — walking through each intermediate package's own +// resolved directory, not `anchorPackageJsonPath`'s. pnpm's isolated +// node_modules means a package only sees its own declared deps' symlinks — +// resolving e.g. `caniuse-lite` straight from a workspace-root anchor can +// silently land in a DIFFERENT physical install than the one the actual +// requiring package resolves, since node_modules isn't hoisted-flat. A +// single-hop path (e.g. `[autoprefixer, caniuse-lite]`) anchors the second +// resolve at `autoprefixer`'s own resolved package.json; a two-hop path +// (e.g. `[autoprefixer, browserslist, caniuse-lite]`) is needed too — +// `browserslist` has its own independent semver range on `caniuse-lite`, +// a THIRD distinct resolution edge pnpm can drift on its own even when +// `autoprefixer`'s direct `caniuse-lite` edge is pinned. +export async function resolveTransitiveVersion(anchorPackageJsonPath, pathNames, requiredPrefix) { + let anchor = anchorPackageJsonPath + for (let i = 0; i < pathNames.length - 1; i++) { + const dir = await resolvePackageDir(anchor, pathNames[i], requiredPrefix) + if (!dir) return null + anchor = path.join(dir, 'package.json') + } + return resolveInstalledVersion(anchor, pathNames[pathNames.length - 1], requiredPrefix) +} diff --git a/packages/compiler/scripts/snapshot-upstream-versions.js b/packages/compiler/scripts/snapshot-upstream-versions.js index 6e82837d..d77cfded 100644 --- a/packages/compiler/scripts/snapshot-upstream-versions.js +++ b/packages/compiler/scripts/snapshot-upstream-versions.js @@ -2,7 +2,8 @@ import { readFile, writeFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { execFileSync } from 'node:child_process' import path from 'node:path' -import { resolveInstalledVersion } from './resolve-installed-version.js' +import { resolveInstalledVersion, resolveTransitiveVersion } from './resolve-installed-version.js' +import { TRANSITIVE_DEP_PATHS } from './transitive-dep-pairs.js' function printUsage() { console.log( @@ -75,6 +76,22 @@ for (const name of Object.keys(kitDeps)) { versions[name] = version } +// Transitive paths are resolved the same way regardless of whether the first +// name is one of the deps kit declares — a package can pull in a transitive +// dep worth tracking even when kit never lists it by name (not the case +// today, but the loop above is deliberately gated on `kitDeps` and this one +// intentionally is not). +const transitiveVersions = {} +for (const depPath of TRANSITIVE_DEP_PATHS) { + const key = depPath.join('>') + const version = await resolveTransitiveVersion(upstreamPkgPath, depPath, diminaFeRoot) + if (!version) { + unresolved.push(key) + continue + } + transitiveVersions[key] = version +} + if (unresolved.length > 0) { console.error( 'snapshot-upstream-versions: could not resolve upstream-installed versions for:\n' + @@ -84,6 +101,6 @@ if (unresolved.length > 0) { process.exit(2) } -const snapshot = { diminaCommit, versions } +const snapshot = { diminaCommit, versions, transitiveVersions } await writeFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`) console.log(`snapshot-upstream-versions: wrote ${Object.keys(versions).length} versions to ${path.relative(root, snapshotPath)} (dimina@${diminaCommit})`) diff --git a/packages/compiler/scripts/test-binary-outputs.js b/packages/compiler/scripts/test-binary-outputs.js index 157e690a..a1856c52 100644 --- a/packages/compiler/scripts/test-binary-outputs.js +++ b/packages/compiler/scripts/test-binary-outputs.js @@ -26,7 +26,7 @@ import { seedMemfs } from '../src/seed-memfs.js' await transform('const __warm = 1', {}) const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TEXT_EXT = new Set([ '.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less', diff --git a/packages/compiler/scripts/test-decompose.js b/packages/compiler/scripts/test-decompose.js index 882e19ba..fcc58732 100644 --- a/packages/compiler/scripts/test-decompose.js +++ b/packages/compiler/scripts/test-decompose.js @@ -11,7 +11,7 @@ import path from 'node:path' import { Volume, createFsFromVolume } from 'memfs' const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TEXT_EXT = new Set([ '.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less', diff --git a/packages/compiler/scripts/test-idle-shrink.js b/packages/compiler/scripts/test-idle-shrink.js index 892cce72..ebb343ae 100644 --- a/packages/compiler/scripts/test-idle-shrink.js +++ b/packages/compiler/scripts/test-idle-shrink.js @@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url' import { spawn } from 'node:child_process' const APP = process.env.TEST_PROJECT - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TMP = fileURLToPath(new URL('../.tmp-idle-shrink/', import.meta.url)) fs.rmSync(TMP, { recursive: true, force: true }) fs.mkdirSync(TMP, { recursive: true }) diff --git a/packages/compiler/scripts/test-lazy-toolchain.js b/packages/compiler/scripts/test-lazy-toolchain.js index 2577704d..5b1df457 100644 --- a/packages/compiler/scripts/test-lazy-toolchain.js +++ b/packages/compiler/scripts/test-lazy-toolchain.js @@ -8,10 +8,18 @@ // - the logic worker loads esbuild + oxc-parser, never sass/cssnano/less/ // @vue/compiler-sfc. // - the view worker loads @vue/compiler-sfc, never sass/cssnano/less. -// - the style worker loads sass, never esbuild. oxc-parser is NOT part of that -// boundary: env.js (imported by every stage) parses component scripts with -// `parseSync` to read their styleIsolation, so its native binding is present -// in every realm regardless of stage. +// - the style worker loads sass, never htmlparser2. htmlparser2 has two entry +// paths — view-compiler.js:10 imports it directly, and logic-compiler.js:8 +// reaches it through common/compatibility.js:1 — but style-compiler.js's +// top-level imports are only utils.js/env.js/sourcemap.js, none of which +// reach either, so the style realm can never load it. "never esbuild" is +// NOT a real boundary here: style-compiler.js:7 +// imports esbuild statically at its own top level — same as logic-compiler.js:7 and +// view-compiler.js:9 — so all three stages load it unconditionally regardless of +// which stage's toolchain a realm actually warms. oxc-parser is NOT part of the +// boundary either, for a related but distinct reason: env.js (imported by every +// stage) parses component scripts with `parseSync` to read their styleIsolation, so +// its native binding is present in every realm regardless of stage. // // The probe reads each realm's CJS require cache, so only packages with a CJS // footprint are observable: cheerio resolves pure-ESM and never appears there @@ -31,7 +39,7 @@ import { fileURLToPath } from 'node:url' import { createRequire } from 'node:module' const APP = process.env.TEST_PROJECT - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TMP = fileURLToPath(new URL('../.tmp-lazy-toolchain/', import.meta.url)) fs.rmSync(TMP, { recursive: true, force: true }) fs.mkdirSync(TMP, { recursive: true }) @@ -95,7 +103,7 @@ for (const name of ['sass', 'cssnano', 'less']) notLoaded('view', name) isLoaded('view', '@vue/compiler-sfc') isLoaded('style', 'sass') -notLoaded('style', 'esbuild') +notLoaded('style', 'htmlparser2') // --- condition 4: a freshly spawned pool preloads by stage identity, before any build --- const coldPool = makePool() diff --git a/packages/compiler/scripts/test-node.js b/packages/compiler/scripts/test-node.js index 34513142..f38d6147 100644 --- a/packages/compiler/scripts/test-node.js +++ b/packages/compiler/scripts/test-node.js @@ -9,7 +9,7 @@ import { Volume, createFsFromVolume } from 'memfs' // Default to the example shipped in dimina-kit's `dimina` submodule. const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TEXT_EXT = new Set([ '.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less', diff --git a/packages/compiler/scripts/test-pool-hardening.js b/packages/compiler/scripts/test-pool-hardening.js index 1996b124..234100d6 100644 --- a/packages/compiler/scripts/test-pool-hardening.js +++ b/packages/compiler/scripts/test-pool-hardening.js @@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url' import path from 'node:path' const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TMP = fileURLToPath(new URL('../.tmp-pool-hardening/', import.meta.url)) fs.rmSync(TMP, { recursive: true, force: true }) fs.mkdirSync(TMP, { recursive: true }) diff --git a/packages/compiler/scripts/test-pool-node.js b/packages/compiler/scripts/test-pool-node.js index a8a781a3..91560a8d 100644 --- a/packages/compiler/scripts/test-pool-node.js +++ b/packages/compiler/scripts/test-pool-node.js @@ -14,7 +14,7 @@ import { createHash } from 'node:crypto' import path from 'node:path' const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TMP = fileURLToPath(new URL('../.tmp-pool-node/', import.meta.url)) rmSync(TMP, { recursive: true, force: true }) @@ -103,6 +103,23 @@ console.log('[ours] resident pool build ×2 (warm)') const pool = createNodeCompilerPool() const appInfo1 = await pool.build(dir('ours1'), APP, true, { sourcemap: true }) const appInfo2 = await pool.build(dir('ours2'), APP, true, { sourcemap: true }) + +// --- sourcemapTargetPath must not leak across a warm realm's sourcemap on/off/on --- +// __setEnableSourcemap(v, targetPath) clears sourcemapTargetPath to null whenever v is +// false (build-compiler.js's exportAppend). A worker that skipped that clear — e.g. if +// a future edit made it conditional on v changing — would carry the LAST enabled build's +// target path into a later disabled build (harmless, since writeCompileRes never reads it +// when sourcemap is off) and, worse, into a later RE-enabled build pointed at a different +// output dir, rebasing `sources` against the wrong directory instead of its own. +console.log('[ours] sourcemap off then on again (warm realm, no dispose between)') +const appInfoNoMap = await pool.build(dir('ours-nomap'), APP, true, { sourcemap: false }) +const treeNoMap = readTree(path.join(dir('ours-nomap'), appInfoNoMap.appId)) +if (treeNoMap['main/logic.js.map']) fail('[nomap] logic.js.map emitted despite sourcemap:false') +const jsNoMap = (treeNoMap['main/logic.js'] || Buffer.alloc(0)).toString('utf8') +if (/sourceMappingURL/.test(jsNoMap)) fail('[nomap] logic.js still references a sourceMappingURL despite sourcemap:false') +else console.log('[nomap] ✅ sourcemap:false emits no .map and no sourceMappingURL reference') +const appInfo3 = await pool.build(dir('ours3'), APP, true, { sourcemap: true }) + await pool.dispose() // The whole return value must match dmcc's, `path` included — dmcc's mainPages[1] @@ -116,6 +133,10 @@ if (appInfo1.path !== appInfo2.path) fail('path not stable across warm rebuilds' checkAgainstDmcc(readTree(path.join(dir('ours1'), appInfo1.appId)), 'cold') checkAgainstDmcc(readTree(path.join(dir('ours2'), appInfo2.appId)), 'warm') +// Re-enabled after an intervening off build, into yet another output dir: this is the +// case a leaked/stale sourcemapTargetPath would break (sources rebased against ours1's +// or ours2's directory, or against the disabled build's, instead of ours3's own). +checkAgainstDmcc(readTree(path.join(dir('ours3'), appInfo3.appId)), 'warm-after-off') console.log(`[sourcemap] logic.js.map: ${refMap.sources.length} sources, sourcesContent present, matches dmcc ✅`) diff --git a/packages/compiler/scripts/test-pool-scopehash.js b/packages/compiler/scripts/test-pool-scopehash.js index 5c8dbdea..82d7c10d 100644 --- a/packages/compiler/scripts/test-pool-scopehash.js +++ b/packages/compiler/scripts/test-pool-scopehash.js @@ -20,7 +20,7 @@ import path from 'node:path' import { Volume, createFsFromVolume } from 'memfs' const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/vant', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/vant', import.meta.url)) const TEXT_EXT = new Set([ '.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less', diff --git a/packages/compiler/scripts/test-pool-toolchain-death.js b/packages/compiler/scripts/test-pool-toolchain-death.js index 2151380e..7fef5c1b 100644 --- a/packages/compiler/scripts/test-pool-toolchain-death.js +++ b/packages/compiler/scripts/test-pool-toolchain-death.js @@ -21,7 +21,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TMP = fileURLToPath(new URL('../.tmp-pool-toolchain-death/', import.meta.url)) fs.rmSync(TMP, { recursive: true, force: true }) fs.mkdirSync(TMP, { recursive: true }) diff --git a/packages/compiler/scripts/test-realm-reuse.js b/packages/compiler/scripts/test-realm-reuse.js index 2c7c8f30..4ee3bac7 100644 --- a/packages/compiler/scripts/test-realm-reuse.js +++ b/packages/compiler/scripts/test-realm-reuse.js @@ -11,7 +11,7 @@ import path from 'node:path' import { Volume, createFsFromVolume } from 'memfs' const APP = process.env.APP_DIR - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TEXT_EXT = new Set([ '.json', '.js', '.ts', '.wxml', '.ddml', '.wxss', '.ddss', '.less', diff --git a/packages/compiler/scripts/test-stage-worker-message-order.js b/packages/compiler/scripts/test-stage-worker-message-order.js index 4d25168d..60996885 100644 --- a/packages/compiler/scripts/test-stage-worker-message-order.js +++ b/packages/compiler/scripts/test-stage-worker-message-order.js @@ -10,7 +10,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const APP = process.env.TEST_PROJECT - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TMP = fileURLToPath(new URL('../.tmp-stage-worker-message-order/', import.meta.url)) fs.rmSync(TMP, { recursive: true, force: true }) fs.mkdirSync(TMP, { recursive: true }) diff --git a/packages/compiler/scripts/test-warm-default.js b/packages/compiler/scripts/test-warm-default.js index 58a2f89d..3f0f31b7 100644 --- a/packages/compiler/scripts/test-warm-default.js +++ b/packages/compiler/scripts/test-warm-default.js @@ -20,7 +20,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const APP = process.env.TEST_PROJECT - || fileURLToPath(new URL('../../../dimina/fe/example/base', import.meta.url)) + || fileURLToPath(new URL('../../../dimina/examples/miniprogram/base', import.meta.url)) const TMP = fileURLToPath(new URL('../.tmp-warm-default/', import.meta.url)) fs.rmSync(TMP, { recursive: true, force: true }) fs.mkdirSync(TMP, { recursive: true }) diff --git a/packages/compiler/scripts/transitive-dep-pairs.js b/packages/compiler/scripts/transitive-dep-pairs.js new file mode 100644 index 00000000..55287ec6 --- /dev/null +++ b/packages/compiler/scripts/transitive-dep-pairs.js @@ -0,0 +1,27 @@ +// Resolution PATHS for dependencies dmcc's own `autoprefixer` call pulls in +// transitively that neither kit nor dmcc declares directly — but whose +// resolved version DOES affect compiled output: +// `overrideBrowserslist: ['cover 99.5%']` is a usage-share query, and +// caniuse-lite ships new usage data every few days, so a patch-level drift +// alone can flip whether a property gets an extra vendor prefix. Because +// neither side declares these, the direct-dep loops in +// check-wasm-alignment.js/snapshot-upstream-versions.js never see them — +// they only ever surface as an unexplained CSS byte diff. +// +// Each entry is the full chain of package names to walk from the anchor, +// not just an immediate [parent, child] pair: `caniuse-lite` is required +// TWICE, independently, on two different edges — once directly by +// `autoprefixer` (feature-support lookups) and once by `browserslist` +// (usage-share stats, itself reached only via `autoprefixer`'s own +// `browserslist` dependency). pnpm resolves each edge on its own; pinning +// only the direct `autoprefixer>caniuse-lite` edge leaves +// `autoprefixer>browserslist>caniuse-lite` free to drift to whatever's +// newest in the store. Both edges are tracked here so both scripts resolve +// each `name` through the exact chain that requires it, not through kit's/ +// dmcc's package root — pnpm's isolated node_modules means those +// resolutions can land in different physical installs. +export const TRANSITIVE_DEP_PATHS = [ + ['autoprefixer', 'browserslist'], + ['autoprefixer', 'caniuse-lite'], + ['autoprefixer', 'browserslist', 'caniuse-lite'], +] diff --git a/packages/compiler/src/compile-core.js b/packages/compiler/src/compile-core.js index 54688a89..68ed0823 100644 --- a/packages/compiler/src/compile-core.js +++ b/packages/compiler/src/compile-core.js @@ -76,7 +76,19 @@ export function preloadStage(stage) { if (!STAGE_IMPORTERS[stage]) { return Promise.reject(new Error(`[compiler] unknown compile stage "${stage}" (expected ${STAGE_NAMES.join('/')})`)) } - return loadStageModule(stage).then(() => {}) + return loadStageModule(stage).then((mod) => { + // style-compiler.js's own toolchain (sass) is intentionally lazy — see loadSass + // there — so importing the module above only warms its STATIC deps (esbuild, + // @vue/compiler-sfc, postcss, autoprefixer), not sass. A stage worker calling + // preloadStage already knows its stage identity at spawn time, before any project + // ever picked a `.scss`/`.sass` file, so warm sass here too — "preload by stage + // identity alone", matching the module import's own eagerness for the other + // realms. Fire-and-forget: a failed sass load surfaces again, unchanged, the next + // time style-compiler.js's own loadSass() runs (same memoized loader) — this + // opportunistic warm-up can never mask or swallow a real toolchain failure, and a + // rejection here must never take the worker down. + if (stage === 'style') mod.__preloadStyleToolchain().catch(() => {}) + }) } function makeProgress() { @@ -380,15 +392,22 @@ export const STAGE_NAMES = Object.keys(STAGES) * worker message, which this path never sends), style takes it per call. Kept * separate from `compileStage` so the Node stage worker can drive it directly with * native fs, no shim. + * `sourcemapTargetPath` mirrors dmcc's own `sourcemapTargetPath: targetPath` worker + * message field: the FINAL published output dir (what `publishToDist` resolves, + * appId suffix included), used by logic's `writeCompileRes` to rebase `sources` + * against where the map actually ships, not the staging dir it compiles into. A + * caller that doesn't know the final dir yet (or doesn't care, sourcemap off) omits + * it and `__setEnableSourcemap` falls back to the staging dir, exactly like dmcc's + * own `targetPath || getTargetPath()`. * @param {'logic'|'view'|'style'} stage * @param {object} pages - * @param {{ sourcemap?: boolean }} [opts] + * @param {{ sourcemap?: boolean, sourcemapTargetPath?: string|null }} [opts] */ -export async function runStage(stage, pages, { sourcemap = false } = {}) { +export async function runStage(stage, pages, { sourcemap = false, sourcemapTargetPath = null } = {}) { const run = STAGES[stage] if (!run) throw new Error(`[compiler] unknown compile stage "${stage}" (expected ${STAGE_NAMES.join('/')})`) if (stage === 'logic' || stage === 'view') { - (await loadStageModule(stage)).__setEnableSourcemap(!!sourcemap) + (await loadStageModule(stage)).__setEnableSourcemap(!!sourcemap, sourcemapTargetPath) } await run(pages, makeProgress(), { sourcemap }) } @@ -457,14 +476,14 @@ export async function setupCompile({ fs, workPath = '/work', options = {}, npmSc * from `setupCompile`. Self-contained: it points the fs shim at `fs` and restores * the compiler env from the bundle, so it can run in a fresh worker realm. * Products are written into `fs`. - * @param {{ stage: 'logic'|'view'|'style', pages: object, storeInfo: object, fs: object, sourcemap?: boolean }} opts + * @param {{ stage: 'logic'|'view'|'style', pages: object, storeInfo: object, fs: object, sourcemap?: boolean, sourcemapTargetPath?: string|null }} opts */ -export async function compileStage({ stage, pages, storeInfo: bundle, fs, sourcemap = false } = {}) { +export async function compileStage({ stage, pages, storeInfo: bundle, fs, sourcemap = false, sourcemapTargetPath = null } = {}) { assertFs(fs) setFs(fs) try { resetStoreInfo(bundle) - await runStage(stage, pages, { sourcemap }) + await runStage(stage, pages, { sourcemap, sourcemapTargetPath }) } finally { resetFs() } diff --git a/packages/compiler/src/pool-node.js b/packages/compiler/src/pool-node.js index cc9f63e3..2d285185 100644 --- a/packages/compiler/src/pool-node.js +++ b/packages/compiler/src/pool-node.js @@ -176,13 +176,23 @@ export function createNodeCompilerPool({ } const { storeInfo, pages } = ctx + // dmcc's own worker message carries `sourcemapTargetPath: targetPath` — the FINAL + // published dir, not the staging one — so writeCompileRes rebases `sources` against + // where the map actually ships. This driver knows that dir up front (it's exactly + // what publishToDist below resolves outputDir/useAppIdDir into), so compute it once + // here and thread it through, instead of letting each worker fall back to its own + // staging dir (a tmpdir several levels deeper than the real publish target). + const sourcemapTargetPath = useAppIdDir + ? `${nodePath.resolve(process.cwd(), outputDir)}${nodePath.sep}${ctx.appId}` + : nodePath.resolve(process.cwd(), outputDir) + // 2) Fan out to the resident stage workers. They restore the same storeInfo (so their // getTargetPath() is the same staging dir) and write disjoint files concurrently. // Worker DEATH (timeout/crash/exit) arrives as a coded rejection; the worker's own // { type:'error' } replies (real compile errors) are normalized below. const results = await settleAll(workers.map((x) => x.slot.request( - { stage: x.stage, pages, storeInfo, sourcemap, wantHeartbeat: true }, + { stage: x.stage, pages, storeInfo, sourcemap, sourcemapTargetPath, wantHeartbeat: true }, { timeoutMs: sendTimeoutMs, description: `stage '${x.stage}' build` }, ).catch((err) => { if (err && err.code && !err.stage) err.stage = x.stage diff --git a/packages/compiler/src/shims/module.js b/packages/compiler/src/shims/module.js new file mode 100644 index 00000000..ec38b005 --- /dev/null +++ b/packages/compiler/src/shims/module.js @@ -0,0 +1,17 @@ +// node:module shim for the browser build. +// +// cssnano 9.x builds its own `require` via createRequire(import.meta.url) so it +// can dynamically load a user-supplied preset/plugin/configFile given as a +// string (see cssnano's `require(pluginDef)` calls). Kit's browser pipeline +// always calls cssnano() with no options — the fixed default preset — so that +// require is constructed but never invoked. Match esbuild's own "dynamic +// require of X is not supported" runtime stub instead of silently returning +// a real loader: any future caller that DOES pass a custom preset/plugin +// string fails loudly at the call site instead of pulling in a fake resolver. +export function createRequire() { + return function shimmedRequire(id) { + throw new Error(`Dynamic require of "${id}" is not supported in the browser compiler build`) + } +} + +export default { createRequire } diff --git a/packages/compiler/src/stage-worker-node.js b/packages/compiler/src/stage-worker-node.js index 59168e59..7b8d4fd8 100644 --- a/packages/compiler/src/stage-worker-node.js +++ b/packages/compiler/src/stage-worker-node.js @@ -71,7 +71,7 @@ async function handleMessage(msg) { parentPort.postMessage({ type: 'introspect', stage: declaredStage || null, loaded: loadedHeavyPackages() }) return } - const { stage, pages, storeInfo, sourcemap } = msg || {} + const { stage, pages, storeInfo, sourcemap, sourcemapTargetPath } = msg || {} // Opt-in per message (wantHeartbeat), mirroring stage-worker.js: only a supervising // pool that treats heartbeats as out-of-band liveness asks for them. First beat goes // out immediately so even a timeout shorter than the cadence sees life first. @@ -88,7 +88,7 @@ async function handleMessage(msg) { resetCompilerState() // Restore the env singletons (paths/config/targetPath) from the main thread's setup. resetStoreInfo(storeInfo) - await runStage(stage, pages, { sourcemap }) + await runStage(stage, pages, { sourcemap, sourcemapTargetPath }) parentPort.postMessage({ type: 'done', stage, appId: getAppId(), name: getAppName() }) } catch (error) { parentPort.postMessage({ diff --git a/packages/compiler/tsconfig.types.json b/packages/compiler/tsconfig.types.json index d5238bb6..7a4d6699 100644 --- a/packages/compiler/tsconfig.types.json +++ b/packages/compiler/tsconfig.types.json @@ -11,8 +11,15 @@ "target": "es2022", "lib": ["es2022", "dom"], "types": [], - "rootDir": "src", - "outDir": "dist/types" + // compile-core.js imports the dimina/fe submodule's source by relative + // path, so tsc's program includes it too — rootDir must cover both trees + // (repo root is their common ancestor) or those files' declarations + // escape outDir via the resulting "../.." relative path and land back + // inside the submodule's own source tree. scripts/build-types.js keeps + // only the packages/compiler/src branch from this scratch output and + // discards the rest; don't point outDir straight at dist/types here. + "rootDir": "../..", + "outDir": "dist/types/.raw" }, "include": [ "src/compile-core.js", diff --git a/packages/compiler/upstream-lockfile-snapshot.json b/packages/compiler/upstream-lockfile-snapshot.json index 541a0970..90355ca2 100644 --- a/packages/compiler/upstream-lockfile-snapshot.json +++ b/packages/compiler/upstream-lockfile-snapshot.json @@ -1,19 +1,24 @@ { - "diminaCommit": "d7f6f702b19b79b118552af34d9f910bfe435e5a", + "diminaCommit": "a66e5b7b4a1b207950477610d56d799a6c9213e6", "versions": { - "@vue/compiler-sfc": "3.5.41", + "@vue/compiler-sfc": "3.5.42", "autoprefixer": "10.5.4", "cheerio": "1.2.0", - "cssnano": "8.0.6", + "cssnano": "9.0.3", "esbuild": "0.28.2", "htmlparser2": "12.0.0", - "less": "4.9.0", + "less": "4.9.1", "magic-string": "1.2.3", - "oxc-parser": "0.145.0", + "oxc-parser": "0.148.0", "oxc-walker": "1.1.1", - "postcss": "8.5.26", - "postcss-selector-parser": "7.1.5", - "sass": "1.102.0", + "postcss": "8.5.28", + "postcss-selector-parser": "7.1.6", + "sass": "1.104.0", "source-map-js": "1.2.1" + }, + "transitiveVersions": { + "autoprefixer>browserslist": "4.28.7", + "autoprefixer>caniuse-lite": "1.0.30001806", + "autoprefixer>browserslist>caniuse-lite": "1.0.30001806" } } diff --git a/packages/devtools/e2e/dock-real-drag.spec.ts b/packages/devtools/e2e/dock-real-drag.spec.ts index df18be16..f364d8db 100644 --- a/packages/devtools/e2e/dock-real-drag.spec.ts +++ b/packages/devtools/e2e/dock-real-drag.spec.ts @@ -47,6 +47,7 @@ import { DEMO_APP_DIR, installConsoleCollector, readConsoleErrors, + isNonAppConsoleNoise, } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -395,20 +396,10 @@ test('native anchor: the simulator (draggable:false) cannot be torn out — a dr test('cumulative: no uncaught console errors across the whole drag sequence', async () => { const errors = await readConsoleErrors(electronApp) - // Filter out unrelated, pre-existing noise (DevTools/CDP, favicon, etc.) and - // surface only genuine page errors that a drag could plausibly cause. The - // 'editor' dock body lazily attaches the embedded A2 workbench WebContentsView; - // that third-party VS Code bundle emits its own startup warnings (extension-host - // iframe sandbox notes, permissions-policy) from its 127.0.0.1 COI origin — - // unrelated to the drag gesture, so filter by their workbench origin/source. - const relevant = errors.filter((e) => { - if (/favicon|DevTools|Autofill|net::ERR|Failed to load resource/i.test(e.message)) return false - if (/ExtensionHost|a2-spike|local-network-access|allow-scripts and allow-same-origin/i.test( - `${e.message} ${e.source} ${e.url}`, - )) return false - if (/json\.schemas is not a registered configuration|Unable to resolve nonexistent file '\/workspace'/i.test(e.message)) return false - return true - }) + // Keep only entries from our own renderer — the collector also sees the + // hosted A2 workbench and Chromium's DevTools front-end (see + // isNonAppConsoleNoise), neither of which a drag gesture can be blamed for. + const relevant = errors.filter((e) => !isNonAppConsoleNoise(e)) expect( relevant, `no uncaught errors during drag (saw: ${JSON.stringify(relevant.slice(0, 5))})`, diff --git a/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts b/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts index 94eca698..858f6b79 100644 --- a/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts +++ b/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts @@ -52,7 +52,7 @@ import { } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR, applyInEditorLayoutPreset } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -78,6 +78,11 @@ test.beforeAll(async () => { }) workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) + // `col-main` is the `inEditor` preset's column split; the default tree has no + // editor and therefore no such split (see applyInEditorLayoutPreset). Without + // this the spec passes only on a profile where an earlier run left the editor + // docked. + await applyInEditorLayoutPreset(workbench) }) test.afterAll(async () => { diff --git a/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts b/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts index 2fa34ab4..96d7dbf8 100644 --- a/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts +++ b/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts @@ -42,7 +42,7 @@ import { import path from 'path' import fs from 'fs' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR, applyInEditorLayoutPreset } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -73,6 +73,9 @@ test.beforeAll(async () => { }) workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) + // `col-main` is the `inEditor` preset's column split; the default tree has no + // editor and therefore no such split (see applyInEditorLayoutPreset). + await applyInEditorLayoutPreset(workbench) }) test.afterAll(async () => { diff --git a/packages/devtools/e2e/dock-resize-sync.spec.ts b/packages/devtools/e2e/dock-resize-sync.spec.ts index 950e5479..733b4d1f 100644 --- a/packages/devtools/e2e/dock-resize-sync.spec.ts +++ b/packages/devtools/e2e/dock-resize-sync.spec.ts @@ -43,7 +43,7 @@ import { } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR, applyInEditorLayoutPreset } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -69,6 +69,11 @@ test.beforeAll(async () => { }) workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) + // `col-main` is the `inEditor` preset's column split; the default tree has no + // editor and therefore no such split (see applyInEditorLayoutPreset). Without + // this the spec passes only on a profile where an earlier run left the editor + // docked. + await applyInEditorLayoutPreset(workbench) }) test.afterAll(async () => { diff --git a/packages/devtools/e2e/dock-separator-drag.spec.ts b/packages/devtools/e2e/dock-separator-drag.spec.ts index f8d49327..95b1c61a 100644 --- a/packages/devtools/e2e/dock-separator-drag.spec.ts +++ b/packages/devtools/e2e/dock-separator-drag.spec.ts @@ -19,7 +19,7 @@ import { import path from 'path' import { rmSync } from 'fs' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR, applyInEditorLayoutPreset } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -48,6 +48,9 @@ test.beforeAll(async () => { }) workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) await workbench.waitForSelector('[data-deck-resize-handle]', { timeout: 15000 }) + // The editor/debug separator only exists in the `inEditor` preset — the + // default tree ships without the editor at all (see applyInEditorLayoutPreset). + await applyInEditorLayoutPreset(workbench) }) test.afterAll(async () => { diff --git a/packages/devtools/e2e/dock-tab-reorder.spec.ts b/packages/devtools/e2e/dock-tab-reorder.spec.ts index 91804037..dc7b5857 100644 --- a/packages/devtools/e2e/dock-tab-reorder.spec.ts +++ b/packages/devtools/e2e/dock-tab-reorder.spec.ts @@ -28,6 +28,7 @@ import { DEMO_APP_DIR, installConsoleCollector, readConsoleErrors, + isNonAppConsoleNoise, } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -214,24 +215,11 @@ test('a debug tab cannot be torn OUT of its group into another region', async () const editorGroupAfter = groupsAfter.find((g) => g.panels.includes('editor'))! expect(editorGroupAfter.panels).not.toContain('storage') - // No genuine renderer errors from the reorder gestures (filter pre-existing - // DevTools/network noise, mirroring dock-real-drag.spec.ts). The editor body - // is now a live dock body whose anchor lazily attaches the embedded A2 - // workbench WebContentsView; that third-party VS Code bundle emits its own - // startup warnings/errors (extension-host sandbox notes, settings-schema and - // workspace-mirror chatter) which are unrelated to the drag gesture — filter - // them by their workbench origin/source markers. + // No genuine renderer errors from the reorder gestures. Keep only entries + // from our own renderer — the collector also sees the hosted A2 workbench and + // Chromium's DevTools front-end (see isNonAppConsoleNoise). const errors = await readConsoleErrors(electronApp) - const relevant = errors.filter((e) => { - if (/favicon|DevTools|Autofill|net::ERR|Failed to load resource/i.test(e.message)) return false - // A2 workbench WCV noise (served from a 127.0.0.1 COI origin; bundles named - // localExtensionHost / webWorkerExtensionHost / the workbench index chunk). - if (/ExtensionHost|a2-spike|local-network-access|allow-scripts and allow-same-origin/i.test( - `${e.message} ${e.source} ${e.url}`, - )) return false - if (/json\.schemas is not a registered configuration|Unable to resolve nonexistent file '\/workspace'/i.test(e.message)) return false - return true - }) + const relevant = errors.filter((e) => !isNonAppConsoleNoise(e)) expect(relevant, `unexpected renderer errors: ${JSON.stringify(relevant)}`).toEqual([]) }) diff --git a/packages/devtools/e2e/helpers.ts b/packages/devtools/e2e/helpers.ts index e94a8b5c..e149fb71 100644 --- a/packages/devtools/e2e/helpers.ts +++ b/packages/devtools/e2e/helpers.ts @@ -51,6 +51,32 @@ function resolveDemoAppDir(): string { /** Source demo mini-app for compilation tests. */ export const DEMO_APP_DIR = resolveDemoAppDir() +const MINI_GAME_CANDIDATES = [ + path.resolve(__dirname, '..', '..', '..', 'dimina', 'examples', 'miniprogram', 'air-battle'), +] + +let miniGameAppDir: string | undefined + +/** + * Real mini-game fixture from the dimina submodule — `compileType: "game"`, + * `game.json`/`game.js`, no `app.json`. + * + * Lives in the submodule, so a bump that moves the examples tree must fail + * naming the paths it looked at, not as an unexplained assertion failure in + * every mini-game spec. Resolved on call rather than at module load so that + * failure reaches only the specs that need the fixture, instead of every spec + * that imports this module. + */ +export function resolveMiniGameAppDir(): string { + if (miniGameAppDir) return miniGameAppDir + const found = MINI_GAME_CANDIDATES.find((dir) => fs.existsSync(path.join(dir, 'game.json'))) + if (!found) { + throw new Error(`No mini-game fixture found. Checked: ${MINI_GAME_CANDIDATES.join(', ')}`) + } + miniGameAppDir = found + return found +} + // ── URL markers ──────────────────────────────────────────────────────── /** @@ -506,6 +532,55 @@ export async function readConsoleErrors(electronApp: ElectronApplication): Promi }) as Promise } +/** + * Whether a collected console entry came from something other than our own + * renderer, and so cannot be evidence that a gesture under test broke the app. + * + * `installConsoleCollector` attaches to EVERY WebContents, including pages we + * only host: the embedded A2 workbench (a third-party VS Code bundle served + * from a 127.0.0.1 COI origin, which logs extension-host sandbox notes, + * settings-schema and workspace-mirror chatter at startup) and Chromium's own + * DevTools front-end (`devtools://`, which logs failed CDP requests such as + * `Network.loadNetworkResource … Frame not found` whenever an inspected frame + * goes away — routine during a drag that moves docked views). + * + * Single owner on purpose: every "no unexpected console errors" assertion has + * to agree on what counts as ours. Match against source and url as well as the + * message — a DevTools-front-end entry names `devtools://` only in the former + * two. + */ +export function isNonAppConsoleNoise(entry: ConsoleErrorEntry): boolean { + const whole = `${entry.message} ${entry.source} ${entry.url}` + if (/favicon|DevTools|devtools:\/\/|Autofill|net::ERR|Failed to load resource/i.test(whole)) return true + if (/ExtensionHost|a2-spike|local-network-access|allow-scripts and allow-same-origin/i.test(whole)) return true + if (/json\.schemas is not a registered configuration|Unable to resolve nonexistent file '\/workspace'/i.test(entry.message)) return true + return false +} + +/** + * Put the dock into the `inEditor` preset: simulator beside a `col-main` column + * holding `[editor over debug]`. + * + * The DEFAULT tree is NOT this shape. `buildDefaultDockTree` deliberately ships + * without the editor — the project window is a debugger first, so a fresh + * profile opens as `[g-sim | g-debug]` with no `col-main` and no horizontal + * separator. Any spec that targets the editor/debug split therefore has to + * establish that layout instead of assuming it, or it passes only on a profile + * where some earlier run happened to leave the editor docked. + * + * Drives the real toolbar control (the 调试器位置:在编辑器面板中 preset button), + * which rebuilds the tree through `buildPresetDockTree` — the same path a user + * takes. Idempotent: the button applies the preset even when already active. + */ +export async function applyInEditorLayoutPreset( + workbench: Page, + timeout = 15000, +): Promise { + await workbench.click('[data-testid="layout-toolbar-devtools-inEditor"]') + await workbench.waitForSelector('[data-deck-split="col-main"]', { timeout }) + await workbench.waitForSelector('[data-deck-panel-body="editor"]', { timeout }) +} + /** * Wait until the editor dock body is mounted in the main window. * diff --git a/packages/devtools/e2e/home-button-no-tabbar.spec.ts b/packages/devtools/e2e/home-button-no-tabbar.spec.ts index 3376d61f..2db6cb7b 100644 --- a/packages/devtools/e2e/home-button-no-tabbar.spec.ts +++ b/packages/devtools/e2e/home-button-no-tabbar.spec.ts @@ -27,6 +27,7 @@ import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' import path from 'path' import fs from 'fs' +import os from 'os' import { fileURLToPath } from 'url' import { WebSocket } from 'ws' import { @@ -47,7 +48,17 @@ import { DEFAULT_SCENE } from '../src/shared/constants' // flipping every other spec into native-host mode. const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'home-button-app') + +// openFixtureAt() drives real IPC (SaveCompileConfig -> ... -> fs.writeFileSync) +// against project.config.json, so pointing it at the committed fixture path +// directly would dirty the repo on every run. Per-run temp copy, mirroring +// resolveDemoAppDir() in helpers.ts — keyed by process.pid like this spec's +// own userDataDir below, since this file runs a single serial Electron +// instance rather than helpers.ts's multi-worker demo app. +const FIXTURE_SOURCE_DIR = path.resolve(__dirname, 'fixtures', 'home-button-app') +const FIXTURE_DIR = path.join(os.tmpdir(), 'dimina-kit-e2e', `home-button-no-tabbar-${process.pid}`) +fs.rmSync(FIXTURE_DIR, { recursive: true, force: true }) +fs.cpSync(FIXTURE_SOURCE_DIR, FIXTURE_DIR, { recursive: true }) const HOME_PAGE = 'pages/home/home' const INNER_PAGE = 'pages/inner/inner' diff --git a/packages/devtools/e2e/minigame-launch.spec.ts b/packages/devtools/e2e/minigame-launch.spec.ts index 20eed539..9c227c62 100644 --- a/packages/devtools/e2e/minigame-launch.spec.ts +++ b/packages/devtools/e2e/minigame-launch.spec.ts @@ -1,5 +1,3 @@ -import path from 'path' -import { fileURLToPath } from 'url' import { test, expect } from './fixtures' import { ipcInvoke, @@ -11,13 +9,11 @@ import { RENDER_GUEST_URL_MARKER, installConsoleCollector, readConsoleErrors, + resolveMiniGameAppDir, } from './helpers' import { ProjectsChannel, ProjectChannel } from '../src/shared/ipc-channels' -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -/** Real submodule fixture — `compileType: "game"`, `game.json`/`game.js`, no `app.json`. */ -const MINI_GAME_DIR = path.resolve(__dirname, '..', '..', '..', 'dimina', 'fe', 'example', 'air-battle') +const MINI_GAME_DIR = resolveMiniGameAppDir() interface OpenProjectResult { success: boolean diff --git a/packages/devtools/e2e/native-host-devtools-elements-navigate.spec.ts b/packages/devtools/e2e/native-host-devtools-elements-navigate.spec.ts index 18c29d1a..99db443a 100644 --- a/packages/devtools/e2e/native-host-devtools-elements-navigate.spec.ts +++ b/packages/devtools/e2e/native-host-devtools-elements-navigate.spec.ts @@ -39,7 +39,7 @@ const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'tabbar-app') const ENTRY_ROUTE = 'pages/home/home' const TARGET_ROUTE = 'pages/detail/detail' -interface AppHandle { app: ElectronApplication; win: PwPage; autoPort: number } +interface AppHandle { app: ElectronApplication; win: PwPage; workbench: PwPage; autoPort: number } interface WxmlNode { tagName?: string; children?: WxmlNode[] } async function bootApp(): Promise { @@ -81,7 +81,8 @@ async function bootApp(): Promise { 100, ) as number - await openProjectInUI(win, FIXTURE_DIR, { waitMs: 20000 }) + // A project opens into its OWN window; `win` stays the project-list window. + const workbench = await openProjectInUI(app, FIXTURE_DIR, { waitMs: 20000 }) await waitForSimulatorWebview(app) await pollUntil( @@ -96,18 +97,18 @@ async function bootApp(): Promise { ) await pollUntil( - () => ipcInvoke(win, SimulatorWxmlChannel.GetSnapshot).catch(() => null), + () => ipcInvoke(workbench, SimulatorWxmlChannel.GetSnapshot).catch(() => null), (t) => !!t && typeof (t as WxmlNode).tagName === 'string', 30000, 400, ) - return { app, win, autoPort } + return { app, win, workbench, autoPort } } async function shutdownApp(handle: AppHandle | undefined): Promise { if (!handle) return - await closeProject(handle.win).catch(() => {}) + await closeProject(handle.app).catch(() => {}) await handle.app.close().catch(() => {}) } diff --git a/packages/devtools/e2e/native-host-devtools-elements.spec.ts b/packages/devtools/e2e/native-host-devtools-elements.spec.ts deleted file mode 100644 index 52cc4c73..00000000 --- a/packages/devtools/e2e/native-host-devtools-elements.spec.ts +++ /dev/null @@ -1,306 +0,0 @@ -/** - * E2E: the right-panel Chrome DevTools Elements panel reflects the ACTIVE RENDER - * GUEST's live DOM tree (__frame__.html), NOT the service-host document - * (service.html / "Dimina Service Host"). - * - * In native-host mode (DIMINA_NATIVE_HOST=1) there are two WebContents layers: - * - Service Host (logic layer): URL contains `service.html`, title is "Dimina Service Host". - * - Render guest (view layer): URL contains `__frame__.html`. - * - * The right-panel DevTools front-end (devtools://) natively inspects the service - * host, but the `elements-forward` feature intercepts DOM./CSS./Overlay. commands - * from the front-end and re-routes them to the active render guest so Elements - * reflects the real page DOM. - * - * The two invariants guarded here: - * 1. The elements-forward hook is installed in the front-end realm: - * `globalThis.__diminaElementsHookInstalled === true` and - * `InspectorFrontendHost.__diminaElementsWrapped === true`. - * 2. A `DOM.getDocument` call dispatched via the wrapped - * `InspectorFrontendHost.sendMessageToBackend` returns a root document whose - * `documentURL` points to a `__frame__.html` URL, never to `service.html` / - * "Dimina Service Host". This is the definitive signal that Elements is wired - * to the render guest, not the service host. - * - * Regression target: when elements-forward degrades (hook absent, wrong routing, - * no active guest), Elements falls back to the service-host DOM - * (`Dimina Service Host`). The DOM.getDocument assertion catches - * that regression deterministically without screenshots. - */ -import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' -import path from 'path' -import fs from 'fs' -import { fileURLToPath } from 'url' -import { - openProjectInUI, - waitForSimulatorWebview, - closeProject, - ipcInvoke, - pollUntil, - evalInWebContentsByUrl, - RENDER_GUEST_URL_MARKER, - findMainWindow, -} from './helpers' -import { AutomationChannel, SimulatorWxmlChannel } from '../src/shared/ipc-channels' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'tabbar-app') - -interface AppHandle { app: ElectronApplication; win: PwPage; workbench: PwPage } - -interface WxmlNode { tagName?: string; children?: WxmlNode[] } - -async function bootApp(): Promise { - const appPath = path.resolve(__dirname, 'electron-entry.js') - const userDataDir = path.resolve( - process.env.DIMINA_DEVTOOLS_DATA_DIR - ?? path.resolve(__dirname, '..', 'node_modules', '.cache', 'devtools-e2e'), - 'userdata', - `nh-devtools-elements-${process.pid}`, - ) - fs.mkdirSync(userDataDir, { recursive: true }) - - const app = await _electron.launch({ - args: [appPath, 'auto', '--auto-port', '0', `--user-data-dir=${userDataDir}`], - env: { ...process.env, NODE_ENV: 'test', DIMINA_NATIVE_HOST: '1', DIMINA_E2E_USER_DATA_DIR: userDataDir }, - }) - - const win = await findMainWindow(app) - await win.waitForLoadState('domcontentloaded') - - await app.evaluate(async ({ BrowserWindow }) => { - const w = BrowserWindow.getAllWindows()[0] - if (w && !w.isVisible()) { - await new Promise((resolve) => { - w.once('show', resolve) - setTimeout(resolve, 5000) - }) - } - if (w) { - w.setPosition(-2000, -2000) - w.blur() - } - }) - - await pollUntil( - () => ipcInvoke(win, AutomationChannel.GetPort), - (val) => typeof val === 'number' && val > 0, - 10000, - 100, - ) - - const workbench = await openProjectInUI(app, FIXTURE_DIR, { waitMs: 20000 }) - await waitForSimulatorWebview(app) - - // Wait for at least one render guest (__frame__.html) to exist — the - // elements-forward hook requires an active render guest to route into. - await pollUntil( - () => app.evaluate(({ webContents }, marker) => - webContents.getAllWebContents().some( - (wc) => !wc.isDestroyed() && wc.getURL().includes(marker), - ), - RENDER_GUEST_URL_MARKER), - (present) => present === true, - 25000, - 300, - ) - - // Wait for the WXML tree to mount — this is the readiness signal the WXML panel - // uses and indicates the render guest is fully set up. - await pollUntil( - () => ipcInvoke(workbench, SimulatorWxmlChannel.GetSnapshot).catch(() => null), - (t) => !!t && typeof (t as WxmlNode).tagName === 'string', - 30000, - 400, - ) - - return { app, win, workbench } -} - -async function shutdownApp(handle: AppHandle | undefined): Promise { - if (!handle) return - await closeProject(handle.app).catch(() => {}) - await handle.app.close().catch(() => {}) -} - -/** - * Execute JavaScript in the DevTools front-end realm (devtools:// page). - * Returns null on any error (front-end not yet ready, wc gone, etc.). - */ -function evalInDevtools(app: ElectronApplication, expression: string): Promise { - return evalInWebContentsByUrl(app, 'devtools://', expression).catch(() => null) -} - -/** - * Send a DOM.getDocument command via the WRAPPED InspectorFrontendHost - * (the same path the Elements panel uses) and capture the first response with a - * matching id via window.DevToolsAPI.dispatchMessage interception. - * - * Returns the parsed response object, or null on timeout / error. - * - * The capture arms a one-shot `dispatchMessage` interceptor BEFORE sending the - * command, so the response never races. The interceptor is removed after the - * first matching reply (or on timeout). - * - * Timeout is expressed in milliseconds inside the front-end realm via a - * Promise.race so the call always settles. - */ -async function getDocumentViaFrontend( - app: ElectronApplication, - timeoutMs = 4000, -): Promise | null> { - return evalInWebContentsByUrl | null>( - app, - 'devtools://', - `(function() { - return new Promise(function(resolve) { - try { - var IFH = globalThis.InspectorFrontendHost; - var DTAPI = window.DevToolsAPI; - if (!IFH || typeof IFH.sendMessageToBackend !== 'function') { - return resolve(null); - } - if (!DTAPI || typeof DTAPI.dispatchMessage !== 'function') { - return resolve(null); - } - - var cmdId = Date.now(); - var settled = false; - var timer = setTimeout(function() { - if (settled) return; - settled = true; - window.DevToolsAPI.dispatchMessage = origDispatch; - resolve(null); - }, ${timeoutMs}); - - var origDispatch = DTAPI.dispatchMessage.bind(DTAPI); - - // Intercept dispatchMessage to capture the response with our cmdId. - DTAPI.dispatchMessage = function(messageStr) { - try { - var msg = (typeof messageStr === 'string') ? JSON.parse(messageStr) : messageStr; - if (msg && msg.id === cmdId && !settled) { - settled = true; - clearTimeout(timer); - window.DevToolsAPI.dispatchMessage = origDispatch; - origDispatch(messageStr); - resolve(msg); - return; - } - } catch(_) {} - origDispatch(messageStr); - }; - - // Send DOM.getDocument via the (possibly wrapped) sendMessageToBackend. - IFH.sendMessageToBackend(JSON.stringify({ - id: cmdId, - method: 'DOM.getDocument', - params: { depth: 2 } - })); - } catch(e) { - resolve(null); - } - }); - })()`, - ).catch(() => null) -} - -test.describe('native-host DevTools Elements panel reflects the render guest DOM', () => { - test.describe.configure({ mode: 'serial' }) - test.setTimeout(180_000) - - let handle: AppHandle | undefined - - test.beforeAll(async () => { - test.setTimeout(180_000) - handle = await bootApp() - }) - - test.afterAll(async () => { - await shutdownApp(handle) - }) - - test('elements-forward hook sentinels are installed in the DevTools front-end realm', async () => { - const { app } = handle! - - // The hook is installed by a polling interval (up to ~10s after dom-ready). - // Poll until both sentinels appear so we don't race the install timer. - const hookInstalled = await pollUntil( - () => evalInDevtools( - app, - '!!(globalThis.__diminaElementsHookInstalled === true && ' + - 'globalThis.InspectorFrontendHost && ' + - 'globalThis.InspectorFrontendHost.__diminaElementsWrapped === true)', - ), - (ok) => ok === true, - 30000, - 300, - ) - - expect( - hookInstalled, - 'DevTools front-end (devtools://) must have __diminaElementsHookInstalled=true ' + - 'and InspectorFrontendHost.__diminaElementsWrapped=true — the elements-forward hook is not installed', - ).toBe(true) - }) - - test('DOM.getDocument via the front-end hook returns the render guest document (__frame__.html), not the service host', async () => { - const { app } = handle! - - // Wait until the hook is installed before probing (may have already settled - // from the previous test, but this describe is serial so the guard is cheap). - await pollUntil( - () => evalInDevtools( - app, - '!!(globalThis.__diminaElementsHookInstalled)', - ), - (ok) => ok === true, - 15000, - 300, - ) - - // Retry the getDocument call: the drain interval is 150ms and the render guest - // may still be priming (DOM.enable in flight). A few retries handle that window. - let response: Record | null = null - const deadline = Date.now() + 30000 - while (Date.now() < deadline) { - response = await getDocumentViaFrontend(app, 4000) - if (response && response.result) break - await new Promise((r) => setTimeout(r, 500)) - } - - expect( - response, - 'DOM.getDocument sent via InspectorFrontendHost.sendMessageToBackend must receive a response within 30s', - ).toBeTruthy() - - const result = (response as { result?: { root?: { documentURL?: string; baseURL?: string } } })?.result - expect( - result, - `DOM.getDocument response should have a "result" field; got: ${JSON.stringify(response)}`, - ).toBeTruthy() - - const root = result?.root - expect( - root, - `DOM.getDocument result should contain a "root" node; got result=${JSON.stringify(result)}`, - ).toBeTruthy() - - // The discriminating assertion: the root document URL must point to the render - // guest (__frame__.html). When elements-forward is absent or broken, this URL - // resolves to the service host's own document (containing "service.html" or - // having title "Dimina Service Host"). - const docUrl: string = String(root?.documentURL ?? root?.baseURL ?? '') - - expect( - docUrl, - `DOM.getDocument root.documentURL should point to the render guest (__frame__.html) ` + - `but got: "${docUrl}". This means Elements is inspecting the service host instead of the render guest.`, - ).toContain(RENDER_GUEST_URL_MARKER) - - expect( - docUrl, - `DOM.getDocument root.documentURL must not point to the service host (service.html); got: "${docUrl}"`, - ).not.toContain('service.html') - }) -}) diff --git a/packages/devtools/e2e/native-host-navigate-after-relaunch.spec.ts b/packages/devtools/e2e/native-host-navigate-after-relaunch.spec.ts index a55aa619..833c37ba 100644 --- a/packages/devtools/e2e/native-host-navigate-after-relaunch.spec.ts +++ b/packages/devtools/e2e/native-host-navigate-after-relaunch.spec.ts @@ -183,7 +183,8 @@ test.describe('native-host reLaunch then real-click navigateTo e2e', () => { 100, ) as number - await openProjectInUI(mainWindow, FIXTURE_DIR, { waitMs: 20000 }) + // A project opens into its OWN window; `mainWindow` stays the project list. + await openProjectInUI(electronApp, FIXTURE_DIR, { waitMs: 20000 }) await waitForSimulatorWebview(electronApp) await pollUntil( @@ -206,7 +207,7 @@ test.describe('native-host reLaunch then real-click navigateTo e2e', () => { }) test.afterAll(async () => { - await closeProject(mainWindow).catch(() => {}) + await closeProject(electronApp).catch(() => {}) await electronApp?.close().catch(() => {}) }) diff --git a/packages/devtools/e2e/native-host-nh1.spec.ts b/packages/devtools/e2e/native-host-nh1.spec.ts new file mode 100644 index 00000000..7ac2fc60 --- /dev/null +++ b/packages/devtools/e2e/native-host-nh1.spec.ts @@ -0,0 +1,708 @@ +/** + * E2E (native-host merged group "NH-1"): captureThumbnail render-guest + * sizing, the white-screen reconcile regression guard, App.callWxMethod + * routing for a non-nav wx method, the DevTools Elements render-guest + * forwarding hook, and the popover 重新编译 recompile+relaunch contract. + * + * These five specs launch electron-entry.js with byte-identical + * DIMINA_NATIVE_HOST=1 args/env against the same tabbar-app fixture and each + * used to pay its own ~16.7s Electron cold boot for a single beforeAll. They + * are merged into one shared launch + one opened project. + * + * Ordering is load-bearing, not cosmetic: + * - `white-screen-reconcile` drives the device picker through 4 presets and + * resets to the default device at the end of its own test — later tests + * (and later spec files sharing this worker) must not inherit a + * non-default device. + * - `recompile-button-recompiles` must run LAST: its second test edits + * source on disk, changes the selected compile start page and forces a + * real recompile+relaunch onto pages/cart/cart. Any test after it would + * run against the wrong start page. The shared afterAll restores both + * mutated files (home.wxml, project.config.json) unconditionally so the + * on-disk fixture — and the compile-mode list that governs the start page + * for the next run — is back to its pre-suite state regardless of which + * tests ran or failed. + */ +import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' +import path from 'path' +import fs from 'fs' +import { fileURLToPath } from 'url' +import { WebSocket } from 'ws' +import { + openProjectInUI, + waitForSimulatorWebview, + closeProject, + ipcInvoke, + pollUntil, + evalInSimulator, + evalInWebContentsByUrl, + RENDER_GUEST_URL_MARKER, + findMainWindow, + selectDeviceInPicker, + openCompileModePopover, + clickCompileModeMenuRow, + clickCompileModeMenuAction, + fillCompileModeForm, + submitCompileModeForm, +} from './helpers' +import { + ProjectChannel, + AutomationChannel, + SimulatorWxmlChannel, + WorkbenchSettingsChannel, +} from '../src/shared/ipc-channels' +import { DEFAULT_DEVICE, DEVICE_NAMES, findDevice } from '@devicekit/devices' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'tabbar-app') +const HOME_WXML = path.join(FIXTURE_DIR, 'pages', 'home', 'home.wxml') +const PROJECT_CONFIG_PATH = path.join(FIXTURE_DIR, 'project.config.json') +const PROBE_TITLE = 'C5-PROBE' +const SENTINEL = 'RECOMPILE-BUTTON-SENTINEL' + +interface WxmlNode { tagName?: string; children?: WxmlNode[] } + +/** + * Parse the pixel dimensions from a PNG data URL by reading the IHDR chunk. + * PNG layout: 8-byte signature, then chunks. The first chunk is always IHDR: + * bytes 8–11: chunk length (4 bytes) + * bytes 12–15: "IHDR" + * bytes 16–19: width (big-endian uint32) + * bytes 20–23: height (big-endian uint32) + */ +function parsePngSize(dataUrl: string): { width: number; height: number } | null { + const prefix = 'data:image/png;base64,' + if (!dataUrl.startsWith(prefix)) return null + const buf = Buffer.from(dataUrl.slice(prefix.length), 'base64') + if (buf.length < 24) return null + return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) } +} + +// Cycle across presets with different widths AND heights so each switch changes +// the simulator column width → dock relayout → the geometry sentinel opens, +// reproducing the transient that used to detach the view. +const CYCLE = [ + DEFAULT_DEVICE, + findDevice(DEVICE_NAMES.iPhone_14_Pro), + findDevice(DEVICE_NAMES.iPhone_16_Pro), + findDevice(DEVICE_NAMES.iPhone_SE) ?? DEFAULT_DEVICE, +].filter(Boolean) as { name: string }[] + +let electronApp: ElectronApplication +let mainWindow: PwPage +let workbench: PwPage +let autoPort = 0 +let originalWxml = '' +let originalProjectConfig = '' + +async function selectDevice(win: PwPage, deviceName: string): Promise { + await selectDeviceInPicker(win, electronApp, deviceName) +} + +// One-shot JSON-RPC call to the miniprogram-automator WebSocket server. Shared +// by the wx-method and recompile-button tests below (they never run +// concurrently — this describe is serial — so a single RPC id is safe). +function wsCall>( + method: string, + params: Record = {}, + timeoutMs = 12000, +): Promise { + return new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://127.0.0.1:${autoPort}`) + const timer = setTimeout(() => { ws.close(); reject(new Error(`wsCall ${method} timed out`)) }, timeoutMs) + ws.on('open', () => ws.send(JSON.stringify({ id: 'nh1-merged-rpc', method, params }))) + ws.on('message', (raw) => { + let msg: { id?: string; result?: unknown; error?: { message?: string } } + try { msg = JSON.parse(String(raw)) } catch { return } + if (msg.id !== 'nh1-merged-rpc') return + clearTimeout(timer) + ws.close() + if (msg.error) reject(new Error(msg.error.message || 'rpc error')) + else resolve(msg.result as T) + }) + ws.on('error', (err) => { clearTimeout(timer); reject(err) }) + }) +} + +async function navBarTitle(): Promise { + return evalInSimulator( + electronApp, + `(() => { const e = document.querySelector('.nav-bar__title-text'); return e ? (e.textContent || '') : '' })()`, + ).catch(() => '') +} + +async function readActivePageText(): Promise { + return evalInWebContentsByUrl( + electronApp, + RENDER_GUEST_URL_MARKER, + 'document.body.innerText', + ).catch(() => '') +} + +/** + * Reproduce "unchanged start page, force a real recompile" — the popover no + * longer has an inline 重新编译 button; instead `handleSelect` in popover.tsx + * always sends `relaunch: true`, even when reselecting the row that is + * already current. Re-clicking 普通编译 while it is selected therefore + * triggers the same recompile+relaunch-at-entry-page contract the old + * button did, without creating any custom mode. + */ +async function reselectNormalCompileInPopover(): Promise { + await openCompileModePopover(workbench, electronApp) + await clickCompileModeMenuRow(electronApp, '普通编译') +} + +/** + * Reproduce "pick a different start page, then trigger 重新编译" — the new + * popover collapses that into one action: creating a mode sends an `add` + * command, main's `CompileModeStore` appends the new entry and selects it, + * and that always relaunches, so filling and submitting the "添加编译模式" + * form both selects the page AND fires the same real recompile. + */ +async function createAndLaunchCompileMode(pathName: string): Promise { + await openCompileModePopover(workbench, electronApp) + await clickCompileModeMenuAction(electronApp, '添加编译模式') + await fillCompileModeForm(electronApp, { pathName }) + await submitCompileModeForm(electronApp) +} + +/** + * Execute JavaScript in the DevTools front-end realm (devtools:// page). + * Returns null on any error (front-end not yet ready, wc gone, etc.). + */ +function evalInDevtools(app: ElectronApplication, expression: string): Promise { + return evalInWebContentsByUrl(app, 'devtools://', expression).catch(() => null) +} + +/** + * Send a DOM.getDocument command via the WRAPPED InspectorFrontendHost + * (the same path the Elements panel uses) and capture the first response with + * a matching id via window.DevToolsAPI.dispatchMessage interception. + * + * The capture arms a one-shot `dispatchMessage` interceptor BEFORE sending the + * command, so the response never races. The interceptor is removed after the + * first matching reply (or on timeout). + */ +async function getDocumentViaFrontend( + app: ElectronApplication, + timeoutMs = 4000, +): Promise | null> { + return evalInWebContentsByUrl | null>( + app, + 'devtools://', + `(function() { + return new Promise(function(resolve) { + try { + var IFH = globalThis.InspectorFrontendHost; + var DTAPI = window.DevToolsAPI; + if (!IFH || typeof IFH.sendMessageToBackend !== 'function') { + return resolve(null); + } + if (!DTAPI || typeof DTAPI.dispatchMessage !== 'function') { + return resolve(null); + } + + var cmdId = Date.now(); + var settled = false; + var timer = setTimeout(function() { + if (settled) return; + settled = true; + window.DevToolsAPI.dispatchMessage = origDispatch; + resolve(null); + }, ${timeoutMs}); + + var origDispatch = DTAPI.dispatchMessage.bind(DTAPI); + + // Intercept dispatchMessage to capture the response with our cmdId. + DTAPI.dispatchMessage = function(messageStr) { + try { + var msg = (typeof messageStr === 'string') ? JSON.parse(messageStr) : messageStr; + if (msg && msg.id === cmdId && !settled) { + settled = true; + clearTimeout(timer); + window.DevToolsAPI.dispatchMessage = origDispatch; + origDispatch(messageStr); + resolve(msg); + return; + } + } catch(_) {} + origDispatch(messageStr); + }; + + // Send DOM.getDocument via the (possibly wrapped) sendMessageToBackend. + IFH.sendMessageToBackend(JSON.stringify({ + id: cmdId, + method: 'DOM.getDocument', + params: { depth: 2 } + })); + } catch(e) { + resolve(null); + } + }); + })()`, + ).catch(() => null) +} + +test.describe('native-host NH-1: thumbnail capture + white-screen reconcile + wx-method + devtools elements + recompile button', () => { + test.describe.configure({ mode: 'serial' }) + test.setTimeout(180_000) + + test.beforeAll(async () => { + // Extend the hook timeout: this beforeAll now stacks every readiness poll + // the 5 original files used to spread across 5 separate beforeAll hooks + // (render guest, DeviceShell, WXML tree) on top of one Electron cold + // boot. Setting it INSIDE the hook overrides the hook's own budget (the + // describe-level setTimeout only covers tests). + test.setTimeout(240_000) + + originalWxml = fs.readFileSync(HOME_WXML, 'utf8') + originalProjectConfig = fs.readFileSync(PROJECT_CONFIG_PATH, 'utf8') + + const appPath = path.resolve(__dirname, 'electron-entry.js') + const userDataDir = path.resolve( + process.env.DIMINA_DEVTOOLS_DATA_DIR + ?? path.resolve(__dirname, '..', 'node_modules', '.cache', 'devtools-e2e'), + 'userdata', + `nh1-merged-${process.pid}`, + ) + fs.mkdirSync(userDataDir, { recursive: true }) + + electronApp = await _electron.launch({ + args: [appPath, 'auto', '--auto-port', '0', `--user-data-dir=${userDataDir}`], + env: { + ...process.env, + NODE_ENV: 'test', + DIMINA_NATIVE_HOST: '1', + DIMINA_E2E_USER_DATA_DIR: userDataDir, + }, + }) + + mainWindow = await findMainWindow(electronApp) + await mainWindow.waitForLoadState('domcontentloaded') + + await electronApp.evaluate(async ({ BrowserWindow }) => { + const win = BrowserWindow.getAllWindows()[0] + if (win && !win.isVisible()) { + await new Promise((resolve) => { + win.once('show', resolve) + setTimeout(resolve, 5000) + }) + } + if (win) { + win.setPosition(-2000, -2000) + win.blur() + } + }) + + autoPort = await pollUntil( + () => ipcInvoke(mainWindow, AutomationChannel.GetPort), + (val) => typeof val === 'number' && val > 0, + 10000, + 100, + ) as number + + // recompile-button-recompiles (the last test below) needs autoBuild off + // BEFORE the project opens, so openProject never starts a chokidar + // watcher — that's the only way to isolate "the button itself forced a + // recompile" from "the watcher already picked up the edit". None of the + // other merged tests touch FIXTURE_DIR source files or depend on live + // auto-rebuild, so disabling it for the whole shared session is safe for + // them too. + const settings = await ipcInvoke<{ compile: { autoBuild: boolean }, [k: string]: unknown }>( + mainWindow, + WorkbenchSettingsChannel.Get, + ) + await ipcInvoke(mainWindow, WorkbenchSettingsChannel.Save, { + ...settings, + compile: { ...settings.compile, autoBuild: false }, + }) + + workbench = await openProjectInUI(electronApp, FIXTURE_DIR, { waitMs: 20000 }) + await waitForSimulatorWebview(electronApp) + + // Render guest readiness — captureThumbnail falls back to the outer WVC + // when the guest is absent, and devtools-elements needs it to route into, + // so both must run only after the guest (__frame__.html) is mounted. + await pollUntil( + () => electronApp.evaluate(({ webContents }, marker) => + webContents.getAllWebContents().some( + (wc) => !wc.isDestroyed() && wc.getURL().includes(marker), + ), + RENDER_GUEST_URL_MARKER), + (present) => present === true, + 30000, + 300, + ) + + // DeviceShell readiness — white-screen-reconcile and recompile-button + // drive the real UI against it; wx-method re-asserts this explicitly in + // its own test body (kept as a first-class assertion, not just a gate). + await pollUntil( + () => evalInSimulator( + electronApp, + `(() => !!document.querySelector('.device-shell-root'))()`, + ).catch(() => false), + (ok) => ok === true, + 25000, + 300, + ) + + // WXML tree readiness — the signal the WXML panel uses; devtools-elements + // needs the render guest fully set up before probing DOM.getDocument. + await pollUntil( + () => ipcInvoke(workbench, SimulatorWxmlChannel.GetSnapshot).catch(() => null), + (t) => !!t && typeof (t as WxmlNode).tagName === 'string', + 30000, + 400, + ) + }) + + test.afterAll(async () => { + // recompile-button-recompiles (last test) edits both files on disk; + // restore them unconditionally regardless of which tests ran/failed so + // the git-tracked fixture is back to its pre-suite state for the next spec. + fs.writeFileSync(HOME_WXML, originalWxml) + fs.writeFileSync(PROJECT_CONFIG_PATH, originalProjectConfig) + await closeProject(electronApp).catch(() => {}) + await electronApp?.close().catch(() => {}) + }) + + test('thumbnail PNG dimensions match the render guest, not the outer simulator WVC', async () => { + // Measure the pixel dimensions of the outer simulator WVC (simulator.html) + // and the render guest (__frame__.html) via PNG header in the main process. + // Both captures happen before the IPC call so the timing is deterministic. + const wcSizes = await electronApp.evaluate(async ({ webContents }, marker) => { + const all = webContents.getAllWebContents() + const simWc = all.find( + (wc) => wc.getURL().includes('simulator.html') && !wc.isDestroyed(), + ) + const guestWc = all.find( + (wc) => wc.getURL().includes(marker) && !wc.isDestroyed(), + ) + if (!simWc || !guestWc) return null + + const [simImg, guestImg] = await Promise.all([ + simWc.capturePage(), + guestWc.capturePage(), + ]) + + const simPng = simImg.toPNG() + const guestPng = guestImg.toPNG() + + // PNG IHDR: bytes 16–19 = width (big-endian), 20–23 = height (big-endian). + return { + sim: { + width: simPng.readUInt32BE(16), + height: simPng.readUInt32BE(20), + }, + guest: { + width: guestPng.readUInt32BE(16), + height: guestPng.readUInt32BE(20), + }, + } + }, RENDER_GUEST_URL_MARKER) + + expect(wcSizes, 'both simulator WVC and render guest must be capturable').toBeTruthy() + + // Guard: the test can only discriminate by size when the outer WVC is + // strictly larger than the guest. The outer WVC includes the desk + // background and phone bezels, so its PNG must be wider or taller than + // the guest screen rectangle. + // If this precondition fails the environment is unusual; the size + // comparison below would be inconclusive. Fail fast with a clear message. + const simIsLarger = + wcSizes!.sim.width > wcSizes!.guest.width || + wcSizes!.sim.height > wcSizes!.guest.height + expect( + simIsLarger, + `outer simulator WVC (${wcSizes!.sim.width}×${wcSizes!.sim.height}) must be larger ` + + `than render guest (${wcSizes!.guest.width}×${wcSizes!.guest.height}). ` + + `If they are the same size the desk/chrome padding is absent and the size discriminator cannot be used.`, + ).toBe(true) + + // Call captureThumbnail via IPC (the renderer-facing entry point). + const thumbnailDataUrl = await ipcInvoke( + workbench, + ProjectChannel.CaptureThumbnail, + FIXTURE_DIR, + ) + expect(thumbnailDataUrl, 'captureThumbnail must return a non-null data URL').toBeTruthy() + + const thumbSize = parsePngSize(thumbnailDataUrl!) + expect(thumbSize, 'returned data URL must be a valid PNG').toBeTruthy() + + // Core contract: the thumbnail's pixel dimensions must match the render + // guest, NOT the outer simulator WVC. + // + // captureThumbnail targets getActiveRenderWc() (the render guest), so + // its PNG dimensions equal the guest screen area only — no phone bezels, + // no gray desk background. A regression that uses the outer simulatorWc + // instead produces a PNG matching wcSizes.sim (the larger outer WVC); + // this assertion catches that regression. + expect( + thumbSize!.width, + `thumbnail width ${thumbSize!.width} must match render guest width ${wcSizes!.guest.width}, ` + + `not outer simulator WVC width ${wcSizes!.sim.width}`, + ).toBe(wcSizes!.guest.width) + + expect( + thumbSize!.height, + `thumbnail height ${thumbSize!.height} must match render guest height ${wcSizes!.guest.height}, ` + + `not outer simulator WVC height ${wcSizes!.sim.height}`, + ).toBe(wcSizes!.guest.height) + }) + + test('repeated device switches keep the simulator content view visible (no white screen)', async () => { + // Rapid-fire switches: a short gap so each triggers a relayout while the + // previous sentinel window may still be open — the conditions that used to + // strand the view detached. + for (let i = 0; i < 16; i++) { + await selectDevice(workbench, CYCLE[i % CYCLE.length]!.name) + await new Promise((r) => setTimeout(r, 120)) + } + // Let the last relayout settle. + await new Promise((r) => setTimeout(r, 2500)) + + // The exact bug signal: the content WCV's visibilityState stuck at 'hidden'. + const visibility = await evalInSimulator(electronApp, 'document.visibilityState') + expect( + visibility, + 'after repeated device switches the simulator content view must stay visible ' + + "(the white-screen bug left it stuck at 'hidden' after a spurious detach)", + ).toBe('visible') + + // It is actually laid out / painting, not a zero-area ghost. + const innerWidth = await evalInSimulator(electronApp, 'window.innerWidth') + expect(innerWidth, 'DeviceShell viewport should have a non-zero width (rendering)').toBeGreaterThan(0) + + // visibilityState + innerWidth above are the authoritative no-white-screen + // signals: a content WebContentsView that was detached OR setVisible(false) + // reports document.visibilityState 'hidden' — the exact symptom the bug + // report flagged. Both being 'visible'/non-zero after the switch storm is + // the proof the reconciler self-healed every relayout transient. + + // Isolation: this test left the device on whatever CYCLE ended on + // (iPhone SE). Reset to the default device before any later test in this + // shared session runs — native-host-thumbnail-capture's PNG-size + // discriminator assumes a fixed relationship between the outer WVC and + // render-guest dimensions for the CURRENT device, and later tests should + // not inherit a non-default device just because they happen to run after + // this one in the same Electron instance. + await selectDevice(workbench, DEFAULT_DEVICE.name) + await pollUntil( + () => evalInSimulator(electronApp, 'document.visibilityState').catch(() => ''), + (v) => v === 'visible', + 10000, + 200, + ) + }) + + test('App.callWxMethod setNavigationBarTitle updates the DeviceShell nav bar (service-host wx.*)', async () => { + // Gate on the native render path being live (DeviceShell mounted), same as + // the sibling render spec: this proves the service host + automation + // pipeline are up before we drive the RPC. + const shellMounted = await pollUntil( + () => evalInSimulator( + electronApp, + `(() => !!document.querySelector('.device-shell-root'))()`, + ).catch(() => false), + (ok) => ok === true, + 25000, + 300, + ) + expect(shellMounted, 'DeviceShell .device-shell-root should mount under DIMINA_NATIVE_HOST=1').toBe(true) + + // Baseline: the entry page's window config overrides the app-level default + // ("TabBar Fixture") with "Home", and is not already our probe value (so + // the post-call assertion is meaningful). + const before = await pollUntil( + () => navBarTitle(), + (t) => typeof t === 'string' && t.length > 0, + 25000, + 300, + ) + expect(before, 'nav-bar title should render the entry page title before the call').toBe('Home') + expect(before, 'baseline title must differ from the probe value').not.toBe(PROBE_TITLE) + + // The contract under test: a NON-navigation wx method invoked via + // App.callWxMethod must run on the running mini-app's authoritative + // (service-host) `wx`, so its UI effect is real. Under native-host today + // this REJECTS ("wx.setNavigationBarTitle is not a function") because the + // call lands on the simulator top-window `wx`, which lacks this method. + await wsCall('App.callWxMethod', { method: 'setNavigationBarTitle', args: [{ title: PROBE_TITLE }] }) + + // The effect flows service-host wx → DeviceShell nav-bar reducer → DOM. + const after = await pollUntil( + () => navBarTitle(), + (t) => t === PROBE_TITLE, + 15000, + 400, + ) + expect(after, `DeviceShell nav-bar title should update to ${PROBE_TITLE} via service-host wx`).toBe(PROBE_TITLE) + }) + + test('elements-forward hook sentinels are installed in the DevTools front-end realm', async () => { + // The hook is installed by a polling interval (up to ~10s after dom-ready). + // Poll until both sentinels appear so we don't race the install timer. + const hookInstalled = await pollUntil( + () => evalInDevtools( + electronApp, + '!!(globalThis.__diminaElementsHookInstalled === true && ' + + 'globalThis.InspectorFrontendHost && ' + + 'globalThis.InspectorFrontendHost.__diminaElementsWrapped === true)', + ), + (ok) => ok === true, + 30000, + 300, + ) + + expect( + hookInstalled, + 'DevTools front-end (devtools://) must have __diminaElementsHookInstalled=true ' + + 'and InspectorFrontendHost.__diminaElementsWrapped=true — the elements-forward hook is not installed', + ).toBe(true) + }) + + test('DOM.getDocument via the front-end hook returns the render guest document (__frame__.html), not the service host', async () => { + // Wait until the hook is installed before probing (may have already settled + // from the previous test, but this describe is serial so the guard is cheap). + await pollUntil( + () => evalInDevtools( + electronApp, + '!!(globalThis.__diminaElementsHookInstalled)', + ), + (ok) => ok === true, + 15000, + 300, + ) + + // Retry the getDocument call: the drain interval is 150ms and the render guest + // may still be priming (DOM.enable in flight). A few retries handle that window. + let response: Record | null = null + const deadline = Date.now() + 30000 + while (Date.now() < deadline) { + response = await getDocumentViaFrontend(electronApp, 4000) + if (response && response.result) break + await new Promise((r) => setTimeout(r, 500)) + } + + expect( + response, + 'DOM.getDocument sent via InspectorFrontendHost.sendMessageToBackend must receive a response within 30s', + ).toBeTruthy() + + const result = (response as { result?: { root?: { documentURL?: string; baseURL?: string } } })?.result + expect( + result, + `DOM.getDocument response should have a "result" field; got: ${JSON.stringify(response)}`, + ).toBeTruthy() + + const root = result?.root + expect( + root, + `DOM.getDocument result should contain a "root" node; got result=${JSON.stringify(result)}`, + ).toBeTruthy() + + // The discriminating assertion: the root document URL must point to the render + // guest (__frame__.html). When elements-forward is absent or broken, this URL + // resolves to the service host's own document (containing "service.html" or + // having title "Dimina Service Host"). + const docUrl: string = String(root?.documentURL ?? root?.baseURL ?? '') + + expect( + docUrl, + `DOM.getDocument root.documentURL should point to the render guest (__frame__.html) ` + + `but got: "${docUrl}". This means Elements is inspecting the service host instead of the render guest.`, + ).toContain(RENDER_GUEST_URL_MARKER) + + expect( + docUrl, + `DOM.getDocument root.documentURL must not point to the service host (service.html); got: "${docUrl}"`, + ).not.toContain('service.html') + }) + + test('clicking 重新编译 recompiles the edited source and shows it — no auto-rebuild happens first', async () => { + // ── Sanity: the entry page renders its pre-edit content. ──────────────── + const initial = await pollUntil( + () => readActivePageText(), + (txt) => txt.includes('HOME PAGE'), + 20000, + 400, + ) + expect(initial).toContain('HOME PAGE') + + // ── Edit the source on disk WITHOUT going through the app — autoBuild is + // off, so no watcher exists to pick this up. ──────────────────────────── + fs.writeFileSync( + HOME_WXML, + originalWxml.replace('HOME PAGE', `HOME PAGE ${SENTINEL}`), + ) + + // ── Confirm the edit does NOT surface on its own within a generous + // window — proves there is no live auto-compile in this configuration. ── + await new Promise((resolve) => setTimeout(resolve, 8000)) + const stillStale = await readActivePageText() + expect( + stillStale.includes(SENTINEL), + 'autoBuild is off — the sentinel must NOT appear without an explicit recompile', + ).toBe(false) + + // ── Reselect 普通编译 in the popover — this must trigger a REAL recompile, + // not just a reattach to the stale build. ──────────────────────────────── + await reselectNormalCompileInPopover() + + const afterRecompile = await pollUntil( + () => readActivePageText(), + (txt) => txt.includes(SENTINEL), + 30000, + 500, + ) + expect( + afterRecompile, + '重新编译 must recompile the edited source and reload onto it — a reattach-only implementation would still show the stale build', + ).toContain(SENTINEL) + }) + + test('changing the compile start page then clicking 重新编译 opens that page', async () => { + const routeBefore = await pollUntil( + () => wsCall<{ path?: string }>('App.getCurrentPage').catch(() => null), + (page) => !!page?.path?.includes('pages/home/home'), + 20000, + 500, + ) + expect(routeBefore?.path).toContain('pages/home/home') + + const before = await pollUntil( + () => readActivePageText(), + (txt) => txt.includes('HOME PAGE'), + 20000, + 400, + ) + expect(before).toContain('HOME PAGE') + + await createAndLaunchCompileMode('pages/cart/cart') + + const routeAfter = await pollUntil( + () => wsCall<{ path?: string }>('App.getCurrentPage').catch(() => null), + (page) => !!page?.path?.includes('pages/cart/cart'), + 30000, + 500, + ) + expect( + routeAfter?.path, + '重新编译后的 active page route must be pages/cart/cart instead of the previous HOME route', + ).toContain('pages/cart/cart') + + const afterRecompile = await pollUntil( + () => readActivePageText(), + (txt) => txt.includes('CART PAGE'), + 30000, + 500, + ) + expect( + afterRecompile, + '首页 → 选择 pages/cart/cart → 重新编译 must relaunch the simulator at CART instead of retaining HOME', + ).toContain('CART PAGE') + expect(afterRecompile).not.toContain('HOME PAGE') + }) +}) diff --git a/packages/devtools/e2e/native-host-thumbnail-capture.spec.ts b/packages/devtools/e2e/native-host-thumbnail-capture.spec.ts deleted file mode 100644 index 96cf565a..00000000 --- a/packages/devtools/e2e/native-host-thumbnail-capture.spec.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * E2E (native-host only): ProjectChannel.CaptureThumbnail captures the - * render guest (__frame__.html) content, not the outer simulator - * WebContentsView (DeviceShell chrome). - * - * Under native-host the simulator panel WebContentsView (simulator.html) - * hosts the DeviceShell HTML — phone bezels, gray "desk" background and the - * phone chrome. The actual mini-app content lives in a SEPARATE WebContents - * (`__frame__.html`) hosted as a nested inside the DeviceShell. - * - * The discriminating observable is image pixel dimensions: - * - The outer simulator WVC captures the full DeviceShell page, which - * includes the desk padding and phone chrome around the screen. Its - * PNG is LARGER in at least one dimension. - * - The render guest captures only the phone screen content area (no - * chrome, no desk). Its PNG is smaller — sized to the screen rectangle. - * - * captureThumbnail must return a PNG whose pixel dimensions match the render - * guest, NOT the outer simulator WVC. Capturing the outer WVC would include - * phone bezels and the gray desk background, which are visually wrong for a - * project thumbnail. - * - * Sizes are measured by parsing the PNG IHDR chunk (bytes 16–23) both in - * the main process (for the two live WebContents) and in the test runner - * (for the returned data URL). This keeps the unit consistent: actual device - * pixels, independent of CSS DPR scaling. - * - * captureThumbnail targets the render guest; its PNG dimensions must equal - * the guest's screen area (smaller). Targeting the outer WVC produces a PNG - * matching the WVC dimensions (larger) and the size assertions below fail. - */ -import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' -import path from 'path' -import fs from 'fs' -import { fileURLToPath } from 'url' -import { - openProjectInUI, - waitForSimulatorWebview, - closeProject, - ipcInvoke, - pollUntil, - RENDER_GUEST_URL_MARKER, - findMainWindow, -} from './helpers' -import { ProjectChannel, AutomationChannel } from '../src/shared/ipc-channels' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'tabbar-app') - -/** - * Parse the pixel dimensions from a PNG data URL by reading the IHDR chunk. - * PNG layout: 8-byte signature, then chunks. The first chunk is always IHDR: - * bytes 8–11: chunk length (4 bytes) - * bytes 12–15: "IHDR" - * bytes 16–19: width (big-endian uint32) - * bytes 20–23: height (big-endian uint32) - */ -function parsePngSize(dataUrl: string): { width: number; height: number } | null { - const prefix = 'data:image/png;base64,' - if (!dataUrl.startsWith(prefix)) return null - const buf = Buffer.from(dataUrl.slice(prefix.length), 'base64') - if (buf.length < 24) return null - return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) } -} - -test.describe('native-host captureThumbnail targets the render guest, not the simulator WVC', () => { - test.describe.configure({ mode: 'serial' }) - test.setTimeout(180_000) - - let electronApp: ElectronApplication - let mainWindow: PwPage - let workbench: PwPage - - test.beforeAll(async () => { - // Extend the hook timeout: Electron cold-boot + first fixture compile can - // exceed the 60s config default. Setting it INSIDE the hook overrides the - // hook's own budget (the describe-level setTimeout only covers tests). - test.setTimeout(180_000) - const appPath = path.resolve(__dirname, 'electron-entry.js') - const userDataDir = path.resolve( - process.env.DIMINA_DEVTOOLS_DATA_DIR - ?? path.resolve(__dirname, '..', 'node_modules', '.cache', 'devtools-e2e'), - 'userdata', - `nh-thumb-capture-${process.pid}`, - ) - fs.mkdirSync(userDataDir, { recursive: true }) - - electronApp = await _electron.launch({ - args: [appPath, 'auto', '--auto-port', '0', `--user-data-dir=${userDataDir}`], - env: { - ...process.env, - NODE_ENV: 'test', - DIMINA_NATIVE_HOST: '1', - DIMINA_E2E_USER_DATA_DIR: userDataDir, - }, - }) - - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') - - await electronApp.evaluate(async ({ BrowserWindow }) => { - const win = BrowserWindow.getAllWindows()[0] - if (win && !win.isVisible()) { - await new Promise((resolve) => { - win.once('show', resolve) - setTimeout(resolve, 5000) - }) - } - if (win) { - win.setPosition(-2000, -2000) - win.blur() - } - }) - - await pollUntil( - () => ipcInvoke(mainWindow, AutomationChannel.GetPort), - (val) => typeof val === 'number' && val > 0, - 10000, - 100, - ) - - workbench = await openProjectInUI(electronApp, FIXTURE_DIR, { waitMs: 20000 }) - await waitForSimulatorWebview(electronApp) - - // Wait until the render guest (__frame__.html) is mounted and has a URL — - // captureThumbnail falls back to the outer WVC when the guest is absent, - // so the test must call it only after the guest is live. - await pollUntil( - () => electronApp.evaluate(({ webContents }, marker) => - webContents.getAllWebContents().some( - (wc) => !wc.isDestroyed() && wc.getURL().includes(marker), - ), - RENDER_GUEST_URL_MARKER), - (present) => present === true, - 30000, - 300, - ) - }) - - test.afterAll(async () => { - await closeProject(electronApp).catch(() => {}) - await electronApp?.close().catch(() => {}) - }) - - test('thumbnail PNG dimensions match the render guest, not the outer simulator WVC', async () => { - // Measure the pixel dimensions of the outer simulator WVC (simulator.html) - // and the render guest (__frame__.html) via PNG header in the main process. - // Both captures happen before the IPC call so the timing is deterministic. - const wcSizes = await electronApp.evaluate(async ({ webContents }, marker) => { - const all = webContents.getAllWebContents() - const simWc = all.find( - (wc) => wc.getURL().includes('simulator.html') && !wc.isDestroyed(), - ) - const guestWc = all.find( - (wc) => wc.getURL().includes(marker) && !wc.isDestroyed(), - ) - if (!simWc || !guestWc) return null - - const [simImg, guestImg] = await Promise.all([ - simWc.capturePage(), - guestWc.capturePage(), - ]) - - const simPng = simImg.toPNG() - const guestPng = guestImg.toPNG() - - // PNG IHDR: bytes 16–19 = width (big-endian), 20–23 = height (big-endian). - return { - sim: { - width: simPng.readUInt32BE(16), - height: simPng.readUInt32BE(20), - }, - guest: { - width: guestPng.readUInt32BE(16), - height: guestPng.readUInt32BE(20), - }, - } - }, RENDER_GUEST_URL_MARKER) - - expect(wcSizes, 'both simulator WVC and render guest must be capturable').toBeTruthy() - - // Guard: the test can only discriminate by size when the outer WVC is - // strictly larger than the guest. The outer WVC includes the desk - // background and phone bezels, so its PNG must be wider or taller than - // the guest screen rectangle. - // If this precondition fails the environment is unusual; the size - // comparison below would be inconclusive. Fail fast with a clear message. - const simIsLarger = - wcSizes!.sim.width > wcSizes!.guest.width || - wcSizes!.sim.height > wcSizes!.guest.height - expect( - simIsLarger, - `outer simulator WVC (${wcSizes!.sim.width}×${wcSizes!.sim.height}) must be larger ` + - `than render guest (${wcSizes!.guest.width}×${wcSizes!.guest.height}). ` + - `If they are the same size the desk/chrome padding is absent and the size discriminator cannot be used.`, - ).toBe(true) - - // Call captureThumbnail via IPC (the renderer-facing entry point). - const thumbnailDataUrl = await ipcInvoke( - workbench, - ProjectChannel.CaptureThumbnail, - FIXTURE_DIR, - ) - expect(thumbnailDataUrl, 'captureThumbnail must return a non-null data URL').toBeTruthy() - - const thumbSize = parsePngSize(thumbnailDataUrl!) - expect(thumbSize, 'returned data URL must be a valid PNG').toBeTruthy() - - // Core contract: the thumbnail's pixel dimensions must match the render - // guest, NOT the outer simulator WVC. - // - // captureThumbnail targets getActiveRenderWc() (the render guest), so - // its PNG dimensions equal the guest screen area only — no phone bezels, - // no gray desk background. A regression that uses the outer simulatorWc - // instead produces a PNG matching wcSizes.sim (the larger outer WVC); - // this assertion catches that regression. - expect( - thumbSize!.width, - `thumbnail width ${thumbSize!.width} must match render guest width ${wcSizes!.guest.width}, ` + - `not outer simulator WVC width ${wcSizes!.sim.width}`, - ).toBe(wcSizes!.guest.width) - - expect( - thumbSize!.height, - `thumbnail height ${thumbSize!.height} must match render guest height ${wcSizes!.guest.height}, ` + - `not outer simulator WVC height ${wcSizes!.sim.height}`, - ).toBe(wcSizes!.guest.height) - }) -}) diff --git a/packages/devtools/e2e/native-host-websocket-contract.spec.ts b/packages/devtools/e2e/native-host-websocket-contract.spec.ts index 67e5d0d8..5678eaa7 100644 --- a/packages/devtools/e2e/native-host-websocket-contract.spec.ts +++ b/packages/devtools/e2e/native-host-websocket-contract.spec.ts @@ -30,7 +30,7 @@ import { const HERE = path.dirname(fileURLToPath(import.meta.url)) const UPSTREAM_PAGE = path.resolve( HERE, - '../../../dimina/fe/example/base/pages/socket-test', + '../../../dimina/examples/miniprogram/base/pages/socket-test', ) interface ContractResult { diff --git a/packages/devtools/e2e/native-host-white-screen-reconcile.spec.ts b/packages/devtools/e2e/native-host-white-screen-reconcile.spec.ts deleted file mode 100644 index c998f458..00000000 --- a/packages/devtools/e2e/native-host-white-screen-reconcile.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * E2E (native-host): the placement reconciler keeps content WebContentsViews - * mounted + visible through repeated device switches. - * - * Regression guard for the reported white-screen bug: rapidly switching the - * simulator device/orientation a dozen-plus times made the whole workbench go - * white and STAY white. A relayout transient (a dock slot momentarily measuring - * 0×0 mid-switch, coalesced away by ResizeObserver) made the RAF geometry - * sentinel publish a spurious detach; the main process removed the content - * WebContentsView from the contentView and nothing ever re-attached it. - * - * Under the reconciler, hiding a base view is setVisible(false) — never a - * detach — and every reconcile re-derives the actual tree from the desired - * snapshot, so a transient is at worst a one-tick flicker that self-heals. - * This drives the REAL device picker many times in quick succession and - * asserts the simulator content view stays visible: document.visibilityState - * === 'visible' is the exact signal the bug report flagged as stuck at - * 'hidden', and the WCV stays in contentView.children (not detached). - */ -import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' -import path from 'path' -import fs from 'fs' -import { fileURLToPath } from 'url' -import { - openProjectInUI, - waitForSimulatorWebview, - closeProject, - ipcInvoke, - pollUntil, - evalInSimulator, - findMainWindow, - selectDeviceInPicker, -} from './helpers' -import { AutomationChannel } from '../src/shared/ipc-channels' -import { DEFAULT_DEVICE, DEVICE_NAMES, findDevice } from '@devicekit/devices' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'tabbar-app') - -// Cycle across presets with different widths AND heights so each switch changes -// the simulator column width → dock relayout → the geometry sentinel opens, -// reproducing the transient that used to detach the view. -const CYCLE = [ - DEFAULT_DEVICE, - findDevice(DEVICE_NAMES.iPhone_14_Pro), - findDevice(DEVICE_NAMES.iPhone_16_Pro), - findDevice(DEVICE_NAMES.iPhone_SE) ?? DEFAULT_DEVICE, -].filter(Boolean) as { name: string }[] - -let electronApp: ElectronApplication -let mainWindow: PwPage -let workbench: PwPage - -async function selectDevice(win: PwPage, deviceName: string): Promise { - await selectDeviceInPicker(win, electronApp, deviceName) -} - -test.describe('native-host white-screen reconcile e2e', () => { - test.describe.configure({ mode: 'serial' }) - test.setTimeout(180_000) - - test.beforeAll(async () => { - const appPath = path.resolve(__dirname, 'electron-entry.js') - const userDataDir = path.resolve( - process.env.DIMINA_DEVTOOLS_DATA_DIR - ?? path.resolve(__dirname, '..', 'node_modules', '.cache', 'devtools-e2e'), - 'userdata', - `nh-whitescreen-${process.pid}`, - ) - fs.mkdirSync(userDataDir, { recursive: true }) - - electronApp = await _electron.launch({ - args: [appPath, 'auto', '--auto-port', '0', `--user-data-dir=${userDataDir}`], - env: { ...process.env, NODE_ENV: 'test', DIMINA_NATIVE_HOST: '1', DIMINA_E2E_USER_DATA_DIR: userDataDir }, - }) - - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') - - await electronApp.evaluate(async ({ BrowserWindow }) => { - const win = BrowserWindow.getAllWindows()[0] - if (win && !win.isVisible()) { - await new Promise((resolve) => { - win.once('show', resolve) - setTimeout(resolve, 5000) - }) - } - if (win) { - win.setPosition(-2000, -2000) - win.blur() - } - }) - - await pollUntil( - () => ipcInvoke(mainWindow, AutomationChannel.GetPort), - (val) => typeof val === 'number' && val > 0, - 10000, - 100, - ) - - workbench = await openProjectInUI(electronApp, FIXTURE_DIR, { waitMs: 20000 }) - await waitForSimulatorWebview(electronApp) - - await pollUntil( - () => evalInSimulator( - electronApp, - `(() => !!document.querySelector('.device-shell-root'))()`, - ).catch(() => false), - (ok) => ok === true, - 25000, - 300, - ) - }) - - test.afterAll(async () => { - await closeProject(electronApp).catch(() => {}) - await electronApp?.close().catch(() => {}) - }) - - test('repeated device switches keep the simulator content view visible (no white screen)', async () => { - // Rapid-fire switches: a short gap so each triggers a relayout while the - // previous sentinel window may still be open — the conditions that used to - // strand the view detached. - for (let i = 0; i < 16; i++) { - await selectDevice(workbench, CYCLE[i % CYCLE.length]!.name) - await new Promise((r) => setTimeout(r, 120)) - } - // Let the last relayout settle. - await new Promise((r) => setTimeout(r, 2500)) - - // The exact bug signal: the content WCV's visibilityState stuck at 'hidden'. - const visibility = await evalInSimulator(electronApp, 'document.visibilityState') - expect( - visibility, - 'after repeated device switches the simulator content view must stay visible ' - + "(the white-screen bug left it stuck at 'hidden' after a spurious detach)", - ).toBe('visible') - - // It is actually laid out / painting, not a zero-area ghost. - const innerWidth = await evalInSimulator(electronApp, 'window.innerWidth') - expect(innerWidth, 'DeviceShell viewport should have a non-zero width (rendering)').toBeGreaterThan(0) - - // visibilityState + innerWidth above are the authoritative no-white-screen - // signals: a content WebContentsView that was detached OR setVisible(false) - // reports document.visibilityState 'hidden' — the exact symptom the bug - // report flagged. Both being 'visible'/non-zero after the switch storm is - // the proof the reconciler self-healed every relayout transient. - }) -}) diff --git a/packages/devtools/e2e/native-host-wx-method.spec.ts b/packages/devtools/e2e/native-host-wx-method.spec.ts deleted file mode 100644 index 8c2f27c6..00000000 --- a/packages/devtools/e2e/native-host-wx-method.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * E2E (native-host only): `App.callWxMethod` must work for a NON-navigation - * `wx.*` method under DIMINA_NATIVE_HOST=1. - * - * Navigation methods (navigateTo/redirectTo/reLaunch/switchTab/navigateBack) - * route correctly under native-host — see - * `native-host-render.spec.ts` ("App.callWxMethod switchTab navigates …"): - * the handler special-cases NAV_METHODS and runs them on the authoritative - * service-host `wx`. This spec pins the same guarantee for a NON-nav wx - * method that drives the mini-app UI (here `setNavigationBarTitle`): it must - * ALSO reach the service-host `wx` so its effect is real, rather than falling - * through to `evalInSim` → the simulator TOP-window `wx`, which under - * native-host exposes only nav + a few sync helpers and does NOT carry - * `setNavigationBarTitle`. - * - * Chosen method + assertion (see report): `setNavigationBarTitle` with - * `{ title: 'C5-PROBE' }`, asserting the DeviceShell nav-bar title text - * (`.nav-bar__title-text` in the simulator WebContentsView DOM, see - * `packages/dimina-electron-runtime/src/simulator-ui/navigation-bar.tsx`) changes from the entry - * page's configured "Home" title to "C5-PROBE". - * - * Why the side-effect contract and NOT `getSystemInfoSync`: the simulator - * top-window `wx` happens to carry that sync helper, so - * `App.callWxMethod('getSystemInfoSync')` returns a usable result regardless - * of whether non-nav methods route to the service-host `wx` — it does NOT - * discriminate the contract. `setNavigationBarTitle` is a non-nav method that - * the top-window `wx` lacks, so it is the discriminating contract for - * "non-nav wx must route to the service-host wx under native-host". - */ -import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' -import path from 'path' -import fs from 'fs' -import { fileURLToPath } from 'url' -import { WebSocket } from 'ws' -import { - openProjectInUI, - waitForSimulatorWebview, - closeProject, - ipcInvoke, - pollUntil, - evalInSimulator, - findMainWindow, -} from './helpers' -import { AutomationChannel } from '../src/shared/ipc-channels' - -// NOTE: scope DIMINA_NATIVE_HOST to THIS spec's electron launch (below), never -// `process.env` — a module-top mutation poisons the shared --workers=1 runner, -// flipping every other spec into native-host mode (panel ripple → mass failures). - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'tabbar-app') -const PROBE_TITLE = 'C5-PROBE' - -let electronApp: ElectronApplication -let mainWindow: PwPage -let autoPort = 0 - -// One-shot JSON-RPC call to the miniprogram-automator WebSocket server. Drives -// the SAME automation handlers the SDK uses, so it exercises the native-host -// automation pipeline (App.callWxMethod → service-host wx.*) end-to-end. Mirrors -// the helper in native-host-render.spec.ts / automator-compat.spec.ts. Note: -// rejects on an RPC-level error (e.g. "wx.X is not a function"). -function wsCall>( - method: string, - params: Record = {}, - timeoutMs = 12000, -): Promise { - return new Promise((resolve, reject) => { - const ws = new WebSocket(`ws://127.0.0.1:${autoPort}`) - const timer = setTimeout(() => { ws.close(); reject(new Error(`wsCall ${method} timed out`)) }, timeoutMs) - ws.on('open', () => ws.send(JSON.stringify({ id: 'nhwx1', method, params }))) - ws.on('message', (raw) => { - let msg: { id?: string; result?: unknown; error?: { message?: string } } - try { msg = JSON.parse(String(raw)) } catch { return } - if (msg.id !== 'nhwx1') return - clearTimeout(timer) - ws.close() - if (msg.error) reject(new Error(msg.error.message || 'rpc error')) - else resolve(msg.result as T) - }) - ws.on('error', (err) => { clearTimeout(timer); reject(err) }) - }) -} - -async function navBarTitle(): Promise { - return evalInSimulator( - electronApp, - `(() => { const e = document.querySelector('.nav-bar__title-text'); return e ? (e.textContent || '') : '' })()`, - ).catch(() => '') -} - -test.describe('native-host App.callWxMethod (non-nav) e2e', () => { - test.describe.configure({ mode: 'serial' }) - test.setTimeout(180_000) - - test.beforeAll(async () => { - const appPath = path.resolve(__dirname, 'electron-entry.js') - const userDataDir = path.resolve( - process.env.DIMINA_DEVTOOLS_DATA_DIR - ?? path.resolve(__dirname, '..', 'node_modules', '.cache', 'devtools-e2e'), - 'userdata', - `nh-wxmethod-${process.pid}`, - ) - fs.mkdirSync(userDataDir, { recursive: true }) - - electronApp = await _electron.launch({ - args: [appPath, 'auto', '--auto-port', '0', `--user-data-dir=${userDataDir}`], - env: { ...process.env, NODE_ENV: 'test', DIMINA_NATIVE_HOST: '1', DIMINA_E2E_USER_DATA_DIR: userDataDir }, - }) - - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') - - await electronApp.evaluate(async ({ BrowserWindow }) => { - const win = BrowserWindow.getAllWindows()[0] - if (win && !win.isVisible()) { - await new Promise((resolve) => { - win.once('show', resolve) - setTimeout(resolve, 5000) - }) - } - if (win) { - win.setPosition(-2000, -2000) - win.blur() - } - }) - - autoPort = await pollUntil( - () => ipcInvoke(mainWindow, AutomationChannel.GetPort), - (val) => typeof val === 'number' && val > 0, - 10000, - 100, - ) as number - - await openProjectInUI(electronApp, FIXTURE_DIR, { waitMs: 20000 }) - await waitForSimulatorWebview(electronApp) - }) - - test.afterAll(async () => { - await closeProject(electronApp).catch(() => {}) - await electronApp?.close().catch(() => {}) - }) - - test('App.callWxMethod setNavigationBarTitle updates the DeviceShell nav bar (service-host wx.*)', async () => { - // Gate on the native render path being live (DeviceShell mounted), same as - // the sibling render spec: this proves the service host + automation - // pipeline are up before we drive the RPC. - const shellMounted = await pollUntil( - () => evalInSimulator( - electronApp, - `(() => !!document.querySelector('.device-shell-root'))()`, - ).catch(() => false), - (ok) => ok === true, - 25000, - 300, - ) - expect(shellMounted, 'DeviceShell .device-shell-root should mount under DIMINA_NATIVE_HOST=1').toBe(true) - - // Baseline: the entry page's window config overrides the app-level default - // ("TabBar Fixture") with "Home", and is not already our probe value (so - // the post-call assertion is meaningful). - const before = await pollUntil( - () => navBarTitle(), - (t) => typeof t === 'string' && t.length > 0, - 25000, - 300, - ) - expect(before, 'nav-bar title should render the entry page title before the call').toBe('Home') - expect(before, 'baseline title must differ from the probe value').not.toBe(PROBE_TITLE) - - // The contract under test: a NON-navigation wx method invoked via - // App.callWxMethod must run on the running mini-app's authoritative - // (service-host) `wx`, so its UI effect is real. Under native-host today - // this REJECTS ("wx.setNavigationBarTitle is not a function") because the - // call lands on the simulator top-window `wx`, which lacks this method. - await wsCall('App.callWxMethod', { method: 'setNavigationBarTitle', args: [{ title: PROBE_TITLE }] }) - - // The effect flows service-host wx → DeviceShell nav-bar reducer → DOM. - const after = await pollUntil( - () => navBarTitle(), - (t) => t === PROBE_TITLE, - 15000, - 400, - ) - expect(after, `DeviceShell nav-bar title should update to ${PROBE_TITLE} via service-host wx`).toBe(PROBE_TITLE) - }) -}) diff --git a/packages/devtools/e2e/project-list-category.spec.ts b/packages/devtools/e2e/project-list-category.spec.ts index 140ad441..23bd7287 100644 --- a/packages/devtools/e2e/project-list-category.spec.ts +++ b/packages/devtools/e2e/project-list-category.spec.ts @@ -1,13 +1,8 @@ -import path from 'path' -import { fileURLToPath } from 'url' import { test, expect } from './fixtures' -import { DEMO_APP_DIR, ipcInvoke, addProject, refreshProjectList, selectProjectCategoryInUI } from './helpers' +import { DEMO_APP_DIR, ipcInvoke, addProject, refreshProjectList, selectProjectCategoryInUI, resolveMiniGameAppDir } from './helpers' import { ProjectsChannel } from '../src/shared/ipc-channels' -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -/** Real submodule fixture — `compileType: "game"`, `game.json`/`game.js`, no `app.json`. */ -const MINI_GAME_DIR = path.resolve(__dirname, '..', '..', '..', 'dimina', 'fe', 'example', 'air-battle') +const MINI_GAME_DIR = resolveMiniGameAppDir() test.describe('project list: category rail filters by project type', () => { test.afterEach(async ({ mainWindow }) => { diff --git a/packages/devtools/e2e/recompile-button-recompiles.spec.ts b/packages/devtools/e2e/recompile-button-recompiles.spec.ts deleted file mode 100644 index 968ef374..00000000 --- a/packages/devtools/e2e/recompile-button-recompiles.spec.ts +++ /dev/null @@ -1,276 +0,0 @@ -/** - * The popover "重新编译" button must trigger a REAL compiler recompile, not - * just re-attach the simulator to whatever was compiled before (微信开发者 - * 工具语义: 重新编译 = 重新编译 + 回到启动页重启). - * - * Two user-visible contracts are covered: - * - with "自动编译" off, source changes appear only after clicking 重新编译, - * proving the button performs a real compiler rebuild; - * - changing the compile start page from HOME to CART before clicking - * 重新编译 relaunches the simulator at CART, matching 微信开发者工具. - */ -import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' -import path from 'path' -import fs from 'fs' -import { fileURLToPath } from 'url' -import { WebSocket } from 'ws' -import { - openProjectInUI, - closeProject, - pollUntil, - ipcInvoke, - waitForSimulatorWebview, - evalInSimulator, - evalInWebContentsByUrl, - RENDER_GUEST_URL_MARKER, - findMainWindow, - openCompileModePopover, - clickCompileModeMenuRow, - clickCompileModeMenuAction, - fillCompileModeForm, - submitCompileModeForm, -} from './helpers' -import { AutomationChannel, WorkbenchSettingsChannel } from '../src/shared/ipc-channels' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const FIXTURE_DIR = path.resolve(__dirname, 'fixtures', 'tabbar-app') -const HOME_WXML = path.join(FIXTURE_DIR, 'pages', 'home', 'home.wxml') -const PROJECT_CONFIG_PATH = path.join(FIXTURE_DIR, 'project.config.json') -const SENTINEL = 'RECOMPILE-BUTTON-SENTINEL' - -let electronApp: ElectronApplication -let mainWindow: PwPage -let workbench: PwPage -let originalWxml = '' -// The popover now persists compile modes to project.config.json on every -// apply — even reselecting 普通编译 writes an (empty) modes list — so this -// git-tracked fixture (unlike the per-worker DEMO_APP_DIR) needs the same -// snapshot/restore hygiene as the wxml sentinel edit below. -let originalProjectConfig = '' -let autoPort = 0 - -function wsCall>( - method: string, - params: Record = {}, - timeoutMs = 12000, -): Promise { - return new Promise((resolve, reject) => { - const ws = new WebSocket(`ws://127.0.0.1:${autoPort}`) - const timer = setTimeout(() => { - ws.close() - reject(new Error(`wsCall ${method} timed out`)) - }, timeoutMs) - ws.on('open', () => ws.send(JSON.stringify({ id: 'recompile-route', method, params }))) - ws.on('message', (raw) => { - let msg: { id?: string; result?: unknown; error?: { message?: string } } - try { msg = JSON.parse(String(raw)) } catch { return } - if (msg.id !== 'recompile-route') return - clearTimeout(timer) - ws.close() - if (msg.error) reject(new Error(msg.error.message || 'rpc error')) - else resolve(msg.result as T) - }) - ws.on('error', (err) => { - clearTimeout(timer) - reject(err) - }) - }) -} - -async function readActivePageText(): Promise { - return evalInWebContentsByUrl( - electronApp, - RENDER_GUEST_URL_MARKER, - 'document.body.innerText', - ).catch(() => '') -} - -/** - * Reproduce "unchanged start page, force a real recompile" — the popover no - * longer has an inline 重新编译 button; instead `handleSelect` in popover.tsx - * always sends `relaunch: true`, even when reselecting the row that is - * already current. Re-clicking 普通编译 while it is selected therefore - * triggers the same recompile+relaunch-at-entry-page contract the old - * button did, without creating any custom mode. - */ -async function reselectNormalCompileInPopover(): Promise { - await openCompileModePopover(workbench, electronApp) - await clickCompileModeMenuRow(electronApp, '普通编译') -} - -/** - * Reproduce "pick a different start page, then trigger 重新编译" — the new - * popover collapses that into one action: creating a mode sends an `add` - * command, main's `CompileModeStore` appends the new entry and selects it, - * and that always relaunches, so filling and submitting the "添加编译模式" - * form both selects the page AND fires the same real recompile. - */ -async function createAndLaunchCompileMode(pathName: string): Promise { - await openCompileModePopover(workbench, electronApp) - await clickCompileModeMenuAction(electronApp, '添加编译模式') - await fillCompileModeForm(electronApp, { pathName }) - await submitCompileModeForm(electronApp) -} - -test.describe('popover 重新编译 rebuilds and relaunches at the selected start page', () => { - test.describe.configure({ mode: 'serial' }) - test.setTimeout(180_000) - - test.beforeAll(async () => { - originalWxml = fs.readFileSync(HOME_WXML, 'utf8') - originalProjectConfig = fs.readFileSync(PROJECT_CONFIG_PATH, 'utf8') - const appPath = path.resolve(__dirname, 'electron-entry.js') - const userDataDir = path.resolve( - process.env.DIMINA_DEVTOOLS_DATA_DIR - ?? path.resolve(__dirname, '..', 'node_modules', '.cache', 'devtools-e2e'), - 'userdata', - `recompile-btn-${process.pid}`, - ) - fs.mkdirSync(userDataDir, { recursive: true }) - - electronApp = await _electron.launch({ - args: [appPath, 'auto', '--auto-port', '0', `--user-data-dir=${userDataDir}`], - env: { ...process.env, NODE_ENV: 'test', DIMINA_NATIVE_HOST: '1', DIMINA_E2E_USER_DATA_DIR: userDataDir }, - }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') - - autoPort = await pollUntil( - () => ipcInvoke(mainWindow, AutomationChannel.GetPort), - (port) => typeof port === 'number' && port > 0, - 10000, - 100, - ) as number - - await electronApp.evaluate(async ({ BrowserWindow }) => { - const win = BrowserWindow.getAllWindows()[0] - if (win && !win.isVisible()) { - await new Promise((resolve) => { - win.once('show', resolve) - setTimeout(resolve, 5000) - }) - } - if (win) { - win.setPosition(-2000, -2000) - win.blur() - } - }) - - // Turn autoBuild off BEFORE opening the project, so openProject never - // starts a chokidar watcher — the only way to isolate "the button itself - // recompiled" from "the watcher already picked up the edit". - const settings = await ipcInvoke<{ compile: { autoBuild: boolean }, [k: string]: unknown }>( - mainWindow, - WorkbenchSettingsChannel.Get, - ) - await ipcInvoke(mainWindow, WorkbenchSettingsChannel.Save, { - ...settings, - compile: { ...settings.compile, autoBuild: false }, - }) - - workbench = await openProjectInUI(electronApp, FIXTURE_DIR, { waitMs: 20000 }) - await waitForSimulatorWebview(electronApp) - - await pollUntil( - () => evalInSimulator( - electronApp, - `(() => !!document.querySelector('.device-shell-root'))()`, - ).catch(() => false), - (ok) => ok === true, - 25000, - 300, - ) - }) - - test.afterAll(async () => { - fs.writeFileSync(HOME_WXML, originalWxml) - fs.writeFileSync(PROJECT_CONFIG_PATH, originalProjectConfig) - await closeProject(electronApp).catch(() => {}) - await electronApp?.close().catch(() => {}) - }) - - test('clicking 重新编译 recompiles the edited source and shows it — no auto-rebuild happens first', async () => { - // ── Sanity: the entry page renders its pre-edit content. ──────────────── - const initial = await pollUntil( - () => readActivePageText(), - (txt) => txt.includes('HOME PAGE'), - 20000, - 400, - ) - expect(initial).toContain('HOME PAGE') - - // ── Edit the source on disk WITHOUT going through the app — autoBuild is - // off, so no watcher exists to pick this up. ──────────────────────────── - fs.writeFileSync( - HOME_WXML, - originalWxml.replace('HOME PAGE', `HOME PAGE ${SENTINEL}`), - ) - - // ── Confirm the edit does NOT surface on its own within a generous - // window — proves there is no live auto-compile in this configuration. ── - await new Promise((resolve) => setTimeout(resolve, 8000)) - const stillStale = await readActivePageText() - expect( - stillStale.includes(SENTINEL), - 'autoBuild is off — the sentinel must NOT appear without an explicit recompile', - ).toBe(false) - - // ── Reselect 普通编译 in the popover — this must trigger a REAL recompile, - // not just a reattach to the stale build. ──────────────────────────────── - await reselectNormalCompileInPopover() - - const afterRecompile = await pollUntil( - () => readActivePageText(), - (txt) => txt.includes(SENTINEL), - 30000, - 500, - ) - expect( - afterRecompile, - '重新编译 must recompile the edited source and reload onto it — a reattach-only implementation would still show the stale build', - ).toContain(SENTINEL) - }) - - test('changing the compile start page then clicking 重新编译 opens that page', async () => { - const routeBefore = await pollUntil( - () => wsCall<{ path?: string }>('App.getCurrentPage').catch(() => null), - (page) => !!page?.path?.includes('pages/home/home'), - 20000, - 500, - ) - expect(routeBefore?.path).toContain('pages/home/home') - - const before = await pollUntil( - () => readActivePageText(), - (txt) => txt.includes('HOME PAGE'), - 20000, - 400, - ) - expect(before).toContain('HOME PAGE') - - await createAndLaunchCompileMode('pages/cart/cart') - - const routeAfter = await pollUntil( - () => wsCall<{ path?: string }>('App.getCurrentPage').catch(() => null), - (page) => !!page?.path?.includes('pages/cart/cart'), - 30000, - 500, - ) - expect( - routeAfter?.path, - '重新编译后的 active page route must be pages/cart/cart instead of the previous HOME route', - ).toContain('pages/cart/cart') - - const afterRecompile = await pollUntil( - () => readActivePageText(), - (txt) => txt.includes('CART PAGE'), - 30000, - 500, - ) - expect( - afterRecompile, - '首页 → 选择 pages/cart/cart → 重新编译 must relaunch the simulator at CART instead of retaining HOME', - ).toContain('CART PAGE') - expect(afterRecompile).not.toContain('HOME PAGE') - }) -}) diff --git a/packages/devtools/e2e/standalone-devtools-window-churn.spec.ts b/packages/devtools/e2e/standalone-devtools-window-churn.spec.ts index 2aee50c1..57298de6 100644 --- a/packages/devtools/e2e/standalone-devtools-window-churn.spec.ts +++ b/packages/devtools/e2e/standalone-devtools-window-churn.spec.ts @@ -141,14 +141,19 @@ async function windowCount(): Promise { /** How many floating debug windows exist right now — the orphan check: a * project window that goes away must take its floating window with it, and a * reopened project must not stack a second one next to a survivor. */ +// `getAllWindows()` can still list a window whose native peer is already gone +// during a close/reopen cycle, and every getter on it (getTitle included) +// throws "Object has been destroyed". Skip those the same way the webContents +// lookups above do — a destroyed window is not a surviving orphan, which is +// what these two actually measure. async function floatingWindowCount(): Promise { return electronApp.evaluate(({ BrowserWindow }) => - BrowserWindow.getAllWindows().filter((x) => x.getTitle().includes('调试')).length) + BrowserWindow.getAllWindows().filter((x) => !x.isDestroyed() && x.getTitle().includes('调试')).length) } async function floatingVisible(): Promise { return electronApp.evaluate(({ BrowserWindow }) => { - const w = BrowserWindow.getAllWindows().find((x) => x.getTitle().includes('调试')) + const w = BrowserWindow.getAllWindows().find((x) => !x.isDestroyed() && x.getTitle().includes('调试')) return w ? w.isVisible() : null }) } @@ -158,7 +163,7 @@ async function floatingVisible(): Promise { * internal-devtools-window.spec.ts's own close mechanics. */ async function closeFloatingNatively(): Promise { await electronApp.evaluate(({ BrowserWindow }) => { - const w = BrowserWindow.getAllWindows().find((x) => x.getTitle().includes('调试')) + const w = BrowserWindow.getAllWindows().find((x) => !x.isDestroyed() && x.getTitle().includes('调试')) if (!w) throw new Error('floating devtools window not found') w.close() }) diff --git a/packages/devtools/playwright.config.ts b/packages/devtools/playwright.config.ts index c99f9f1f..993e75ef 100644 --- a/packages/devtools/playwright.config.ts +++ b/packages/devtools/playwright.config.ts @@ -1,5 +1,11 @@ import { defineConfig } from '@playwright/test' +// PLAYWRIGHT_WORKERS is the explicit throughput/stress override: when set it +// replaces BOTH the top-level cap and every project's own `workers` below, so +// `PLAYWRIGHT_WORKERS=2 npx playwright test --project=native-host` behaves +// exactly like the pre-projects single-`workers` config did. +const explicitWorkers = process.env.PLAYWRIGHT_WORKERS ? Number(process.env.PLAYWRIGHT_WORKERS) : undefined + export default defineConfig({ testDir: './e2e', timeout: 60_000, @@ -7,15 +13,22 @@ export default defineConfig({ // File-parallel via workers; within-file serial (Playwright default when // fullyParallel is false). Each worker owns one Electron + one user-data-dir. fullyParallel: false, - // Each worker owns a full Electron runtime. The native-host specs are heavy - // enough that local file-parallel runs can starve new Electron launches and - // leave firstWindow() waiting forever. Keep the default deterministic; use - // PLAYWRIGHT_WORKERS for an explicit throughput/stress run. - workers: process.env.CI - ? 1 - : process.env.PLAYWRIGHT_WORKERS - ? Number(process.env.PLAYWRIGHT_WORKERS) - : 1, + // Top-level cap on TOTAL concurrent workers across all projects. Playwright + // schedules the projects below concurrently, so this — not any single + // project's own `workers` — is what bounds peak memory. + // + // This is an empirical ceiling, not the sum of the per-project caps. On a + // 10-core/16GB machine a full 262-test run was OOM-killed by the OS at 6 + // (97 tests in) and again at 3 (194 tests in) — in both cases the suite was + // otherwise healthy, so the limit is host memory, not test correctness. + // A 26-test subset of the shared-fixture project alone did fit at 3 + // (~4.8GB RSS / ~39 processes); the full suite mixes in the heavier + // native-host double-WebContents files and does not. Raise only with a + // completed full run and a fresh memory measurement. + // + // CI stays serial; PLAYWRIGHT_WORKERS overrides everything uniformly (see + // `explicitWorkers` above) for an explicit throughput/stress run. + workers: process.env.CI ? 1 : (explicitWorkers ?? 2), retries: process.env.CI ? 2 : 0, reporter: [['html', { open: 'never' }], ['list']], use: { @@ -23,4 +36,132 @@ export default defineConfig({ screenshot: 'only-on-failure', video: 'retain-on-failure', }, + projects: [ + { + name: 'native-host', + // Each worker owns a full Electron runtime, and DIMINA_NATIVE_HOST=1 + // mode adds a SECOND WebContents layer per window (service host + + // render guest, see native-host-devtools-elements.spec.ts) — the + // cold-boot phase is a heavier CPU/IO contention window than the other + // two projects. Local file-parallel runs here can starve new Electron + // launches and leave firstWindow() waiting forever, so no two files in + // THIS project ever run at once. + // + // A per-project cap alone is NOT equivalent to the pre-sharding global + // `workers: 1` — it stops this project from running two of its own + // files at once, but the other two projects still run alongside it. That + // was measured, not assumed: with all three overlapping, home-button's + // simulator-webview wait blew its 30s budget while the same file passed + // serially in 20.5s. Hence `dependencies` on the other two: this project + // runs alone first, under exactly the old conditions, and only then do + // the two lighter projects start. + workers: explicitWorkers ?? 1, + testMatch: [ + 'device-frame-integration.spec.ts', + 'home-button-no-tabbar.spec.ts', + 'home-button.spec.ts', + 'native-host-console-project-switch.spec.ts', + 'native-host-current-page.spec.ts', + 'native-host-devtools-console.spec.ts', + 'native-host-devtools-elements-navigate.spec.ts', + 'native-host-devtools-elements-respawn.spec.ts', + 'native-host-navigate-after-relaunch.spec.ts', + 'native-host-network-response-body.spec.ts', + 'native-host-open-in-editor.spec.ts', + 'native-host-render.spec.ts', + 'native-host-websocket-contract.spec.ts', + 'native-host-websocket-panel.spec.ts', + 'native-host-wxml-highlight.spec.ts', + 'window-info-follows-device.spec.ts', + 'native-host-nh1.spec.ts', + ], + }, + { + name: 'custom-launch-light', + // Every file here launches its own Electron instance in beforeAll, but + // without the native-host double-WebContents layer — structurally + // lighter at cold-boot than the native-host project. NOT empirically + // load-tested at workers>1 in this pass (only the shared-fixture + // project below was); kept at a conservative 2 pending a dedicated + // verification run. Drop to 1 if it turns out to reproduce the same + // firstWindow() starvation as native-host. + workers: explicitWorkers ?? 2, + // Starts only after native-host is done — see that project's comment. + dependencies: ['native-host'], + testMatch: [ + 'automator-compat.spec.ts', + 'automator-label-semantics.spec.ts', + 'console-filter-live.spec.ts', + 'dialog-zorder.spec.ts', + 'dock-consolidation-smoke.spec.ts', + 'dock-devtools-position-preset.spec.ts', + 'dock-keepalive-and-collapse.spec.ts', + 'dock-real-drag.spec.ts', + 'dock-resize-sync-regressions.spec.ts', + 'dock-resize-sync-regressions2.spec.ts', + 'dock-resize-sync.spec.ts', + 'dock-separator-drag.spec.ts', + 'dock-tab-reorder.spec.ts', + 'extension-host.spec.ts', + 'failure-behaviors.spec.ts', + 'host-dialog.spec.ts', + 'host-sidebar.spec.ts', + 'host-toolbar-lifecycle.spec.ts', + 'host-toolbar-port.spec.ts', + 'host-toolbar-replay.spec.ts', + 'host-toolbar.spec.ts', + 'internal-devtools-console-reopen.spec.ts', + 'qdml-filetypes.spec.ts', + 'standalone-devtools-flood-cold-start.spec.ts', + 'standalone-devtools-window-churn.spec.ts', + 'update-dialog.spec.ts', + 'workbench-backend.spec.ts', + ], + }, + { + name: 'shared-fixture', + // Electron cold-starts equal the WORKER count here, not the file count: + // fixtures.ts's `electronApp` fixture is worker-scoped, so every file a + // given worker picks up reuses that worker's single already-launched + // instance. Empirically verified: an 8-file/26-test subset of this + // project (app-launch, appdata-edit, broken-project, compile-mode-ux, + // console-filter-reset, device-picker-overlay, devtools-panel, + // devtools-tab-order) passed cleanly with zero failures/flakes and no + // leftover Electron processes at both workers:2 (73.9s) and workers:3 + // (86.9s, peak ~4.8GB RSS across ~39 Electron-related processes) — + // 2026-09-07, this machine. relaunch-resilience is the longest file in + // the whole suite (131s/6 tests); it's included below like every other + // member, but watch it first if this project's shard balance ever + // needs manual tuning. + workers: explicitWorkers ?? 3, + // Starts only after native-host is done — see that project's comment. + dependencies: ['native-host'], + testMatch: [ + 'app-launch.spec.ts', + 'appdata-edit.spec.ts', + 'broken-project.spec.ts', + 'compile-mode-ux.spec.ts', + 'console-filter-reset.spec.ts', + 'device-picker-overlay.spec.ts', + 'devtools-panel.spec.ts', + 'devtools-tab-order.spec.ts', + 'disk-sync.spec.ts', + 'editor-hot-reload.spec.ts', + 'editor-project-switch.spec.ts', + 'editor-theme-boot-no-white-flash.spec.ts', + 'extension-panels.spec.ts', + 'internal-devtools-window.spec.ts', + 'ipc-workflow.spec.ts', + 'minigame-launch.spec.ts', + 'panel-switching.spec.ts', + 'project-card-proportions.spec.ts', + 'project-edit.spec.ts', + 'project-list-category.spec.ts', + 'relaunch-resilience.spec.ts', + 'settings.spec.ts', + 'toolbar-visual.spec.ts', + 'wal-audit.spec.ts', + ], + }, + ], }) diff --git a/packages/devtools/src/main/services/projects/builtin-templates.test.ts b/packages/devtools/src/main/services/projects/builtin-templates.test.ts index 3dfa2779..00729b08 100644 --- a/packages/devtools/src/main/services/projects/builtin-templates.test.ts +++ b/packages/devtools/src/main/services/projects/builtin-templates.test.ts @@ -44,7 +44,7 @@ describe('BUILTIN_TEMPLATES', () => { expect(fs.existsSync(path.join(dir, 'pages', 'index', 'index.json'))).toBe(true) }) - it("the 'taro-todo' template ships its compiled-bundle file set (copied from dimina/fe/example/taro-todo)", () => { + it("the 'taro-todo' template ships its compiled-bundle file set (copied from dimina/examples/miniprogram/taro-todo)", () => { const taro = BUILTIN_TEMPLATES.find((t) => t.id === 'taro-todo')! const dir = taro.source!.path expect(fs.existsSync(path.join(dir, 'app.json'))).toBe(true) diff --git a/packages/devtools/src/simulator/service-apis/file/index.js b/packages/devtools/src/simulator/service-apis/file/index.js index 4e647011..7545a0a0 100644 --- a/packages/devtools/src/simulator/service-apis/file/index.js +++ b/packages/devtools/src/simulator/service-apis/file/index.js @@ -31,11 +31,14 @@ * fallback, by scanning the source text for that literal list. */ -import { getFileSystemManager as getUpstreamFileSystemManager } from './upstream-impl.js' +import { getFileSystemManager as getUpstreamFileSystemManager, VIRTUAL_FILE_PREFIX } from './upstream-impl.js' // Mirrors the value `wx.env.USER_DATA_PATH` answers (upstream // `core/base/index.js` owns that env object; this overlay never touches it). -const USER_DATA_PATH = 'difile://usr' +// `VIRTUAL_FILE_PREFIX` is re-exported below because `core/base/index.js` +// imports it directly from this module's path, and is derived from rather than +// duplicated here so the two stay in step if the scheme ever changes. +const USER_DATA_PATH = `${VIRTUAL_FILE_PREFIX}usr` /** Async methods with a working container backend in the devtools simulator. */ export const fileSystemManagerAPINames = [ @@ -74,12 +77,24 @@ function makeAsyncFail(name) { } } +// Upstream's `getFileSystemManager()` copies every prototype method onto the +// instance as a bound own property (so `Object.keys(fsm)` enumerates the API +// surface for compatibility layers). Own properties shadow the prototype, so a +// replacement written to the prototype would never run. Collect the surface +// from both, and always install the replacement as an own property on the +// singleton — that wins whichever way upstream exposes a method, and leaves the +// shared prototype untouched. function withUnsupportedSurfaceReplaced(fsm) { const proto = Object.getPrototypeOf(fsm) const supported = new Set(fileSystemManagerAPINames) - for (const name of Object.getOwnPropertyNames(proto)) { + const names = new Set([...Object.getOwnPropertyNames(proto), ...Object.getOwnPropertyNames(fsm)]) + for (const name of names) { if (name === 'constructor' || supported.has(name)) continue - proto[name] = name.endsWith('Sync') ? makeSyncThrow(name) : makeAsyncFail(name) + // Read through descriptors rather than `fsm[name]` so an accessor on + // either object is classified without being invoked. + const descriptor = Object.getOwnPropertyDescriptor(fsm, name) ?? Object.getOwnPropertyDescriptor(proto, name) + if (typeof descriptor?.value !== 'function') continue + fsm[name] = name.endsWith('Sync') ? makeSyncThrow(name) : makeAsyncFail(name) } return fsm } @@ -93,4 +108,4 @@ export function getFileSystemManager() { return instance } -export { USER_DATA_PATH } +export { USER_DATA_PATH, VIRTUAL_FILE_PREFIX } diff --git a/packages/devtools/src/simulator/service-apis/file/index.test.ts b/packages/devtools/src/simulator/service-apis/file/index.test.ts index 0e4287bd..86651049 100644 --- a/packages/devtools/src/simulator/service-apis/file/index.test.ts +++ b/packages/devtools/src/simulator/service-apis/file/index.test.ts @@ -26,6 +26,8 @@ import { describe, expect, it } from 'vitest' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' +import { getFileSystemManager } from './index.js' +import { upstreamCalls } from './upstream-impl.js' /** The full documented async FSM surface (see simulator-api-fsm.test.ts). */ const EXPECTED_ASYNC_METHODS = [ @@ -108,3 +110,32 @@ describe('fileSystemManagerAPINames (drives wx.canIUse for FileSystemManager.*)' expect(syncNames, `no *Sync name should be canIUse-advertised (found ${JSON.stringify(syncNames)})`).toEqual([]) }) }) + +describe('getFileSystemManager() unsupported-surface replacement', () => { + it('writeFileSync throws naming the method and why sync is unsupported', () => { + const fsm = getFileSystemManager() + expect(() => fsm.writeFileSync('difile://usr/a.txt', 'x')).toThrowError( + /FileSystemManager\.writeFileSync .*not supported/i, + ) + }) + + it('unzip fails through the fail callback and never reaches the upstream stand-in', () => { + const fsm = getFileSystemManager() + upstreamCalls.length = 0 + let failResult: { errMsg?: string } | undefined + fsm.unzip({ + zipFilePath: 'difile://usr/a.zip', + targetPath: 'difile://usr/out', + fail: (err: { errMsg?: string }) => { failResult = err }, + }) + expect(failResult?.errMsg, 'fail callback should fire with an unzip:fail errMsg').toMatch(/^unzip:fail/) + expect(upstreamCalls.some(([name]) => name === 'unzip'), 'upstream stand-in unzip should never run').toBe(false) + }) + + it('writeFile still delegates to the upstream stand-in', () => { + const fsm = getFileSystemManager() + upstreamCalls.length = 0 + fsm.writeFile({ filePath: 'difile://usr/a.txt', data: 'x' }) + expect(upstreamCalls.some(([name]) => name === 'writeFile'), 'supported methods must still reach upstream').toBe(true) + }) +}) diff --git a/packages/devtools/src/simulator/service-apis/file/upstream-impl.js b/packages/devtools/src/simulator/service-apis/file/upstream-impl.js index a63f4892..a2a2b82f 100644 --- a/packages/devtools/src/simulator/service-apis/file/upstream-impl.js +++ b/packages/devtools/src/simulator/service-apis/file/upstream-impl.js @@ -8,15 +8,64 @@ * the bundle. Outside that build (vitest importing the shim directly), this * stand-in satisfies the import with an inert manager so the shim module * loads and its static exports stay checkable. + * + * `getFileSystemManager()` here also has to reproduce the *shape* upstream + * builds, not just answer with any object: upstream copies its prototype + * methods onto the instance as bound own properties (see + * `dimina/fe/packages/service/src/api/core/file/index.js`), which shadow + * whatever the shim later assigns onto the prototype. Only a stand-in that + * does the same binding lets a vitest suite exercise the shim's + * `withUnsupportedSurfaceReplaced()` against that shadowing. */ -class StandInFileSystemManager {} +/** Calls the stand-in methods below observed, in call order, as `[name, opts]`. */ +export const upstreamCalls = [] + +class StandInFileSystemManager { + // Representative of the supported async surface (`fileSystemManagerAPINames` + // in `./index.js`): the shim must leave this one alone. + writeFile(opts = {}) { + upstreamCalls.push(['writeFile', opts]) + opts.success?.() + opts.complete?.() + } + + // Representative of an async method with no simulator container backend: + // the shim must replace this with a `fail`-callback stub. + unzip(opts = {}) { + upstreamCalls.push(['unzip', opts]) + opts.success?.() + opts.complete?.() + } + + // Representative of a `*Sync` method: the shim must replace this with a + // throwing stub, since the service thread's container bridge is async. + writeFileSync(...args) { + upstreamCalls.push(['writeFileSync', args]) + } +} + +const standInMethodNames = Object + .getOwnPropertyNames(StandInFileSystemManager.prototype) + .filter(name => name !== 'constructor') let instance export function getFileSystemManager() { if (!instance) { - instance = new StandInFileSystemManager() + const fsm = new StandInFileSystemManager() + // Mirror upstream's own binding: copy prototype methods as bound own + // properties so they shadow anything later assigned onto the prototype. + for (const name of standInMethodNames) { + fsm[name] = fsm[name].bind(fsm) + } + instance = fsm } return instance } + +// Real upstream derives this from `resolveVirtualFilePrefix()` +// (service/src/api/core/file/virtual-file-prefix.js); this stand-in only +// needs to satisfy the shim's re-export, so it hardcodes the same default +// scheme upstream falls back to when no override is configured. +export const VIRTUAL_FILE_PREFIX = 'difile://' diff --git a/packages/dimina-electron-runtime/build-assets.mjs b/packages/dimina-electron-runtime/build-assets.mjs index 2f8317ea..d1cf08f5 100644 --- a/packages/dimina-electron-runtime/build-assets.mjs +++ b/packages/dimina-electron-runtime/build-assets.mjs @@ -6,19 +6,26 @@ import { fileURLToPath } from 'node:url' const packageDir = dirname(fileURLToPath(import.meta.url)) const devtoolsDir = join(packageDir, '../devtools') const workspaceDir = join(packageDir, '../..') -// pnpm injects its cross-platform JS entry for package scripts. This asset -// assembly runs outside Turbo's cached graph, so the path is not a cache input. +// pnpm points this at its own entry for package scripts. This asset assembly +// runs outside Turbo's cached graph, so the path is not a cache input. // eslint-disable-next-line turbo/no-undeclared-env-vars const pnpmCliPath = process.env.npm_execpath if (!pnpmCliPath) { throw new Error('build-assets.mjs must be run from a pnpm script') } -const inspectBuild = spawnSync( - process.execPath, - [pnpmCliPath, '--filter', '@dimina-kit/inspect', 'build'], - { cwd: workspaceDir, stdio: 'inherit' }, -) +// The entry is a JS file under npm and older pnpm, but a platform-native +// executable under pnpm installed through corepack (…/pnpm//pnpm-native). +// Handing that to `node` makes it parse a Mach-O/ELF header as ESM and die with +// a SyntaxError, so dispatch on what the path actually is. +const pnpmEntryIsJs = /\.[cm]?js$/i.test(pnpmCliPath) +function runPnpm(args, cwd) { + return pnpmEntryIsJs + ? spawnSync(process.execPath, [pnpmCliPath, ...args], { cwd, stdio: 'inherit' }) + : spawnSync(pnpmCliPath, args, { cwd, stdio: 'inherit' }) +} + +const inspectBuild = runPnpm(['--filter', '@dimina-kit/inspect', 'build'], workspaceDir) if (inspectBuild.status !== 0) process.exit(inspectBuild.status ?? 1) for (const script of [ @@ -27,10 +34,7 @@ for (const script of [ 'build:preload', 'build:native-host', ]) { - const result = spawnSync(process.execPath, [pnpmCliPath, 'run', script], { - cwd: devtoolsDir, - stdio: 'inherit', - }) + const result = runPnpm(['run', script], devtoolsDir) if (result.status !== 0) process.exit(result.status ?? 1) } diff --git a/packages/dimina-electron-runtime/e2e/electron-entry.js b/packages/dimina-electron-runtime/e2e/electron-entry.js index 15204578..af6a7c8e 100644 --- a/packages/dimina-electron-runtime/e2e/electron-entry.js +++ b/packages/dimina-electron-runtime/e2e/electron-entry.js @@ -215,7 +215,30 @@ function waitForActivePage(bridge, { since, timeoutMs }) { async function runNativeHostNav(bridge, serviceWc, method, args) { const arg = method === 'navigateBack' ? (args[0] ?? { delta: 1 }) : (args[0] ?? {}) const since = bridge.getActiveBridgeId() - await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`) + // Wrapped the same way runNativeHostGeneric wraps its call. A bare + // executeJavaScript still awaits whatever `wx.()` returns, so a + // rejected nav promise fails this call — but Electron reports it as the + // generic "Script failed to execute", naming neither the method nor the + // reason. Keep awaiting the same outcome; only make the message readable. + await serviceWc.executeJavaScript(` + new Promise((resolve, reject) => { + const describe = (e) => { + if (e && e.errMsg) return String(e.errMsg) + if (e && e.message) return String(e.message) + try { return JSON.stringify(e) } catch { return String(e) } + } + try { + if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') { + reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host')) + return + } + Promise.resolve(wx.${method}(${JSON.stringify(arg)})).then( + () => resolve(undefined), + (e) => reject(new Error('wx.' + ${JSON.stringify(method)} + ' rejected: ' + describe(e))), + ) + } catch (e) { reject(new Error('wx.' + ${JSON.stringify(method)} + ' threw: ' + describe(e))) } + }) + `) const timeoutMs = method === 'navigateBack' ? 1500 : 2000 await waitForActivePage(bridge, { since, timeoutMs }) return { result: undefined } @@ -237,6 +260,28 @@ async function runNativeHostGeneric(serviceWc, method, args) { return { result } } +/** + * How many pages the SERVICE realm's router currently holds, or -1 when the + * service host isn't reachable yet. + * + * Render-side readiness (`getCurrentPage`, which reads the visible render + * webContents' URL) does NOT imply this: the service router's stack is + * populated over the bridge and can still be empty when a page is already + * painted. `wx.switchTab` and friends dereference `router.getPageInfo().route` + * synchronously, and the router's empty-stack fallback (`{ id }`, see + * dimina/fe/packages/service/src/core/router.js) carries no `route` — so + * driving navigation before this count is positive throws inside the service + * realm rather than failing the navigation cleanly. + */ +async function getServicePageCount(appId) { + const bridge = getBridge() + const serviceWc = bridge.getServiceWc(appId) + if (!serviceWc) return -1 + return serviceWc + .executeJavaScript(`(() => typeof getCurrentPages === 'function' ? getCurrentPages().length : -1)()`) + .catch(() => -1) +} + async function callWxMethod(method, args = [], appId) { const bridge = getBridge() const serviceWc = bridge.getServiceWc(appId) @@ -282,6 +327,7 @@ globalThis.__diminaE2eHooks = { getCurrentPage, getPageStack, getPageData, + getServicePageCount, callWxMethod, setDevice: setDeviceHook, } diff --git a/packages/dimina-electron-runtime/e2e/helpers.ts b/packages/dimina-electron-runtime/e2e/helpers.ts index eaa0e23c..b5c4f5ab 100644 --- a/packages/dimina-electron-runtime/e2e/helpers.ts +++ b/packages/dimina-electron-runtime/e2e/helpers.ts @@ -214,6 +214,41 @@ export async function getPageData( }, { appId, path }) } +/** + * Block until the SERVICE realm's router holds at least one page. + * + * A spec that boots an app and then drives navigation needs BOTH sides ready. + * `getCurrentPage` only proves the render side — it reads the visible render + * webContents' URL — and the service router's stack is filled separately over + * the bridge. `wx.switchTab`/`reLaunch`/`redirectTo` read + * `router.getPageInfo().route` synchronously, and the router's empty-stack + * fallback carries no `route`, so calling them in that window throws a bare + * TypeError inside the service realm instead of failing the navigation. + * + * This is a readiness gate on a real signal, not a settling delay: it returns + * as soon as the service side reports a page. + */ +export async function waitForServiceRouterReady( + electronApp: ElectronApplication, + appId?: string, + timeoutMs = 15000, +): Promise { + await pollUntil( + () => + electronApp + .evaluate((_electron, payload) => { + const hooks = (globalThis as Record).__diminaE2eHooks as { + getServicePageCount: (appId?: string) => Promise + } + return hooks.getServicePageCount(payload.appId) + }, { appId }) + .catch(() => -1), + (n) => n > 0, + timeoutMs, + 200, + ) +} + // ── Simulator helpers (ported verbatim from packages/devtools/e2e/helpers.ts — // these only ever touched webContents.getAllWebContents()/executeJavaScript, // never devtools' own UI) ──────────────────────────────────────────────── diff --git a/packages/dimina-electron-runtime/e2e/native-host-switchtab-rerender.spec.ts b/packages/dimina-electron-runtime/e2e/native-host-switchtab-rerender.spec.ts index 81e54671..36104a82 100644 --- a/packages/dimina-electron-runtime/e2e/native-host-switchtab-rerender.spec.ts +++ b/packages/dimina-electron-runtime/e2e/native-host-switchtab-rerender.spec.ts @@ -17,6 +17,7 @@ import { fileURLToPath } from 'url' import { openProject, waitForSimulatorWebview, closeProject, pollUntil, evalInSimulator, evalInWebContentsByUrl, getCurrentPage, callWxMethod, + waitForServiceRouterReady, } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -77,6 +78,9 @@ test.describe('native-host switchTab keeps rendered content on return', () => { await pollUntil(() => evalInSimulator(electronApp, `(() => document.querySelectorAll('.device-shell__webview').length)()`).catch(() => 0), (n) => n >= 1, 30000, 400) await waitActive('pages/home/home') + // waitActive only proves the RENDER side is on home; switchTab below reads + // the SERVICE router synchronously and throws if its stack is still empty. + await waitForServiceRouterReady(electronApp) }) test.afterAll(async () => { await closeProject(electronApp).catch(() => {}); await electronApp?.close().catch(() => {}) }) diff --git a/packages/workbench/src/dimina-json-schemas.ts b/packages/workbench/src/dimina-json-schemas.ts index 07b5da5a..82080f38 100644 --- a/packages/workbench/src/dimina-json-schemas.ts +++ b/packages/workbench/src/dimina-json-schemas.ts @@ -10,7 +10,7 @@ * page-level `usingComponents` / `component`. * - dimina/fe/packages/compiler/src/core/config-compiler.js processTabBarIcons: * reads `tabBar.list[].{pagePath,iconPath,selectedIconPath}` (+ text per examples). - * - example app.json files (dimina/fe/example/**\/app.json) for window keys, + * - example app.json files (dimina/examples/miniprogram/**\/app.json) for window keys, * `permission`, `requiredPrivateInfos`, `networkTimeout`, `debug`. * - project.config.json fields from the fixture + example projects * (description / setting / compileType / libVersion / appid / condition / diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a917b931..697e362b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,104 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.3.4 + version: 12.3.4 + +packages: + + '@pnpm/exe.darwin-arm64@12.3.4': + resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.3.4': + resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.linux-arm64-musl@12.3.4': + resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.3.4': + resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.3.4': + resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.3.4': + resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.3.4': + resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.3.4': + resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} + cpu: [x64] + os: [win32] + + pnpm@12.3.4: + resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.darwin-arm64@12.3.4': + optional: true + + '@pnpm/exe.darwin-x64@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64@12.3.4': + optional: true + + '@pnpm/exe.linux-x64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-x64@12.3.4': + optional: true + + '@pnpm/exe.win32-arm64@12.3.4': + optional: true + + '@pnpm/exe.win32-x64@12.3.4': + optional: true + + pnpm@12.3.4: + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.3.4 + '@pnpm/exe.darwin-x64': 12.3.4 + '@pnpm/exe.linux-arm64': 12.3.4 + '@pnpm/exe.linux-arm64-musl': 12.3.4 + '@pnpm/exe.linux-x64': 12.3.4 + '@pnpm/exe.linux-x64-musl': 12.3.4 + '@pnpm/exe.win32-arm64': 12.3.4 + '@pnpm/exe.win32-x64': 12.3.4 + +--- lockfileVersion: '9.0' settings: @@ -8,6 +109,9 @@ overrides: html-encoding-sniffer: 4.0.0 jsdom: 26.1.0 minimist@<1.2.8: ^1.2.8 + autoprefixer>browserslist: 4.28.7 + autoprefixer>caniuse-lite: 1.0.30001806 + browserslist@4.28.7>caniuse-lite: 1.0.30001806 importers: @@ -44,17 +148,17 @@ importers: specifier: ^7.29.7 version: 7.29.7 '@oxc-parser/binding-wasm32-wasi': - specifier: ^0.145.0 - version: 0.145.0 + specifier: ^0.148.0 + version: 0.148.0 '@vue/compiler-sfc': - specifier: ^3.5.41 - version: 3.5.41 + specifier: ^3.5.42 + version: 3.5.42 assert: specifier: ^2.1.0 version: 2.1.0 autoprefixer: - specifier: ^10.5.3 - version: 10.5.4(postcss@8.5.26) + specifier: ^10.5.4 + version: 10.5.4(postcss@8.5.28) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -62,10 +166,10 @@ importers: specifier: ^1.2.0 version: 1.2.0 cssnano: - specifier: 8.0.6 - version: 8.0.6(postcss@8.5.26) + specifier: 9.0.3 + version: 9.0.3(postcss@8.5.28) esbuild: - specifier: ^0.28.1 + specifier: ^0.28.2 version: 0.28.2 estree-walker: specifier: ^3.0.3 @@ -77,8 +181,8 @@ importers: specifier: ^12.0.0 version: 12.0.0 less: - specifier: ^4.9.0 - version: 4.9.0 + specifier: ^4.9.1 + version: 4.9.1 magic-string: specifier: 1.2.3 version: 1.2.3 @@ -86,23 +190,23 @@ importers: specifier: ^4.57.8 version: 4.57.8(tslib@2.8.1) oxc-parser: - specifier: ^0.145.0 - version: 0.145.0 + specifier: ^0.148.0 + version: 0.148.0 oxc-walker: specifier: ^1.1.1 - version: 1.1.1(@oxc-project/types@0.145.0)(oxc-parser@0.145.0)(rolldown@1.0.3) + version: 1.1.1(@oxc-project/types@0.148.0)(oxc-parser@0.148.0)(rolldown@1.0.3) path-browserify: specifier: ^1.0.1 version: 1.0.1 postcss: - specifier: ^8.5.26 - version: 8.5.26 + specifier: ^8.5.28 + version: 8.5.28 postcss-selector-parser: - specifier: ^7.1.5 - version: 7.1.5 + specifier: ^7.1.6 + version: 7.1.6 sass: - specifier: 1.102.0 - version: 1.102.0 + specifier: 1.104.0 + version: 1.104.0 source-map-js: specifier: ^1.2.1 version: 1.2.1 @@ -114,7 +218,7 @@ importers: version: 0.12.5 devDependencies: esbuild-wasm: - specifier: ^0.28.1 + specifier: ^0.28.2 version: 0.28.2 typescript: specifier: 5.9.2 @@ -146,7 +250,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/devkit: dependencies: @@ -189,7 +293,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/devtools: dependencies: @@ -295,7 +399,7 @@ importers: version: 2.0.0-alpha.41(@babel/runtime@7.29.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^4.1.4 version: 4.1.4(vitest@4.1.4) @@ -361,10 +465,10 @@ importers: version: 5.9.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + version: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/dimina-electron-runtime: dependencies: @@ -407,7 +511,7 @@ importers: version: 8.18.1 '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^4.1.4 version: 4.1.4(vitest@4.1.4) @@ -431,7 +535,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/electron-deck: dependencies: @@ -465,7 +569,7 @@ importers: version: 18.3.7(@types/react@18.3.28) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^4.1.4 version: 4.1.4(vitest@4.1.4) @@ -489,10 +593,10 @@ importers: version: 5.9.2 vite: specifier: ^8.0.16 - version: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + version: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/eslint-config: devDependencies: @@ -549,7 +653,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/inspect: dependencies: @@ -589,7 +693,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/typescript-config: {} @@ -609,7 +713,7 @@ importers: version: 18.3.28 '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^4.1.4 version: 4.1.4(vitest@4.1.4) @@ -633,7 +737,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages/workbench: dependencies: @@ -751,10 +855,10 @@ importers: version: 5.9.2 vite: specifier: ^8.0.8 - version: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + version: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) packages: @@ -970,8 +1074,8 @@ packages: '@codingame/monaco-vscode-workbench-service-override@34.0.3': resolution: {integrity: sha512-KKrFKz6I9F6c7pCfhmKGeXCxW1t0+gSyqsI+F8JLcW7Uvop2/IF1EjgAT1S0eKLi3ex5SCEnmEM8ggU7c5H+jQ==} - '@colordx/core@5.6.0': - resolution: {integrity: sha512-EDlcg/Hmlj1WuFh/Os9YhA+A2aasB2XlgwFfGePUDuW7fVHC07HBi5AFIMx0Ct1eM8kHM9NRyOtZ0MuVYa8xvg==} + '@colordx/core@6.3.0': + resolution: {integrity: sha512-skoCSPWAZTfawy3t6Qk1lqXsx1OPb/mCNYBpbG667+PmXIc7kt+rhjoS2SmYywaW9fnKaufPG59NztX6jpRNaw==} '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} @@ -1001,6 +1105,10 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} @@ -1628,12 +1736,6 @@ packages: '@cfworker/json-schema': optional: true - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@napi-rs/wasm-runtime@1.2.3': resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -1661,120 +1763,120 @@ packages: resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} engines: {node: ^18.17.0 || >=20.5.0} - '@oxc-parser/binding-android-arm-eabi@0.145.0': - resolution: {integrity: sha512-3CBAqOv61gR/n/t13hr681uFHYm0dQu3bbsEJtsMWYtLCQ8lSYjuJCIlroD4fbI0wz44jpjw3N+L7CwBCzYHyg==} + '@oxc-parser/binding-android-arm-eabi@0.148.0': + resolution: {integrity: sha512-pHASv9g5pASxb7akHERZNSkrEqPhFaUix98o7d9hbTpolnnFWl7UiRrcMhCsV1+iVO4/cJwKsbKRJTFNs2tdBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm64@0.145.0': - resolution: {integrity: sha512-VOinEGxVMIWI+67MJw+MKmaLDC6X3D4dNDLKJX4fidJ7sO0hsevYHaKBCqLozbg9Ny+1yb25yagJMQAw1U/jQA==} + '@oxc-parser/binding-android-arm64@0.148.0': + resolution: {integrity: sha512-sg/6Ez0KdAygsu0POELux9wN1Po2CP93WY8eNl4DBKIGprsd4QSHBXOb471Pu9i2OCD5sLkISSb2agZEhVn2Zw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.145.0': - resolution: {integrity: sha512-es9sOpZM3cdbXqqLHZ+5PF+6AyKnd2Z6gVJGvmXraX9jGz249Sb1V/Lf42UEuQPZsjp5iXMntxO4fWqHMJ2nRg==} + '@oxc-parser/binding-darwin-arm64@0.148.0': + resolution: {integrity: sha512-yiSJmzGUvCUaJT8X3j40gVcX+ckuHQMuiOtF8DvzTs5+JtB/7XuHFPp4M+vv5u+HlBtDUd4Ks5pyHpWz8mfnkg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.145.0': - resolution: {integrity: sha512-grIPJYT5aTB3IUp7iZz2grzzUQaTnnwIrb81tMWdFTzkCQQcnz0mWrXXEMWxSWidYp4a+DzI7ApLBEDuw2Xyhw==} + '@oxc-parser/binding-darwin-x64@0.148.0': + resolution: {integrity: sha512-6ZeklaamrMy4H2JmhvcJg6iip59tYILtuLaILxyAHT3l5FDxnI5ihVievAft5ZmAbqtlWHErOi1OpJK8gy1wcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.145.0': - resolution: {integrity: sha512-NimvYhXzF5QNIN42C1W339UNHYr5ESub4Th362q2u9NkSiGZ2X9exUshdmyRbIVIlZzo+29HIivhebkWY5g0SQ==} + '@oxc-parser/binding-freebsd-x64@0.148.0': + resolution: {integrity: sha512-vFsPx+a/qFECPnz/H8nC6x6MDvnWscLTCo/5muojEF54ERUq1kdgbvnWo95YnkhjF9sTIcG/uDxQBh1gffaufQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.145.0': - resolution: {integrity: sha512-697j9HvY/HwfhPESnNop7qUBjyoJKl8Q9YrHQgcGhFcoQbcelxAgVSRVv5b3a9lndvjQ6KcV0oFrKzXqLYA6xg==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.148.0': + resolution: {integrity: sha512-eOr3M+6iGbbxNL4PSS0VtsyQ2eOUxSBh00BqO22SbolDimPSYsBuLr/LCrZBkiqW2BoabhR6V4R8jrRAay7hjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.145.0': - resolution: {integrity: sha512-gnKpdjlnPet71TY18hLs29f0lQmt5dFig6w/hEhaUljGJSTri8n5In6CTgHnF2UFG9oiBXfqnf9pHUUrJpO5cQ==} + '@oxc-parser/binding-linux-arm-musleabihf@0.148.0': + resolution: {integrity: sha512-58ZKDw0mQRbCNfrd2IDyV4o8T7enzGERJn41BH2tjrZVGyiKiFzcfDicuB7Zcpb/1xIOrObovr8Dja6lZi8dLw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.145.0': - resolution: {integrity: sha512-2NCBH6l3J2k4YQIYZzcGNZq7J0+/kub//5+cXWTdhtc6UW47mGHpogVm7fwPhHU+Bw7iw9SBm5V0qlGFj6ZY7w==} + '@oxc-parser/binding-linux-arm64-gnu@0.148.0': + resolution: {integrity: sha512-Fnu95O4eZ5i++GPvIzBEZ8y4ddTLR+D9paYa8JRaRk6ZK7nHQiWP5xtrhcPQsXqgat1d7sU/d5rbbI0p1FTHSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-parser/binding-linux-arm64-musl@0.145.0': - resolution: {integrity: sha512-ih5NUVKrWOZN54DLGhP0YyCF2NjXl7stGiK73+xI4baFv2fL7n3K5BrUAjVDi/w73kCnwS3pJ/V/X8n027n/Yw==} + '@oxc-parser/binding-linux-arm64-musl@0.148.0': + resolution: {integrity: sha512-3CQy/BMdx7N7H3qrcPxUL+a2CwUZodUcf6oq8iJuNZ9C6Ol1aq3mcWzsgySJ7CHFLvpX21ZDPp1r1X0QLbu/AQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-parser/binding-linux-ppc64-gnu@0.145.0': - resolution: {integrity: sha512-pLOsK77bB2T0Qu+slW/kRI3zLfBAnbbfubGlzN5GROMbrzt8axAtx+0MZcGRuRX7LNDYzqM+ZeBLQbfYUg4rzQ==} + '@oxc-parser/binding-linux-ppc64-gnu@0.148.0': + resolution: {integrity: sha512-9LkaYvfiF8hMOw900csAvkf1oxE8XlmMeGowu5BcastSSwV8mKvKRMNU7HsV+ycyj1dQD8pX5qgOw8ja6SJacg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@oxc-parser/binding-linux-riscv64-gnu@0.145.0': - resolution: {integrity: sha512-dbS9FC4ziuyNNyFErVM6+bREi2+CbqUA7/xrOajcgnNUeX9QTsleQbOUJ2P6lCoT483H0ikg+gND4unNrygS9g==} + '@oxc-parser/binding-linux-riscv64-gnu@0.148.0': + resolution: {integrity: sha512-2GBiM9h26dR4WJfhoMvnFMnFLf7m/kYs4UMqjvrOfQG4BV1nuTJDH22Zc2MQr3INZF7nSKYQ6xlhD3hQ7A6gug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxc-parser/binding-linux-riscv64-musl@0.145.0': - resolution: {integrity: sha512-GtmdBGsNi+yCvxiJ4Bnrpo8+azePTedFqghDRtvKucyAGXZhqmuPRbqTHacfTGo1ukgFFqrzNlFxtXUnYjUjPA==} + '@oxc-parser/binding-linux-riscv64-musl@0.148.0': + resolution: {integrity: sha512-uPqZexvKJmEgq4mAu36qe2xTfXZE7oyik1R7KtZ5tl8qKlq1U1fIqTFRUEBZqRGvforoTrGIpatRzcoPKO66RA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxc-parser/binding-linux-s390x-gnu@0.145.0': - resolution: {integrity: sha512-U5uJHvwWbDV+DvVugJRA/JkKTmikONTBDtDLFNFQTfEQOBVMs6itFiBGM3IxFIJr0/uVlIKMTDQSireOQLXvpg==} + '@oxc-parser/binding-linux-s390x-gnu@0.148.0': + resolution: {integrity: sha512-9oUHvnTbp7ZraFsTC8PN6XhdhPSSxZumYvixWl7Smi353gEULvK6yV0sXNVrdFMHQeaDKFCi8TgDhNK7/A+Y+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxc-parser/binding-linux-x64-gnu@0.145.0': - resolution: {integrity: sha512-hz3a08R2fWvj/oGmNeQ9Us9AUfyYLagbNMh7nwdBbL/JAK88gu8i3hZg/Zv5xU4EdrFjiFXGmadLOFcZk/AlvA==} + '@oxc-parser/binding-linux-x64-gnu@0.148.0': + resolution: {integrity: sha512-2qhDSJwKzbSZzF7lDqqk8sr/yXsmwr3PeUa4/nazIF+zFAYz1gVPEfC34GQtGxzJUUmklaYAL63368LEfrMeyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-parser/binding-linux-x64-musl@0.145.0': - resolution: {integrity: sha512-im2ckBQcAnIN0qiGUOPuftIqmehwXSRx9q8Bb8gaFvjxoLI1lmW7bUABcr0YixVUae0j23+qiy/3g8DYr0Iqpg==} + '@oxc-parser/binding-linux-x64-musl@0.148.0': + resolution: {integrity: sha512-qQoPDZUFV0bh9xA09XydmkjMBpgc1ukJuhMvzQ9QeVmFaHTS9W5TE5CoLmSl3QQyUP9OuHO3x/WPZTIIZPWR3Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-parser/binding-openharmony-arm64@0.145.0': - resolution: {integrity: sha512-Ban4OInSMDkvzkaoROeg3AJVrYrlqjDA/GfJuLg0QL0MCVixj1WBy5lELiBxvHSTHR9KDo/0MzKZeHWeZ9cW0w==} + '@oxc-parser/binding-openharmony-arm64@0.148.0': + resolution: {integrity: sha512-1UGbaQWEXUCLqAmaR5kwRDjx/R4S5LQKZkM9CHmaHkuKhriOF32aRLfS0jCRNE2yGQJLMEA1z9UucbBVqjXnDw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-wasm32-wasi@0.145.0': - resolution: {integrity: sha512-6I2nvExtEqHeVncEFmVZsPgWUtPwZz+7Rn3QAS9f2TuQRt41E56rUspTFzVmRcWNV9mM9onmfir4VkPFjoIniA==} + '@oxc-parser/binding-wasm32-wasi@0.148.0': + resolution: {integrity: sha512-b6tt7/x2X2GyPaFVfMnsUuidcFWYdiZiYqJkLvOdMjb/ErhQw/8JYhhS+IqlDipRMuJqmc25OQEZpLu7ytcF4g==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - '@oxc-parser/binding-win32-arm64-msvc@0.145.0': - resolution: {integrity: sha512-mbTsMQKGpbdhjtaJUwhLyjoWRb0iIyIWuaguYQbS7IYsSo6KeRJQ3pb2P/9ZYYoQB9pV1oY/nD0SXTGFcfgIOA==} + '@oxc-parser/binding-win32-arm64-msvc@0.148.0': + resolution: {integrity: sha512-pWKdzRDNG2+NK4h/V6U/CYERcfYD6u28h5IB/VJVsrZaD3muvE58tUj22lieL5vLZ+XFi1GPv9YXckZbJZ9BLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.145.0': - resolution: {integrity: sha512-syaw++GhlFT4aJ9yaYgNKxUVAfE4cnP5Wk3iy9d3lLNwzWmmgXTb6sqQrcwVUq3Tv4RXAOZvr33bVe7C28G2Vg==} + '@oxc-parser/binding-win32-ia32-msvc@0.148.0': + resolution: {integrity: sha512-i3p4x+mvwtjcE1J5HM6V7ggsbXiznExN/4MkNyOy3dfXrVV3bnkSfmZxvo6/84qCVX4ShkpNE1SKt9biIF31GQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.145.0': - resolution: {integrity: sha512-dIZyXCxsOLILtDYeUGtiHN8GqgaT3J5FsuGI57YqWJg7EDZWBT+oFH2N+AjYMasupru3zy5K3ttCxxAF2Cscjg==} + '@oxc-parser/binding-win32-x64-msvc@0.148.0': + resolution: {integrity: sha512-Ye6vB7VQulghWYkYkECOBYFRVEizz4XyRTUAv+t8BuyurhKU7uD0P9eowL+mKG5Mf8MSYx+DI3Cm8SKZvYG7bQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1785,8 +1887,8 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - '@oxc-project/types@0.145.0': - resolution: {integrity: sha512-/UDI/xghp0wUJfvBu0fXIkn5nrDZCdesXo++ElC8UpDjzdFhB13k8XPXyMaSihaWPwgvdQXiJjRshVkaPEtaIQ==} + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} '@parcel/watcher-android-arm64@2.6.0': resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} @@ -2764,20 +2866,20 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - '@vue/compiler-core@3.5.41': - resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} + '@vue/compiler-core@3.5.42': + resolution: {integrity: sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==} - '@vue/compiler-dom@3.5.41': - resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} + '@vue/compiler-dom@3.5.42': + resolution: {integrity: sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==} - '@vue/compiler-sfc@3.5.41': - resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} + '@vue/compiler-sfc@3.5.42': + resolution: {integrity: sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==} - '@vue/compiler-ssr@3.5.41': - resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} + '@vue/compiler-ssr@3.5.42': + resolution: {integrity: sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==} - '@vue/shared@3.5.41': - resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} + '@vue/shared@3.5.42': + resolution: {integrity: sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==} '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} @@ -2980,8 +3082,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.11.12: - resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} + baseline-browser-mapping@2.11.21: + resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -3020,18 +3122,13 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.28.6: - resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - browserslist@4.28.8: - resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3098,14 +3195,11 @@ packages: caniuse-api@4.0.0: resolution: {integrity: sha512-B0hQ1OLyJuHTQSOWXvwibWqM6DCoqJdvBA6X1S/53bd4XU7LJ1yurIPlrsouol3mw1jh9pGI4ivubSpmJeIqCA==} - caniuse-lite@1.0.30001788: - resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} - caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} - caniuse-lite@1.0.30001809: - resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} centra@2.7.0: resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==} @@ -3334,23 +3428,23 @@ packages: engines: {node: '>=4'} hasBin: true - cssnano-preset-default@8.0.10: - resolution: {integrity: sha512-34uq3q7OXgmfxXn+MHTgIqysCLH7zRZZUMAwrBiO09F5/KLVD33iDFNRRUNYStKdj0SaAIUVr6Akao2Fpp0aVA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + cssnano-preset-default@9.0.3: + resolution: {integrity: sha512-UxvaUk/pNqHzjctjlaq4SSARlqPOdy7kA9Dho6fb1/xt/bYcar+57BFlSWpF5AvxjIfNK3Qkop5U9VDSUnuTvg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - cssnano-utils@6.0.4: - resolution: {integrity: sha512-j1z2mW4MqtcGM4I8TxXAdOXUifp1DZ5/bZnyYnCpHlzQ/ilCj2voLxE0aqEKnBDA5hF6HahSY13JU8kyZ1s8Mg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + cssnano-utils@7.0.2: + resolution: {integrity: sha512-X5B8Butd9TISH65XOoSYsq/wokHHjDK7eNujX3EcTf67T6uy2BySmT1SoaSXzlFdOsRf431FTENMsPrsc9JYHg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - cssnano@8.0.6: - resolution: {integrity: sha512-KDwqW0R35qIGDDWse4vRhrNtF558sUOcfuqCbB/h0bUP4+aBUpbGT5X2bQlE1gEACk+nDFAL045VyesanJFEug==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + cssnano@9.0.3: + resolution: {integrity: sha512-v4oRMYCXcJtI1MlGcwVgSP/TG/RrznC3iKQeNcFTQZZYWomp2j8CmgT9iiSO2v64K4LJSi6pWdyznmUUutLCKw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -3567,8 +3661,8 @@ packages: electron-publish@26.8.1: resolution: {integrity: sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==} - electron-to-chromium@1.5.404: - resolution: {integrity: sha512-3WJtd7/lVq2Jnuz6wed1l9+1ZD2u2Tet1/1NBc4Iedkmgbu+I7YuAqdAQ8T+VZtnwysMsAf3IqSq9D1gyZjA2g==} + electron-to-chromium@1.5.422: + resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==} electron-winstaller@5.4.0: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} @@ -4473,8 +4567,8 @@ packages: lazy-val@1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} - less@4.9.0: - resolution: {integrity: sha512-umRhrCH7fCi8Uj2RcwKjJdvUORTjeWqkdKx0LbcZvjIwsAVsnIAGcxHaqowPeBFBjQuWOeC/bve0AlpFzF/+SQ==} + less@4.9.1: + resolution: {integrity: sha512-orp15PfJvvNDIqJdVWzMI9Sjpjp3VTiw3sfvbB+67LlISTEn8uVT2EdYSuyl02BLvaftv6sdk9Umnxmm5rckmg==} engines: {node: '>=18'} hasBin: true @@ -4788,8 +4882,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.17: - resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -4832,8 +4926,8 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} hasBin: true - node-releases@2.0.53: - resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} engines: {node: '>=18'} nopt@8.1.0: @@ -4924,8 +5018,8 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxc-parser@0.145.0: - resolution: {integrity: sha512-zLMOUMlzFPqpgiQPTdSYBpJF1pQX1Ym2h+qb/BIT3KoMYf+cn87LDD0ifjcGcikUwKS2uQKH/y4n2t3dHS9bFQ==} + oxc-parser@0.148.0: + resolution: {integrity: sha512-syxUKHeUll89RIABQADcI7sikYrwyssvA6gj4phSSIPezKVM8yMaLAiLLSc7fmzVvrwybfFGFbW5zme9sX87rg==} engines: {node: ^20.19.0 || >=22.12.0} oxc-walker@1.1.1: @@ -5087,47 +5181,47 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-calc@10.1.1: - resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} - engines: {node: ^18.12 || ^20.9 || >=22.0} + postcss-calc@11.0.3: + resolution: {integrity: sha512-G0KjDwYacZXdyG0ijvvQleSh4AOTTNcjHny34t0Fh6v76dvTOMO1kyr7eKq3MPKD8Q7rxHzUXj4lpzT3cebatw==} + engines: {node: ^22.22.3 || ^24.15 || >=26.0} peerDependencies: - postcss: ^8.4.38 + postcss: ^8.5.25 - postcss-colormin@8.0.5: - resolution: {integrity: sha512-U/N+7tkHgHoD24NB6q9k9uFcRfVJRdzVSit0fM0T2L6fvnpUoTIAYgzkyjD56LHEoCoPNfkMcfTB47SW/lNegQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-colormin@9.0.2: + resolution: {integrity: sha512-h6uf6/HVT98tSLcQNQ8V0wuEQ8doBbxFwvgRqptdIeyCe8+aa29zNMPSVOkSLXXbCv25qDZaaGo/h/O4csTK3g==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-convert-values@8.0.4: - resolution: {integrity: sha512-ifBmAJBfpDymi7r/CqxYRJWlG+BLdVmusUC/kFPOqH/+TitdBeHA68CYqY79wXL+iQnqg6e4LGHA2Mte14X6Ig==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-convert-values@9.0.2: + resolution: {integrity: sha512-nsFL7tpxgaoF0G/w+fe8kkWvikxvwIb4ySW7PYzcmD1N60f0SA51sVnEuCrx7mHmwOvS1U1JIFvWHWHoK/bh8g==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-discard-comments@8.0.4: - resolution: {integrity: sha512-SU1uLYRQPKLJJmqEmqzu+Ze+9xDurBm7m/LOzXG/ANBAfAGLjbQF+oIyZnf3jV3F155q5rRMYXsoTNEJsPyHng==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-comments@9.0.2: + resolution: {integrity: sha512-QxOYI1haY3f9rPgY7i0W0+lQihsbvC/edPAAJAaSZmteI+UtGraWNnkWOZ/nCNZ6D+Ab8a9hksOCjJYBg/gF0A==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-discard-duplicates@8.0.4: - resolution: {integrity: sha512-/ugMYYTE+IpHpTmwz6/S8w+blCz9pZNroUgadTdlbPnHoLLWis9QvwmoyWui2mOgFzuRM6Me7AnoGU3i0Ua2ng==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-duplicates@9.0.2: + resolution: {integrity: sha512-FCTKRK9bH2ayM2AkhNicRw7z1ROmgk8p+QFsiLeG8MStgHkDH/NOjhJfQp1scvzk6u1szn1xvY52T1XVAaL68w==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-discard-empty@8.0.5: - resolution: {integrity: sha512-im0QtfNFa6V3SGPrfqzrWYuhzSz1ztWORKsuhZHQbWdKMh0DiNj3pmqBnABbhdKjyeW6nZ1oZYGh4dMc3biyaw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-empty@9.0.2: + resolution: {integrity: sha512-skKth+zcP//uuDos1GD/M0DKwEMDCbUnOZxz/M+BvjflCAS94czq7wwXNt2eug87rxPCik/okyXKJ2kBitHGEQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-discard-overridden@8.0.4: - resolution: {integrity: sha512-NqupxmnSSfWPJJDYbQk1qXWRwr+lw4Kyp/LSOjqwTpy/vSwo34EVXYKywgemSWpgh39P7Cs/G+tqynbRlXoJhw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-discard-overridden@9.0.2: + resolution: {integrity: sha512-cUGXcnnhOBZwE+dLFRYLoXYrLxCQtD0mp8nUEHlQI8KdDXhaIHL7DCWUkXXEcNFon11xROYOP9E+plzJ5MSjXw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} @@ -5159,41 +5253,41 @@ packages: yaml: optional: true - postcss-merge-longhand@8.0.4: - resolution: {integrity: sha512-ammolHhMvuTz/L9YN/ge1SWbUDcD3Y9o2gXlo3S6H4MCJHheyVPwcI/LAnSuNT4gcUmSF9pNTXo7+3M/2MFPAQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-merge-longhand@9.0.3: + resolution: {integrity: sha512-JJLx47+h7TIIThb+VE3pFHZUY/HKXgXa+yAZlN0FBX2IsIURJuOavS8gQwUvj5oluzxxRA9IjR95rLllbeybBw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-merge-rules@8.0.5: - resolution: {integrity: sha512-jz+QaKz4VhjsqTVsJMRuH+EvrX1F9l6kYFuF8g6geVxFoYnPEZN4G+zqCb6zhxOIcc2aeAo38XEl2pOIZi2gBw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-merge-rules@9.0.3: + resolution: {integrity: sha512-Wd/r16vrGdC49ZeQesxgNrNdQkAGoKL51UGHd3MwyH3CHhvqHkRtXZKKEnblecuuWmvjWqptC4VA3BeAHAVrzQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-minify-font-values@8.0.4: - resolution: {integrity: sha512-wZZgJ87U5WbQCEM0EOY/oM7M1a+sqW6vrOClGC9lNbsY0HYmbMFvFuukBn8PzF+ikKmaT0QB+uSAHTRLzMkD+A==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-font-values@9.0.2: + resolution: {integrity: sha512-+QEc9ILK7sz/6LDnFITuvipMnX71A+v7b7VX3AyiN3WCIoJtc4xQovogeiOsX7CxXtwPKmk2z1BuI8h3TSnQEA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-minify-gradients@8.0.6: - resolution: {integrity: sha512-NsqlG34WSO44NOHVT6m6ZWpN80fK2l5Zt73PMDXBJKT6fO6GPnI0NBi6xIVFr/naej3ukVUlIFQGxb7JGaa9Zg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-gradients@9.0.2: + resolution: {integrity: sha512-8EefxPsmS/RqMgJUBhx5N6hoWelHK8tW7DFUN8NLNrH1WSA4jvWRZc1OgzY0IheODiFsr/xiiOAHrPUQJFeMIg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-minify-params@8.0.4: - resolution: {integrity: sha512-TDx9O/ni7KazRK32pVvoo3MjExyVxWE0949vB2XZ0oHnxdAtQFHa9oG21wWkvsOL3osjiOVejwDLe6a0d0EuSw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-params@9.0.2: + resolution: {integrity: sha512-T8h6+8/JZQzTUlsw8oF3xdWIgrjw6dCpqdpUrIatrL852HNqE7w47C4mFrZADuu7IgT+4a3zRAX1nd112sAvwQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-minify-selectors@8.0.5: - resolution: {integrity: sha512-i+PlhVCaPa7xevjhpActH2PrP27kDNSuuFf/ZGk8YrIcd1pd3Mfcfiv8H7dbY1/vz7U+QparioAtJaFV+IEwQg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-minify-selectors@9.0.3: + resolution: {integrity: sha512-mElz5y5+pMisgaN1VgschjOQBh8I9fga/OhJUX+VKyQ14ebukeB3xZRlbu+ebIKI/+qT8tj7dfx0AEQ6gWCPOg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} @@ -5201,97 +5295,97 @@ packages: peerDependencies: postcss: ^8.2.14 - postcss-normalize-charset@8.0.5: - resolution: {integrity: sha512-l0L1rIe9YHbLaf4xfL2pkkQV3gq/QIy8D0bjsaHYdGVgR+eQk7IBp3dUdIKe6XuG7DzFiCwJn248nvAFAi5CjA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-charset@9.0.2: + resolution: {integrity: sha512-2mFe06u9nwCdG+swpN3YmcRBNAZTR1IocNvN6Lzi7kKXjB0LhEOSGeu4B6rbV84NtgdkJwdulzeh1AdABM9N5Q==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-display-values@8.0.4: - resolution: {integrity: sha512-F8DGtzelJDV6S5mhcugc+EJ8sXI+kFv2PBedkfMWqd8mvTE4qyy3kva6AQwjy2+L2lZ9TGTkSumdq+n3EhGeAw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-display-values@9.0.2: + resolution: {integrity: sha512-SHON0J7MuPrwpDAz4fHQgVNBfivLNKW8hgtO0WM3d5P0vFargQ4tCUNYdVBHfXr1xRFmmifRvJKmahksaHHn+w==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-positions@8.0.4: - resolution: {integrity: sha512-anOMhqe6Z0OP4jvchK1v1hIG08kO7rsp8uHrxT2+XaKp8wbvVcUO3QHUzRmQDazWUPtRzy1h2UlixyJenF42sg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-positions@9.0.2: + resolution: {integrity: sha512-2KQaPVbXUm1oUViZXvD6nsuRiAr81J3X2dRSeK1mCdPyMSUBQ76TpAjrVvbceicdbs3Bo2j9qkfO9iZSMJBsGQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-repeat-style@8.0.4: - resolution: {integrity: sha512-vY8+k+/bFT7B8gzlHVye8FxBTMEpUAQxAgb4UYFAGGsKmXfwEvc7VPs+kpFNrTeqVKAvVh8+VVhZ+f3S4TWJPg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-repeat-style@9.0.2: + resolution: {integrity: sha512-+EropmN1W6gVnmwbhP8Tb7BmmqXgRCFJqtWFTcA0nZBz7aGj4zXL9EWyQ20DkA0Vd+ladQOmNuaUsc0jCeXT5Q==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-string@8.0.4: - resolution: {integrity: sha512-dWnV1frIV1XUlSYzJudceAbQ7PGyho9EgVHQXMcAoog6Nwnet3G9rz8IXcCb0EF2eefmpYG9hDT9+yWJOxc5zw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-string@9.0.2: + resolution: {integrity: sha512-vzJqaYpeG/rYWVDMpfxA3I2XNRlkK7XKYFINAfYLKiXn0SDndmvQYZhYEQdpvapDy7QAiK1/4iPVcfxsjWfU1w==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-timing-functions@8.0.4: - resolution: {integrity: sha512-VANJsokAWdQ03ZJwmcdEKXJjVHRtMgalB/BgbCgN3CRTdbwB5TXlSDRmdShSCxpnZs8WLAm0mPa//p5o5D/ZTg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-timing-functions@9.0.2: + resolution: {integrity: sha512-CZ5T2XvUra6kJDBHZ+KQYuY0QJhpGv3atiLbkCLVk3BnBwh49zhNid/+IQrxQFwYkJ54oVJQuqrj6J4eaOFHTw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-unicode@8.0.4: - resolution: {integrity: sha512-SxVEoFdYed0bxxwZwAXnaAZ2lzNb5jtiQvYWyMIEqnGOzCuztWpVO8LvsqpOfUIacOb6oYaWi7WOAZR36OhYqw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-unicode@9.0.2: + resolution: {integrity: sha512-AOT0whCCcKASm1Ee8pchg03xgFxJopQhP1/G7CEt5X9LBD6EV4zL3a8duDJEGXYaCwuxjtwywRm8K5L6o0OJLA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-url@8.0.4: - resolution: {integrity: sha512-JKZW8KRHkLEYkUb/55n3knDUMZeyvxlTv7ZA82bcHiCjL81b0Yyf9fIUBAYKZ6ucDgKfCDM78xhXuteVlk/o9Q==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-url@9.0.2: + resolution: {integrity: sha512-oGfMuPEcmjon+08+gKpuqFUPn6/B4uTAJSHRlJe6h0xeB0baq5Zm/Swk0Jyz8mzVTNOIh8PuMUvWP+tVeSpS4A==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-normalize-whitespace@8.0.5: - resolution: {integrity: sha512-xuFovful3vVIA9fWWmAL2pG1Qr95Udj2MrLgjU50C6r+Xnh3s6t2BN1xXQ8lnCOCAtzMvMpPWYlnAh4nSv5VEQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-normalize-whitespace@9.0.2: + resolution: {integrity: sha512-gLkQNyMsT5xx1kbeT7vUmxwoIt6o5MA+1pX4LBc89ipX1GPYUpf5sv5IA0iikKDn05Zo78IsSFWc/C5FcYxRCQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-ordered-values@8.0.5: - resolution: {integrity: sha512-aDL7nPR7w1foUqo4rN6QY1Kr45+SYELnmUg5taZehh7FfFdxQmUjbuOOBuffIXjiHV45zqgFHCb4azsWW19jyw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-ordered-values@9.0.2: + resolution: {integrity: sha512-9DDzlg3E8ZOU/5bgCF4RCVXaEeoOuh3v23BT+ka8Mh3RpEFfpCg1kfpt1v38voIqV4gRZ1S1QHlnHWrzSzWZoA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-reduce-initial@8.0.5: - resolution: {integrity: sha512-Bh5d7zjH1Ai9LzJ1qULUZIaC18cniD+wA6gFlOPglMxJRC7meXbuPvpJ/zHQGg5QKwQgit++XQhDTiEIhCk8Ow==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-reduce-initial@9.0.2: + resolution: {integrity: sha512-rzSZ5ns9W/OGfwKPSEmlGMr0DsZy2exNT8uEKNU49GK+lgNnF+pKbPq0ZcHvFYn9oO6U+C6RfVuWWNgzGwxCJw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-reduce-transforms@8.0.4: - resolution: {integrity: sha512-ZGU7/R0GmjE3x2mOGB0g9Ohx74X+JjYX62RB5IBYLOe6R0fuHGZ76HtTY2CdM5NE8Yey/ew5clEf8iZQnjDjWA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-reduce-transforms@9.0.2: + resolution: {integrity: sha512-UXR9hvucM/VKwLDdcPFxszYAMyxLmpYACdxlPnNJ+6MKi+sEJHTMiUlVY3KRRuO00kuOjZ+t8M/I7N/gFMvwfg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 postcss-selector-parser@6.1.4: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} - postcss-selector-parser@7.1.5: - resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==} + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} - postcss-svgo@8.0.6: - resolution: {integrity: sha512-auQHNmduFFHzz1sWYM3sroW7IDMzxshlLjnwAALfkrAgPegHFats+jMK6BRCcdIulxPmqXFSQvgoAX4vw8I59g==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-svgo@9.0.2: + resolution: {integrity: sha512-FY1/AvWkMyXIlc3dFtwGdZDLdK12VAQqdqLOBKIQPsh574UZ5GfzMigeccvnuzEWPOySh/4LkTfGELDtnuBPXg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 - postcss-unique-selectors@8.0.4: - resolution: {integrity: sha512-Sby0EtmD1mlvcZSWP5eXjxhxVgvXE5QVRzd+8NH0IbF+lYQIM57ybwAvJYYtI/fexMgCWcRnDETovjKOcLJb8w==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + postcss-unique-selectors@9.0.2: + resolution: {integrity: sha512-Jyl/5yYy8VuWRCkSiI/5n75bEWty8wYho3eB1w4/ZK2XSW6AnNVJ6tGuWaCmJt0M5+ouHb/m+qXzFOsG3tOWww==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -5300,8 +5394,8 @@ packages: resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} postject@1.0.0-alpha.6: @@ -5580,13 +5674,8 @@ packages: sanitize-filename@1.6.4: resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} - sass@1.102.0: - resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==} - engines: {node: '>=20.19.0'} - hasBin: true - - sass@1.103.1: - resolution: {integrity: sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==} + sass@1.104.0: + resolution: {integrity: sha512-btHMApW2bgolvClhRW8AlQJzgI9lUB3pPSofBQQT+E46GWvf9o0TVQ13SYv5riWZVFyPN+JNz3TKW9XhBlc10w==} engines: {node: '>=20.19.0'} hasBin: true @@ -5799,11 +5888,11 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - stylehacks@8.0.4: - resolution: {integrity: sha512-irgZeyYBFVkb8k7yTRQ9No/bTniTGqzptGVMG1/Mj3N2YnI8nIozzY1pcok4i01NfrCSqvc0PdQB6O6kr6dJHw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + stylehacks@9.0.3: + resolution: {integrity: sha512-ffR7soMPCLSZoye/H4gEcOt7fpRmm+1/Mq7j4L9Hex2284Ci0oUkPSiRSpDW0X3tptLtZqGUsOxZ64fy6izthA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} peerDependencies: - postcss: ^8.5.26 + postcss: ^8.5.28 sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} @@ -6045,8 +6134,8 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - update-browserslist-db@1.3.1: - resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6456,10 +6545,10 @@ snapshots: '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -6471,8 +6560,8 @@ snapshots: '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -6481,7 +6570,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.8 + browserslist: 4.28.9 lru-cache: 5.1.1 semver: 6.3.1 @@ -6490,7 +6579,7 @@ snapshots: '@babel/helper-module-imports@7.28.6': dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -6512,7 +6601,7 @@ snapshots: '@babel/helpers@7.29.2': dependencies: '@babel/template': 7.28.6 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/parser@7.29.7': dependencies: @@ -6527,17 +6616,17 @@ snapshots: '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.28.6 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -6730,7 +6819,7 @@ snapshots: '@codingame/monaco-vscode-view-status-bar-service-override': 34.0.3 '@codingame/monaco-vscode-view-title-bar-service-override': 34.0.3 - '@colordx/core@5.6.0': {} + '@colordx/core@6.3.0': {} '@csstools/color-helpers@5.1.0': {} @@ -6752,6 +6841,8 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.14.0 @@ -7470,14 +7561,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 @@ -7516,74 +7607,74 @@ snapshots: dependencies: semver: 7.8.5 - '@oxc-parser/binding-android-arm-eabi@0.145.0': + '@oxc-parser/binding-android-arm-eabi@0.148.0': optional: true - '@oxc-parser/binding-android-arm64@0.145.0': + '@oxc-parser/binding-android-arm64@0.148.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.145.0': + '@oxc-parser/binding-darwin-arm64@0.148.0': optional: true - '@oxc-parser/binding-darwin-x64@0.145.0': + '@oxc-parser/binding-darwin-x64@0.148.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.145.0': + '@oxc-parser/binding-freebsd-x64@0.148.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.145.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.148.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.145.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.148.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.145.0': + '@oxc-parser/binding-linux-arm64-gnu@0.148.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.145.0': + '@oxc-parser/binding-linux-arm64-musl@0.148.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.145.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.148.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.145.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.148.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.145.0': + '@oxc-parser/binding-linux-riscv64-musl@0.148.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.145.0': + '@oxc-parser/binding-linux-s390x-gnu@0.148.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.145.0': + '@oxc-parser/binding-linux-x64-gnu@0.148.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.145.0': + '@oxc-parser/binding-linux-x64-musl@0.148.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.145.0': + '@oxc-parser/binding-openharmony-arm64@0.148.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.145.0': + '@oxc-parser/binding-wasm32-wasi@0.148.0': dependencies: '@emnapi/core': 2.0.0-alpha.3 '@emnapi/runtime': 2.0.0-alpha.3 '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) - '@oxc-parser/binding-win32-arm64-msvc@0.145.0': + '@oxc-parser/binding-win32-arm64-msvc@0.148.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.145.0': + '@oxc-parser/binding-win32-ia32-msvc@0.148.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.145.0': + '@oxc-parser/binding-win32-x64-msvc@0.148.0': optional: true '@oxc-project/types@0.124.0': {} '@oxc-project/types@0.133.0': {} - '@oxc-project/types@0.145.0': {} + '@oxc-project/types@0.148.0': {} '@parcel/watcher-android-arm64@2.6.0': optional: true @@ -8125,14 +8216,14 @@ snapshots: dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': @@ -8399,20 +8490,20 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) - '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) - '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': dependencies: @@ -8426,7 +8517,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + vitest: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/expect@4.1.4': dependencies: @@ -8437,29 +8528,29 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.4(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) - '@vitest/mocker@4.1.4(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.4(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.4': dependencies: @@ -8491,37 +8582,37 @@ snapshots: '@vscode/l10n@0.0.18': {} - '@vue/compiler-core@3.5.41': + '@vue/compiler-core@3.5.42': dependencies: '@babel/parser': 7.29.8 - '@vue/shared': 3.5.41 + '@vue/shared': 3.5.42 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.41': + '@vue/compiler-dom@3.5.42': dependencies: - '@vue/compiler-core': 3.5.41 - '@vue/shared': 3.5.41 + '@vue/compiler-core': 3.5.42 + '@vue/shared': 3.5.42 - '@vue/compiler-sfc@3.5.41': + '@vue/compiler-sfc@3.5.42': dependencies: '@babel/parser': 7.29.8 - '@vue/compiler-core': 3.5.41 - '@vue/compiler-dom': 3.5.41 - '@vue/compiler-ssr': 3.5.41 - '@vue/shared': 3.5.41 + '@vue/compiler-core': 3.5.42 + '@vue/compiler-dom': 3.5.42 + '@vue/compiler-ssr': 3.5.42 + '@vue/shared': 3.5.42 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.26 + postcss: 8.5.28 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.41': + '@vue/compiler-ssr@3.5.42': dependencies: - '@vue/compiler-dom': 3.5.41 - '@vue/shared': 3.5.41 + '@vue/compiler-dom': 3.5.42 + '@vue/shared': 3.5.42 - '@vue/shared@3.5.41': {} + '@vue/shared@3.5.42': {} '@xmldom/xmldom@0.8.13': {} @@ -8735,20 +8826,20 @@ snapshots: autoprefixer@10.5.0(postcss@8.5.10): dependencies: - browserslist: 4.28.2 - caniuse-lite: 1.0.30001788 + browserslist: 4.28.7 + caniuse-lite: 1.0.30001806 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.10 postcss-value-parser: 4.2.0 - autoprefixer@10.5.4(postcss@8.5.26): + autoprefixer@10.5.4(postcss@8.5.28): dependencies: - browserslist: 4.28.6 + browserslist: 4.28.7 caniuse-lite: 1.0.30001806 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -8769,7 +8860,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.11.12: {} + baseline-browser-mapping@2.11.21: {} binary-extensions@2.3.0: {} @@ -8817,29 +8908,21 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: + browserslist@4.28.7: dependencies: - baseline-browser-mapping: 2.11.12 - caniuse-lite: 1.0.30001809 - electron-to-chromium: 1.5.404 - node-releases: 2.0.53 - update-browserslist-db: 1.3.1(browserslist@4.28.2) - - browserslist@4.28.6: - dependencies: - baseline-browser-mapping: 2.11.12 - caniuse-lite: 1.0.30001809 - electron-to-chromium: 1.5.404 - node-releases: 2.0.53 - update-browserslist-db: 1.3.1(browserslist@4.28.6) + baseline-browser-mapping: 2.11.21 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.422 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.7) - browserslist@4.28.8: + browserslist@4.28.9: dependencies: - baseline-browser-mapping: 2.11.12 - caniuse-lite: 1.0.30001809 - electron-to-chromium: 1.5.404 - node-releases: 2.0.53 - update-browserslist-db: 1.3.1(browserslist@4.28.8) + baseline-browser-mapping: 2.11.21 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.422 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.9) buffer-equal@0.0.1: {} @@ -8858,7 +8941,7 @@ snapshots: builder-util-runtime@9.5.1: dependencies: debug: 4.4.3 - sax: 1.6.0 + sax: 1.6.1 transitivePeerDependencies: - supports-color @@ -8939,14 +9022,12 @@ snapshots: caniuse-api@4.0.0: dependencies: - browserslist: 4.28.8 - caniuse-lite: 1.0.30001809 - - caniuse-lite@1.0.30001788: {} + browserslist: 4.28.9 + caniuse-lite: 1.0.30001810 caniuse-lite@1.0.30001806: {} - caniuse-lite@1.0.30001809: {} + caniuse-lite@1.0.30001810: {} centra@2.7.0(debug@4.4.3): dependencies: @@ -9182,48 +9263,47 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@8.0.10(postcss@8.5.26): - dependencies: - browserslist: 4.28.8 - cssnano-utils: 6.0.4(postcss@8.5.26) - postcss: 8.5.26 - postcss-calc: 10.1.1(postcss@8.5.26) - postcss-colormin: 8.0.5(postcss@8.5.26) - postcss-convert-values: 8.0.4(postcss@8.5.26) - postcss-discard-comments: 8.0.4(postcss@8.5.26) - postcss-discard-duplicates: 8.0.4(postcss@8.5.26) - postcss-discard-empty: 8.0.5(postcss@8.5.26) - postcss-discard-overridden: 8.0.4(postcss@8.5.26) - postcss-merge-longhand: 8.0.4(postcss@8.5.26) - postcss-merge-rules: 8.0.5(postcss@8.5.26) - postcss-minify-font-values: 8.0.4(postcss@8.5.26) - postcss-minify-gradients: 8.0.6(postcss@8.5.26) - postcss-minify-params: 8.0.4(postcss@8.5.26) - postcss-minify-selectors: 8.0.5(postcss@8.5.26) - postcss-normalize-charset: 8.0.5(postcss@8.5.26) - postcss-normalize-display-values: 8.0.4(postcss@8.5.26) - postcss-normalize-positions: 8.0.4(postcss@8.5.26) - postcss-normalize-repeat-style: 8.0.4(postcss@8.5.26) - postcss-normalize-string: 8.0.4(postcss@8.5.26) - postcss-normalize-timing-functions: 8.0.4(postcss@8.5.26) - postcss-normalize-unicode: 8.0.4(postcss@8.5.26) - postcss-normalize-url: 8.0.4(postcss@8.5.26) - postcss-normalize-whitespace: 8.0.5(postcss@8.5.26) - postcss-ordered-values: 8.0.5(postcss@8.5.26) - postcss-reduce-initial: 8.0.5(postcss@8.5.26) - postcss-reduce-transforms: 8.0.4(postcss@8.5.26) - postcss-svgo: 8.0.6(postcss@8.5.26) - postcss-unique-selectors: 8.0.4(postcss@8.5.26) - - cssnano-utils@6.0.4(postcss@8.5.26): - dependencies: - postcss: 8.5.26 - - cssnano@8.0.6(postcss@8.5.26): - dependencies: - cssnano-preset-default: 8.0.10(postcss@8.5.26) - lilconfig: 3.1.3 - postcss: 8.5.26 + cssnano-preset-default@9.0.3(postcss@8.5.28): + dependencies: + browserslist: 4.28.9 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 + postcss-calc: 11.0.3(postcss@8.5.28) + postcss-colormin: 9.0.2(postcss@8.5.28) + postcss-convert-values: 9.0.2(postcss@8.5.28) + postcss-discard-comments: 9.0.2(postcss@8.5.28) + postcss-discard-duplicates: 9.0.2(postcss@8.5.28) + postcss-discard-empty: 9.0.2(postcss@8.5.28) + postcss-discard-overridden: 9.0.2(postcss@8.5.28) + postcss-merge-longhand: 9.0.3(postcss@8.5.28) + postcss-merge-rules: 9.0.3(postcss@8.5.28) + postcss-minify-font-values: 9.0.2(postcss@8.5.28) + postcss-minify-gradients: 9.0.2(postcss@8.5.28) + postcss-minify-params: 9.0.2(postcss@8.5.28) + postcss-minify-selectors: 9.0.3(postcss@8.5.28) + postcss-normalize-charset: 9.0.2(postcss@8.5.28) + postcss-normalize-display-values: 9.0.2(postcss@8.5.28) + postcss-normalize-positions: 9.0.2(postcss@8.5.28) + postcss-normalize-repeat-style: 9.0.2(postcss@8.5.28) + postcss-normalize-string: 9.0.2(postcss@8.5.28) + postcss-normalize-timing-functions: 9.0.2(postcss@8.5.28) + postcss-normalize-unicode: 9.0.2(postcss@8.5.28) + postcss-normalize-url: 9.0.2(postcss@8.5.28) + postcss-normalize-whitespace: 9.0.2(postcss@8.5.28) + postcss-ordered-values: 9.0.2(postcss@8.5.28) + postcss-reduce-initial: 9.0.2(postcss@8.5.28) + postcss-reduce-transforms: 9.0.2(postcss@8.5.28) + postcss-svgo: 9.0.2(postcss@8.5.28) + postcss-unique-selectors: 9.0.2(postcss@8.5.28) + + cssnano-utils@7.0.2(postcss@8.5.28): + dependencies: + postcss: 8.5.28 + + cssnano@9.0.3(postcss@8.5.28): + dependencies: + cssnano-preset-default: 9.0.3(postcss@8.5.28) + postcss: 8.5.28 csso@5.0.5: dependencies: @@ -9467,7 +9547,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-to-chromium@1.5.404: {} + electron-to-chromium@1.5.422: {} electron-winstaller@5.4.0: dependencies: @@ -10603,7 +10683,7 @@ snapshots: lazy-val@1.0.5: {} - less@4.9.0: + less@4.9.1: dependencies: copy-anything: 3.0.5 parse-node-version: 1.0.1 @@ -10736,7 +10816,7 @@ snapshots: magicast@0.5.3: dependencies: - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/types': 7.29.7 source-map-js: 1.2.1 @@ -10918,7 +10998,7 @@ snapshots: nanoid@3.3.11: {} - nanoid@3.3.17: {} + nanoid@3.3.18: {} natural-compare@1.4.0: {} @@ -10926,7 +11006,7 @@ snapshots: dependencies: debug: 3.2.7 iconv-lite: 0.4.24 - sax: 1.6.0 + sax: 1.6.1 transitivePeerDependencies: - supports-color optional: true @@ -10975,7 +11055,7 @@ snapshots: transitivePeerDependencies: - supports-color - node-releases@2.0.53: {} + node-releases@2.0.54: {} nopt@8.1.0: dependencies: @@ -11086,34 +11166,34 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxc-parser@0.145.0: + oxc-parser@0.148.0: dependencies: - '@oxc-project/types': 0.145.0 + '@oxc-project/types': 0.148.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.145.0 - '@oxc-parser/binding-android-arm64': 0.145.0 - '@oxc-parser/binding-darwin-arm64': 0.145.0 - '@oxc-parser/binding-darwin-x64': 0.145.0 - '@oxc-parser/binding-freebsd-x64': 0.145.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.145.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.145.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.145.0 - '@oxc-parser/binding-linux-arm64-musl': 0.145.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.145.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.145.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.145.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.145.0 - '@oxc-parser/binding-linux-x64-gnu': 0.145.0 - '@oxc-parser/binding-linux-x64-musl': 0.145.0 - '@oxc-parser/binding-openharmony-arm64': 0.145.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.145.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.145.0 - '@oxc-parser/binding-win32-x64-msvc': 0.145.0 - - oxc-walker@1.1.1(@oxc-project/types@0.145.0)(oxc-parser@0.145.0)(rolldown@1.0.3): + '@oxc-parser/binding-android-arm-eabi': 0.148.0 + '@oxc-parser/binding-android-arm64': 0.148.0 + '@oxc-parser/binding-darwin-arm64': 0.148.0 + '@oxc-parser/binding-darwin-x64': 0.148.0 + '@oxc-parser/binding-freebsd-x64': 0.148.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.148.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.148.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.148.0 + '@oxc-parser/binding-linux-arm64-musl': 0.148.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.148.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.148.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.148.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.148.0 + '@oxc-parser/binding-linux-x64-gnu': 0.148.0 + '@oxc-parser/binding-linux-x64-musl': 0.148.0 + '@oxc-parser/binding-openharmony-arm64': 0.148.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.148.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.148.0 + '@oxc-parser/binding-win32-x64-msvc': 0.148.0 + + oxc-walker@1.1.1(@oxc-project/types@0.148.0)(oxc-parser@0.148.0)(rolldown@1.0.3): optionalDependencies: - '@oxc-project/types': 0.145.0 - oxc-parser: 0.145.0 + '@oxc-project/types': 0.148.0 + oxc-parser: 0.148.0 rolldown: 1.0.3 p-cancelable@2.1.1: {} @@ -11225,169 +11305,168 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-calc@10.1.1(postcss@8.5.26): + postcss-calc@11.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 - postcss-value-parser: 4.2.0 + '@csstools/css-tokenizer': 4.0.0 + postcss: 8.5.28 - postcss-colormin@8.0.5(postcss@8.5.26): + postcss-colormin@9.0.2(postcss@8.5.28): dependencies: - '@colordx/core': 5.6.0 - browserslist: 4.28.8 + '@colordx/core': 6.3.0 + browserslist: 4.28.9 caniuse-api: 4.0.0 - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-convert-values@8.0.4(postcss@8.5.26): + postcss-convert-values@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 + browserslist: 4.28.9 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-discard-comments@8.0.4(postcss@8.5.26): + postcss-discard-comments@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 - postcss-discard-duplicates@8.0.4(postcss@8.5.26): + postcss-discard-duplicates@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-discard-empty@8.0.5(postcss@8.5.26): + postcss-discard-empty@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-discard-overridden@8.0.4(postcss@8.5.26): + postcss-discard-overridden@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-import@15.1.0(postcss@8.5.26): + postcss-import@15.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.26 + postcss: 8.5.10 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.26): + postcss-js@4.1.0(postcss@8.5.10): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.26 + postcss: 8.5.10 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.26)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.10)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.26 + postcss: 8.5.10 yaml: 2.9.0 - postcss-merge-longhand@8.0.4(postcss@8.5.26): + postcss-merge-longhand@9.0.3(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - stylehacks: 8.0.4(postcss@8.5.26) + stylehacks: 9.0.3(postcss@8.5.28) - postcss-merge-rules@8.0.5(postcss@8.5.26): + postcss-merge-rules@9.0.3(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 4.0.0 - cssnano-utils: 6.0.4(postcss@8.5.26) - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 - postcss-minify-font-values@8.0.4(postcss@8.5.26): + postcss-minify-font-values@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-minify-gradients@8.0.6(postcss@8.5.26): + postcss-minify-gradients@9.0.2(postcss@8.5.28): dependencies: - '@colordx/core': 5.6.0 - cssnano-utils: 6.0.4(postcss@8.5.26) - postcss: 8.5.26 + '@colordx/core': 6.3.0 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-minify-params@8.0.4(postcss@8.5.26): + postcss-minify-params@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - cssnano-utils: 6.0.4(postcss@8.5.26) - postcss: 8.5.26 + browserslist: 4.28.9 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-minify-selectors@8.0.5(postcss@8.5.26): + postcss-minify-selectors@9.0.3(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 4.0.0 cssesc: 3.0.0 - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 - postcss-nested@6.2.0(postcss@8.5.26): + postcss-nested@6.2.0(postcss@8.5.10): dependencies: - postcss: 8.5.26 + postcss: 8.5.10 postcss-selector-parser: 6.1.4 - postcss-normalize-charset@8.0.5(postcss@8.5.26): + postcss-normalize-charset@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-normalize-display-values@8.0.4(postcss@8.5.26): + postcss-normalize-display-values@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-positions@8.0.4(postcss@8.5.26): + postcss-normalize-positions@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@8.0.4(postcss@8.5.26): + postcss-normalize-repeat-style@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-string@8.0.4(postcss@8.5.26): + postcss-normalize-string@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@8.0.4(postcss@8.5.26): + postcss-normalize-timing-functions@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@8.0.4(postcss@8.5.26): + postcss-normalize-unicode@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 + browserslist: 4.28.9 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-url@8.0.4(postcss@8.5.26): + postcss-normalize-url@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@8.0.5(postcss@8.5.26): + postcss-normalize-whitespace@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-ordered-values@8.0.5(postcss@8.5.26): + postcss-ordered-values@9.0.2(postcss@8.5.28): dependencies: - cssnano-utils: 6.0.4(postcss@8.5.26) - postcss: 8.5.26 + cssnano-utils: 7.0.2(postcss@8.5.28) + postcss: 8.5.28 postcss-value-parser: 4.2.0 - postcss-reduce-initial@8.0.5(postcss@8.5.26): + postcss-reduce-initial@9.0.2(postcss@8.5.28): dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 caniuse-api: 4.0.0 - postcss: 8.5.26 + postcss: 8.5.28 - postcss-reduce-transforms@8.0.4(postcss@8.5.26): + postcss-reduce-transforms@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 postcss-selector-parser@6.1.4: @@ -11395,21 +11474,21 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.5: + postcss-selector-parser@7.1.6: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@8.0.6(postcss@8.5.26): + postcss-svgo@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser: 4.2.0 svgo: 4.1.0 - postcss-unique-selectors@8.0.4(postcss@8.5.26): + postcss-unique-selectors@9.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 postcss-value-parser@4.2.0: {} @@ -11419,9 +11498,9 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.26: + postcss@8.5.28: dependencies: - nanoid: 3.3.17 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -11758,7 +11837,7 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sass@1.102.0: + sass@1.104.0: dependencies: chokidar: 5.0.0 immutable: 5.1.9 @@ -11766,17 +11845,9 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.6.0 - sass@1.103.1: - dependencies: - chokidar: 5.0.0 - immutable: 5.1.9 - source-map-js: 1.2.1 - optionalDependencies: - '@parcel/watcher': 2.6.0 + sax@1.6.0: optional: true - sax@1.6.0: {} - sax@1.6.1: {} saxes@6.0.0: @@ -12039,11 +12110,11 @@ snapshots: dependencies: min-indent: 1.0.1 - stylehacks@8.0.4(postcss@8.5.26): + stylehacks@9.0.3(postcss@8.5.28): dependencies: - browserslist: 4.28.8 - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 + browserslist: 4.28.9 + postcss: 8.5.28 + postcss-selector-parser: 7.1.6 sucrase@3.35.1: dependencies: @@ -12101,11 +12172,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.26 - postcss-import: 15.1.0(postcss@8.5.26) - postcss-js: 4.1.0(postcss@8.5.26) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.26)(yaml@2.9.0) - postcss-nested: 6.2.0(postcss@8.5.26) + postcss: 8.5.10 + postcss-import: 15.1.0(postcss@8.5.10) + postcss-js: 4.1.0(postcss@8.5.10) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.10)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.10) postcss-selector-parser: 6.1.4 resolve: 1.22.12 sucrase: 3.35.1 @@ -12330,21 +12401,15 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.3.1(browserslist@4.28.2): + update-browserslist-db@1.3.2(browserslist@4.28.7): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.7 escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.3.1(browserslist@4.28.6): + update-browserslist-db@1.3.2(browserslist@4.28.9): dependencies: - browserslist: 4.28.6 - escalade: 3.2.0 - picocolors: 1.1.1 - - update-browserslist-db@1.3.1(browserslist@4.28.8): - dependencies: - browserslist: 4.28.8 + browserslist: 4.28.9 escalade: 3.2.0 picocolors: 1.1.1 @@ -12392,11 +12457,11 @@ snapshots: extsprintf: 1.4.1 optional: true - vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0): + vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.26 + postcss: 8.5.28 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -12404,15 +12469,15 @@ snapshots: esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 - less: 4.9.0 - sass: 1.103.1 + less: 4.9.1 + sass: 1.104.0 yaml: 2.9.0 - vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0): + vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.26 + postcss: 8.5.28 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -12420,15 +12485,15 @@ snapshots: esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 - less: 4.9.0 - sass: 1.103.1 + less: 4.9.1 + sass: 1.104.0 yaml: 2.9.0 - vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0): + vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.26 + postcss: 8.5.10 rolldown: 1.0.0-rc.15 tinyglobby: 0.2.16 optionalDependencies: @@ -12436,14 +12501,14 @@ snapshots: esbuild: 0.28.2 fsevents: 2.3.3 jiti: 1.21.7 - less: 4.9.0 - sass: 1.103.1 + less: 4.9.1 + sass: 1.104.0 yaml: 2.9.0 - vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)): + vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.4(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -12460,7 +12525,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.16(@types/node@22.19.17)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.17 @@ -12469,10 +12534,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)): + vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -12489,7 +12554,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.28.2)(jiti@1.21.7)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.17 @@ -12498,10 +12563,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)): + vitest@4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.4(vite@8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -12518,7 +12583,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0)(sass@1.103.1)(yaml@2.9.0) + vite: 8.0.16(@types/node@24.12.2)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1)(sass@1.104.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.12.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 924b55f4..72fc276b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,38 @@ packages: - packages/* + +# pnpm 11+ only reads auth/registry settings from .npmrc; every other setting +# (hoist-pattern, node-linker, shamefullyHoist, ...) moved here as a camelCase +# key. `shamefully-hoist=true` used to live in .npmrc — silently ignored under +# pnpm 12, which drops publicHoistPattern back to `[]` and breaks anything +# that resolves a hoisted package by walking up from a directory outside any +# workspace package (packages/compiler/scripts/build-compiler.js resolves +# `sass` from dimina/fe/package.json this way). +shamefullyHoist: true + +# Each value below is decided by reading that package's own install/postinstall +# script, not copied from a template — see the comment next to each entry. +allowBuilds: + # install script only runs node-gyp when npm_config_build_from_source=true + # (unset here); the actual native binary loads via the + # @parcel/watcher-darwin-arm64 optionalDependency, so the script is a no-op. + '@parcel/watcher': false + # postinstall only prints a donation banner (writes a dedupe marker to + # os.tmpdir()); no effect on the module itself. + core-js: false + # install script copies vendor/7z-.{exe,dll} to vendor/7z.{exe,dll} + # for the electron-builder Squirrel.Windows target; vendor ships the + # arm64 pair so the copy succeeds and Windows installer builds keep working. + electron-winstaller: true + # postinstall links/downloads the platform esbuild binary; esbuild is a + # direct dependency of compiler/devtools/fs-core/view-anchor and does not + # work at all without it. + esbuild: true + +overrides: + html-encoding-sniffer: 4.0.0 + jsdom: 26.1.0 + minimist@<1.2.8: ^1.2.8 + autoprefixer>browserslist: 4.28.7 + autoprefixer>caniuse-lite: 1.0.30001806 + browserslist@4.28.7>caniuse-lite: 1.0.30001806 diff --git a/scripts/gate.sh b/scripts/gate.sh index a59e8293..c4516ffd 100755 --- a/scripts/gate.sh +++ b/scripts/gate.sh @@ -25,10 +25,18 @@ SUMMARY=() GATE_START=$(date +%s) # 每步的名字与对应命令,顺序即执行顺序,故意用数组而非关联数组以锁定顺序。 -STEP_NAMES=(lint typecheck test "pawl:check") +# +# check:wasm-alignment 排在 typecheck 之后单独一步:它验证 packages/compiler +# 与 dimina/fe 共享依赖(含 autoprefixer 的 browserslist/caniuse-lite 等 +# 传递依赖)解析到同一版本,只在 CI 的 lint-and-types job 里跑过、gate.sh 此前 +# 没覆盖到。这条检查是 pnpm.overrides 静默失效(如未来升级 pnpm 大版本导致 +# package.json#pnpm 字段不再被读取)时唯一能把"覆盖悄悄消失"变成可见失败的 +# 信号,必须留在本地就能跑到的门禁路径里,不能只挂在 CI。 +STEP_NAMES=(lint typecheck "check:wasm-alignment" test "pawl:check") STEP_CMDS=( "pnpm run lint" "pnpm exec turbo run check-types --force" + "pnpm --filter @dimina-kit/compiler run check:wasm-alignment" "pnpm run test" "pnpm run pawl:check" ) From 08d414bc935649c4e53d6a957e3174839f20408a Mon Sep 17 00:00:00 2001 From: lbb00 Date: Tue, 8 Sep 2026 12:26:34 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(devtools):=20=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8F=B0=E5=99=AA=E5=A3=B0=E6=8C=89=20URL=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E5=88=A4=E5=BD=92=E5=B1=9E=EF=BC=8C=E5=B9=B6=E7=BB=99=20allowB?= =?UTF-8?q?uilds=20=E5=8A=A0=E5=AE=8C=E6=95=B4=E6=80=A7=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三条 review 反馈的共同点是:判断做在离权威信息源很远的地方——靠字符串猜来源、把配置位置写死在提示语里、列一张必须穷举的清单却没人检查它穷举完了没有。 1. e2e 的「无控制台错误」断言形同虚设。过滤器把 message、source、url 拼起来匹配 /DevTools/i,而我们自己的 bundle 路径就是 …/packages/devtools/dist/renderer/…,于是每一条真实的应用报错都被当噪声丢掉,断言永远为空。现在改成看 url 的协议(url 为空退回 source):devtools: 是 Chromium 自带的调试面板,http(s): 是内嵌的 A2 workbench,其余算我们自己的页面。真机采样一轮完整拖拽只有 6 条,4 条 devtools://、2 条 http://127.0.0.1,没有一条来自我们的渲染进程——协议判定覆盖了原来那张关键词表的全部作用。规则搬进 e2e/console-noise.ts 单独一个模块(不引 Electron 和 Playwright),配 16 条单元测试;vitest 的 include 相应放宽到 e2e/**/*.test.ts,coverage 的 include 仍然只有 src/**,pawl 的分母不变。 顺带修掉同一处的另一个假绿:readConsoleErrors 在采集器压根没装上时返回空数组,和「真的一条报错都没有」分不出来,现在直接抛。 2. 建议给 electron 补 allowBuilds 这条没有采纳。electron@43.2.0 没有 install 脚本(npm 上 hasInstallScript 为空,lockfile 里也没有 requiresBuild 标记),二进制是 index.js 第一次被 require、发现 dist/ 不存在时才去下载的,写进去是空操作。 3. allowBuilds 少写一个包,pnpm install 会直接以 ERR_PNPM_IGNORED_BUILDS 失败,所以它是一张必须穷举的清单;但没有任何东西检查它是否还穷举着,而 CI 的两处 pnpm install 都带 --ignore-scripts,永远碰不到这个错——漏声明会一路进主干,最后炸在开发者自己机器上。新增 scripts/check-allow-builds.mjs:扫 node_modules/.pnpm 下所有带 install 生命周期脚本(或 binding.gyp)的包,跟 pnpm 自己解析出来的 allowBuilds 对账,挂进 gate.sh 和 CI。清单从 pnpm config list --json 读而不是读配置文件,因为这些键的位置本来就在搬家:pnpm 12 已经不读 package.json 的 pnpm 字段了。 同一个道理,check-wasm-alignment.js 里「改 pnpm-workspace.yaml 的 overrides」这句写死的修复指引改成运行时自己定位(scripts/pnpm-config-location.mjs)。 验证:gate.sh 六步全绿(120s,含新增的 check:allow-builds);console-noise 单元测试 16 条通过;dock-real-drag + dock-tab-reorder 9 条通过;新门禁用 fixture 树验证过能真的红——漏声明的普通包、带 peer 后缀目录名的包、只有 binding.gyp 的包都抓得到,已声明的包和无脚本的包不误报。 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 8 + package.json | 1 + .../compiler/scripts/check-wasm-alignment.js | 8 +- packages/devtools/e2e/console-noise.test.ts | 211 ++++++++++++++++++ packages/devtools/e2e/console-noise.ts | 71 ++++++ packages/devtools/e2e/helpers.ts | 44 +--- packages/devtools/vitest.config.ts | 6 +- pnpm-workspace.yaml | 8 + scripts/check-allow-builds.mjs | 149 +++++++++++++ scripts/gate.sh | 15 +- scripts/pnpm-config-location.mjs | 46 ++++ 11 files changed, 527 insertions(+), 40 deletions(-) create mode 100644 packages/devtools/e2e/console-noise.test.ts create mode 100644 packages/devtools/e2e/console-noise.ts create mode 100644 scripts/check-allow-builds.mjs create mode 100644 scripts/pnpm-config-location.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 783d1755..6e89b030 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,14 @@ jobs: - name: Check electron peerDependencies stay in sync with devDependencies run: pnpm run check:electron-peer-sync + # The install steps in this workflow all pass --ignore-scripts, so CI + # never hits ERR_PNPM_IGNORED_BUILDS itself — an undeclared build script + # would land on main and only fail on a developer's own `pnpm install`. + # The scan works here regardless: --ignore-scripts still extracts every + # package, it only skips running their scripts. + - name: Check allowBuilds covers every installed build script + run: pnpm run check:allow-builds + - name: Lint and type-check run: pnpm turbo run lint check-types --cache-dir=.turbo diff --git a/package.json b/package.json index 277d69c0..0fbc901f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test:coverage": "turbo run test:coverage", "lint": "turbo run lint", "dev": "turbo run dev", + "check:allow-builds": "node scripts/check-allow-builds.mjs", "check:electron-peer-sync": "node scripts/check-electron-peer-sync.mjs", "check:publish-contract": "node .github/scripts/check-publish-contract.js", "pawl:check": "pawl check", diff --git a/packages/compiler/scripts/check-wasm-alignment.js b/packages/compiler/scripts/check-wasm-alignment.js index 10a760ec..c9383347 100644 --- a/packages/compiler/scripts/check-wasm-alignment.js +++ b/packages/compiler/scripts/check-wasm-alignment.js @@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { execFileSync } from 'node:child_process' import path from 'node:path' +import { describePnpmConfigKeyLocation } from '../../../scripts/pnpm-config-location.mjs' import { resolveInstalledVersion, resolveTransitiveVersion } from './resolve-installed-version.js' import { TRANSITIVE_DEP_PATHS } from './transitive-dep-pairs.js' @@ -30,6 +31,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const root = path.resolve(__dirname, '..') const kitPkgPath = path.join(root, 'package.json') const diminaRoot = path.resolve(root, '../../dimina') +const repoRoot = path.resolve(root, '../..') const snapshotPath = path.join(root, 'upstream-lockfile-snapshot.json') async function readJson(p) { @@ -152,8 +154,8 @@ for (const depPath of TRANSITIVE_DEP_PATHS) { continue } if (kitVersion !== upstreamVersion) { - // pnpm@9.15.9 (the version this repo is pinned to) only reliably honors a - // 2-segment override key. A literal join of a 3+-segment path (e.g. + // pnpm only reliably honors a 2-segment override key (still true on the + // pinned 12.3.4). A literal join of a 3+-segment path (e.g. // "autoprefixer>browserslist>caniuse-lite") either throws // ERR_PNPM_INVALID_SELECTOR (unversioned) or silently no-ops (version- // qualified on every segment) — the only form that actually takes effect @@ -166,7 +168,7 @@ for (const depPath of TRANSITIVE_DEP_PATHS) { const suggestedKey = `${parentSpec}>${depPath[depPath.length - 1]}` mismatches.push( ` ${key}: kit resolves ${kitVersion}, upstream snapshot resolves ${upstreamVersion} — ` + - 'pin the kit-side version via pnpm.overrides in the root package.json ' + + `pin the kit-side version under \`overrides\` in ${describePnpmConfigKeyLocation(repoRoot, 'overrides')} ` + `("${suggestedKey}": "${upstreamVersion}"), then pnpm install.`, ) } diff --git a/packages/devtools/e2e/console-noise.test.ts b/packages/devtools/e2e/console-noise.test.ts new file mode 100644 index 00000000..7623627a --- /dev/null +++ b/packages/devtools/e2e/console-noise.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from 'vitest' +import { classifyConsoleSurface, isNonAppConsoleNoise, type ConsoleErrorEntry } from './console-noise' + +/** + * Our own renderer's bundle lives under a path that contains the literal + * substring "devtools" (the package name), so any filter keying off that + * word instead of the `devtools://` protocol misclassifies every real error + * from this page as noise. + */ +const OWN_RENDERER_URL = + 'file:///Volumes/jdisk/code/dimina-kit/packages/devtools/dist/renderer/entries/workbench/index.html' + +function entry(overrides: Partial): ConsoleErrorEntry { + return { + level: 'error', + message: '', + url: '', + source: '', + ...overrides, + } +} + +describe('isNonAppConsoleNoise: real app errors must survive the filter', () => { + it('keeps an Uncaught TypeError whose url/source are our own renderer bundle path', () => { + const result = isNonAppConsoleNoise( + entry({ + message: 'Uncaught TypeError: Cannot read properties of undefined (reading \'foo\')', + url: OWN_RENDERER_URL, + source: OWN_RENDERER_URL, + }), + ) + // The path contains "packages/devtools" — matching that substring against + // "DevTools" is what makes every real renderer error disappear. + expect(result).toBe(false) + }) + + it('keeps an application warning emitted from our own renderer bundle path', () => { + const result = isNonAppConsoleNoise( + entry({ + level: 'warning', + message: 'Warning: a component is changing a controlled input to be uncontrolled', + url: OWN_RENDERER_URL, + source: OWN_RENDERER_URL, + }), + ) + expect(result).toBe(false) + }) + + it('keeps a real error whose message happens to mention the word "devtools" but whose url/source are our own renderer', () => { + const result = isNonAppConsoleNoise( + entry({ + message: 'Uncaught Error: failed to open the built-in devtools inspector for this project', + url: OWN_RENDERER_URL, + source: OWN_RENDERER_URL, + }), + ) + // Only `devtools://` in source/url identifies Chromium's own DevTools + // front-end; the word "devtools" appearing in a message must not count. + expect(result).toBe(false) + }) +}) + +describe('isNonAppConsoleNoise: real noise sources must stay filtered', () => { + it('drops a CDP error reported by Chromium DevTools front-end (url is devtools://)', () => { + const result = isNonAppConsoleNoise( + entry({ + message: 'Network.loadNetworkResource … Frame not found', + url: 'devtools://devtools/bundled/panels/network/network.js', + source: 'devtools://devtools/bundled/panels/network/network.js', + }), + ) + expect(result).toBe(true) + }) + + it('drops a DevTools front-end error when only source is devtools:// and url is empty', () => { + const result = isNonAppConsoleNoise( + entry({ + message: 'Uncaught (in promise) Error', + url: '', + source: 'devtools://devtools/bundled/core/sdk/sdk.js', + }), + ) + expect(result).toBe(true) + }) + + it('drops messages carrying known resource/network-noise text (favicon, Failed to load resource, net::ERR_FILE_NOT_FOUND)', () => { + expect( + isNonAppConsoleNoise(entry({ message: 'Failed to load resource: net::ERR_FILE_NOT_FOUND', url: OWN_RENDERER_URL })), + ).toBe(true) + expect( + isNonAppConsoleNoise(entry({ message: 'GET file:///favicon.ico net::ERR_FILE_NOT_FOUND', url: OWN_RENDERER_URL })), + ).toBe(true) + }) + + it('drops A2 workbench startup chatter identified by ExtensionHost/a2-spike markers in source or url', () => { + expect( + isNonAppConsoleNoise( + entry({ + message: 'Extension host terminated unexpectedly', + source: 'https://127.0.0.1:5173/a2-spike/out/vs/workbench/api/node/localExtensionHostProcess.js', + }), + ), + ).toBe(true) + expect( + isNonAppConsoleNoise( + entry({ + message: 'setting up sandbox', + url: 'https://127.0.0.1:5173/a2-spike/out/vs/code/electron-sandbox/workbench/workbench.js', + }), + ), + ).toBe(true) + }) + + it('drops the A2 workbench settings-schema and workspace-mirror startup messages', () => { + expect( + isNonAppConsoleNoise(entry({ message: "json.schemas is not a registered configuration" })), + ).toBe(true) + expect( + isNonAppConsoleNoise(entry({ message: "Unable to resolve nonexistent file '/workspace'" })), + ).toBe(true) + }) +}) + +describe('classifyConsoleSurface: scheme prefix decides ownership, not substring presence', () => { + it('classifies Chromium DevTools front-end as "devtools" even though its query string embeds an unrelated https:// URL', () => { + // The https:// remoteBase lives inside the query string, after the + // devtools: scheme. A substring-based check would see "https://" and + // could misroute this to "workbench"; only the leading scheme is authoritative. + const result = classifyConsoleSurface({ + url: 'devtools://devtools/bundled/devtools_app.html?remoteBase=https://chrome-devtools-frontend.appspot.com/...', + source: '', + }) + expect(result).toBe('devtools') + }) + + it('classifies the embedded A2 workbench origin (http://127.0.0.1:/…) as "workbench"', () => { + const result = classifyConsoleSurface({ + url: 'http://127.0.0.1:53694/w/11111111-1111-1111-1111-111111111111/index.html?theme=dark', + source: '', + }) + expect(result).toBe('workbench') + }) + + it('classifies our own file:// renderer bundle as "app"', () => { + const result = classifyConsoleSurface({ + url: 'file:///Volumes/jdisk/code/dimina-kit/packages/devtools/dist/renderer/entries/workbench/index.html', + source: '', + }) + expect(result).toBe('app') + }) + + it('falls back to source and classifies as "devtools" when url is empty (worker/iframe cannot report a page url)', () => { + const result = classifyConsoleSurface({ + url: '', + source: 'devtools://devtools/bundled/core/sdk/sdk.js', + }) + expect(result).toBe('devtools') + }) + + it('falls back to source and classifies as "workbench" when url is empty', () => { + const result = classifyConsoleSurface({ + url: '', + source: 'https://127.0.0.1:5173/a2-spike/out/vs/workbench/api/node/localExtensionHostProcess.js', + }) + expect(result).toBe('workbench') + }) + + it('classifies as "app" when both url and source are empty, so an unattributable error fails a test instead of vanishing', () => { + const result = classifyConsoleSurface({ url: '', source: '' }) + expect(result).toBe('app') + }) +}) + +describe('isNonAppConsoleNoise: scheme-based classification must not be misled by message text', () => { + it('keeps a real fetch-failure error from our own page even though its message text contains "http://" and "devtools://" substrings', () => { + // The new classifier reads entry.url/entry.source, not entry.message. This + // asserts that a business error mentioning noise-like substrings in its + // message is still recognized as ours, because the url/source scheme says so. + const result = isNonAppConsoleNoise( + entry({ + message: + 'Uncaught (in promise) TypeError: Failed to fetch http://127.0.0.1:9000/api/x (see devtools:// for details)', + url: 'file:///Volumes/jdisk/code/dimina-kit/packages/devtools/dist/renderer/entries/workbench/index.html', + source: 'file:///Volumes/jdisk/code/dimina-kit/packages/devtools/dist/renderer/entries/workbench/index.html', + }), + ) + expect(result).toBe(false) + }) + + it('keeps a real app error whose url was truncated to 140 characters by the collector but still starts with file:', () => { + // installConsoleCollector truncates url/source to 140 chars before this + // function ever sees them. A deep enough path could, in principle, have + // its leading "file:" pushed past a truncation boundary by a different + // truncation strategy — this pins that the scheme must still be at index 0 + // after truncation for the entry to count as ours. + const deepPath = + `file:///Volumes/jdisk/code/dimina-kit/packages/devtools/dist/renderer/entries/workbench/very/deeply/nested/directory/structure/that/exceeds/one/hundred/and/forty/characters/index.html` + const truncatedUrl = deepPath.slice(0, 140) + expect(truncatedUrl.length).toBe(140) + expect(truncatedUrl.startsWith('file:')).toBe(true) + + const result = isNonAppConsoleNoise( + entry({ + message: 'Uncaught TypeError: Cannot read properties of undefined (reading \'bar\')', + url: truncatedUrl, + source: truncatedUrl, + }), + ) + expect(result).toBe(false) + }) +}) diff --git a/packages/devtools/e2e/console-noise.ts b/packages/devtools/e2e/console-noise.ts new file mode 100644 index 00000000..7b445e9f --- /dev/null +++ b/packages/devtools/e2e/console-noise.ts @@ -0,0 +1,71 @@ +/** One collected console entry from `installConsoleCollector`. */ +export interface ConsoleErrorEntry { + level: 'error' | 'warning' + message: string + url: string + source: string +} + +/** + * Which of the three surfaces a console entry came from. + * + * `installConsoleCollector` attaches to EVERY WebContents, so its entries mix + * three unrelated products: our own app, the embedded A2 workbench (a + * third-party VS Code bundle we merely host, served over a 127.0.0.1 COI + * origin) and Chromium's own DevTools front-end (`devtools://`). Only the + * first can be evidence that a gesture under test broke something. + */ +export type ConsoleSurface = 'app' | 'workbench' | 'devtools' + +/** + * Decide whose page an entry came from, from the URL scheme alone. + * + * The scheme is the authoritative answer and the reason this is not keyword + * matching: our own bundle is loaded from `file://…/packages/devtools/…`, so + * any rule keyed on the product name "devtools" appearing SOMEWHERE in the + * entry classifies our own errors as someone else's and silently empties the + * assertions that depend on it. A scheme also survives `installConsoleCollector` + * truncating url/source to 140 characters, because it sits at the front. + * + * `url` is the page; `source` is the script. A worker or an iframe can report + * an empty page url, so fall back to the script. When neither says anything, + * the entry counts as ours — an unattributable error should fail a test, not + * disappear from it. + */ +export function classifyConsoleSurface(entry: Pick): ConsoleSurface { + const origin = entry.url || entry.source + if (/^devtools:/i.test(origin)) return 'devtools' + if (/^https?:/i.test(origin)) return 'workbench' + return 'app' +} + +/** + * Whether a collected console entry is something other than a real error from + * our own renderer, and so cannot be evidence that a gesture under test broke + * the app. + * + * Single owner on purpose: every "no unexpected console errors" assertion has + * to agree on what counts as ours. + * + * Lives in its own module, free of Electron and Playwright imports, so these + * rules can be unit-tested directly. A filter that quietly swallows real app + * errors turns every caller's assertion into a no-op without failing anything, + * which no end-to-end run can detect. + */ +export function isNonAppConsoleNoise(entry: ConsoleErrorEntry): boolean { + if (classifyConsoleSurface(entry) !== 'app') return true + + // Chromium writes these about our page rather than from it: a failed + // subresource load and a missing source map are not JS errors and no + // gesture under test can cause them. Matched on the message alone — + // matching them against url/source is what lets a path substring stand in + // for an actual diagnosis. + if (/net::ERR|Failed to load resource|DevTools failed to load source ?map/i.test(entry.message)) return true + + // The A2 workbench's startup chatter is normally attributed to its own + // 127.0.0.1 origin and already handled above. These two arrive with no + // origin at all, so the message text is the only thing left to key on. + if (/json\.schemas is not a registered configuration|Unable to resolve nonexistent file '\/workspace'/i.test(entry.message)) return true + + return false +} diff --git a/packages/devtools/e2e/helpers.ts b/packages/devtools/e2e/helpers.ts index e149fb71..f20104aa 100644 --- a/packages/devtools/e2e/helpers.ts +++ b/packages/devtools/e2e/helpers.ts @@ -1,4 +1,5 @@ import type { Page, ElectronApplication } from '@playwright/test' +import type { ConsoleErrorEntry } from './console-noise' import fs from 'fs' import os from 'os' import path from 'path' @@ -467,12 +468,8 @@ export async function evalInWebContentsByUrl( }, { urlSubstring, expression }) as Promise } -export interface ConsoleErrorEntry { - level: 'error' | 'warning' - message: string - url: string - source: string -} +export type { ConsoleErrorEntry } from './console-noise' +export { isNonAppConsoleNoise } from './console-noise' /** * Collect `error`/`warning` console messages from EVERY webContents (existing @@ -528,35 +525,18 @@ export async function installConsoleCollector(electronApp: ElectronApplication): export async function readConsoleErrors(electronApp: ElectronApplication): Promise { return electronApp.evaluate(() => { const g = globalThis as unknown as { __e2eConsoleErrors?: ConsoleErrorEntry[] } - return (g.__e2eConsoleErrors ?? []).slice() + // "Collected nothing" and "never collected" both used to come back as an + // empty array, so a spec that skipped installConsoleCollector — or + // installed it on a different Electron instance than it later read — + // compared an empty list against an empty list and passed without ever + // observing the app. + if (!g.__e2eConsoleErrors) { + throw new Error('readConsoleErrors: installConsoleCollector never ran on this Electron instance') + } + return g.__e2eConsoleErrors.slice() }) as Promise } -/** - * Whether a collected console entry came from something other than our own - * renderer, and so cannot be evidence that a gesture under test broke the app. - * - * `installConsoleCollector` attaches to EVERY WebContents, including pages we - * only host: the embedded A2 workbench (a third-party VS Code bundle served - * from a 127.0.0.1 COI origin, which logs extension-host sandbox notes, - * settings-schema and workspace-mirror chatter at startup) and Chromium's own - * DevTools front-end (`devtools://`, which logs failed CDP requests such as - * `Network.loadNetworkResource … Frame not found` whenever an inspected frame - * goes away — routine during a drag that moves docked views). - * - * Single owner on purpose: every "no unexpected console errors" assertion has - * to agree on what counts as ours. Match against source and url as well as the - * message — a DevTools-front-end entry names `devtools://` only in the former - * two. - */ -export function isNonAppConsoleNoise(entry: ConsoleErrorEntry): boolean { - const whole = `${entry.message} ${entry.source} ${entry.url}` - if (/favicon|DevTools|devtools:\/\/|Autofill|net::ERR|Failed to load resource/i.test(whole)) return true - if (/ExtensionHost|a2-spike|local-network-access|allow-scripts and allow-same-origin/i.test(whole)) return true - if (/json\.schemas is not a registered configuration|Unable to resolve nonexistent file '\/workspace'/i.test(entry.message)) return true - return false -} - /** * Put the dock into the `inEditor` preset: simulator beside a `col-main` column * holding `[editor over debug]`. diff --git a/packages/devtools/vitest.config.ts b/packages/devtools/vitest.config.ts index 5e9f4c05..6c147433 100644 --- a/packages/devtools/vitest.config.ts +++ b/packages/devtools/vitest.config.ts @@ -10,7 +10,11 @@ export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', - include: ['src/**/*.test.{ts,tsx}'], + // `e2e/` holds Playwright specs, which vitest must not pick up — hence the + // `*.test.ts` suffix here versus the `*.spec.ts` those use. It matches the + // pure helper modules e2e specs share, whose rules decide whether an + // assertion has anything left to assert. + include: ['src/**/*.test.{ts,tsx}', 'e2e/**/*.test.ts'], globals: true, setupFiles: ['./src/renderer/test-setup.ts'], coverage: { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 72fc276b..dd25644a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,6 +12,14 @@ shamefullyHoist: true # Each value below is decided by reading that package's own install/postinstall # script, not copied from a template — see the comment next to each entry. +# +# Once this key exists, a dependency with a build script that is NOT listed +# fails the install outright (ERR_PNPM_IGNORED_BUILDS), so this map has to +# cover every such dependency, not just the interesting ones. +# +# electron is deliberately absent: 43.x ships no install script at all — its +# index.js downloads the binary on first require if `dist/` is missing — so +# there is nothing here to allow. allowBuilds: # install script only runs node-gyp when npm_config_build_from_source=true # (unset here); the actual native binary loads via the diff --git a/scripts/check-allow-builds.mjs b/scripts/check-allow-builds.mjs new file mode 100644 index 00000000..58e32842 --- /dev/null +++ b/scripts/check-allow-builds.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/** + * allowBuilds completeness gate. Fails if any installed dependency runs an + * install lifecycle script that `allowBuilds` does not decide on. + * + * `allowBuilds` is an exhaustive list by construction: once the key exists, + * pnpm aborts the install (ERR_PNPM_IGNORED_BUILDS) as soon as a dependency + * with a build script is missing from it. Nothing checked that it stayed + * exhaustive, and nothing could: both CI install steps pass --ignore-scripts, + * so the one place the omission surfaces is a developer's own `pnpm install` + * — after a lockfile bump has already landed on main. + * + * The list is read back from pnpm itself rather than from a file, because + * where it lives is not stable: pnpm 12 stopped reading `package.json#pnpm` + * and moved these keys to pnpm-workspace.yaml. `pnpm config list --json` + * reports the resolved value wherever it currently comes from. + * + * Exit codes: 0 clean, 1 undeclared build scripts found, 2 cannot measure. + */ +import { execFileSync } from 'node:child_process' +import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describePnpmConfigKeyLocation } from './pnpm-config-location.mjs' + +const TAG = '[check-allow-builds]' +const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall'] + +const here = dirname(fileURLToPath(import.meta.url)) +const repoRoot = join(here, '..') +// Overridable so the gate itself can be exercised against a fixture tree +// instead of only against whatever this machine happens to have installed. +const storeRoot = process.env.CHECK_ALLOW_BUILDS_STORE ?? join(repoRoot, 'node_modules', '.pnpm') + +function fail(message) { + console.error(`${TAG} ${message}`) + process.exit(2) +} + +function readAllowBuilds() { + let raw + try { + raw = execFileSync('pnpm', ['config', 'list', '--json'], { cwd: repoRoot, encoding: 'utf8' }) + } + catch (error) { + fail(`could not run \`pnpm config list --json\`: ${error.message}`) + } + + let config + try { + config = JSON.parse(raw) + } + catch { + fail('`pnpm config list --json` did not return JSON; cannot read allowBuilds.') + } + + // A missing key is not "allow everything": pnpm blocks every build script + // when nothing is declared, so an absent key is an empty decision map. + return config.allowBuilds ?? {} +} + +/** + * Package name for one `node_modules/.pnpm` directory. + * + * Entries are `@` and may carry a + * `_` suffix that contains further `@` characters, so the + * version separator is the first `@` after position 0, not the last one. + */ +function packageNameFromStoreDir(dir) { + const at = dir.indexOf('@', dir.startsWith('@') ? 1 : 0) + if (at <= 0) return null + return dir.slice(0, at).replace('+', '/') +} + +/** Every installed dependency whose install pnpm would have to decide on. */ +function scanInstalledBuildScripts() { + if (!existsSync(storeRoot)) { + fail(`${storeRoot} does not exist — run \`pnpm install\` first; an unmeasured tree is not a passing one.`) + } + + const found = new Map() + for (const dir of readdirSync(storeRoot)) { + const name = packageNameFromStoreDir(dir) + if (!name) continue + + const packageDir = join(storeRoot, dir, 'node_modules', ...name.split('/')) + const manifestPath = join(packageDir, 'package.json') + if (!existsSync(manifestPath)) continue + // Sibling entries in the same node_modules are symlinks to other store + // entries; only the real directory is this entry's own package. + if (lstatSync(packageDir).isSymbolicLink()) continue + + let manifest + try { + manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) + } + catch { + continue + } + if (manifest.name && manifest.name !== name) continue + + const hooks = LIFECYCLE_SCRIPTS.filter(hook => typeof manifest.scripts?.[hook] === 'string') + // pnpm also treats a package as needing a build when it ships a + // binding.gyp, even with no script of its own to run. + if (hooks.length === 0 && !existsSync(join(packageDir, 'binding.gyp'))) continue + + const reason = hooks.length > 0 ? hooks.join(', ') : 'binding.gyp' + const versions = found.get(name) ?? new Map() + versions.set(manifest.version ?? dir, reason) + found.set(name, versions) + } + return found +} + +const allowBuilds = readAllowBuilds() +const installed = scanInstalledBuildScripts() + +if (installed.size === 0) { + fail(`scanned ${storeRoot} and found no package with an install script at all — that is implausible, so treat the scan as broken rather than clean.`) +} + +const undeclared = [] +for (const [name, versions] of installed) { + if (Object.hasOwn(allowBuilds, name)) continue + for (const [version, reason] of versions) { + undeclared.push(`${name}@${version} (${reason})`) + } +} + +// Declared but no longer installed. Not a failure: the entry may cover a +// platform-specific dependency that this machine's tree does not contain. +const stale = Object.keys(allowBuilds).filter(name => !installed.has(name)) + +if (undeclared.length > 0) { + console.error(`${TAG} these installed dependencies run an install script that allowBuilds does not decide on:`) + for (const line of undeclared.sort()) console.error(` ${line}`) + console.error('') + console.error(`Read each package's own script, then add it to \`allowBuilds\` in ${describePnpmConfigKeyLocation(repoRoot, 'allowBuilds')}`) + console.error('as `true` (the build is needed) or `false` (running it changes nothing here), with a') + console.error('comment saying which. Leaving one out makes `pnpm install` fail with ERR_PNPM_IGNORED_BUILDS.') + process.exit(1) +} + +console.log(`${TAG} OK — allowBuilds decides on all ${installed.size} installed package(s) with an install script.`) +if (stale.length > 0) { + console.log(`${TAG} note: declared but not installed on this platform: ${stale.sort().join(', ')}`) +} +console.log(`${TAG} scanned ${storeRoot}; this only sees the current platform's tree.`) +process.exit(0) diff --git a/scripts/gate.sh b/scripts/gate.sh index c4516ffd..61a9243f 100755 --- a/scripts/gate.sh +++ b/scripts/gate.sh @@ -29,14 +29,21 @@ GATE_START=$(date +%s) # check:wasm-alignment 排在 typecheck 之后单独一步:它验证 packages/compiler # 与 dimina/fe 共享依赖(含 autoprefixer 的 browserslist/caniuse-lite 等 # 传递依赖)解析到同一版本,只在 CI 的 lint-and-types job 里跑过、gate.sh 此前 -# 没覆盖到。这条检查是 pnpm.overrides 静默失效(如未来升级 pnpm 大版本导致 -# package.json#pnpm 字段不再被读取)时唯一能把"覆盖悄悄消失"变成可见失败的 -# 信号,必须留在本地就能跑到的门禁路径里,不能只挂在 CI。 -STEP_NAMES=(lint typecheck "check:wasm-alignment" test "pawl:check") +# 没覆盖到。这条检查是 overrides 静默失效时唯一能把"覆盖悄悄消失"变成可见 +# 失败的信号 —— 这不是假想:pnpm 12 已经不再读 package.json 的 pnpm 字段, +# overrides 现在写在 pnpm-workspace.yaml,下一次大版本同样可能再挪一次。 +# 必须留在本地就能跑到的门禁路径里,不能只挂在 CI。 +# +# check:allow-builds 同理,而且更极端:CI 的两处 pnpm install 都带 +# --ignore-scripts,永远碰不到 ERR_PNPM_IGNORED_BUILDS,漏声明的构建脚本只会 +# 在开发者自己 pnpm install 时炸——那时改动已经进主干了。它只读 node_modules +# 现有的树,不装任何东西,秒级。 +STEP_NAMES=(lint typecheck "check:wasm-alignment" "check:allow-builds" test "pawl:check") STEP_CMDS=( "pnpm run lint" "pnpm exec turbo run check-types --force" "pnpm --filter @dimina-kit/compiler run check:wasm-alignment" + "pnpm run check:allow-builds" "pnpm run test" "pnpm run pawl:check" ) diff --git a/scripts/pnpm-config-location.mjs b/scripts/pnpm-config-location.mjs new file mode 100644 index 00000000..433f6ec9 --- /dev/null +++ b/scripts/pnpm-config-location.mjs @@ -0,0 +1,46 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +/** + * Which file currently holds a given pnpm config key, e.g. `overrides`. + * + * Gate scripts have to tell whoever they fail where to go and fix it, and + * naming a file in prose goes stale silently: pnpm 12 stopped reading + * `package.json#pnpm` and moved these keys to pnpm-workspace.yaml, which left + * every message that said "the pnpm field in package.json" pointing at a file + * that no longer has any effect. Looking the location up keeps the advice + * correct across the next move too. + * + * Returns the file name, or null when the key is not declared anywhere yet — + * callers decide what to say in that case, since where a NEW key should go is + * a different question from where an existing one lives. + */ +export function locatePnpmConfigKey(repoRoot, key) { + const workspaceFile = join(repoRoot, 'pnpm-workspace.yaml') + if (existsSync(workspaceFile)) { + // Top-level YAML keys start at column 0, which is enough to tell a real + // declaration from the same word inside a comment or a nested value. + const topLevelKey = new RegExp(`^${key}:`, 'm') + if (topLevelKey.test(readFileSync(workspaceFile, 'utf8'))) return 'pnpm-workspace.yaml' + } + + const rootManifest = join(repoRoot, 'package.json') + if (existsSync(rootManifest)) { + try { + const pkg = JSON.parse(readFileSync(rootManifest, 'utf8')) + if (pkg.pnpm && Object.hasOwn(pkg.pnpm, key)) return 'the `pnpm` field in package.json' + } + catch { + // An unreadable root manifest is a different problem; the caller's + // own failure is the one worth reporting. + } + } + + return null +} + +/** The same lookup, phrased for a remediation message. */ +export function describePnpmConfigKeyLocation(repoRoot, key) { + const location = locatePnpmConfigKey(repoRoot, key) + return location ?? 'whichever file pnpm reads config from (`pnpm config list --json` shows the resolved value)' +}