From 1fcebceed835bba3e04f4a82e0042d82f48f8c6e Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 23 Sep 2026 11:38:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(cli):=20qjsc-kmp=20=E6=94=B9=E4=BB=8E?= =?UTF-8?q?=20npm=20=E9=A2=84=E7=BC=96=E8=AF=91=E5=8C=85=E5=8F=96=EF=BC=8C?= =?UTF-8?q?quickjs-kmp=20=E5=8D=87=E5=88=B0=200.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M5.2。tinyui-cli 依赖 qjsc-kmp@0.1.2,查找顺序为 --qjsc → TINYUI_QJSC → npm 包 → PATH;pnpm install 之后即可编字节码,不必克隆 quickjs-kmp、 装 JDK。qjsc-kmp 版本与 catalog 的 quickjsKmp 由测试钉成相等:字节码 记着编它的引擎,版本错开就会被 App 当成 engine 不符拒掉。 CI 不再现编 qjsc-kmp(仍克隆 quickjs-kmp 编 Android host 测试要的 JNI 库),由此也覆盖了 npm 包这条路径;build.test 的真字节码用例改按 findQjsc 判断是否跳过,本机与 CI 都会真跑。 Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build.yml | 5 ++-- .github/workflows/ios.yml | 5 ++-- .github/workflows/publish.yml | 5 ++-- gradle/libs.versions.toml | 2 +- packages/cli/README.md | 2 +- packages/cli/package.json | 3 ++- packages/cli/src/bin.ts | 2 +- packages/cli/src/build.ts | 4 +-- packages/cli/src/qjsc.ts | 18 ++++++++++++- packages/cli/test/build.test.ts | 3 ++- packages/cli/test/qjsc.test.ts | 18 +++++++++++++ pnpm-lock.yaml | 47 +++++++++++++++++++++++++++++++++ 12 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 packages/cli/test/qjsc.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4cfcd3..98a632f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,12 +22,11 @@ jobs: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - - name: quickjs-kmp host tools (qjsc-kmp for bytecode, host JNI for Android host tests) + - name: quickjs-kmp host JNI for Android host tests (qjsc-kmp comes from the npm package) run: | version=$(sed -n 's/^quickjsKmp = "\(.*\)"/\1/p' gradle/libs.versions.toml) git clone --depth 1 --branch "$version" https://github.com/HarlonWang/quickjs-kmp.git "$RUNNER_TEMP/quickjs-kmp" - (cd "$RUNNER_TEMP/quickjs-kmp" && ./gradlew :library:buildHostTools :library:buildNativeHostJni --no-daemon) - echo "TINYUI_QJSC=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-tools/bin/qjsc-kmp" >> "$GITHUB_ENV" + (cd "$RUNNER_TEMP/quickjs-kmp" && ./gradlew :library:buildNativeHostJni --no-daemon) echo "TINYUI_QUICKJS_HOST_JNI=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-jni/lib" >> "$GITHUB_ENV" - run: ./gradlew :compose:testAndroidHostTest :compose:apiCheck :sample:androidApp:assembleDebug --no-daemon diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index d052936..e205bc1 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -22,11 +22,10 @@ jobs: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - - name: quickjs-kmp host tools + - name: quickjs-kmp host JNI (qjsc-kmp comes from the npm package) run: | version=$(sed -n 's/^quickjsKmp = "\(.*\)"/\1/p' gradle/libs.versions.toml) git clone --depth 1 --branch "$version" https://github.com/HarlonWang/quickjs-kmp.git "$RUNNER_TEMP/quickjs-kmp" - (cd "$RUNNER_TEMP/quickjs-kmp" && ./gradlew :library:buildHostTools :library:buildNativeHostJni --no-daemon) - echo "TINYUI_QJSC=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-tools/bin/qjsc-kmp" >> "$GITHUB_ENV" + (cd "$RUNNER_TEMP/quickjs-kmp" && ./gradlew :library:buildNativeHostJni --no-daemon) echo "TINYUI_QUICKJS_HOST_JNI=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-jni/lib" >> "$GITHUB_ENV" - run: ./gradlew build --no-daemon diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ee4d26a..1231137 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,12 +26,11 @@ jobs: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - - name: quickjs-kmp host tools (qjsc-kmp for bytecode, host JNI for Android host tests) + - name: quickjs-kmp host JNI for Android host tests (qjsc-kmp comes from the npm package) run: | version=$(sed -n 's/^quickjsKmp = "\(.*\)"/\1/p' gradle/libs.versions.toml) git clone --depth 1 --branch "$version" https://github.com/HarlonWang/quickjs-kmp.git "$RUNNER_TEMP/quickjs-kmp" - (cd "$RUNNER_TEMP/quickjs-kmp" && ./gradlew :library:buildHostTools :library:buildNativeHostJni --no-daemon) - echo "TINYUI_QJSC=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-tools/bin/qjsc-kmp" >> "$GITHUB_ENV" + (cd "$RUNNER_TEMP/quickjs-kmp" && ./gradlew :library:buildNativeHostJni --no-daemon) echo "TINYUI_QUICKJS_HOST_JNI=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-jni/lib" >> "$GITHUB_ENV" - run: ./gradlew :compose:testAndroidHostTest :compose:apiCheck --no-daemon - name: Publish to Maven Central diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eaacffc..3a221ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ android-targetSdk = "37" androidxActivityCompose = "1.13.0" kotlinxCoroutines = "1.11.0" kotlinxSerialization = "1.11.0" -quickjsKmp = "0.1.1" +quickjsKmp = "0.1.2" vanniktechMavenPublish = "0.37.0" binaryCompatibilityValidator = "0.18.2" okio = "3.18.2" diff --git a/packages/cli/README.md b/packages/cli/README.md index 32f6d9e..76d13b1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4,4 +4,4 @@ `tinyui publish` 把 `bundle` 的产物传到热下发服务并切换 channel 指针,`tinyui apps` / `packages` / `tokens` / `releases` 是这套服务的管理面(建 app、登记包的验签公钥、签发与吊销发布 token、列 release 与回滚 / 晋级 / 改灰度)。实例地址取 `--url`、`$TINYUI_UPDATES_URL`、`https://updates.tinyui.app` 三者之一;凭据只从环境变量读,不接受命令行参数:`$TINYUI_TOKEN` 是发布 token,`$TINYUI_ADMIN_TOKEN` 是实例管理 token。`--app` 可用 `$TINYUI_APP` 代替,`--pkg` 缺省取当前目录 `tinyui.config.json` 的 `name`,`--channel` 必须每次显式写。 -字节码编译需要 quickjs-kmp 的宿主工具 `qjsc-kmp`(`--qjsc` 或 `TINYUI_QJSC`)。用法见仓库 `docs/build-chain.md` 与 `docs/updates.md`;与 `tinyui-core` 同版本号。 +字节码编译用 quickjs-kmp 的宿主工具 `qjsc-kmp`:依赖里的 npm 包 `qjsc-kmp` 带 macOS 与 Linux 的预编译版,装完即用;其他平台用 `--qjsc` 或 `TINYUI_QJSC` 指向自己编的那份。`qjsc-kmp` 的版本钉在与 tinyui 所用 quickjs-kmp 相同的版本上,编出的字节码才认 App 里的引擎。用法见仓库 `docs/build-chain.md` 与 `docs/updates.md`;与 `tinyui-core` 同版本号。 diff --git a/packages/cli/package.json b/packages/cli/package.json index 4e57be1..aa3806b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -33,7 +33,8 @@ "dependencies": { "esbuild": "0.28.2", "magic-string": "1.4.1", - "oxc-parser": "0.150.0" + "oxc-parser": "0.150.0", + "qjsc-kmp": "0.1.2" }, "devDependencies": { "tinyui-core": "workspace:*", diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 9d0ecf5..2c38709 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -30,7 +30,7 @@ const USAGE = `usage: tinyui build [--root ] [--out ] [--qjsc ] build compile the pages of the package described by /tinyui.config.json --root project root (default: cwd) --out output directory (default: /dist) - --qjsc path to qjsc-kmp (default: $TINYUI_QJSC, then PATH) + --qjsc path to qjsc-kmp (default: $TINYUI_QJSC, then the qjsc-kmp npm package, then PATH) --js-only emit ESM sources and source maps only, skip bytecode --version package version to record (default: -) diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 3f8c6da..2449728 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -21,7 +21,7 @@ export interface BuildOptions { root: string; /** Output directory; defaults to `/dist`. */ out?: string; - /** Path to `qjsc-kmp`; falls back to `TINYUI_QJSC` and then `PATH`. */ + /** Path to `qjsc-kmp`; falls back to `TINYUI_QJSC`, the `qjsc-kmp` npm package, then `PATH`. */ qjsc?: string; /** Emit only the ESM sources and skip bytecode; for debugging the transform without an engine build. */ jsOnly?: boolean; @@ -74,7 +74,7 @@ export async function build(options: BuildOptions): Promise { const qjsc = options.jsOnly ? undefined : await findQjsc(options.qjsc); if (!options.jsOnly && !qjsc) { - throw new Error("qjsc-kmp not found: pass --qjsc, set TINYUI_QJSC, or put it on PATH (or use --js-only)"); + throw new Error("qjsc-kmp not found: the qjsc-kmp npm package has no binary for this machine or was installed without optional dependencies; pass --qjsc, set TINYUI_QJSC, or put it on PATH (or use --js-only)"); } // stale outputs would otherwise be packaged along with the current pages diff --git a/packages/cli/src/qjsc.ts b/packages/cli/src/qjsc.ts index 5176f71..a35ea16 100644 --- a/packages/cli/src/qjsc.ts +++ b/packages/cli/src/qjsc.ts @@ -7,19 +7,35 @@ const execFileAsync = promisify(execFile); const EXECUTABLE = process.platform === "win32" ? "qjsc-kmp.exe" : "qjsc-kmp"; -/** Locates the quickjs-kmp host compiler: explicit path, then `TINYUI_QJSC`, then `PATH`. */ +/** + * Locates the quickjs-kmp host compiler: explicit path, then `TINYUI_QJSC`, then the prebuilt binary of the + * `qjsc-kmp` npm package, then `PATH`. + */ export async function findQjsc(explicit?: string): Promise { const candidates = [explicit, process.env["TINYUI_QJSC"]].filter((p): p is string => !!p); for (const p of candidates) { if (await isExecutable(p)) return p; throw new Error(`qjsc-kmp not found at ${p}`); } + const prebuilt = await prebuiltQjsc(); + if (prebuilt) return prebuilt; for (const dir of (process.env["PATH"] ?? "").split(delimiter)) { if (dir && (await isExecutable(join(dir, EXECUTABLE)))) return join(dir, EXECUTABLE); } return undefined; } +/** The binary `qjsc-kmp` installed for this machine; none on a platform it has no build for, or with optional dependencies omitted. */ +async function prebuiltQjsc(): Promise { + try { + const { binaryPath } = await import("qjsc-kmp"); + const path = binaryPath(); + return (await isExecutable(path)) ? path : undefined; + } catch { + return undefined; + } +} + export interface CompileOptions { qjsc: string; input: string; diff --git a/packages/cli/test/build.test.ts b/packages/cli/test/build.test.ts index 79785c1..c191f6c 100644 --- a/packages/cli/test/build.test.ts +++ b/packages/cli/test/build.test.ts @@ -7,6 +7,7 @@ import { build, type BuildResult } from "../src/build.ts"; import { findQjsc } from "../src/qjsc.ts"; const root = join(import.meta.dirname, "fixtures", "app"); +const qjscFound = await findQjsc(); describe("tinyui build", () => { let out: string; @@ -158,7 +159,7 @@ describe("tinyui build", () => { } }); - it("compiles every module to bytecode when qjsc-kmp is available", { skip: !process.env["TINYUI_QJSC"] && "TINYUI_QJSC not set" }, async () => { + it("compiles every module to bytecode when qjsc-kmp is available", { skip: !qjscFound && "qjsc-kmp not found" }, async () => { for (const m of [...result.runtime, ...result.pages]) { assert.ok(m.bin, `${m.name} has bytecode`); const bytes = await readFile(m.bin!); diff --git a/packages/cli/test/qjsc.test.ts b/packages/cli/test/qjsc.test.ts new file mode 100644 index 0000000..d6fd76f --- /dev/null +++ b/packages/cli/test/qjsc.test.ts @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "node:test"; + +const root = join(import.meta.dirname, "..", "..", ".."); + +describe("qjsc-kmp", () => { + // bytecode names the engine it was compiled for; a prebuilt compiler from another quickjs-kmp would build + // packages every App rejects as an engine mismatch + it("depends on the qjsc-kmp release of the quickjs-kmp version the Kotlin side uses", async () => { + const catalog = await readFile(join(root, "gradle", "libs.versions.toml"), "utf8"); + const kotlin = catalog.match(/^quickjsKmp\s*=\s*"([^"]+)"/m)?.[1]; + const cli = JSON.parse(await readFile(join(root, "packages", "cli", "package.json"), "utf8")) as { dependencies: Record }; + assert.ok(kotlin, "gradle/libs.versions.toml has no quickjsKmp version"); + assert.equal(cli.dependencies["qjsc-kmp"], kotlin); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4440e70..6362622 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: oxc-parser: specifier: 0.150.0 version: 0.150.0 + qjsc-kmp: + specifier: 0.1.2 + version: 0.1.2 devDependencies: '@types/node': specifier: latest @@ -356,6 +359,26 @@ packages: '@oxc-project/types@0.150.0': resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + '@qjsc-kmp/darwin-arm64@0.1.2': + resolution: {integrity: sha512-WWl+kN3GGl3mvHZl7vWfyfskEwrsZoQ3FOgKTb1KHDx4aGuv6/Qd0y38JYNgG9nQuTPT/wBXROY2HOYaLlD5HQ==} + cpu: [arm64] + os: [darwin] + + '@qjsc-kmp/darwin-x64@0.1.2': + resolution: {integrity: sha512-Dw2WBaYPgbD5N7yCz8iSEpLoMlfPddnmwH6EzQItHsmBXnmf4pAO6MYfvfOwQ5wYbypjyvPT9vXOUh61mVE5Rg==} + cpu: [x64] + os: [darwin] + + '@qjsc-kmp/linux-arm64@0.1.2': + resolution: {integrity: sha512-2qHMVnAR0psnok/5/QnAXVqgpvk8g3H71uIxIx7IGtKd9DIUB7b4UuF92nAPpoW6l5bOtiOeN6oHcuKAafopFg==} + cpu: [arm64] + os: [linux] + + '@qjsc-kmp/linux-x64@0.1.2': + resolution: {integrity: sha512-vs9ITUQccN4R39O3KaPKszElXpxYg4Ts+WFqvBWbI127lh5gLGvy3QdG3tSvnIA4aYbJTl7Tv52WE4Pt/Ivi7A==} + cpu: [x64] + os: [linux] + '@types/node@22.20.3': resolution: {integrity: sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==} @@ -491,6 +514,11 @@ packages: resolution: {integrity: sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==} engines: {node: ^20.19.0 || >=22.12.0} + qjsc-kmp@0.1.2: + resolution: {integrity: sha512-JDx7fslSos8pHww+/GAst8yqhwhxu3ThUYsn+3xq9P9Nf8JobeSJJsiBoHMBoKSLFmVxwcI/6r+NA84LLn5kQg==} + engines: {node: '>=18'} + hasBin: true + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -640,6 +668,18 @@ snapshots: '@oxc-project/types@0.150.0': {} + '@qjsc-kmp/darwin-arm64@0.1.2': + optional: true + + '@qjsc-kmp/darwin-x64@0.1.2': + optional: true + + '@qjsc-kmp/linux-arm64@0.1.2': + optional: true + + '@qjsc-kmp/linux-x64@0.1.2': + optional: true + '@types/node@22.20.3': dependencies: undici-types: 6.21.0 @@ -761,6 +801,13 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.150.0 '@oxc-parser/binding-win32-x64-msvc': 0.150.0 + qjsc-kmp@0.1.2: + optionalDependencies: + '@qjsc-kmp/darwin-arm64': 0.1.2 + '@qjsc-kmp/darwin-x64': 0.1.2 + '@qjsc-kmp/linux-arm64': 0.1.2 + '@qjsc-kmp/linux-x64': 0.1.2 + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 From c0622e3aa7139c9b60ad8e78a61e42325f2528ef Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 23 Sep 2026 11:56:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20qjsc-kmp=20=E7=9A=84=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E6=94=B9=E4=B8=BA=20npm=20=E5=8C=85=E2=80=94=E2=80=94?= =?UTF-8?q?build-chain=E3=80=81roadmap=E3=80=81README=20=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 构建链文档还写着查找顺序只有 --qjsc / TINYUI_QJSC / PATH、CI 现编 qjsc-kmp,roadmap 仍标待做;按 M5.2 的实际做法更新。 Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 8 ++++---- docs/build-chain.md | 16 ++++++++-------- docs/roadmap.md | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f653ce9..ed8e51e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ build-logic/ Gradle convention plugins ## 安装 - Kotlin:`implementation("app.tinyui:tinyui:")`(Maven Central;自带 `wang.harlon:quickjs-kmp`);热下发另加 `app.tinyui:tinyui-updates`,同版本号 -- JS:`pnpm add tinyui-core tinyui-native` 与 `pnpm add -D tinyui-cli`,三包同版本号;`tinyui build` 另需 quickjs-kmp 的宿主工具 `qjsc-kmp`(见下) +- JS:`pnpm add tinyui-core tinyui-native` 与 `pnpm add -D tinyui-cli`,三包同版本号;`tinyui build` 用的 quickjs-kmp 宿主工具 `qjsc-kmp` 随 `tinyui-cli` 的依赖装好(macOS、Linux 预编译版) 版本号与 git tag 一致,不带 `v` 前缀,Maven 与 npm 三包同号同 tag 发:推 tag 触发 `publish.yml`,Maven 走 Sonatype,npm 走 trusted publishing(OIDC,无 token)。 @@ -39,10 +39,10 @@ pnpm build && pnpm test # 三个 npm 包 pnpm schema # 改了 schema/ 之后重新生成两侧代码 ``` -sample 的 Gradle 构建会调 `tinyui build` 把 `sample/js` 编成字节码,需要 quickjs-kmp 的宿主工具 `qjsc-kmp`,Android host 测试还需要宿主 JNI 库。两种来源: +sample 的 Gradle 构建会调 `tinyui build` 把 `sample/js` 编成字节码,`qjsc-kmp` 来自 `pnpm install` 装好的 npm 包;Android host 测试还需要宿主 JNI 库。两种来源: -- 本地联调:`local.properties` 加 `quickjs-kmp.dir=`,SDK 从源码构建(composite build),两样宿主产物由该仓的任务自动编出,零配置 -- 否则:`TINYUI_QJSC` 指向 `qjsc-kmp`,`TINYUI_QUICKJS_HOST_JNI` 指向含 `libquickjs_kmp` 的目录(CI 的做法见 `.github/workflows/build.yml`) +- 本地联调:`local.properties` 加 `quickjs-kmp.dir=`,SDK 从源码构建(composite build),`qjsc-kmp` 与 JNI 库都由该仓的任务自动编出,零配置 +- 否则:`TINYUI_QUICKJS_HOST_JNI` 指向含 `libquickjs_kmp` 的目录(CI 的做法见 `.github/workflows/build.yml`);没有预编译 `qjsc-kmp` 的平台再用 `TINYUI_QJSC` 指向自己编的那份 ## License diff --git a/docs/build-chain.md b/docs/build-chain.md index 7a3fbe8..e827b33 100644 --- a/docs/build-chain.md +++ b/docs/build-chain.md @@ -2,7 +2,7 @@ - 状态:已完成(2026-09-16,PR #2;2026-09-17 加 §7 错误上报与 source map);2026-09-19 修订模块名规则——加包名前缀、去 `pages/` 段,引入 `tinyui.config.json`([ADR-006](./adr-006-hot-updates.md) §2.10),随热下发 M1 实施,sample 与 TrendingAI 的现有模块名一并改 - 来源:[ADR-005](./adr-005-engine.md) §4 "语言目标 / 模块" 两行的展开;roadmap A 组"构建链打通" -- 范围:`tinyui-cli` 的 `tinyui build`、`compose/` 的 K0 加载序列、sample 的接线与 CI。自动 thunk(roadmap C 组)、`qjsc-kmp` 二进制分发、页面级配置文件不在本期;字节码行号回映射见 §7 +- 范围:`tinyui-cli` 的 `tinyui build`、`compose/` 的 K0 加载序列、sample 的接线与 CI。自动 thunk(roadmap C 组)、页面级配置文件不在本期(`qjsc-kmp` 二进制分发已于 2026-09-23 随热下发 M5.2 做完,见 §5);字节码行号回映射见 §7 ## 1. 目标与验收 @@ -76,27 +76,27 @@ rolldown 是真正的备选(Rollup 同形 API、oxc 转译)。CLI 只包一 ## 5. 字节码由 CLI 生成,宿主工具从 quickjs-kmp 来 -字节码编译放 CLI 而不放 Gradle:业务 App 不会有本仓的 build-logic,`tinyui build` 必须自己出最终产物。`qjsc-kmp` 二进制查找顺序:`--qjsc` 参数 → `TINYUI_QJSC` 环境变量 → PATH。 +字节码编译放 CLI 而不放 Gradle:业务 App 不会有本仓的 build-logic,`tinyui build` 必须自己出最终产物。`qjsc-kmp` 二进制查找顺序:`--qjsc` 参数 → `TINYUI_QJSC` 环境变量 → npm 包 `qjsc-kmp` 的预编译版 → PATH。 -字节码绑定引擎构建(文件头记上游 commit,quickjs-kmp `docs/native-build.md`"字节码与宿主工具"),CLI 用的 `qjsc-kmp` 必须与 App 链接的 quickjs-kmp 同一上游 commit。 +字节码绑定引擎构建(文件头记上游 commit,quickjs-kmp `docs/native-build.md`"字节码与宿主工具"),CLI 用的 `qjsc-kmp` 必须与 App 链接的 quickjs-kmp 同一上游 commit。`tinyui-cli` 依赖的 `qjsc-kmp` 版本钉成与 catalog 的 `quickjsKmp` 相等(`packages/cli/test/qjsc.test.ts` 校验),对齐因此只剩"JS 工程的 `tinyui-cli` 与 App 的 `tinyui` 同版本"。 | 场景 | 来源 | |---|---| -| 本地 | `local.properties` 有 `quickjs-kmp.dir` 时,Gradle 把 `/library/build/native/host-tools/bin/qjsc-kmp` 传给 CLI,零配置 | -| CI(本期) | clone quickjs-kmp 的 catalog 对应 tag,`./gradlew :library:buildHostTools` 现编,多花一两分钟 | -| 长期 | quickjs-kmp 在 tag 发布时把 macOS / Linux 宿主二进制挂到 GitHub Release,CLI 按版本下载(esbuild 的平台包模式);属 quickjs-kmp 仓改动,记 roadmap | +| 默认 | npm 包 `qjsc-kmp`:入口包加 `@qjsc-kmp/{darwin-arm64,darwin-x64,linux-x64,linux-arm64}` 平台包(esbuild 模式,Linux 为 musl 全静态),npm 只装与本机匹配的那个;由 quickjs-kmp 随 tag 经 OIDC 发布 | +| 本地联调 | `local.properties` 有 `quickjs-kmp.dir` 时,Gradle 把 `/library/build/native/host-tools/bin/qjsc-kmp` 经 `--qjsc` 传给 CLI,用的是本地源码编出的那份 | +| 其他平台 | Windows 等没有预编译版的平台用 `--qjsc` / `TINYUI_QJSC` 指向自己编的那份 | ## 6. 各处职责 | 位置 | 改动 | |---|---| -| `packages/cli` | `bin` 入口 `tinyui`,子命令 `build`,读工程根 `tinyui.config.json`(包名、公钥、页面目录),参数 `--root` / `--out` / `--qjsc`;依赖 esbuild。测试:夹具 TSX 打包后断言 `h(` 调用、裸说明符保留、相对 import 已合并、`.map` 存在;qjsc 步骤在 `TINYUI_QJSC` 缺失时跳过 | +| `packages/cli` | `bin` 入口 `tinyui`,子命令 `build`,读工程根 `tinyui.config.json`(包名、公钥、页面目录),参数 `--root` / `--out` / `--qjsc`;依赖 esbuild。测试:夹具 TSX 打包后断言 `h(` 调用、裸说明符保留、相对 import 已合并、`.map` 存在;qjsc 步骤在找不到 `qjsc-kmp` 时跳过 | | `packages/core` / `native` | 不动运行时 API,只保证入口能打成单模块,各导出 `VERSION` 供 sample 显示 | | `compose/` | ADR-002 K0 最小版:注册 core → 注册 native → 运行页面模块 → 返回 namespace。命名待 M1 定,本期只保证序列在库里而不在 sample 里。commonTest 用 `JsBytecode.compile` 现场编字节码,不依赖宿主工具 | | `sample/js` | 新 pnpm workspace 成员,包名 `sample`,`src/pages/home.tsx`。本期页面用纯 TS 只 import `VERSION`:`h` 的签名是 C 组的事,不在 core 放临时实现;JSX 变换的正确性由 CLI 单元测试覆盖 | | `sample/shared` | Gradle:`pnpm run build` → `tinyui build` → `Sync` 只取 `.bin` 与清单 → `compose.resources.customDirectory`,`Res.readBytes("files/tinyui/…")` 读入后走 K0。AGP 9 的 KMP 库插件默认不处理 assets,Compose resources 在 Android 走 assets,必须 `androidResources { enable = true }`,否则 APK 里没有资源且构建不报错(2026-09-16 实测) | | `settings.gradle.kts` | 有 `quickjs-kmp.dir` 时把 `qjsc-kmp` 路径传给 Exec 任务 | -| CI | gradle job 加 setup-node + pnpm,clone quickjs-kmp 的 catalog 版本 tag 编 `buildHostTools`(qjsc-kmp)与 `buildNativeHostJni`(Android host 测试要加载宿主 JNI 库,Maven 包里没有),经 `TINYUI_QJSC` / `TINYUI_QUICKJS_HOST_JNI` 传入;ios.yml 同样 | +| CI | gradle job 加 setup-node + pnpm,clone quickjs-kmp 的 catalog 版本 tag 编 `buildNativeHostJni`(Android host 测试要加载宿主 JNI 库,Maven 包里没有),经 `TINYUI_QUICKJS_HOST_JNI` 传入;`qjsc-kmp` 来自 npm 包(2026-09-23 起);ios.yml 同样 | ## 7. 错误上报与 source map diff --git a/docs/roadmap.md b/docs/roadmap.md index d9ea805..a02b4b5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -47,7 +47,7 @@ |---|---|---| | ~~`createStore`(Solid 式 Proxy 深层响应式)~~ | ADR-005 | 已完成(2026-09-17,定名 `observable`,[runtime-api.md](./runtime-api.md) §2.6):直接赋值、属性级订阅、只深代理纯对象与数组;`reconcile` / `produce` 不做,触发条件"服务端全量刷新导致整表重绑成为性能问题" | | ~~热下发~~ | ADR-005 | 已定(2026-09-18,[ADR-006](./adr-006-hot-updates.md) + [updates.md](./updates.md);2026-09-19 定服务端与签名、改多包模型),实现待开,顺序见下方第 7 条;ADR-006 §4.3 的推迟项(控制台、遥测、D1、非 CF 适配器、计费)各带触发条件 | -| `qjsc-kmp` 宿主二进制分发 | build-chain.md §5 | **触发已到**(2026-09-19,第二个 App 接入,其 CI 不能现编 quickjs-kmp):形态改为 npm 平台包(`qjsc-kmp-darwin-arm64` / `-linux-x64` 等作 `tinyui-cli` 的 `optionalDependencies`,esbuild 模式)而非 GitHub Release——内网 npm 代理天然能拉;属 quickjs-kmp 仓改动,待做 | +| `qjsc-kmp` 宿主二进制分发 | build-chain.md §5 | **已完成**(2026-09-23,热下发 M5.2):npm 入口包 `qjsc-kmp` 加 `@qjsc-kmp/-` 平台包(esbuild 模式)而非 GitHub Release——内网 npm 代理天然能拉;quickjs-kmp 0.1.2 起随 tag 发布,`tinyui-cli` 依赖它 | | ~~App 级共享模块(业务共享代码进 external 列表与模块表)~~ | build-chain.md §3 | 砍掉(2026-09-19):多包模型下包自包含,体积问题的出口是拆包(ADR-006 §2.10) | | 向 bellard/mquickjs 上报 S4 段错误 | ADR-005 §3.2 | 用户决定 | | `ErrorBoundary` 分支级兜底 | ADR-002 | 整页失败的比例成为问题 | From c1de940ff4504ce47892d8097f4daa2fdf8a03cf Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 23 Sep 2026 12:08:01 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test(cli):=20CI=20=E9=87=8C=E6=89=BE?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=20qjsc-kmp=20=E5=8D=B3=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E8=80=8C=E4=B8=8D=E6=98=AF=E8=B7=B3=E8=BF=87=EF=BC=9B=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E6=96=87=E6=A1=A3=E6=A0=87=20M5.2=20=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 必然装有 npm 预编译版,找不到说明查找路径坏了,跳过会把它掩盖。 roadmap 已写 M5.2 完成,updates-plan 同步。 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/updates-plan.md | 4 ++-- packages/cli/test/build.test.ts | 3 ++- packages/cli/test/bundle.test.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/updates-plan.md b/docs/updates-plan.md index 01fb266..bc486d0 100644 --- a/docs/updates-plan.md +++ b/docs/updates-plan.md @@ -134,6 +134,6 @@ M1 之后 M2 → M3 与 M4 并行;M5.1、M5.2 与 tinyui 0.3.0(npm 三包 + | M4 服务 MVP | 已完成(tiny-ui/tinyui-updates-server PR #1,2026-09-22):Hono + 单 R2 桶,投递 / 发布 / release / 管理四组端点,Workers 运行时一致性测试 12 条;已部署 `updates.tinyui.app`(R2 桶 `tinyui-updates`,`ADMIN_TOKEN` 存 HarlonWang/secrets 的 `tinyui-updates/admin-token.txt`),Cloudflare Workers Builds 已连 tiny-ui/tinyui-updates-server 的 main,push 即部署(`wrangler deploy --config wrangler.tinyui.toml`) | | M5 CLI 发布与管理 | 已完成(2026-09-22):`publish`(发现 bundle 目录、并发上传、指针最后)、`apps` / `packages`(含 `rotate-key`)/ `tokens` / `releases` 四组命令;凭据只走环境变量,`--app` / `--channel` / `--pkg` 按名字规则校验;CLI 侧契约测试 15 条,另对本地 `wrangler dev` 起的真服务端跑通发布 / 幂等重发 / 晋级 / 改灰度 / 回滚 / 跨 channel 被拒 / 吊销即时生效 | | M5.1 改名 `hostVersion` | 已完成(2026-09-23,tinyui PR #17、tinyui-updates-server PR #2,服务端已部署):manifest 字段、Kotlin API、CLI `--host-version`、服务端路由与校验、文档全部改名;CLI、`Updates` 构造、服务端路径与 manifest 五处限定正整数;共用签名夹具换钥重签,Android host 与 iOS 模拟器两端验签通过 | -| M5.2 `qjsc-kmp` 平台包 | 待开;之后发 tinyui 0.3.0 | -| M6 TrendingAI 接入 | 拍板完成(2026-09-23),待 M5.1、M5.2、0.3.0 | +| M5.2 `qjsc-kmp` 平台包 | 已完成(2026-09-23,quickjs-kmp PR #13 与 0.1.2、tinyui PR #18):入口包 `qjsc-kmp` + `@qjsc-kmp/{darwin-arm64,darwin-x64,linux-x64,linux-arm64}`,Linux 为 musl 全静态;首版本地发布,之后随 quickjs-kmp 的 tag 经 OIDC 发布;`tinyui-cli` 依赖它并由测试钉住与 `quickjsKmp` 同版本,CI 不再现编 `qjsc-kmp` | +| M6 TrendingAI 接入 | 拍板完成(2026-09-23),待发 tinyui 0.3.0 | | M7 第二个 App | 待开 | diff --git a/packages/cli/test/build.test.ts b/packages/cli/test/build.test.ts index c191f6c..b69341f 100644 --- a/packages/cli/test/build.test.ts +++ b/packages/cli/test/build.test.ts @@ -7,6 +7,7 @@ import { build, type BuildResult } from "../src/build.ts"; import { findQjsc } from "../src/qjsc.ts"; const root = join(import.meta.dirname, "fixtures", "app"); +// CI always has the npm prebuilt, so a missing compiler there is a failure, not a skip const qjscFound = await findQjsc(); describe("tinyui build", () => { @@ -159,7 +160,7 @@ describe("tinyui build", () => { } }); - it("compiles every module to bytecode when qjsc-kmp is available", { skip: !qjscFound && "qjsc-kmp not found" }, async () => { + it("compiles every module to bytecode when qjsc-kmp is available", { skip: !qjscFound && !process.env["CI"] && "qjsc-kmp not found" }, async () => { for (const m of [...result.runtime, ...result.pages]) { assert.ok(m.bin, `${m.name} has bytecode`); const bytes = await readFile(m.bin!); diff --git a/packages/cli/test/bundle.test.ts b/packages/cli/test/bundle.test.ts index 0234971..6311e4e 100644 --- a/packages/cli/test/bundle.test.ts +++ b/packages/cli/test/bundle.test.ts @@ -150,7 +150,7 @@ describe("tinyui bundle", () => { await assert.rejects(bundle({ dist: chinese, hostVersion: "1", signingKey }), /every segment must match \[A-Za-z0-9\._-\]\+ to survive the delivery URL/); }); - it("bundles a real build end to end", { skip: !qjsc && "qjsc-kmp not found" }, async () => { + it("bundles a real build end to end", { skip: !qjsc && !process.env["CI"] && "qjsc-kmp not found" }, async () => { const root = join(tmp, "app"); await cp(join(fixtures, "app"), root, { recursive: true }); await writeFile(join(root, "tinyui.config.json"), JSON.stringify(config));