From 555be9648b07ad83672f9f21effba2ff0b70d99e Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 7 Oct 2026 11:47:33 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E5=AD=97=E8=8A=82=E7=A0=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8E=E7=BC=80=20.bin=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20.qjsb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-Droid 的 scanner 对源码树里后缀为 .bin 的二进制文件一律判为 prebuilt binary 并中止构建,宿主仓提交的内置页面包因此过不了(TrendingAI 自 1.9.0 起卡在 fdroiddata)。 QuickJS 字节码本身不变,只换文件名,CLI 产出、manifest 派生路径、Bundle 读取、 sample 的资源收集与文档同步改。 格式变化:tinyui 0.9 的 Bundle 只认 .qjsb,旧宿主读不了新包,宿主升级时须加 hostVersion (docs/updates.md §4.1);热下发服务端的路径派生另行同步。 Co-Authored-By: Claude Fable 5.1 --- .../kotlin/app/tinyui/BundleSmokeTest.kt | 2 +- .../kotlin/app/tinyui/BuildManifest.kt | 4 ++-- .../src/commonMain/kotlin/app/tinyui/Bundle.kt | 4 ++-- .../commonTest/kotlin/app/tinyui/BundleTest.kt | 6 +++--- docs/adr-006-hot-updates.md | 4 ++-- docs/build-chain.md | 10 +++++----- docs/js-runtime.html | 2 +- docs/updates-plan.md | 6 +++--- docs/updates.md | 14 +++++++------- packages/cli/src/build.ts | 6 +++--- packages/cli/src/bundle.ts | 2 +- packages/cli/src/payload.ts | 2 +- packages/cli/test/admin.test.ts | 2 +- packages/cli/test/bundle.test.ts | 10 +++++----- packages/cli/test/helpers.ts | 4 ++-- packages/cli/test/publish.test.ts | 6 +++--- packages/cli/test/pull.test.ts | 6 +++--- sample/shared/build.gradle.kts | 4 ++-- .../kotlin/app/tinyui/updates/Fixture.kt | 2 +- .../kotlin/app/tinyui/updates/UpdatesTest.kt | 18 +++++++++--------- 20 files changed, 57 insertions(+), 57 deletions(-) diff --git a/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt b/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt index 0d4f0cc..a3b7513 100644 --- a/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt +++ b/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt @@ -29,7 +29,7 @@ class BundleSmokeTest { engine.evaluateModule("import \"tinyui-core\"; export const v = typeof globalThis.__tinyui.mount;").use { assertEquals(JsValue.Str("function"), it.get("v")) } - engine.runBytecode(out.resolve("pages/counter.bin").readBytes()).let { if (it is AutoCloseable) it.close() } + engine.runBytecode(out.resolve("pages/counter.qjsb").readBytes()).let { if (it is AutoCloseable) it.close() } } } diff --git a/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt b/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt index eee50a0..ae6f172 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt @@ -28,11 +28,11 @@ class BuildManifest( ) { /** Every file of the package besides the manifest, as (path, key into [hashes]). */ val payload: List> - get() = pages.map { file(it) + ".bin" to it } + (i18n?.files?.values?.map { it to it } ?: emptyList()) + get() = pages.map { file(it) + ".qjsb" to it } + (i18n?.files?.values?.map { it to it } ?: emptyList()) fun buildId(module: String): String = buildIds[module] ?: "" - /** Output path of [module] without extension (`pages/home`): append `.bin` or `.js.map`. */ + /** Output path of [module] without extension (`pages/home`): append `.qjsb` or `.js.map`. */ fun file(module: String): String = files[module] ?: module companion object { diff --git a/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt b/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt index fe0aec4..773d315 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt @@ -3,7 +3,7 @@ package app.tinyui import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -/** Where a package's bytes come from; [path] is relative to the package (`manifest.json`, `pages/home.bin`). Null when absent. */ +/** Where a package's bytes come from; [path] is relative to the package (`manifest.json`, `pages/home.qjsb`). Null when absent. */ fun interface BundleFiles { suspend fun read(path: String): ByteArray? } @@ -24,7 +24,7 @@ class Bundle(val manifest: BuildManifest, private val files: BundleFiles) { suspend fun page(name: String): LoadedPage { require(name in manifest.pages) { "page $name is not in package ${manifest.name}: ${manifest.pages}" } - val bytecode = read(manifest.file(name) + ".bin") + val bytecode = read(manifest.file(name) + ".qjsb") val module = PageModule(name, bytecode, manifest.buildId(name), i18n()) return LoadedPage(module, sourceMaps(name), this) } diff --git a/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt b/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt index 07c07f3..c15e6e8 100644 --- a/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt +++ b/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt @@ -36,8 +36,8 @@ class BundleTest { private fun files(vararg extra: Pair) = Files( mapOf( "manifest.json" to manifest, - "pages/home.bin" to "HOME", - "pages/cart.bin" to "CART", + "pages/home.qjsb" to "HOME", + "pages/cart.qjsb" to "CART", *extra, ), ) @@ -62,7 +62,7 @@ class BundleTest { val bundle = Bundle.load(files) bundle.page("shop/home") bundle.page("shop/home") - assertEquals(listOf("manifest.json", "pages/home.bin", "pages/home.js.map", "pages/home.bin"), files.reads, "a missing map is asked for once") + assertEquals(listOf("manifest.json", "pages/home.qjsb", "pages/home.js.map", "pages/home.qjsb"), files.reads, "a missing map is asked for once") } @Test diff --git a/docs/adr-006-hot-updates.md b/docs/adr-006-hot-updates.md index 8e08a6d..d9fce24 100644 --- a/docs/adr-006-hot-updates.md +++ b/docs/adr-006-hot-updates.md @@ -19,7 +19,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi | `PageHost` 只收字节,库零 I/O,读资源在宿主 | `PageHost.kt`、TrendingAI `TinyUIHost.kt` | 热下发对库来说是"多一个字节来源 + 选哪个" | | release 不带 map,靠 `buildId` 离线对映射 | build-chain.md §7 | 错误上报链路不变 | -产物尺寸:`core.bin` 20 KB、`native.bin` 3 KB、页面 1~4 KB;十几页的 App 整包不到 100 KB。运行时随宿主后(§2.11)包里只剩页面,运行时的 23 KB 随库进 App。 +产物尺寸:`core.qjsb` 20 KB、`native.qjsb` 3 KB、页面 1~4 KB;十几页的 App 整包不到 100 KB。运行时随宿主后(§2.11)包里只剩页面,运行时的 23 KB 随库进 App。 ## 2. 候选与取舍 @@ -202,7 +202,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi | 项 | 结论 | |---|---| -| 包 | App 由 N≥1 个包组成;一个包 = 一次 `tinyui build` 的产物(`pages/**/*.bin` + `manifest.json`;运行时随宿主,§2.11),整包原子生效,各包独立;不做单页下发、不做跨包共享模块、不做 zip、不做 diff | +| 包 | App 由 N≥1 个包组成;一个包 = 一次 `tinyui build` 的产物(`pages/**/*.qjsb` + `manifest.json`;运行时随宿主,§2.11),整包原子生效,各包独立;不做单页下发、不做跨包共享模块、不做 zip、不做 diff | | 包名与模块名 | 包名 `[a-z0-9-]+` 来自 `tinyui.config.json`;模块名 = 路由键 = `/<相对路径>`,无 `pages/` 段 | | 兼容键 | 宿主声明的 `hostVersion`,作服务端路径;`engine` 要求相等、`tinyui` 要求与宿主兼容(同 major 且宿主不低于包),只校验;单纯升 tinyui 不加 `hostVersion` | | 运行时 | `tinyui-core` / `tinyui-native` 随库编成字节码进 App,包里不带;运行时公开面只增不删、不改语义(§2.11 铁律) | diff --git a/docs/build-chain.md b/docs/build-chain.md index 2331b73..9ec5dbe 100644 --- a/docs/build-chain.md +++ b/docs/build-chain.md @@ -8,13 +8,13 @@ `tinyui build` 把每个页面的 TSX 编成一个 ESM 模块字节码;`tinyui-core` / `tinyui-native` 各编成一个运行时模块字节码,由库构建时编好、随库发布(ADR-006 §2.11)。每页一个 Runtime 按 ADR-002 K0 的顺序加载。 -验收:sample(包名 `sample`)的 `src/pages/home.tsx` 经 CLI 产出 `pages/home.bin`,Android / iOS 上 App 启动后显示页面 `default` 导出的返回值,其中含 `import.meta.url` 的值 `tinyui:sample/home`——模块名、external、注册顺序三件事一次验完。CI 的 gradle 与 pnpm 两条 job 都经过这条链。 +验收:sample(包名 `sample`)的 `src/pages/home.tsx` 经 CLI 产出 `pages/home.qjsb`,Android / iOS 上 App 启动后显示页面 `default` 导出的返回值,其中含 `import.meta.url` 的值 `tinyui:sample/home`——模块名、external、注册顺序三件事一次验完。CI 的 gradle 与 pnpm 两条 job 都经过这条链。 ## 2. 管线 ``` -src/pages/home.tsx ──esbuild──▶ dist/pages/home.js + .map ──qjsc-kmp -m -n /home──▶ dist/pages/home.bin -tinyui-core 包入口 ──esbuild──▶ runtime/core.js ──qjsc-kmp -m -n tinyui-core──▶ runtime/core.bin (`pnpm runtime` 生成 compose 的 RuntimeBytecode.kt,随库发布) +src/pages/home.tsx ──esbuild──▶ dist/pages/home.js + .map ──qjsc-kmp -m -n /home──▶ dist/pages/home.qjsb +tinyui-core 包入口 ──esbuild──▶ runtime/core.js ──qjsc-kmp -m -n tinyui-core──▶ runtime/core.qjsb (`pnpm runtime` 生成 compose 的 RuntimeBytecode.kt,随库发布) tinyui-native 同上(对它而言 tinyui-core 是 external) ``` @@ -23,7 +23,7 @@ tinyui-native 同上(对它而言 tinyui-core 是 external) | esbuild | TS 类型擦除;JSX → `h()`(`jsxFactory: "h"`,`inject` 一个内存模块自动引入 `h` / `Fragment`,页面不手写);业务内部相对 import 合并进页面模块;`tinyui-core` / `tinyui-native` 在插件 `onResolve` 里标 `external` + `sideEffects: false`,保持裸说明符且未用到时整条 import 被去掉;`format: "esm"`,`target: "esnext"`;输出 source map | 任何语法降级;对运行时模块的解析 | | qjsc-kmp | 源码模块 → 字节码,`-n` 给定模块名,`--strip-source` 保留行号去源码 | 校验模块图(引擎加载时查表) | -**页面名即模块名即路由键**:`/`——包 `subscription` 的 `src/pages/home.tsx` → `subscription/home`,`src/pages/plans/detail.tsx` → `subscription/plans/detail`,无后缀。包名来自工程根的 `tinyui.config.json`(`{ "name", "publicKey", "pages": "src/pages", "defaultLocale", "i18n": "i18n" }`,后两个见 §8,`name` 匹配 `[a-z0-9-]+`,`publicKey` 见 updates.md §7),是命名空间,所以没有 `pages/` 段;一个 App 挂几个包(ADR-006 §2.10),路由键就靠这个前缀不撞。页面路径的每一段限定 `[A-Za-z0-9._-]+`(`tinyui build` 拒绝其余的,含中文文件名):它随产物路径进热下发的投递 URL,而 percent-encoding 要在 CLI、服务端、Kotlin 客户端三处各对称实现一次,任一处偏差就是客户端收不到更新且只能发 App 修(updates.md §6.1、§7 同一类风险)。页面内容与组件文案不受影响,只有文件名受限;日后若要放开,改 build 一处把模块名与输出路径解耦即可(`files` 本就是映射),协议与另外两端不动。运行时模块名固定 `tinyui-core`、`tinyui-native`,由库构建并随库发布,不进 `tinyui build` 的产物。产物目录 `pages/**/*.bin`、`manifest.json`(页面模块清单、每个模块的产物路径 `files` 与 `buildIds`,包身份 `name` / `publicKey` 与热下发用的 `version` / `createdAt` / `engine` / `tinyui` / `hashes` 见 [updates.md](./updates.md) §1.1;Kotlin 侧路由表的来源,宿主用 `BuildManifest.file(name)` 定位 `.bin` / `.js.map`,见 [app-model.md](./app-model.md))。 +**页面名即模块名即路由键**:`/`——包 `subscription` 的 `src/pages/home.tsx` → `subscription/home`,`src/pages/plans/detail.tsx` → `subscription/plans/detail`,无后缀。包名来自工程根的 `tinyui.config.json`(`{ "name", "publicKey", "pages": "src/pages", "defaultLocale", "i18n": "i18n" }`,后两个见 §8,`name` 匹配 `[a-z0-9-]+`,`publicKey` 见 updates.md §7),是命名空间,所以没有 `pages/` 段;一个 App 挂几个包(ADR-006 §2.10),路由键就靠这个前缀不撞。页面路径的每一段限定 `[A-Za-z0-9._-]+`(`tinyui build` 拒绝其余的,含中文文件名):它随产物路径进热下发的投递 URL,而 percent-encoding 要在 CLI、服务端、Kotlin 客户端三处各对称实现一次,任一处偏差就是客户端收不到更新且只能发 App 修(updates.md §6.1、§7 同一类风险)。页面内容与组件文案不受影响,只有文件名受限;日后若要放开,改 build 一处把模块名与输出路径解耦即可(`files` 本就是映射),协议与另外两端不动。运行时模块名固定 `tinyui-core`、`tinyui-native`,由库构建并随库发布,不进 `tinyui build` 的产物。产物目录 `pages/**/*.qjsb`、`manifest.json`(页面模块清单、每个模块的产物路径 `files` 与 `buildIds`,包身份 `name` / `publicKey` 与热下发用的 `version` / `createdAt` / `engine` / `tinyui` / `hashes` 见 [updates.md](./updates.md) §1.1;Kotlin 侧路由表的来源,宿主用 `BuildManifest.file(name)` 定位 `.qjsb` / `.js.map`,见 [app-model.md](./app-model.md))。 业务工程的 tsconfig 用 `jsx: "react-jsx"` + `jsxImportSource: "tinyui-core"` 做类型检查([jsx-transform.md](./jsx-transform.md) §4),而 esbuild 会读到这份 tsconfig 并按它产出 `import { jsx } from "tinyui-core/jsx-runtime"`,即使 `build()` 显式传了 `jsx: "transform"`;引擎里没有这个模块,加载报 `module 'tinyui-core/jsx-runtime' is not registered`(2026-09-16 CI 实测)。页面构建要传 `tsconfigRaw` 覆盖 tsconfig 的 jsx 三项。 @@ -106,7 +106,7 @@ rolldown 是真正的备选(Rollup 同形 API、oxc 转译)。CLI 只包一 | `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 实测) | +| `sample/shared` | Gradle:`pnpm run build` → `tinyui build` → `Sync` 只取 `.qjsb` 与清单 → `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 编 `buildNativeHostJni`(Android host 测试要加载宿主 JNI 库,Maven 包里没有),经 `TINYUI_QUICKJS_HOST_JNI` 传入;`qjsc-kmp` 来自 npm 包(2026-09-23 起);ios.yml 同样 | diff --git a/docs/js-runtime.html b/docs/js-runtime.html index 4b3c0a9..10fd473 100644 --- a/docs/js-runtime.html +++ b/docs/js-runtime.html @@ -93,7 +93,7 @@

0. 六层

TSX 源码业务写的 h() + thunk()esbuild 产物 -ESM 模块字节码pages/home.bin +ESM 模块字节码pages/home.qjsb 运行时对象owner / effect / signal / id patch 消息JSON 数组 Kotlin 节点表ADR-003 diff --git a/docs/updates-plan.md b/docs/updates-plan.md index 65c53fd..b729417 100644 --- a/docs/updates-plan.md +++ b/docs/updates-plan.md @@ -24,9 +24,9 @@ M1 之后 M2 → M3 与 M4 并行;M5.1、M5.2 与 tinyui 0.3.0(npm 三包 + |---|---| | `tinyui.config.json` | 工程根,`name`(`[a-z0-9-]+`,必填)/ `publicKey`(必填)/ `pages`(缺省 `src/pages`);`build` 无配置文件即报错 | | 模块名改 `/<相对路径>` | esbuild 入口 → `-n /home`,`import.meta.url = tinyui:/home`;manifest 的 `pages` / `files` / `buildIds` / `hashes` 键随之改;sample 包名 `sample`,`sample/shared` 与 CLI 测试里的 `pages/…` 全部改 | -| `manifest.json` 加 `name` / `publicKey` / `version` / `createdAt` / `engine` / `protocol` / `hashes` | `name` / `publicKey` 抄配置;`engine` 从第一个 `.bin` 的文件头读(`QJKB` 起偏移 12 的 40 字节 hex);`protocol` 读 `tinyui-core/protocol` 子路径导出,core 包加这个入口 | +| `manifest.json` 加 `name` / `publicKey` / `version` / `createdAt` / `engine` / `protocol` / `hashes` | `name` / `publicKey` 抄配置;`engine` 从第一个 `.qjsb` 的文件头读(`QJKB` 起偏移 12 的 40 字节 hex);`protocol` 读 `tinyui-core/protocol` 子路径导出,core 包加这个入口 | | `tinyui keys generate` | Node `crypto.generateKeyPairSync("ec", { namedCurve: "P-256" })`;输出私钥 PKCS#8 PEM 与 X9.63 裸点 base64 公钥(updates.md §7) | -| `tinyui bundle --runtime-version --signing-key [--rollout] [--out]` | manifest 加 `runtimeVersion` 后写定为 `///manifest.json`,对该文件字节 `crypto.sign("sha256", …, { dsaEncoding: "der" })`,签名与 `version` / `rollout` 写 `//current.json`;`.bin` 复制进 `/`;签名私钥的公钥必须等于 manifest 的 `publicKey`,否则拒绝 | +| `tinyui bundle --runtime-version --signing-key [--rollout] [--out]` | manifest 加 `runtimeVersion` 后写定为 `///manifest.json`,对该文件字节 `crypto.sign("sha256", …, { dsaEncoding: "der" })`,签名与 `version` / `rollout` 写 `//current.json`;`.qjsb` 复制进 `/`;签名私钥的公钥必须等于 manifest 的 `publicKey`,否则拒绝 | | 测试 | 夹具目录 → 字段齐全、模块名带包名、`hashes` 对得上、`current.json` 的签名可用公钥对 `manifest.json` 字节验回、改 `current.json` 的 `rollout` 不影响验签、私钥与 `publicKey` 不配对被拒、缺配置文件被拒 | 验收:`sample/js` 跑 `tinyui build && tinyui bundle`,双端显示 `tinyui:sample/home`,产出目录用 `openssl dgst -sha256 -verify` 对 `/manifest.json` 验 `current.json` 里的签名。 @@ -55,7 +55,7 @@ M1 之后 M2 → M3 与 M4 并行;M5.1、M5.2 与 tinyui 0.3.0(npm 三包 + | 测试 | 假 `fetch` 按协议喂:指针解析失败、签名错(含公钥轮换的诊断信息)、`name` 不匹配、manifest `version` 与指针不符、engine 不匹配、旧于内置、rollout 不中、sha256 错、全通过各一条;两个包一坏一好互不影响;启动选择四种情况 + installed 文件被改一个字节即回内置并 `Failed(integrity)` + 清理被去掉的包与 rv 不匹配的 installed;Android host 与 iOS simulator 各跑一遍验签 | | sample | 第二个包 `sample-extra`(一页即可)验多包;一个开关把 base URL 指向本机 `python3 -m http.server` 起的 `dist/ota/`,手动验"下次启动生效"与回退 | -验收:sample 在 Android 与 iOS 模拟器上从本机静态目录装上新包、重启生效;故意给一个包发坏包(改一个 `.bin` 字节、或页面顶层 `throw`)能回退并收到带 `pkg` 的 `RolledBack`,另一个包不受影响。 +验收:sample 在 Android 与 iOS 模拟器上从本机静态目录装上新包、重启生效;故意给一个包发坏包(改一个 `.qjsb` 字节、或页面顶层 `throw`)能回退并收到带 `pkg` 的 `RolledBack`,另一个包不受影响。 ## M4 · `tinyui-updates-server` MVP diff --git a/docs/updates.md b/docs/updates.md index 6559f85..9a75845 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -19,7 +19,7 @@ ``` manifest.json -pages/**/*.bin +pages/**/*.qjsb ``` ### 1.1 `manifest.json` @@ -33,8 +33,8 @@ pages/**/*.bin | `publicKey` | `tinyui build` | 验签公钥(§7 格式),读自 `tinyui.config.json`;**信任来源只有内置包里的这份**,下载的 manifest 里的只用于诊断 | | `version` | `tinyui build` | 包标识,目录名,只求唯一:`-`,如 `20260918T100212Z-3f2a1c9e04b7`;`--version` 可覆盖 | | `createdAt` | `tinyui build` | ISO 8601 UTC,新旧比较只看它 | -| `engine` | `tinyui build` | 字节码文件头里的引擎 commit(40 位 hex,所有 `.bin` 一致,取第一个);客户端要求等于宿主 | -| `hashes` | `tinyui build` | 模块名 → 该模块 `.bin` 的 sha256 hex,键与 `files` 一致;另含每个 i18n 文件,键为其相对路径(`i18n/en.json`) | +| `engine` | `tinyui build` | 字节码文件头里的引擎 commit(40 位 hex,所有 `.qjsb` 一致,取第一个);客户端要求等于宿主 | +| `hashes` | `tinyui build` | 模块名 → 该模块 `.qjsb` 的 sha256 hex,键与 `files` 一致;另含每个 i18n 文件,键为其相对路径(`i18n/en.json`) | | `i18n` | `tinyui build` | `{ "default": 默认语言, "files": { 语言: 相对路径 } }`,包没有 i18n 资源时省略(build-chain.md §8) | | `tinyui` | `tinyui build` | JS 工程构建时的 `tinyui-core` 版本,即页面需要的最低运行时(能用哪些内置组件与运行时 API);`publish` 核对目标宿主版本的 tinyui 下限与它兼容(§1.3),客户端核对它与 `TinyUI.version` 兼容(§4.3)。**兼容 = major 相同,且宿主的版本不低于包的**(semver `^`;0.x 之间按版本号全序比较,1.0 之前不允许破坏公开面)。成立的前提是运行时公开面只增不删(runtime-api.md §11);升 major 时宿主加 `hostVersion`(§4.1),旧 major 的包因不兼容被拒收 | | `requires` | `tinyui build` | 页面模块名 → `{ components, capabilities, channels }`:该页用到的带点宿主组件名、`host.call` 能力名与 `http.client` 通道名,各自排序;内置组件与 `default` 通道不列。`publish` 据此核对目标宿主版本(§1.3),客户端不读 | @@ -53,7 +53,7 @@ tinyui bundle --host-version --signing-key <私钥 PEM> [--rollout ``` dist/ota///current.json 指针:{ "version", "rollout", "signature" } dist/ota////manifest.json build 的 manifest + hostVersion,写定后不再变 -dist/ota////pages/**/*.bin +dist/ota////pages/**/*.qjsb dist/ota////i18n/.json manifest 的 i18n 列出的文件(有才写) ``` @@ -102,7 +102,7 @@ tinyui pull --channel production --host-version --out <宿主资源目录>/< |---|---|---| | `//current.json` | 可变指针 | `Cache-Control: no-store` | | `///manifest.json` | 不可变,签名覆盖其原始字节 | `Cache-Control: public, max-age=31536000, immutable` | -| `///.bin` | 不可变内容 | 同上 | +| `///.qjsb` | 不可变内容 | 同上 | | `///` | 不可变内容(包内 i18n 资源,build-chain.md §8) | 同上 | - `pkg` 库从内置 manifest 读,`hostVersion` 宿主给;宿主只拼 base,一个 App 不管几个包都是一个 base URL、一个 `fetch` @@ -134,7 +134,7 @@ base = https://updates.tinyui.app// 把宿主现在手写的"读 manifest → 读页面与 map"收进库;运行时字节码随库(ADR-006 §2.11),不从包里读: ```kotlin -fun interface BundleFiles { suspend fun read(path: String): ByteArray? } // path 如 pages/home.bin、manifest.json +fun interface BundleFiles { suspend fun read(path: String): ByteArray? } // path 如 pages/home.qjsb、manifest.json class Bundle(val manifest: BuildManifest, files: BundleFiles) { val name: String get() = manifest.name @@ -247,7 +247,7 @@ fetch //current.json ─解析失败──────── │ / engine ≠ QuickJs.upstreamCommit / tinyui 与 TinyUI.version 不兼容 ▶ Skipped(incompatible) // 发错目录,上报 ├─ createdAt ≤ embedded.createdAt(内置可用)────────────────────────▶ Skipped(older-than-embedded) │ - ▼ 逐文件 fetch ///(files 的 .bin 与 i18n.files)→ /staging//,每个核对 sha256 + ▼ 逐文件 fetch ///(files 的 .qjsb 与 i18n.files)→ /staging//,每个核对 sha256 ├─ 任一失败 ─删 staging──────────────────────────────────────────────▶ Failed(download | integrity) ▼ manifest 原始字节写入 staging//manifest.json,staging/ 改名 installed/,state.installed = version,删其他 installed ▼ Installed(version) // 下次启动生效 diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 663aa75..6affdb1 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -49,7 +49,7 @@ export interface BuildResult { /** `manifest.json` as `tinyui build` writes it (docs/updates.md §1.1). */ export interface Manifest { pages: string[]; - /** Module name → output path without extension (`pages/home`); hosts locate `.bin` / `.js.map` through it. */ + /** Module name → output path without extension (`pages/home`); hosts locate `.qjsb` / `.js.map` through it. */ files: Record; buildIds: Record; name: string; @@ -60,7 +60,7 @@ export interface Manifest { engine: string; /** Version of the `tinyui-core` the pages were built against: the oldest runtime they run on (docs/updates.md §1.1). */ tinyui: string; - /** Module name → sha256 hex of its `.bin`; empty when built with `jsOnly`. */ + /** Module name → sha256 hex of its `.qjsb`; empty when built with `jsOnly`. */ hashes: Record; /** Page module name → the host capabilities, http channels and host components it uses (docs/updates.md §1.3). */ requires: Record; @@ -141,7 +141,7 @@ async function finish(root: string, modules: BuiltModule[], qjsc: string | undef m.buildId = createHash("sha256").update(await readFile(m.js)).digest("hex").slice(0, 8); await rootRelativeSources(root, m.map); if (!qjsc) continue; - m.bin = m.js.replace(/\.js$/, ".bin"); + m.bin = m.js.replace(/\.js$/, ".qjsb"); await compileModule({ qjsc, input: m.js, output: m.bin, name: m.name }); } } diff --git a/packages/cli/src/bundle.ts b/packages/cli/src/bundle.ts index 514d790..2222f5e 100644 --- a/packages/cli/src/bundle.ts +++ b/packages/cli/src/bundle.ts @@ -60,7 +60,7 @@ export async function bundle(options: BundleOptions): Promise { if (publicKeyOf(privateKeyPem) !== manifest.publicKey) { throw new Error(`${options.signingKey} does not match the publicKey in ${dist}/manifest.json (tinyui.config.json)`); } - if (!manifest.engine) throw new Error(`${dist} has no bytecode (built with --js-only); a package needs .bin files`); + if (!manifest.engine) throw new Error(`${dist} has no bytecode (built with --js-only); a package needs .qjsb files`); // everything is checked before anything is written const files: { source: string; target: string }[] = []; diff --git a/packages/cli/src/payload.ts b/packages/cli/src/payload.ts index ac19ab9..9443a96 100644 --- a/packages/cli/src/payload.ts +++ b/packages/cli/src/payload.ts @@ -11,7 +11,7 @@ export interface PayloadManifest { /** Every file of the package: its path inside the package, and its key into `hashes`. */ export function payload(manifest: PayloadManifest): { path: string; hashKey: string }[] { return [ - ...manifest.pages.map((module) => ({ path: `${manifest.files[module]}.bin`, hashKey: module })), + ...manifest.pages.map((module) => ({ path: `${manifest.files[module]}.qjsb`, hashKey: module })), ...Object.values(manifest.i18n?.files ?? {}).map((path) => ({ path, hashKey: path })), ]; } diff --git a/packages/cli/test/admin.test.ts b/packages/cli/test/admin.test.ts index 3d90692..dec673c 100644 --- a/packages/cli/test/admin.test.ts +++ b/packages/cli/test/admin.test.ts @@ -85,7 +85,7 @@ describe("management commands", () => { it("encodes each segment and holds names to the rule the server enforces", () => { assert.equal(segments("demo", "a/b", "1"), "/demo/a%2Fb/1"); // page paths are ASCII by construction (tinyui build refuses the rest); encoding is the last line of defence - assert.equal(segments("demo", "fixture", "1", "订单.bin"), "/demo/fixture/1/%E8%AE%A2%E5%8D%95.bin"); + assert.equal(segments("demo", "fixture", "1", "订单.qjsb"), "/demo/fixture/1/%E8%AE%A2%E5%8D%95.qjsb"); // encodeURIComponent leaves dots alone, so a traversing value is refused rather than escaped for (const traversal of ["..", ".", ""]) assert.throws(() => segments("demo", traversal, "1"), /cannot be a path segment/); assert.throws(() => requireName("--app", ".."), /--app must match \[a-z0-9-\]\+/); diff --git a/packages/cli/test/bundle.test.ts b/packages/cli/test/bundle.test.ts index 15a27e3..351eb2d 100644 --- a/packages/cli/test/bundle.test.ts +++ b/packages/cli/test/bundle.test.ts @@ -36,8 +36,8 @@ describe("tinyui bundle", () => { const versionDir = join(result.dir, result.version); assert.deepEqual((await readdir(versionDir, { recursive: true })).filter((f) => f.includes(".")).sort(), [ "manifest.json", - "pages/home.bin", - "pages/orders.bin", + "pages/home.qjsb", + "pages/orders.qjsb", ]); const pointer = JSON.parse(await readFile(result.pointer, "utf8")) as Pointer; assert.deepEqual(Object.keys(pointer), ["version", "rollout", "signature"]); @@ -89,7 +89,7 @@ describe("tinyui bundle", () => { it("refuses a build whose bytecode changed since manifest.json was written, touching nothing", async () => { const dist = await fakeDist(join(tmp, "dist-stale")); - await writeFile(join(dist, "pages", "home.bin"), "tampered"); + await writeFile(join(dist, "pages", "home.qjsb"), "tampered"); await assert.rejects(bundle({ dist, hostVersion: "1", signingKey }), /does not match manifest.hashes/); await assert.rejects(readdir(join(dist, "ota")), /ENOENT/, "nothing was written"); }); @@ -123,7 +123,7 @@ describe("tinyui bundle", () => { await assert.rejects(bundle({ dist: name, hostVersion: "1", signingKey }), /manifest name must match/); const files = await fakeDist(join(tmp, "dist-files"), (m) => { m.files["fixture/home"] = "../outside/core"; }); await mkdir(join(tmp, "outside"), { recursive: true }); - await writeFile(join(tmp, "outside", "core.bin"), "x"); + await writeFile(join(tmp, "outside", "core.qjsb"), "x"); await assert.rejects(bundle({ dist: files, hostVersion: "1", signingKey }), /points outside/); await assert.rejects(readdir(join(tmp, "escape")), /ENOENT/); }); @@ -164,7 +164,7 @@ describe("tinyui bundle", () => { assert.ok(verify(config.publicKey, bytes, pointer.signature)); const manifest = JSON.parse(bytes.toString("utf8")) as Manifest; for (const m of built.pages) { - const copied = await readFile(join(result.dir, result.version, manifest.files[m.name]! + ".bin")); + const copied = await readFile(join(result.dir, result.version, manifest.files[m.name]! + ".qjsb")); assert.equal(createHash("sha256").update(copied).digest("hex"), manifest.hashes[m.name]); } }); diff --git a/packages/cli/test/helpers.ts b/packages/cli/test/helpers.ts index 08fbc8c..2769757 100644 --- a/packages/cli/test/helpers.ts +++ b/packages/cli/test/helpers.ts @@ -4,14 +4,14 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } import { join } from "node:path"; import type { Manifest } from "../src/build.ts"; -/** A `tinyui build` output written by hand: bundle only needs the manifest and the `.bin` files it lists. */ +/** A `tinyui build` output written by hand: bundle only needs the manifest and the `.qjsb` files it lists. */ export async function fakeDist(dir: string, publicKey: string, edit: (m: Manifest) => void = () => {}): Promise { const files = { "fixture/home": "pages/home", "fixture/orders": "pages/orders" }; const hashes: Record = {}; for (const [module, path] of Object.entries(files)) { const bytes = Buffer.concat([Buffer.from("QJKB"), Buffer.alloc(8), Buffer.from("a".repeat(40)), Buffer.from(module)]); await mkdir(join(dir, path, ".."), { recursive: true }); - await writeFile(join(dir, path + ".bin"), bytes); + await writeFile(join(dir, path + ".qjsb"), bytes); hashes[module] = createHash("sha256").update(bytes).digest("hex"); } const manifest: Manifest = { diff --git a/packages/cli/test/publish.test.ts b/packages/cli/test/publish.test.ts index 0abe4fd..c7e9745 100644 --- a/packages/cli/test/publish.test.ts +++ b/packages/cli/test/publish.test.ts @@ -13,7 +13,7 @@ import { fakeDist, startFakeServer, type FakeServer } from "./helpers.ts"; const pair = generateKeyPair(); const VERSION = "20260922T090000Z-3f2a1c"; const HOST_1 = "hostVersion 1\ntinyui 0.3.0\n\ncomponents\n ta.Icon name: string\n\ncapabilities\n billing.prices\n"; -const FILES = ["pages/home.bin", "pages/orders.bin", "manifest.json"]; +const FILES = ["pages/home.qjsb", "pages/orders.qjsb", "manifest.json"]; describe("tinyui publish", () => { let tmp: string; @@ -173,8 +173,8 @@ describe("tinyui publish", () => { const dir = await bundled("symlink"); const secret = join(tmp, "secret.txt"); await writeFile(secret, "not this package's to publish"); - await rm(join(dir, "fixture", "1", VERSION, "pages", "home.bin")); - await symlink(secret, join(dir, "fixture", "1", VERSION, "pages", "home.bin")); + await rm(join(dir, "fixture", "1", VERSION, "pages", "home.qjsb")); + await symlink(secret, join(dir, "fixture", "1", VERSION, "pages", "home.qjsb")); const mark = server.requests.length; // lexical checks pass here; only resolving the link catches it await assert.rejects(publish({ client, dir, app: "demo", channel: "staging" }), /leaves .*a bundle only publishes its own files/); diff --git a/packages/cli/test/pull.test.ts b/packages/cli/test/pull.test.ts index 6e7c833..c2bce97 100644 --- a/packages/cli/test/pull.test.ts +++ b/packages/cli/test/pull.test.ts @@ -62,7 +62,7 @@ describe("tinyui pull", () => { await publish(NEW, "2026-09-23T09:00:00Z"); assert.deepEqual(await pull(into(out)), { version: NEW, changed: true }); - assert.deepEqual((await readdir(join(out, "pages"))).sort(), ["home.bin", "orders.bin"]); + assert.deepEqual((await readdir(join(out, "pages"))).sort(), ["home.qjsb", "orders.qjsb"]); }); it("keeps the embedded package while the channel rolls out, or when it is not newer", async () => { @@ -89,9 +89,9 @@ describe("tinyui pull", () => { await assert.rejects(pull(into(out)), (e: Error) => e.message.includes(`pass --accept-key ${rotated.publicKey}`)); assert.equal(JSON.parse(await readFile(join(out, "manifest.json"), "utf8")).version, OLD); - const page = join(hostDir, ROTATED, "pages", "home.bin"); + const page = join(hostDir, ROTATED, "pages", "home.qjsb"); await writeFile(page, "tampered"); - await assert.rejects(pull(into(out, { acceptKey: rotated.publicKey })), /pages\/home\.bin does not match manifest\.hashes/); + await assert.rejects(pull(into(out, { acceptKey: rotated.publicKey })), /pages\/home\.qjsb does not match manifest\.hashes/); assert.equal(JSON.parse(await readFile(join(out, "manifest.json"), "utf8")).version, OLD); }); }); diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index 132de0e..54c4d78 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -38,7 +38,7 @@ kotlin { val updateHostSnapshot = providers.gradleProperty("tinyui.updateHostSnapshot") -// JS 侧:pnpm 编三个包 → tinyui build 把每个示例包的页面编成字节码 → 只把 .bin 与清单打成 Compose 资源(运行时随库) +// JS 侧:pnpm 编三个包 → tinyui build 把每个示例包的页面编成字节码 → 只把 .qjsb 与清单打成 Compose 资源(运行时随库) // 两个包演示多包模型(docs/updates.md §0):各自的 JS 工程、密钥、资源子目录 val jsPackages = mapOf("sample" to rootDir.resolve("sample/js"), "sample-extra" to rootDir.resolve("sample/js-extra")) val cliOut = layout.buildDirectory.dir("tinyui-cli") @@ -89,7 +89,7 @@ val collectTinyUIResources by tasks.registering(Sync::class) { jsPackages.keys.forEachIndexed { i, pkg -> from(packageBuilds[i]) { into("files/tinyui/$pkg") - include("pages/**/*.bin", "i18n/*.json", "manifest.json") + include("pages/**/*.qjsb", "i18n/*.json", "manifest.json") if (providers.gradleProperty("tinyui.maps").orNull != "false") include("pages/**/*.js.map") } } diff --git a/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt b/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt index f1167d1..02b97ef 100644 --- a/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt +++ b/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt @@ -33,5 +33,5 @@ object Fixture { """.trimIndent() + "\n" /** The bytes whose sha256 the fixture manifest lists. */ - val FILES = mapOf("pages/home.bin" to "HOME2") + val FILES = mapOf("pages/home.qjsb" to "HOME2") } diff --git a/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt b/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt index 5183f77..c07d899 100644 --- a/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt +++ b/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt @@ -43,17 +43,17 @@ class UpdatesTest { engine: String = Fixture.ENGINE, tinyui: String = Fixture.TINYUI, publicKey: String = Fixture.PUBLIC_KEY, - files: Map = mapOf("pages/home.bin" to "HOME-$version"), + files: Map = mapOf("pages/home.qjsb" to "HOME-$version"), hostVersion: String? = "1", ): String { val modules = mapOf("$name/home" to "pages/home") - val hashes = modules.entries.joinToString(",") { (m, p) -> "\"$m\":\"${files.getValue("$p.bin").encodeUtf8().sha256().hex()}\"" } + val hashes = modules.entries.joinToString(",") { (m, p) -> "\"$m\":\"${files.getValue("$p.qjsb").encodeUtf8().sha256().hex()}\"" } val hostVersion = if (hostVersion == null) "" else ",\"hostVersion\":\"$hostVersion\"" return """{"pages":["$name/home"],"files":{${modules.entries.joinToString(",") { "\"${it.key}\":\"${it.value}\"" }}},"buildIds":{},"name":"$name","publicKey":"$publicKey","version":"$version","createdAt":"$createdAt","engine":"$engine","tinyui":"$tinyui","hashes":{$hashes}$hostVersion}""" } private fun embedded(name: String = "shop", version: String = "v0", createdAt: String = "2026-09-22T09:00:00Z"): Bundle { - val files = mapOf("pages/home.bin" to "HOME-$version") + val files = mapOf("pages/home.qjsb" to "HOME-$version") val json = manifest(name, version, createdAt, files = files, hostVersion = null) return Bundle(BuildManifest.parse(json)) { path -> files[path]?.encodeToByteArray() } } @@ -64,7 +64,7 @@ class UpdatesTest { val version = pointerVersion ?: m.version server["$pkg/$hostVersion/current.json"] = """{"version":"$version","rollout":$rollout,"signature":"$signature"}""".encodeToByteArray() server["$pkg/$hostVersion/$version/manifest.json"] = manifestJson.encodeToByteArray() - val content = files ?: mapOf("pages/home.bin" to "HOME-${m.version}") + val content = files ?: mapOf("pages/home.qjsb" to "HOME-${m.version}") for ((path, bytes) in content) server["$pkg/$hostVersion/$version/$path"] = bytes.encodeToByteArray() } @@ -82,7 +82,7 @@ class UpdatesTest { assertEquals(UpdateEvent.Installed("shop", "v1"), events.last()) assertEquals("HOME-v0", pageBytes(first.current("shop")), "the process keeps what it started with") assertTrue(fs.exists(dir / "shop/installed/v1/manifest.json")) - assertTrue(fs.exists(dir / "shop/installed/v1/pages/home.bin")) + assertTrue(fs.exists(dir / "shop/installed/v1/pages/home.qjsb")) assertFalse(fs.exists(dir / "shop/staging")) assertContains(fs.read(dir / "shop/state.json") { readUtf8() }, "\"installed\":\"v1\"") @@ -187,11 +187,11 @@ class UpdatesTest { @Test fun aFileThatDoesNotMatchItsHashOrCannotBeFetchedLeavesNothingBehind() = runTest { - publish(manifest(), files = mapOf("pages/home.bin" to "tampered")) + publish(manifest(), files = mapOf("pages/home.qjsb" to "tampered")) val integrity = assertIs(updates(embedded()).check("shop")) assertEquals(FailStage.INTEGRITY, integrity.stage) - assertContains(integrity.message, "pages/home.bin") - server.remove("shop/1/v1/pages/home.bin") + assertContains(integrity.message, "pages/home.qjsb") + server.remove("shop/1/v1/pages/home.qjsb") publish(manifest(), files = emptyMap()) val download = assertIs(updates(embedded()).check("shop")) assertEquals(FailStage.DOWNLOAD, download.stage) @@ -240,7 +240,7 @@ class UpdatesTest { fun startupChecksTheInstalledBytesAndBlacklistsACorruptedPackage() = runTest { publish(manifest()) updates(embedded()).check("shop") - fs.write(dir / "shop/installed/v1/pages/home.bin") { writeUtf8("HOME-v1 ") } + fs.write(dir / "shop/installed/v1/pages/home.qjsb") { writeUtf8("HOME-v1 ") } events.clear() val u = updates(embedded()) assertEquals("HOME-v0", pageBytes(u.current("shop"))) From 8e5b6bb56c615fec4b245a0fd18218aa2f4b6b3d Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 7 Oct 2026 11:47:34 +0800 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20=E7=89=88=E6=9C=AC=E5=8F=B7=200.9.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt | 4 ++-- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/version.ts | 2 +- packages/native/package.json | 2 +- packages/native/src/index.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt b/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt index 0f5be6b..a78e804 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt @@ -4,10 +4,10 @@ package app.tinyui.generated /** Base64 of the runtime modules' bytecode, split to fit JVM string constants. */ internal object RuntimeBytecode { val core: Array = arrayOf( - "UUpLQjQAAAABAAAAMDRiZTI0NjAwMTU5OWY1OTk1ZmEyZjJkOGM5MWEwZjE5OGQzZjM0YwWbAhZ0aW55dWktY29yZQZCb3gMQnV0dG9uDENvbHVtbgZGb3IQRnJhZ21lbnQSSG9zdEVycm9yCEljb24UTGF6eUNvbHVtbg5Mb2FkaW5nFlJhZGlvQnV0dG9uBlJvdwhTaG93DFNwYWNlcghUZXh0ElRleHRGaWVsZA5WRVJTSU9ODGVmZmVjdAJoEGludGVybmFsEG1hbmlmZXN0CG1lbW8Ub2JzZXJ2YWJsZRJvbkNsZWFudXAWcGFnZVZpc2libGUGcmVmEHJlc291cmNlDHNpZ25hbAp0aHVuaw51bnRyYWNrDHVud3JhcBJfX2RlZlByb3AQX19leHBvcnQaY3VycmVudEVmZmVjdBhjdXJyZW50T3duZXIOcGVuZGluZwZyYW4kTUFYX1JVTlNfUEVSX0ZMVVNIFmNyZWF0ZU93bmVyGHJ1bldpdGhPd25lchhkaXNwb3NlT3duZXIYaW5zaWRlUmVuZGVyEHRyYWNraW5nFHJ1blBlbmRpbmckY3JlYXRlRWZmZWN0UmVjb3JkEnJ1bkVmZmVjdBpkaXNwb3NlRWZmZWN0FnVuc3Vic2NyaWJlFnJ1bkNsZWFudXBzCnRyYWNrDG5vdGlmeQ5DT05UUk9MClRodW5rDnBhdGNoZXMQaGFuZGxlcnMMbmV4dElkEHJlc2V0SWRzCFNsb3QaY2FsbENvbXBvbmVudBpjcmVhdGVFbGVtZW50EGJpbmRQcm9wDHNjYWxhcg5mbGF0dGVuGnJlbmRlckluT3duZXIQcGVuZGluZzIQbmV4dENiSWQKcXVlcnkIY2FsbAhzZW5kDHJlcG9ydBxyZXNvbHZlUGVuZGluZxpyZWplY3RQZW5kaW5nGmNhbmNlbFBlbmRpbmcYYWxsb2NhdGVDYklkHnJlZ2lzdGVyUGVuZGluZxRzZXRUaW1lb3V0GGNsZWFyVGltZW91dBxpbnN0YWxsR2xvYmFscwhyb290DmN1cnJlbnQOdmlzaWJsZRRzZXRWaXNpYmxlDHRvcGljcwxvbkVtaXQMbGlzdGVuDmd1YXJkZWQOZW50cmllcw5pbnN0YWxsBlJBVwhLRVlTDnByb3hpZXMWc3Vic2NyaWJlcnMUdW53cmFwRGVlcBp1bndyYXBTaGFsbG93FmlzV3JhcHBhYmxlCHdyYXAIc3Vicw5vYnNlcnZlDmNoYW5nZWQOaXNJbmRleBJoYW5kbGVyczIgaW50ZXJuYWxfZXhwb3J0cxhfX2hvc3RfYXBwbHkYX19ob3N0X3F1ZXJ5Fl9faG9zdF9jYWxsFl9faG9zdF9zZW5kGl9faG9zdF9yZXBvcnQOUmVmbGVjdAowLjguMRx0aW55dWkuY29udHJvbAptb3VudAxpbnNlcnQIbW92ZQxyZW1vdmUMb2Zmc2V0DGFzc2lnbhRjb21wb25lbnRzGGNhcGFiaWxpdGllcw51bm1vdW50EGRpc3BhdGNoCGVtaXQKZmx1c2gUdGlueXVpLnJhdxZ0aW55dWkua2V5cwZhbGwOZWZmZWN0cxBjbGVhbnVwcxBkaXNwb3NlZApvd25lcgRmbhJwcmV2T3duZXIUcHJldkVmZmVjdAJlAmMMc3BsaWNlDnJldmVyc2UIaW5pdApzdWJzMgJ2DmVmZmVjdDICbQpkaXJ0eQhwdXNobG9uQ2xlYW51cCgpIGNhbGxlZCBvdXRzaWRlIGEgc3luY2hyb25vdXMgcmVuZGVyIHBlcmlvZAhwcmV2AmkMcXVldWVkCHJ1bnN8dXBkYXRlIGxvb3A6IGFuIGVmZmVjdCByZS1yYW4gbW9yZSB0aGFuIDEwMCB0aW1lcyBpbiBvbmUgZmx1c2hmZWZmZWN0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCGRlcHMMcGFyZW50EGVudHJpZXMyEHBvc2l0aW9uCm5vZGVzCG5vZGUEYXQEdG8OaW5kZXhPZixtb3ZlOiBub2RlIG5vdCBpbiBzbG90MHJlbW92ZTogbm9kZSBub3QgaW4gc2xvdAJyAm4EaWQGY21kCGFyZ3NecmVmLmNtZCgpIGJlZm9yZSB0aGUgcmVmIHdhcyBhdHRhY2hlZCB0byBhIG5vZGUCeBJGcmFnbWVudDIKcHJvcHMQY2hpbGRyZW4IdHlwZVxoKCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kAnAGa2V5DHJlc3VsdAh3aGF0fGEgUHJvbWlzZSAoYXN5bmMgY29tcG9uZW50cyBhcmUgbm90IHN1cHBvcnRlZDsgdXNlIHJlc291cmNlKCkpDmlzQXJyYXlUc2V2ZXJhbCBub2RlcyAod3JhcCB0aGVtIGluIG9uZSBjb250YWluZXIpTmEgRm9yIC8gU2hvdyAod3JhcCBpdCBpbiBvbmUgY29udGFpbmVyKRRjb21wb25lbnQgFjxhbm9ueW1vdXM+RiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlLCBnb3QgAmsKY2hpbGQIdGVzdAI8AiAoPiBtdXN0IGJlIGEgZnVuY3Rpb24COghsYXN0CmZpcnN0AnRWPiBpcyBhIGZ1bmN0aW9uOyBkaWQgeW91IGZvcmdldCB0byBjYWxsIGl0P6oBPiBtdXN0IGJlIGEgc3RyaW5nLCBudW1iZXIsIGJvb2xlYW4gb3IgbnVsbDsgcHJvcHMgZG8gbm90IGNyb3NzIHRoZSBicmlkZ2UgYXMgb2JqZWN0cwZvdXQMcmVuZGVyOiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlCGNvZGUOZGV0YWlscw5oZWFkZXJzCGJvZHkScGF0Y2hKc29uCnBhcnNlEnN0cmluZ2lmeQhjYklkCHNpdGUIa2luZAxkZXRhaWwUcmVzdWx0SnNvbhRFX0JBRF9KU09OMmhvc3QgcmVzdWx0IGlzIG5vdCBKU09OOiASZXJyb3JKc29uCmVycm9yIGhvc3QgY2FsbCBmYWlsZWQEbXMcdGltZXIuc2NoZWR1bGUYdGltZXIuY2FuY2VsAmcKdG9waWNmb25FbWl0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCmVudHJ5BmVycgRFMQhwYWdlEnByb3BzSnNvbhBob3N0SnNvbihtb3VudCgpIGNhbGxlZCB0d2ljZU5wYWdlIG11c3QgcmV0dXJuIGV4YWN0bHkgb25lIG5vZGUsIGdvdCASYSBQcm9taXNlCmNsZWFyDG5vZGVJZApldmVudBZwYXlsb2FkSnNvbg5oYW5kbGVyDnBheWxvYWQIanNvbhBfX3Rpbnl1aVpvYnNlcnZhYmxlKCkgdGFrZXMgYSBwbGFpbiBvYmplY3Qgb3IgYW4gYXJyYXkIc2VlbgJ1CnByb3RvEGlzRnJvemVuDGNyZWF0ZQJzEHJlY2Vp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ZAIACe0p5gMPmQYTAxEcAwgDGw4NDRYUAAxCBgEAAAAAAQABAAIA+gUAAQDbKOYDBZwGDgIMAAxCBgEAAQABAgADABIB7gUBAADkBQASAJQIAxoAkggCGgBkAADoAilkAQDP7Q5kAgAJ7SnmAxGeBgcDBhwDCAMREgcBDQ8WFAAMAgYBAAAAAAEAAQACAOwEMEIB2yjmAwWsBggCDAAMAgYBAAAAAAEAAQACAI4FQ0IB2yjmAwWtBgoCDAAMAgYBAAAAAAEAAQACAIwFQkIB2yjmAwWuBgoCDAAMAgYBAAAAAAEAAQACAOoEL0IB2yjmAwWvBgkCDAAMAgYBAAAAAAEAAQACAO4EMUIB2yjmAwWwBggCDAA=", + "UUpLQjQAAAABAAAAMDRiZTI0NjAwMTU5OWY1OTk1ZmEyZjJkOGM5MWEwZjE5OGQzZjM0YwWbAhZ0aW55dWktY29yZQZCb3gMQnV0dG9uDENvbHVtbgZGb3IQRnJhZ21lbnQSSG9zdEVycm9yCEljb24UTGF6eUNvbHVtbg5Mb2FkaW5nFlJhZGlvQnV0dG9uBlJvdwhTaG93DFNwYWNlcghUZXh0ElRleHRGaWVsZA5WRVJTSU9ODGVmZmVjdAJoEGludGVybmFsEG1hbmlmZXN0CG1lbW8Ub2JzZXJ2YWJsZRJvbkNsZWFudXAWcGFnZVZpc2libGUGcmVmEHJlc291cmNlDHNpZ25hbAp0aHVuaw51bnRyYWNrDHVud3JhcBJfX2RlZlByb3AQX19leHBvcnQaY3VycmVudEVmZmVjdBhjdXJyZW50T3duZXIOcGVuZGluZwZyYW4kTUFYX1JVTlNfUEVSX0ZMVVNIFmNyZWF0ZU93bmVyGHJ1bldpdGhPd25lchhkaXNwb3NlT3duZXIYaW5zaWRlUmVuZGVyEHRyYWNraW5nFHJ1blBlbmRpbmckY3JlYXRlRWZmZWN0UmVjb3JkEnJ1bkVmZmVjdBpkaXNwb3NlRWZmZWN0FnVuc3Vic2NyaWJlFnJ1bkNsZWFudXBzCnRyYWNrDG5vdGlmeQ5DT05UUk9MClRodW5rDnBhdGNoZXMQaGFuZGxlcnMMbmV4dElkEHJlc2V0SWRzCFNsb3QaY2FsbENvbXBvbmVudBpjcmVhdGVFbGVtZW50EGJpbmRQcm9wDHNjYWxhcg5mbGF0dGVuGnJlbmRlckluT3duZXIQcGVuZGluZzIQbmV4dENiSWQKcXVlcnkIY2FsbAhzZW5kDHJlcG9ydBxyZXNvbHZlUGVuZGluZxpyZWplY3RQZW5kaW5nGmNhbmNlbFBlbmRpbmcYYWxsb2NhdGVDYklkHnJlZ2lzdGVyUGVuZGluZxRzZXRUaW1lb3V0GGNsZWFyVGltZW91dBxpbnN0YWxsR2xvYmFscwhyb290DmN1cnJlbnQOdmlzaWJsZRRzZXRWaXNpYmxlDHRvcGljcwxvbkVtaXQMbGlzdGVuDmd1YXJkZWQOZW50cmllcw5pbnN0YWxsBlJBVwhLRVlTDnByb3hpZXMWc3Vic2NyaWJlcnMUdW53cmFwRGVlcBp1bndyYXBTaGFsbG93FmlzV3JhcHBhYmxlCHdyYXAIc3Vicw5vYnNlcnZlDmNoYW5nZWQOaXNJbmRleBJoYW5kbGVyczIgaW50ZXJuYWxfZXhwb3J0cxhfX2hvc3RfYXBwbHkYX19ob3N0X3F1ZXJ5Fl9faG9zdF9jYWxsFl9faG9zdF9zZW5kGl9faG9zdF9yZXBvcnQOUmVmbGVjdAowLjkuMBx0aW55dWkuY29udHJvbAptb3VudAxpbnNlcnQIbW92ZQxyZW1vdmUMb2Zmc2V0DGFzc2lnbhRjb21wb25lbnRzGGNhcGFiaWxpdGllcw51bm1vdW50EGRpc3BhdGNoCGVtaXQKZmx1c2gUdGlueXVpLnJhdxZ0aW55dWkua2V5cwZhbGwOZWZmZWN0cxBjbGVhbnVwcxBkaXNwb3NlZApvd25lcgRmbhJwcmV2T3duZXIUcHJldkVmZmVjdAJlAmMMc3BsaWNlDnJldmVyc2UIaW5pdApzdWJzMgJ2DmVmZmVjdDICbQpkaXJ0eQhwdXNobG9uQ2xlYW51cCgpIGNhbGxlZCBvdXRzaWRlIGEgc3luY2hyb25vdXMgcmVuZGVyIHBlcmlvZAhwcmV2AmkMcXVldWVkCHJ1bnN8dXBkYXRlIGxvb3A6IGFuIGVmZmVjdCByZS1yYW4gbW9yZSB0aGFuIDEwMCB0aW1lcyBpbiBvbmUgZmx1c2hmZWZmZWN0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCGRlcHMMcGFyZW50EGVudHJpZXMyEHBvc2l0aW9uCm5vZGVzCG5vZGUEYXQEdG8OaW5kZXhPZixtb3ZlOiBub2RlIG5vdCBpbiBzbG90MHJlbW92ZTogbm9kZSBub3QgaW4gc2xvdAJyAm4EaWQGY21kCGFyZ3NecmVmLmNtZCgpIGJlZm9yZSB0aGUgcmVmIHdhcyBhdHRhY2hlZCB0byBhIG5vZGUCeBJGcmFnbWVudDIKcHJvcHMQY2hpbGRyZW4IdHlwZVxoKCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kAnAGa2V5DHJlc3VsdAh3aGF0fGEgUHJvbWlzZSAoYXN5bmMgY29tcG9uZW50cyBhcmUgbm90IHN1cHBvcnRlZDsgdXNlIHJlc291cmNlKCkpDmlzQXJyYXlUc2V2ZXJhbCBub2RlcyAod3JhcCB0aGVtIGluIG9uZSBjb250YWluZXIpTmEgRm9yIC8gU2hvdyAod3JhcCBpdCBpbiBvbmUgY29udGFpbmVyKRRjb21wb25lbnQgFjxhbm9ueW1vdXM+RiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlLCBnb3QgAmsKY2hpbGQIdGVzdAI8AiAoPiBtdXN0IGJlIGEgZnVuY3Rpb24COghsYXN0CmZpcnN0AnRWPiBpcyBhIGZ1bmN0aW9uOyBkaWQgeW91IGZvcmdldCB0byBjYWxsIGl0P6oBPiBtdXN0IGJlIGEgc3RyaW5nLCBudW1iZXIsIGJvb2xlYW4gb3IgbnVsbDsgcHJvcHMgZG8gbm90IGNyb3NzIHRoZSBicmlkZ2UgYXMgb2JqZWN0cwZvdXQMcmVuZGVyOiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlCGNvZGUOZGV0YWlscw5oZWFkZXJzCGJvZHkScGF0Y2hKc29uCnBhcnNlEnN0cmluZ2lmeQhjYklkCHNpdGUIa2luZAxkZXRhaWwUcmVzdWx0SnNvbhRFX0JBRF9KU09OMmhvc3QgcmVzdWx0IGlzIG5vdCBKU09OOiASZXJyb3JKc29uCmVycm9yIGhvc3QgY2FsbCBmYWlsZWQEbXMcdGltZXIuc2NoZWR1bGUYdGltZXIuY2FuY2VsAmcKdG9waWNmb25FbWl0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCmVudHJ5BmVycgRFMQhwYWdlEnByb3BzSnNvbhBob3N0SnNvbihtb3VudCgpIGNhbGxlZCB0d2ljZU5wYWdlIG11c3QgcmV0dXJuIGV4YWN0bHkgb25lIG5vZGUsIGdvdCASYSBQcm9taXNlCmNsZWFyDG5vZGVJZApldmVudBZwYXlsb2FkSnNvbg5oYW5kbGVyDnBheWxvYWQIanNvbhBfX3Rpbnl1aVpvYnNlcnZhYmxlKCkgdGFrZXMgYSBwbGFpbiBvYmplY3Qgb3IgYW4gYXJyYXkIc2VlbgJ1CnByb3RvEGlzRnJvemVuDGNyZWF0ZQJzEHJlY2Vp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", ) val native: Array = arrayOf( - "UUpLQjQAAAABAAAAMDRiZTI0NjAwMTU5OWY1OTk1ZmEyZjJkOGM5MWEwZjE5OGQzZjM0YwWmARp0aW55dWktbmF0aXZlFnRpbnl1aS1jb3JlEkhvc3RFcnJvcg5WRVJTSU9OEmFuYWx5dGljcwxjYWNoZWQMZXZlbnRzCGhvc3QIaHR0cAhpMThuDmxpbmtpbmcUbmF2aWdhdGlvbg5zZXNzaW9uDnN0b3JhZ2UKc3RvcmUEdWkQaW50ZXJuYWwMc2lnbmFsEm9uQ2xlYW51cBJpbnRlcm5hbDISaW50ZXJuYWwzEmludGVybmFsNBJpbnRlcm5hbDUSaW50ZXJuYWw2DnNpZ25hbDISaW50ZXJuYWw3DnNpZ25hbDMSaW50ZXJuYWw4DnNpZ25hbDQSaW50ZXJuYWw5FGludGVybmFsMTAUSG9zdEVycm9yMhRpbnRlcm5hbDExFEhvc3RFcnJvcjMSX19kZWZQcm9wEF9fZXhwb3J0JG5hdmlnYXRpb25fZXhwb3J0cwhwdXNoBnBvcBBvblJlc3VsdBpzdG9yZV9leHBvcnRzCndhdGNoHGV2ZW50c19leHBvcnRzCGVtaXQEb24YaHR0cF9leHBvcnRzDmNoYW5uZWwQZmFsbGJhY2sMY2xpZW50DnJlcXVlc3QIZ2V0Mghwb3N0BnB1dAZkZWwYaG9zdF9leHBvcnRzCGNhbGwec3RvcmFnZV9leHBvcnRzCGdldDMIc2V0MgxyZW1vdmUKY2xlYXIYaTE4bl9leHBvcnRzDmN1cnJlbnQUc2V0Q3VycmVudBRzdWJzY3JpYmVkDGZvbGxvdwxsb2NhbGUCdB5zZXNzaW9uX2V4cG9ydHMQY3VycmVudDIWc2V0Q3VycmVudDIWc3Vic2NyaWJlZDIKc3RhdGUMc2lnbkluFHVpX2V4cG9ydHMKdG9hc3QKYWxlcnQOY29uZmlybR5saW5raW5nX2V4cG9ydHMOb3BlblVybCJhbmFseXRpY3NfZXhwb3J0cwp0cmFjaxBsb2dnZWRJbgx1c2VySWQKMC44LjEGYWxsCHBhZ2UMcGFyYW1zCHNlbmQebmF2aWdhdGlvbi5wdXNoDHJlc3VsdBxuYXZpZ2F0aW9uLnBvcARmbgxvbkVtaXQibmF2aWdhdGlvbi5yZXN1bHQCcAJyBmtleQJ2CnF1ZXJ5EnN0b3JlLmdldBJzdG9yZS5zZXQSc3RyaW5naWZ5EHNldFZhbHVlHnN0b3JlLnN1YnNjcmliZQxzdG9yZToKdG9waWMOcGF5bG9hZBZldmVudHMuZW1pdCBldmVudHMuc3Vic2NyaWJlEHJlcXVlc3QyDG1ldGhvZAZ1cmwIYm9keQ5vcHRpb25zGGh0dHAucmVxdWVzdAZHRVQIUE9TVAZQVVQMREVMRVRFCGFyZ3MWc3RvcmFnZS5nZXQIY29kZRZzdG9yYWdlLnNldBpzdG9yYWdlLnNldCgiFiIpIGZhaWxlZDogHHN0b3JhZ2UucmVtb3ZlGnN0b3JhZ2UuY2xlYXIOZmV0Y2hlcghkYXRhDnNldERhdGEObG9hZGluZxRzZXRMb2FkaW5nCmVycm9yEHNldEVycm9yEGRpc3Bvc2VkDGxhdGVzdAZydW4OcmVmZXRjaAhydW4yEGN1cnJlbnQzDnBlbmRpbmcCZRZpMThuLmxvY2FsZRxpMThuLnN1YnNjcmliZQxsaXN0ZW4IcmVzdAh0ZXh0DGkxOG4udA5yZXBsYWNlAm0Wc2Vzc2lvbi5nZXQic2Vzc2lvbi5zdWJzY3JpYmUcc2Vzc2lvbi5zaWduSW4QdWkudG9hc3QQdWkuYWxlcnQUdWkuY29uZmlybR5saW5raW5nLm9wZW5VcmwKcHJvcHMCaw5lbnRyaWVzEkVfSU5WQUxJRCJhbmFseXRpY3MudHJhY2soIggiKTogUiBpcyBub3QgYSBzdHJpbmcsIG51bWJlciwgYm9vbGVhbiBvciBudWxsHmFuYWx5dGljcy50cmFjaw3mAwzoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwIOABLqAwBG7AMARO4DADDwAwAd8gMAKfQDACD2AwAx+AMAQvoDABX8AwA4/gMAK4AEABmCBAA+hAQAEwAAhgQAAQCGBAECAIgEAQMAhgQCBACGBAMFAIYEBAYA6gMFBwCGBAUIAIoEBQkAiAQFCgCGBAYLAIgEBgwAhgQHDQCIBAcOAIYECA8AhgQJEADqAwoRAIYEChIA6gMLAAwgBgGoAQAAAAUASzihBgCGBAAfAIwEAR8AiAQCHwCOBAMfAJAEBB8AkgQFHwDqAwYfAJQEBx8AigQIHwCWBAkfAJgECh8AmgQLHwCcBAwf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TAAAAKroQV8DAJUESgAAAKroNV8DAJUESwAAAKroKWQBABEElAEAAASVAQAAPmAAAADPBJYBAABfAgAElwEAACQEACECADB+AOihDoJkAgA+SwEAAASYAQAAC89JOgAAANBJkQEAACQCACnmAyyAAgB2LhEMGxIHAVM5EQQRJhEELy4RBC8uEQQwgQFhWCAWOVkRJygHERR1CgA=", + "UUpLQjQAAAABAAAAMDRiZTI0NjAwMTU5OWY1OTk1ZmEyZjJkOGM5MWEwZjE5OGQzZjM0YwWmARp0aW55dWktbmF0aXZlFnRpbnl1aS1jb3JlEkhvc3RFcnJvcg5WRVJTSU9OEmFuYWx5dGljcwxjYWNoZWQMZXZlbnRzCGhvc3QIaHR0cAhpMThuDmxpbmtpbmcUbmF2aWdhdGlvbg5zZXNzaW9uDnN0b3JhZ2UKc3RvcmUEdWkQaW50ZXJuYWwMc2lnbmFsEm9uQ2xlYW51cBJpbnRlcm5hbDISaW50ZXJuYWwzEmludGVybmFsNBJpbnRlcm5hbDUSaW50ZXJuYWw2DnNpZ25hbDISaW50ZXJuYWw3DnNpZ25hbDMSaW50ZXJuYWw4DnNpZ25hbDQSaW50ZXJuYWw5FGludGVybmFsMTAUSG9zdEVycm9yMhRpbnRlcm5hbDExFEhvc3RFcnJvcjMSX19kZWZQcm9wEF9fZXhwb3J0JG5hdmlnYXRpb25fZXhwb3J0cwhwdXNoBnBvcBBvblJlc3VsdBpzdG9yZV9leHBvcnRzCndhdGNoHGV2ZW50c19leHBvcnRzCGVtaXQEb24YaHR0cF9leHBvcnRzDmNoYW5uZWwQZmFsbGJhY2sMY2xpZW50DnJlcXVlc3QIZ2V0Mghwb3N0BnB1dAZkZWwYaG9zdF9leHBvcnRzCGNhbGwec3RvcmFnZV9leHBvcnRzCGdldDMIc2V0MgxyZW1vdmUKY2xlYXIYaTE4bl9leHBvcnRzDmN1cnJlbnQUc2V0Q3VycmVudBRzdWJzY3JpYmVkDGZvbGxvdwxsb2NhbGUCdB5zZXNzaW9uX2V4cG9ydHMQY3VycmVudDIWc2V0Q3VycmVudDIWc3Vic2NyaWJlZDIKc3RhdGUMc2lnbkluFHVpX2V4cG9ydHMKdG9hc3QKYWxlcnQOY29uZmlybR5saW5raW5nX2V4cG9ydHMOb3BlblVybCJhbmFseXRpY3NfZXhwb3J0cwp0cmFjaxBsb2dnZWRJbgx1c2VySWQKMC45LjAGYWxsCHBhZ2UMcGFyYW1zCHNlbmQebmF2aWdhdGlvbi5wdXNoDHJlc3VsdBxuYXZpZ2F0aW9uLnBvcARmbgxvbkVtaXQibmF2aWdhdGlvbi5yZXN1bHQCcAJyBmtleQJ2CnF1ZXJ5EnN0b3JlLmdldBJzdG9yZS5zZXQSc3RyaW5naWZ5EHNldFZhbHVlHnN0b3JlLnN1YnNjcmliZQxzdG9yZToKdG9waWMOcGF5bG9hZBZldmVudHMuZW1pdCBldmVudHMuc3Vic2NyaWJlEHJlcXVlc3QyDG1ldGhvZAZ1cmwIYm9keQ5vcHRpb25zGGh0dHAucmVxdWVzdAZHRVQIUE9TVAZQVVQMREVMRVRFCGFyZ3MWc3RvcmFnZS5nZXQIY29kZRZzdG9yYWdlLnNldBpzdG9yYWdlLnNldCgiFiIpIGZhaWxlZDogHHN0b3JhZ2UucmVtb3ZlGnN0b3JhZ2UuY2xlYXIOZmV0Y2hlcghkYXRhDnNldERhdGEObG9hZGluZxRzZXRMb2FkaW5nCmVycm9yEHNldEVycm9yEGRpc3Bvc2VkDGxhdGVzdAZydW4OcmVmZXRjaAhydW4yEGN1cnJlbnQzDnBlbmRpbmcCZRZpMThuLmxvY2FsZRxpMThuLnN1YnNjcmliZQxsaXN0ZW4IcmVzdAh0ZXh0DGkxOG4udA5yZXBsYWNlAm0Wc2Vzc2lvbi5nZXQic2Vzc2lvbi5zdWJzY3JpYmUcc2Vzc2lvbi5zaWduSW4QdWkudG9hc3QQdWkuYWxlcnQUdWkuY29uZmlybR5saW5raW5nLm9wZW5VcmwKcHJvcHMCaw5lbnRyaWVzEkVfSU5WQUxJRCJhbmFseXRpY3MudHJhY2soIggiKTogUiBpcyBub3QgYSBzdHJpbmcsIG51bWJlciwgYm9vbGVhbiBvciBudWxsHmFuYWx5dGljcy50cmFjaw3mAwzoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwLoAwIOABLqAwBG7AMARO4DADDwAwAd8gMAKfQDACD2AwAx+AMAQvoDABX8AwA4/gMAK4AEABmCBAA+hAQAEwAAhgQAAQCGBAECAIgEAQMAhgQCBACGBAMFAIYEBAYA6gMFBwCGBAUIAIoEBQkAiAQFCgCGBAYLAIgEBgwAhgQHDQCIBAcOAIYECA8AhgQJEADqAwoRAIYEChIA6gMLAAwgBgGoAQAAAAUASzihBgCGBAAfAIwEAR8AiAQCHwCOBAMfAJAEBB8AkgQFHwDqAwYfAJQEBx8AigQIHwCWBAkfAJgECh8AmgQLHwCcBAwfAJ4EDR8AoAQOHwCiBA8fAKQEEB8ApgQRHwCoBBIfAKoEAAYArAQBBgCuBAIGALAEA0YBsgQERgG0BAVGAbYEBgYAigEHRgGMAQhGAbgECUYBugQKBgC8BAtGAb4EDEYBwAQNBgDCBA5GAcQEDwYAxgQQRgHIBBEGAMoEEgYAzAQTBgDOBBQGANAEFQYA0gQWBgDUBBdGAdYEGAYA2AQZRgHaBBpGAdwEG0YB3gQcRgHwAx1GAeAEHgYA4gQfBgDkBCAGAOYEIQYA6AQiRgHqBCNGAewEJEYB7gQlBgDwBCYGAPIEJwYA9AQoBgD2BClGAfgEKkYB+gQrBgD8BCxGAf4ELUYBgAUuRgGCBS8GAIQFMEYBhgUxBgCIBTJGAewDMwYA3AIABQCqAwAFANICAAUAygIABQAIaIcAAAC+BFwWAL4FXBcAvgZcGAC+ClwaAL4LXBsAvgxcHAC+D1weAL4QXB8AvhdcIQC+GFwjAL4aXCoAviBcLAC+IVwtAL4iXC4AviNcLwC+JFwwAL4nXDUAvihcNgC+KVw3AL4sXDwAvi1cPQC+MVw/AL4yXEAAvjNcQQC+NVxDAL43XEUAKThKAD1pAAAAXBMAvgBKFgEAAFwUAAtcFQBbFABbFQALvgFKGgEAAEkaAQAAvgJKGQEAAEkZAQAAvgNKGAEAAEkYAQAA7g4LXBkAWxQAWxkAC74HSkUAAABJRQAAAL4ISkYAAABJRgAAAL4JShwBAABJHAEAAO4OC1wdAFsUAFsdAAu+DUoeAQAASR4BAAC+DkofAQAASR8BAADuDgtcIABbFABbIAALvhFKIwEAAEkjAQAAvhJKCQAAAEkJAAAAvhNKRQAAAElFAAAAvhRKJgEAAEkmAQAAvhVKJwEAAEknAQAAvhZKJAEAAEkkAQAA7g5bIQAEFgAAAO1dIgA9JAEAAFwkAFsiAD1FAAAAXCUAWyIAPSYBAABcJgBbIgA9JwEAAFwnAFsiAD0JAAAAXCgAC1wpAFsUAFspAAu+GUoqAQAASSoBAADuDgtcKwBbFABbKwALvhtK+AAAAEn4AAAAvhxKLwEAAEkvAQAAvh1KRQAAAElFAAAAvh5KLgEAAEkuAQAAvh9KRgAAAElGAAAA7g4LXDEAWxQAWzEAC74lSjUBAABJNQEAAL4mSjYBAABJNgEAAO4OBhHw6RF7fgAOXDIAfgAOXDMAguoJDmQLAL/t6ukJXDQAC1w4AFsUAFs4AAu+Kko8AQAASTwBAAC+K0o7AQAASTsBAADuDgYR8OkRe34ADlw5AH4ADlw6AILqFQ5kDQALCUlFAQAAB0lGAQAA7erdCVw7AAtcPgBbFABbPgALvi5KPwEAAEk/AQAAvi9KQAEAAElAAQAAvjBKPgEAAEk+AQAA7g4LXEIAWxQAW0IAC740SkIBAABJQgEAAO4OC1xEAFsUAFtEAAu+NkpEAQAASUQBAADuDgRHAQAAXEYABi/mA7gBAAAAiQEAIBEMGw81AQAEDBQSLRESygEAAygUEiMREsoBAAMqFhIlERKOAQADHBISIRESAEwAAQACJg4qDgABCA8REBsTEgEREBsTEgQREBsTEgIREBsTEgQREBsTGRISIRESUgEAAxIYEicREgBAAAEAA4IBEhIhERKOAQAYCigWDhcnAAQsChInERKOAQAYCixSDhcpAAQeARIdERLKAQADIhgSJxESUgEAAxIcEisRElIBAAceCAAMAgYBAAIBAgcAAQAeA7QBAQAAkAUBAAB0AAAAqgQTAgDQeuoWx9vPwwvQw0NJRQAAAApJQwAAAO8OfejpDinmAxIBDwMIHB0HFAcQDBoHCAcBPjEADAIGAQAAAAABAAEAAgC0BBhCAdso5gMECQwCDAAMAgYBAAAAAAEAAQACALIEF0IB2yjmAwQKBwIMAAwCBgEAAAAAAQABAAIAsAQWQgHbKOYDBAsIAgwADEEGAbAEAgIBBQABAC8EkgUBAACUBQEAAJIF/////w8AoJQFAQCghgQAGgBeAQBeAADPx9AR8OgEDgvYyGQAAD5LAQAABEwBAAALz0lJAQAA0ElKAQAAJAIAKeYDCA4AWAQREHUKAAxBBgGyBAEBAAUAAQAkApoFAQAAmgX/////DwCghgQAGgBeAADPEfDoBA4H18dkAAA+SwEAAAROAQAAC89JTQEAACQCACnmAwgRAD8EERBXCgAMQwYBtAQBAAEEAQEBEwGeBQEAQIYEABoAZAAAPlABAAAEUQEAAL4AJAIAKeYDCBQAAwQRED4OAAxCBgEAAQEBAwABABgCpAUBAACmBQAAsJ4FAAEAXgAAz8fbXwAAPU0BAABfAAA9fwAAAO4p5gMSFScSMQcDCA8HBhECGxIRAhsXAAwCBgEAAAAAAQABAAIAigEaQgHbKOYDBB4HAgwADAIGAQAAAAABAAEAAgCMARtCAdso5gMEHwcCDAAMAgYBAAAAAAEAAQACALgEHEIB2yjmAwQgCQIMAAxDBgGKAQEBAQUAAQAnAqgFAQAAqgUAALCMBAEaAF4AAGQAAD5WAQAABFcBAAALz0lUAQAAJAIAx18AAPHoAwYoXwAAKOYDEiMAEhgRElcMESEIAREEGygROQAMQwYBjAECAAIHAAIAKQKoBQEAAIgBAQAAjAQBGgDcAkcDAGQAAD5LAQAABFgBAAALz0lUAQAAOAEAPlkBAADQJAEASUQAAAAkAgAp5gMQJwADBBESV0IRCBsWBwEqUwAMQwYBuAQBAgEFAQMBWQOoBQEAAIgBAACwtAUBAPCIBAIaAIoBGkIBjAQBGgBeAQBeAAAGEfDpDXt+AA7HfgAOyILqCw5kAADcz+3t6utkAgA+SwEAAARbAQAAC89JVAEAACQCAA5kAgA+UAEAAARcAQAAPmAAAADPJAEAvgAkAgAOXwAAKOYDHioAezgRDgcIBwEHBxI/ERJXChcbERJNIiATFxERDQAMQgYBAAEAAQIBAQEIAaQFAQBAtAUBGABkAAC+ACMBAOYDBi0jAg4bEAAMQgYBAAAAAAIAAQANAKQFAAEA2z1EAAAAEa7oAw4GKOYDBi0zAgwHAgAMAgYBAAAAAAEAAQACALwEHkIB2yjmAwQ0CAIMAAwCBgEAAAAAAQABAAIAvgQfQgHbKOYDBDUGAgwADEEGAbwEAgIBBQABAC8EugUBAAC8BQEAALoF/////w8AoLwFAQCgjgQDGgBeAQBeAADPx9AR8OgEDgvYyGQAAD5LAQAABF8BAAALz0ldAQAA0EleAQAAJAIAKeYDCDgAWAQREnUKAAxDBgG+BAIAAgUAAQAmAroFAQAAngUBAACOBAMaAGQAAD5LAQAABGABAAALz0ldAQAAJAIADmQAAD5QAQAAz9AkAgAp5gMSOwADBBESVwoXGxESGxAHDgcPAAwCBgEAAAAAAQABAAIAxgQjQgHbKOYDBEMKAgwADAIGAQAAAAABAAEAAgDQBCgCANso5gMERAoCDAAMAgYBAAAAAAEAAQACAMoEJQIA2yjmAwRFBwIMAAwCBgEAAAAAAQABAAIAzAQmAgDbKOYDBEYIAgwADAIGAQAAAAABAAEAAgDOBCcCANso5gMERwcCDAAMAgYBAAAAAAEAAQACAMgEJAIA2yjmAwRICwIMAAxDBgHCBAEBAQICAQVFAnQBAEDCBQAB8JAEBBoAXgAAvgBKYQEAAMcLXwAASSQBAAC+AUpFAAAASUUAAAC+AkomAQAASSYBAAC+A0onAQAASScBAAC+BEoJAAAASQkAAAAo5gMKSwA1Ig4HADgBFQAMQAYBAAQEAwYAAgBXCMQFAQAAxgUBAADIBQEAAMoFAQAAxAX/////DwCgxgUBAKDIBQIAoMoFAwCgkAQAGgB0AAEAXgMAXgIAXgEAXgAAz8fQyNHJ0hHw6AQOC9rKZAAAPioBAAAEZgEAAAvcSSEBAADPSWIBAADQSWMBAADRBqoR6AkOC9FJZAEAAAdQBg4O0gdQBg4OJQIA5gMRTBOJShESOUJcLAwKVzYgowEADEIGAQACAAIFAAEADgLGBQEAAMoFAQAAwgUAGABkAAAEZwEAAM8G0CMEAOYDCk8JAiQqIAwaBykADEIGAQADAAMFAAEADgPGBQEAAMgFAQAAygUBAADCBQAYAGQAAARoAQAAz9DRIwQA5gMMUAoCMCoiBwoHDAcnAAxCBgEAAwADBQABAA4DxgUBAADIBQEAAMoFAQAAwgUAGABkAAAEaQEAAM/Q0SMEAOYDDFEJAjAqIAcKBwwHJQAMQgYBAAIAAgUAAQAOAsYFAQAAygUBAADCBQAYAGQAAARqAQAAzwbQIwQA5gMKUgwCJComDBoHLwAMQwYBxgQBAAECAAEABQF0AQAAwgQhQgHbzyMBAOYDCFYAAxIHEAcdAAwCBgEAAAAAAQABAAIA1AQqQgHbKOYDBGIIAgwADEEGAdQEAgIBBAABAB4EdAEAANYFAQAAdP////8PAKDWBQEAoJIEBRoAXgEAXgAAz8fQEfDoBA4L2MhkAAA+KgEAAM/QJQIA5gMMZQBYEhESGwwHDAc3AAwCBgEAAAAAAQABAAIA8AMwQgHbKOYDBGwKAgwADAIGAQAAAAABAAEAAgDeBC9CAdso5gMEbQkCDAAMAgYBAAAAAAEAAQACANgELEIB2yjmAwRuBwIMAAwCBgEAAAAAAQABAAIA3AQuQgHbKOYDBG8KAgwADAIGAQAAAAABAAEAAgDaBC1CAdso5gMEcAcCDAAMQwYB2AQBAQEFAAEAJwKoBQEAAKoFAACwlAQHGgBeAABkAAA+VgEAAARsAQAAC89JVAEAACQCAMdfAADx6AMGKF8AACjmAxJzABIYERJXDBEhCAERBBsoETkADEMGAdoEAgECCAADAFQDqAUBAACIAQEAANoFAACwlAQHGgDcAkcDAOoDBhoAXgAAZAAAPlYBAAAEbgEAAAvPSVQBAAA4AQA+WQEAANAkAQBJRAAAACQCAMdfAADx6SJkAgARXwAABG8BAAA+YAAAAM8EcAEAAF8AACQDACECADAp5gMidwASHhESV0gRCBsWBwEqVxEhCA0RChIGFhRDLCAiIE8RJQAMQwYB3AQBAAEFAAEAGAGoBQEAAJQEBxoAZAAAPksBAAAEcQEAAAvPSVQBAAAkAgAp5gMIfAADBBESVwoADEMGAd4EAAAABAABABIAlAQHGgBkAAA+SwEAAARyAQAACyQCACnmAwh/AAMEERI5CgAMQwYB8AMCCQIFBwUCpQELqAUBBEDmBQEGQOgFAACw6gUBAvDsBQIAsO4FAwPw8AUEALDyBQUF8PQFBgDg9gUHAeD4BQgAsNoELUIBqgNIAwCWBAkaANgELEIBigQIGgBeCABeBwBeBgBeBQBeBABeAwBeAgBeAQBeAAAGEfDpDXt+AA7HfgAOyILqCw5kAgDez+3t6usGEfDpDXt+AA7JfgAOyoLqCQ5kAgAJ7ertBhHw6Q97fgAOwQR+AA7BBYLqCQ5kAgAG7errCcEGs8EHZAQAvgDtDr4BSnwBAADBCF8IAOwOXwAAC18CAEl2AQAAXwQASXgBAABfCABJfQEAACYCACjmAyuCAQDkNBEQBwoHAQcJbAEWDnYVFg4XJxIDDRUbEjICAAI4ExEGDQpmQjlRAAxCBgEAAAAAAgABAAYA9AUGEAAKEWUAACnmAwWIAQwDDwAMQgYBAAAEAAQCCQNgBPwFAADw/gUBAfCABgIAoIIGAwCD9AUGEAD2BQcQAOoFARgA7gUDGADaBABCAagFAAEA8gUFGADmBQEBAKoDAQMAXgIAXgEAXgAAZAEAjRFlAQDHvgBKfwEAAMhkAwAK7Q5kBgAG7Q4GyWsOAAAAWwcA7GACAA7qGsprFgAAADgIAD6GAAAAxiQBAGACAA7qAjBfAgA+hAAAAL4BvgIkAgAp5gMliwEOMAoRAxsDKwgIHRYUDRMWEDIIEQ5FDREOGxAHATYzEQ4vCgAMQgYBAAAAAAIAAwAQAPQFABIA/AUAGAD2BQESAGQAAJQR6AkOZAEAZAIAqSjmAwuNARUCDioYERIRBwAMQgYBAAEBAQQBBgEyAqoFAQBAggYAAIP+BQEYAOoFAhoA7gUDGgDaBARCAagFBQIA8gUGGgBkAADslOgCKWQBAL4A7Q5kAgAJ7Q5rDQAAAN5bBADP7g4OKcdrDAAAAGQFAMPtDg4pMOYDH5YBEQMLERAXFQgDGw4NDRYUJw8HChEKBws2BxESBwEADEIGAQAAAAABAAEAAgCqBQABANso5gMFmQEOAgwADEIGAQABAAECAAMAFAGCBgEAAP4FARgA8gUGGgDuBQMaAGQAAOyU6AIpZAEAz+0OZAIACe0p5gMToAEHAwgREBcVCAMREgcBDQ8WFAAMAgYBAAAAAAEAAQACAOoENkIB2yjmAwWuAQoCDAAMAgYBAAAAAAEAAQACAOwEN0IB2yjmAwWvAQUCDAAMQwYB6AQAAAAEAAMBPgDkBDMCAOYENAIAmAQKGgDc6AIpCuDbZAIAPlYBAAAEggEAACQBAO0OZAIAPksBAAAEgwEAAAskAgAOZAIAPoQBAAAEggEAAL4AJAIAKeYDH7QBAAMMEgMIAw0ABxYREjQMER8NExESOQoXGxESPg4ADEIGAQABAAECAAEACgGkBQEAAOQEAAIA2889NQEAACMBAOYDC7oBIgIOBxYHAhsDAAxDBgHqBAAAAAEAAgAHAOgENUIB4gQyAgDb7A7cIwAA5gMLvAEAAwQHDA0CBw0ADEEGAewEAgIBBQEEA0gEqAUBAACKBgEAAIwGAACw1gUBAPDSAkkDAOgENUIB4gQyAgCYBAoaAA0BANReAQBeAADc7A7d7A5kAwA+VgEAAASHAQAAC89JVAEAACQCAMfQs0PIXwEA6BRfAAA+iAEAAL0AvQE0vgIkAgAoXwAAKOYDJ8ABADUEBwwNCwcODQwRElcMESEIBAwIBwsIBxsOEQg+EBaeARHRAQAHJlx7KFtBLVphLXowLTlfXSspXH0HdAEAAgAyAAAADgYAAAAGDfX///8TAAF7ABMBJAQAMAA5AEEAWgBfAF8AYQB6AA7o////FAEBfQAUABAMQgYBAAIAAgMAAgATApIGAQAAdAEAANYFARgA0gIAAwDQZAAApugLOAEAZAAA0EPtKM8o5gMVxQE1AhoHEBEFERQRDhEKBwEHCQweAAwCBgEAAAAAAQABAAIA+AQ9QgHbKOYDBcsBCgIMAAwCBgEAAAAAAQABAAIA9gQ8QgHbKOYDBcwBCQIMAAxDBgH2BAAAAAQABAFCAPIEOgIA9AQ7AgCcBAwaAPAEOQIA3JToOwrg22QCAD5WAQAABIoBAAAkAQDtDmQCAD5LAQAABIsBAAALJAIADmQCAD6EAQAABP8AAAC+ACQCAA7eIwAA5gMh0QEAAw4XBQ0ABxgREjQMER8NFRESOQoXGxESPg4YFQcNAAxCBgEAAQABAgABAAUBpAUBAADyBAACANvPIwEA5gMJ1gEgAg4HGAcBAAxBBgH4BAEBAAUAAQAjAuABAQAA4AH/////DwCgnAQMGgBeAADPEfDoBA6/18dkAAA+KgEAAASMAQAAC89JcAAAACUCAOYDCdoBAD8SERJXHwAMAgYBAAAAAAEAAQACAP4EQEIB2yjmAwXhAQkCDAAMAgYBAAAAAAEAAQACAIAFQUIB2yjmAwXiAQsCDAAMAgYBAAAAAAEAAQACAPwEP0IB2yjmAwXjAQkCDAAMQQYB/AQCAgEGAAEALgRsAQAAygUBAABs/////w8AoMoFAQCgoAQOGgBeAQBeAADPx9AR8OgEDgvYyGQAAD4qAQAABI0BAAALz0k2AAAA0AdQBg4OJQIA5gML5gEAWBIREldAIF8ADEMGAf4EAQABBgABABcBygUBAACgBA4aAGQAAD4qAQAABI4BAAALzwdQBg4OJQIA5gML6QEAAxIREjkuIE0ADEMGAYAFAQABBgABABcBygUBAACgBA4aAGQAAD4qAQAABI8BAAALzwdQBg4OJQIA5gML7AEAAxIREjkyIFEADAIGAQAAAAABAAEAAgCEBUNCAdso5gMF8wELAgwADEMGAYQFAQABBQABABcBxgUBAACiBA8aAGQAAD4qAQAABJABAAALz0ljAQAAJQIA5gMJ9gEAAxIRFFchAAwCBgEAAAAAAQABAAIAiAVFQgHbKOYDBf0BCQIMAAxBBgGIBQIEAQwAAwCmAQZ0AQAAogYBAAB0/////w8AoKIGAQCgpAYAALCqBQMAsMoCSgMApAQQGgCmBBEaAF4BAF4AAM/H0BHw6AQOC9jIXgMAXgIAOAAAPpMBAADQJAEAe+pde34ADsl+AA7Kgl8DAPHpTV8DAJUETAAAAKroQV8DAJUESgAAAKroNV8DAJUESwAAAKroKWQBABEElAEAAASVAQAAPmAAAADPBJYBAABfAgAElwEAACQEACECADB+AOihDoJkAgA+SwEAAASYAQAAC89JOgAAANBJkQEAACQCACnmAyyAAgB2LhEMGxIHAVM5EQQRJhEELy4RBC8uEQQwgQFhWCAWOVkRJygHERR1CgA=", ) } diff --git a/packages/cli/package.json b/packages/cli/package.json index bd4f418..e784774 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "tinyui-cli", - "version": "0.8.1", + "version": "0.9.0", "description": "TinyUI build tool: TSX -> h() (ES2025) -> ESM module bytecode.", "license": "MIT", "type": "module", diff --git a/packages/core/package.json b/packages/core/package.json index c9f937d..b9e121a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "tinyui-core", - "version": "0.8.1", + "version": "0.9.0", "description": "TinyUI runtime: signals, ownership, h/For/Show, patch emission.", "license": "MIT", "type": "module", diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index cf8a763..64d5065 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,2 +1,2 @@ /** The tinyui-core version; a package records it as the runtime it needs (docs/updates.md §1.1). */ -export const VERSION = "0.8.1"; +export const VERSION = "0.9.0"; diff --git a/packages/native/package.json b/packages/native/package.json index ece1952..4a6117b 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "tinyui-native", - "version": "0.8.1", + "version": "0.9.0", "description": "TinyUI host capability API for business code: http, storage, toast, navigation, i18n.", "license": "MIT", "type": "module", diff --git a/packages/native/src/index.ts b/packages/native/src/index.ts index 26c52b4..921a291 100644 --- a/packages/native/src/index.ts +++ b/packages/native/src/index.ts @@ -1,4 +1,4 @@ -export const VERSION = "0.8.1"; +export const VERSION = "0.9.0"; export * as navigation from "./navigation.ts"; export * as store from "./store.ts"; export * as events from "./events.ts"; From bd9735742ca547756f3e7587180dc76c164f1e54 Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 7 Oct 2026 11:55:24 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(cli):=20manifest=20=E7=9A=84=20tinyui?= =?UTF-8?q?=20=E5=8F=96=20core=20=E4=B8=8E=20CLI=20=E8=BE=83=E6=96=B0?= =?UTF-8?q?=E8=80=85=EF=BC=9Bsample=20=E5=AE=BF=E4=B8=BB=E7=89=88=E6=9C=AC?= =?UTF-8?q?=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 产物布局由 CLI 决定,同版本起的库才读得了:tinyui-cli 0.9 配旧 tinyui-core 时 manifest 只记 core 版本,publish 会放行给 tinyui 0.8 的宿主,旧 Bundle 取 .bin 取不到。sample 的字节码 后缀随之变了,按 docs/updates.md §4.1 加 hostVersion 并生成快照 4。 Co-Authored-By: Claude Fable 5.1 --- docs/updates.md | 2 +- packages/cli/src/build.ts | 8 ++++++++ .../src/commonMain/kotlin/app/tinyui/sample/Host.kt | 2 +- sample/shared/tinyui-host/4.txt | 10 ++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 sample/shared/tinyui-host/4.txt diff --git a/docs/updates.md b/docs/updates.md index 9a75845..4c54b02 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -36,7 +36,7 @@ pages/**/*.qjsb | `engine` | `tinyui build` | 字节码文件头里的引擎 commit(40 位 hex,所有 `.qjsb` 一致,取第一个);客户端要求等于宿主 | | `hashes` | `tinyui build` | 模块名 → 该模块 `.qjsb` 的 sha256 hex,键与 `files` 一致;另含每个 i18n 文件,键为其相对路径(`i18n/en.json`) | | `i18n` | `tinyui build` | `{ "default": 默认语言, "files": { 语言: 相对路径 } }`,包没有 i18n 资源时省略(build-chain.md §8) | -| `tinyui` | `tinyui build` | JS 工程构建时的 `tinyui-core` 版本,即页面需要的最低运行时(能用哪些内置组件与运行时 API);`publish` 核对目标宿主版本的 tinyui 下限与它兼容(§1.3),客户端核对它与 `TinyUI.version` 兼容(§4.3)。**兼容 = major 相同,且宿主的版本不低于包的**(semver `^`;0.x 之间按版本号全序比较,1.0 之前不允许破坏公开面)。成立的前提是运行时公开面只增不删(runtime-api.md §11);升 major 时宿主加 `hostVersion`(§4.1),旧 major 的包因不兼容被拒收 | +| `tinyui` | `tinyui build` | 页面需要的最低运行时:JS 工程构建时的 `tinyui-core` 版本(能用哪些内置组件与运行时 API)与 `tinyui-cli` 版本(产物布局由同版本起的库读取)取较新者;`publish` 核对目标宿主版本的 tinyui 下限与它兼容(§1.3),客户端核对它与 `TinyUI.version` 兼容(§4.3)。**兼容 = major 相同,且宿主的版本不低于包的**(semver `^`;0.x 之间按版本号全序比较,1.0 之前不允许破坏公开面)。成立的前提是运行时公开面只增不删(runtime-api.md §11);升 major 时宿主加 `hostVersion`(§4.1),旧 major 的包因不兼容被拒收 | | `requires` | `tinyui build` | 页面模块名 → `{ components, capabilities, channels }`:该页用到的带点宿主组件名、`host.call` 能力名与 `http.client` 通道名,各自排序;内置组件与 `default` 通道不列。`publish` 据此核对目标宿主版本(§1.3),客户端不读 | | `hostVersion` | `tinyui bundle` | 发布目标,等于宿主声明值;写入后文件定稿,签名覆盖它的原始字节(§7) | diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 6affdb1..b6559c3 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -12,6 +12,7 @@ import { compileModule, findQjsc } from "./qjsc.ts"; import { analyzePage, RequiresError, type PageRequires } from "./requires.ts"; import { SvgError, svgToIcon } from "./svg.ts"; import { TransformError, transformJsx } from "./transform.ts"; +import { compareVersions } from "./version.ts"; const execFileAsync = promisify(execFile); @@ -245,7 +246,14 @@ async function engineCommit(bin: string): Promise { return commit; } +/** The oldest tinyui the pages run on: the runtime they were built against, or this CLI if newer, since the file layout it writes is read by a library of the same version or later. */ async function tinyuiVersion(root: string): Promise { + const core = await coreVersion(root); + const cli = (JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as { version: string }).version; + return compareVersions(cli, core) > 0 ? cli : core; +} + +async function coreVersion(root: string): Promise { // the package's exports map hides package.json, so walk up from a file it does export let dir = dirname(createRequire(join(root, "package.json")).resolve("tinyui-core")); for (;;) { diff --git a/sample/shared/src/commonMain/kotlin/app/tinyui/sample/Host.kt b/sample/shared/src/commonMain/kotlin/app/tinyui/sample/Host.kt index 00331f8..7b076ec 100644 --- a/sample/shared/src/commonMain/kotlin/app/tinyui/sample/Host.kt +++ b/sample/shared/src/commonMain/kotlin/app/tinyui/sample/Host.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import okio.Path /** Raise when [sampleHost] changes what pages can use; `tinyui-host/.txt` records each value (docs/updates.md §4.1). */ -internal const val HOST_VERSION = "3" +internal const val HOST_VERSION = "4" /** The App's language setting; the language button in [App] flips it. */ internal val sampleLocale = MutableStateFlow("en") diff --git a/sample/shared/tinyui-host/4.txt b/sample/shared/tinyui-host/4.txt new file mode 100644 index 0000000..8c8af4e --- /dev/null +++ b/sample/shared/tinyui-host/4.txt @@ -0,0 +1,10 @@ +hostVersion 4 +tinyui 0.9.0 + +components + +capabilities + session.signIn + +channels + todos From dc5c1d87ecb0629db29abdff78a9264bc0a14f08 Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 7 Oct 2026 12:02:02 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs(cli):=20manifest.tinyui=20=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=B3=A8=E9=87=8A=E4=B8=8E=E5=8F=96=E5=80=BC?= =?UTF-8?q?=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- packages/cli/src/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index b6559c3..575b612 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -59,7 +59,7 @@ export interface Manifest { createdAt: string; /** Engine commit the bytecode is bound to; empty when built with `jsOnly`. */ engine: string; - /** Version of the `tinyui-core` the pages were built against: the oldest runtime they run on (docs/updates.md §1.1). */ + /** The oldest tinyui the pages run on: the `tinyui-core` they were built against or the CLI that wrote them, whichever is newer (docs/updates.md §1.1). */ tinyui: string; /** Module name → sha256 hex of its `.qjsb`; empty when built with `jsOnly`. */ hashes: Record; From 1678fd43f38eea1ad78dfddf50f2c8ceaa43afc4 Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 7 Oct 2026 12:15:07 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E5=AD=97=E8=8A=82=E7=A0=81=E5=90=8E?= =?UTF-8?q?=E7=BC=80=E5=AE=9A=E4=B8=BA=20.jsb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../kotlin/app/tinyui/BundleSmokeTest.kt | 2 +- .../kotlin/app/tinyui/BuildManifest.kt | 4 ++-- .../src/commonMain/kotlin/app/tinyui/Bundle.kt | 4 ++-- .../commonTest/kotlin/app/tinyui/BundleTest.kt | 6 +++--- docs/adr-006-hot-updates.md | 4 ++-- docs/build-chain.md | 10 +++++----- docs/js-runtime.html | 2 +- docs/updates-plan.md | 6 +++--- docs/updates.md | 14 +++++++------- packages/cli/src/build.ts | 6 +++--- packages/cli/src/bundle.ts | 2 +- packages/cli/src/payload.ts | 2 +- packages/cli/test/admin.test.ts | 2 +- packages/cli/test/bundle.test.ts | 10 +++++----- packages/cli/test/helpers.ts | 4 ++-- packages/cli/test/publish.test.ts | 6 +++--- packages/cli/test/pull.test.ts | 6 +++--- sample/shared/build.gradle.kts | 4 ++-- .../kotlin/app/tinyui/updates/Fixture.kt | 2 +- .../kotlin/app/tinyui/updates/UpdatesTest.kt | 18 +++++++++--------- 20 files changed, 57 insertions(+), 57 deletions(-) diff --git a/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt b/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt index a3b7513..aa7bfdc 100644 --- a/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt +++ b/compose/src/androidHostTest/kotlin/app/tinyui/BundleSmokeTest.kt @@ -29,7 +29,7 @@ class BundleSmokeTest { engine.evaluateModule("import \"tinyui-core\"; export const v = typeof globalThis.__tinyui.mount;").use { assertEquals(JsValue.Str("function"), it.get("v")) } - engine.runBytecode(out.resolve("pages/counter.qjsb").readBytes()).let { if (it is AutoCloseable) it.close() } + engine.runBytecode(out.resolve("pages/counter.jsb").readBytes()).let { if (it is AutoCloseable) it.close() } } } diff --git a/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt b/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt index ae6f172..1eacba4 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt @@ -28,11 +28,11 @@ class BuildManifest( ) { /** Every file of the package besides the manifest, as (path, key into [hashes]). */ val payload: List> - get() = pages.map { file(it) + ".qjsb" to it } + (i18n?.files?.values?.map { it to it } ?: emptyList()) + get() = pages.map { file(it) + ".jsb" to it } + (i18n?.files?.values?.map { it to it } ?: emptyList()) fun buildId(module: String): String = buildIds[module] ?: "" - /** Output path of [module] without extension (`pages/home`): append `.qjsb` or `.js.map`. */ + /** Output path of [module] without extension (`pages/home`): append `.jsb` or `.js.map`. */ fun file(module: String): String = files[module] ?: module companion object { diff --git a/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt b/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt index 773d315..5c11f03 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/Bundle.kt @@ -3,7 +3,7 @@ package app.tinyui import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -/** Where a package's bytes come from; [path] is relative to the package (`manifest.json`, `pages/home.qjsb`). Null when absent. */ +/** Where a package's bytes come from; [path] is relative to the package (`manifest.json`, `pages/home.jsb`). Null when absent. */ fun interface BundleFiles { suspend fun read(path: String): ByteArray? } @@ -24,7 +24,7 @@ class Bundle(val manifest: BuildManifest, private val files: BundleFiles) { suspend fun page(name: String): LoadedPage { require(name in manifest.pages) { "page $name is not in package ${manifest.name}: ${manifest.pages}" } - val bytecode = read(manifest.file(name) + ".qjsb") + val bytecode = read(manifest.file(name) + ".jsb") val module = PageModule(name, bytecode, manifest.buildId(name), i18n()) return LoadedPage(module, sourceMaps(name), this) } diff --git a/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt b/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt index c15e6e8..903c5ce 100644 --- a/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt +++ b/compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt @@ -36,8 +36,8 @@ class BundleTest { private fun files(vararg extra: Pair) = Files( mapOf( "manifest.json" to manifest, - "pages/home.qjsb" to "HOME", - "pages/cart.qjsb" to "CART", + "pages/home.jsb" to "HOME", + "pages/cart.jsb" to "CART", *extra, ), ) @@ -62,7 +62,7 @@ class BundleTest { val bundle = Bundle.load(files) bundle.page("shop/home") bundle.page("shop/home") - assertEquals(listOf("manifest.json", "pages/home.qjsb", "pages/home.js.map", "pages/home.qjsb"), files.reads, "a missing map is asked for once") + assertEquals(listOf("manifest.json", "pages/home.jsb", "pages/home.js.map", "pages/home.jsb"), files.reads, "a missing map is asked for once") } @Test diff --git a/docs/adr-006-hot-updates.md b/docs/adr-006-hot-updates.md index d9fce24..5f65ae4 100644 --- a/docs/adr-006-hot-updates.md +++ b/docs/adr-006-hot-updates.md @@ -19,7 +19,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi | `PageHost` 只收字节,库零 I/O,读资源在宿主 | `PageHost.kt`、TrendingAI `TinyUIHost.kt` | 热下发对库来说是"多一个字节来源 + 选哪个" | | release 不带 map,靠 `buildId` 离线对映射 | build-chain.md §7 | 错误上报链路不变 | -产物尺寸:`core.qjsb` 20 KB、`native.qjsb` 3 KB、页面 1~4 KB;十几页的 App 整包不到 100 KB。运行时随宿主后(§2.11)包里只剩页面,运行时的 23 KB 随库进 App。 +产物尺寸:`core.jsb` 20 KB、`native.jsb` 3 KB、页面 1~4 KB;十几页的 App 整包不到 100 KB。运行时随宿主后(§2.11)包里只剩页面,运行时的 23 KB 随库进 App。 ## 2. 候选与取舍 @@ -202,7 +202,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi | 项 | 结论 | |---|---| -| 包 | App 由 N≥1 个包组成;一个包 = 一次 `tinyui build` 的产物(`pages/**/*.qjsb` + `manifest.json`;运行时随宿主,§2.11),整包原子生效,各包独立;不做单页下发、不做跨包共享模块、不做 zip、不做 diff | +| 包 | App 由 N≥1 个包组成;一个包 = 一次 `tinyui build` 的产物(`pages/**/*.jsb` + `manifest.json`;运行时随宿主,§2.11),整包原子生效,各包独立;不做单页下发、不做跨包共享模块、不做 zip、不做 diff | | 包名与模块名 | 包名 `[a-z0-9-]+` 来自 `tinyui.config.json`;模块名 = 路由键 = `/<相对路径>`,无 `pages/` 段 | | 兼容键 | 宿主声明的 `hostVersion`,作服务端路径;`engine` 要求相等、`tinyui` 要求与宿主兼容(同 major 且宿主不低于包),只校验;单纯升 tinyui 不加 `hostVersion` | | 运行时 | `tinyui-core` / `tinyui-native` 随库编成字节码进 App,包里不带;运行时公开面只增不删、不改语义(§2.11 铁律) | diff --git a/docs/build-chain.md b/docs/build-chain.md index 9ec5dbe..9b149e3 100644 --- a/docs/build-chain.md +++ b/docs/build-chain.md @@ -8,13 +8,13 @@ `tinyui build` 把每个页面的 TSX 编成一个 ESM 模块字节码;`tinyui-core` / `tinyui-native` 各编成一个运行时模块字节码,由库构建时编好、随库发布(ADR-006 §2.11)。每页一个 Runtime 按 ADR-002 K0 的顺序加载。 -验收:sample(包名 `sample`)的 `src/pages/home.tsx` 经 CLI 产出 `pages/home.qjsb`,Android / iOS 上 App 启动后显示页面 `default` 导出的返回值,其中含 `import.meta.url` 的值 `tinyui:sample/home`——模块名、external、注册顺序三件事一次验完。CI 的 gradle 与 pnpm 两条 job 都经过这条链。 +验收:sample(包名 `sample`)的 `src/pages/home.tsx` 经 CLI 产出 `pages/home.jsb`,Android / iOS 上 App 启动后显示页面 `default` 导出的返回值,其中含 `import.meta.url` 的值 `tinyui:sample/home`——模块名、external、注册顺序三件事一次验完。CI 的 gradle 与 pnpm 两条 job 都经过这条链。 ## 2. 管线 ``` -src/pages/home.tsx ──esbuild──▶ dist/pages/home.js + .map ──qjsc-kmp -m -n /home──▶ dist/pages/home.qjsb -tinyui-core 包入口 ──esbuild──▶ runtime/core.js ──qjsc-kmp -m -n tinyui-core──▶ runtime/core.qjsb (`pnpm runtime` 生成 compose 的 RuntimeBytecode.kt,随库发布) +src/pages/home.tsx ──esbuild──▶ dist/pages/home.js + .map ──qjsc-kmp -m -n /home──▶ dist/pages/home.jsb +tinyui-core 包入口 ──esbuild──▶ runtime/core.js ──qjsc-kmp -m -n tinyui-core──▶ runtime/core.jsb (`pnpm runtime` 生成 compose 的 RuntimeBytecode.kt,随库发布) tinyui-native 同上(对它而言 tinyui-core 是 external) ``` @@ -23,7 +23,7 @@ tinyui-native 同上(对它而言 tinyui-core 是 external) | esbuild | TS 类型擦除;JSX → `h()`(`jsxFactory: "h"`,`inject` 一个内存模块自动引入 `h` / `Fragment`,页面不手写);业务内部相对 import 合并进页面模块;`tinyui-core` / `tinyui-native` 在插件 `onResolve` 里标 `external` + `sideEffects: false`,保持裸说明符且未用到时整条 import 被去掉;`format: "esm"`,`target: "esnext"`;输出 source map | 任何语法降级;对运行时模块的解析 | | qjsc-kmp | 源码模块 → 字节码,`-n` 给定模块名,`--strip-source` 保留行号去源码 | 校验模块图(引擎加载时查表) | -**页面名即模块名即路由键**:`/`——包 `subscription` 的 `src/pages/home.tsx` → `subscription/home`,`src/pages/plans/detail.tsx` → `subscription/plans/detail`,无后缀。包名来自工程根的 `tinyui.config.json`(`{ "name", "publicKey", "pages": "src/pages", "defaultLocale", "i18n": "i18n" }`,后两个见 §8,`name` 匹配 `[a-z0-9-]+`,`publicKey` 见 updates.md §7),是命名空间,所以没有 `pages/` 段;一个 App 挂几个包(ADR-006 §2.10),路由键就靠这个前缀不撞。页面路径的每一段限定 `[A-Za-z0-9._-]+`(`tinyui build` 拒绝其余的,含中文文件名):它随产物路径进热下发的投递 URL,而 percent-encoding 要在 CLI、服务端、Kotlin 客户端三处各对称实现一次,任一处偏差就是客户端收不到更新且只能发 App 修(updates.md §6.1、§7 同一类风险)。页面内容与组件文案不受影响,只有文件名受限;日后若要放开,改 build 一处把模块名与输出路径解耦即可(`files` 本就是映射),协议与另外两端不动。运行时模块名固定 `tinyui-core`、`tinyui-native`,由库构建并随库发布,不进 `tinyui build` 的产物。产物目录 `pages/**/*.qjsb`、`manifest.json`(页面模块清单、每个模块的产物路径 `files` 与 `buildIds`,包身份 `name` / `publicKey` 与热下发用的 `version` / `createdAt` / `engine` / `tinyui` / `hashes` 见 [updates.md](./updates.md) §1.1;Kotlin 侧路由表的来源,宿主用 `BuildManifest.file(name)` 定位 `.qjsb` / `.js.map`,见 [app-model.md](./app-model.md))。 +**页面名即模块名即路由键**:`/`——包 `subscription` 的 `src/pages/home.tsx` → `subscription/home`,`src/pages/plans/detail.tsx` → `subscription/plans/detail`,无后缀。包名来自工程根的 `tinyui.config.json`(`{ "name", "publicKey", "pages": "src/pages", "defaultLocale", "i18n": "i18n" }`,后两个见 §8,`name` 匹配 `[a-z0-9-]+`,`publicKey` 见 updates.md §7),是命名空间,所以没有 `pages/` 段;一个 App 挂几个包(ADR-006 §2.10),路由键就靠这个前缀不撞。页面路径的每一段限定 `[A-Za-z0-9._-]+`(`tinyui build` 拒绝其余的,含中文文件名):它随产物路径进热下发的投递 URL,而 percent-encoding 要在 CLI、服务端、Kotlin 客户端三处各对称实现一次,任一处偏差就是客户端收不到更新且只能发 App 修(updates.md §6.1、§7 同一类风险)。页面内容与组件文案不受影响,只有文件名受限;日后若要放开,改 build 一处把模块名与输出路径解耦即可(`files` 本就是映射),协议与另外两端不动。运行时模块名固定 `tinyui-core`、`tinyui-native`,由库构建并随库发布,不进 `tinyui build` 的产物。产物目录 `pages/**/*.jsb`、`manifest.json`(页面模块清单、每个模块的产物路径 `files` 与 `buildIds`,包身份 `name` / `publicKey` 与热下发用的 `version` / `createdAt` / `engine` / `tinyui` / `hashes` 见 [updates.md](./updates.md) §1.1;Kotlin 侧路由表的来源,宿主用 `BuildManifest.file(name)` 定位 `.jsb` / `.js.map`,见 [app-model.md](./app-model.md))。 业务工程的 tsconfig 用 `jsx: "react-jsx"` + `jsxImportSource: "tinyui-core"` 做类型检查([jsx-transform.md](./jsx-transform.md) §4),而 esbuild 会读到这份 tsconfig 并按它产出 `import { jsx } from "tinyui-core/jsx-runtime"`,即使 `build()` 显式传了 `jsx: "transform"`;引擎里没有这个模块,加载报 `module 'tinyui-core/jsx-runtime' is not registered`(2026-09-16 CI 实测)。页面构建要传 `tsconfigRaw` 覆盖 tsconfig 的 jsx 三项。 @@ -106,7 +106,7 @@ rolldown 是真正的备选(Rollup 同形 API、oxc 转译)。CLI 只包一 | `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` 只取 `.qjsb` 与清单 → `compose.resources.customDirectory`,`Res.readBytes("files/tinyui/…")` 读入后走 K0。AGP 9 的 KMP 库插件默认不处理 assets,Compose resources 在 Android 走 assets,必须 `androidResources { enable = true }`,否则 APK 里没有资源且构建不报错(2026-09-16 实测) | +| `sample/shared` | Gradle:`pnpm run build` → `tinyui build` → `Sync` 只取 `.jsb` 与清单 → `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 编 `buildNativeHostJni`(Android host 测试要加载宿主 JNI 库,Maven 包里没有),经 `TINYUI_QUICKJS_HOST_JNI` 传入;`qjsc-kmp` 来自 npm 包(2026-09-23 起);ios.yml 同样 | diff --git a/docs/js-runtime.html b/docs/js-runtime.html index 10fd473..b5b74b4 100644 --- a/docs/js-runtime.html +++ b/docs/js-runtime.html @@ -93,7 +93,7 @@

0. 六层

TSX 源码业务写的 h() + thunk()esbuild 产物 -ESM 模块字节码pages/home.qjsb +ESM 模块字节码pages/home.jsb 运行时对象owner / effect / signal / id patch 消息JSON 数组 Kotlin 节点表ADR-003 diff --git a/docs/updates-plan.md b/docs/updates-plan.md index b729417..3507703 100644 --- a/docs/updates-plan.md +++ b/docs/updates-plan.md @@ -24,9 +24,9 @@ M1 之后 M2 → M3 与 M4 并行;M5.1、M5.2 与 tinyui 0.3.0(npm 三包 + |---|---| | `tinyui.config.json` | 工程根,`name`(`[a-z0-9-]+`,必填)/ `publicKey`(必填)/ `pages`(缺省 `src/pages`);`build` 无配置文件即报错 | | 模块名改 `/<相对路径>` | esbuild 入口 → `-n /home`,`import.meta.url = tinyui:/home`;manifest 的 `pages` / `files` / `buildIds` / `hashes` 键随之改;sample 包名 `sample`,`sample/shared` 与 CLI 测试里的 `pages/…` 全部改 | -| `manifest.json` 加 `name` / `publicKey` / `version` / `createdAt` / `engine` / `protocol` / `hashes` | `name` / `publicKey` 抄配置;`engine` 从第一个 `.qjsb` 的文件头读(`QJKB` 起偏移 12 的 40 字节 hex);`protocol` 读 `tinyui-core/protocol` 子路径导出,core 包加这个入口 | +| `manifest.json` 加 `name` / `publicKey` / `version` / `createdAt` / `engine` / `protocol` / `hashes` | `name` / `publicKey` 抄配置;`engine` 从第一个 `.jsb` 的文件头读(`QJKB` 起偏移 12 的 40 字节 hex);`protocol` 读 `tinyui-core/protocol` 子路径导出,core 包加这个入口 | | `tinyui keys generate` | Node `crypto.generateKeyPairSync("ec", { namedCurve: "P-256" })`;输出私钥 PKCS#8 PEM 与 X9.63 裸点 base64 公钥(updates.md §7) | -| `tinyui bundle --runtime-version --signing-key [--rollout] [--out]` | manifest 加 `runtimeVersion` 后写定为 `///manifest.json`,对该文件字节 `crypto.sign("sha256", …, { dsaEncoding: "der" })`,签名与 `version` / `rollout` 写 `//current.json`;`.qjsb` 复制进 `/`;签名私钥的公钥必须等于 manifest 的 `publicKey`,否则拒绝 | +| `tinyui bundle --runtime-version --signing-key [--rollout] [--out]` | manifest 加 `runtimeVersion` 后写定为 `///manifest.json`,对该文件字节 `crypto.sign("sha256", …, { dsaEncoding: "der" })`,签名与 `version` / `rollout` 写 `//current.json`;`.jsb` 复制进 `/`;签名私钥的公钥必须等于 manifest 的 `publicKey`,否则拒绝 | | 测试 | 夹具目录 → 字段齐全、模块名带包名、`hashes` 对得上、`current.json` 的签名可用公钥对 `manifest.json` 字节验回、改 `current.json` 的 `rollout` 不影响验签、私钥与 `publicKey` 不配对被拒、缺配置文件被拒 | 验收:`sample/js` 跑 `tinyui build && tinyui bundle`,双端显示 `tinyui:sample/home`,产出目录用 `openssl dgst -sha256 -verify` 对 `/manifest.json` 验 `current.json` 里的签名。 @@ -55,7 +55,7 @@ M1 之后 M2 → M3 与 M4 并行;M5.1、M5.2 与 tinyui 0.3.0(npm 三包 + | 测试 | 假 `fetch` 按协议喂:指针解析失败、签名错(含公钥轮换的诊断信息)、`name` 不匹配、manifest `version` 与指针不符、engine 不匹配、旧于内置、rollout 不中、sha256 错、全通过各一条;两个包一坏一好互不影响;启动选择四种情况 + installed 文件被改一个字节即回内置并 `Failed(integrity)` + 清理被去掉的包与 rv 不匹配的 installed;Android host 与 iOS simulator 各跑一遍验签 | | sample | 第二个包 `sample-extra`(一页即可)验多包;一个开关把 base URL 指向本机 `python3 -m http.server` 起的 `dist/ota/`,手动验"下次启动生效"与回退 | -验收:sample 在 Android 与 iOS 模拟器上从本机静态目录装上新包、重启生效;故意给一个包发坏包(改一个 `.qjsb` 字节、或页面顶层 `throw`)能回退并收到带 `pkg` 的 `RolledBack`,另一个包不受影响。 +验收:sample 在 Android 与 iOS 模拟器上从本机静态目录装上新包、重启生效;故意给一个包发坏包(改一个 `.jsb` 字节、或页面顶层 `throw`)能回退并收到带 `pkg` 的 `RolledBack`,另一个包不受影响。 ## M4 · `tinyui-updates-server` MVP diff --git a/docs/updates.md b/docs/updates.md index 4c54b02..aacf216 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -19,7 +19,7 @@ ``` manifest.json -pages/**/*.qjsb +pages/**/*.jsb ``` ### 1.1 `manifest.json` @@ -33,8 +33,8 @@ pages/**/*.qjsb | `publicKey` | `tinyui build` | 验签公钥(§7 格式),读自 `tinyui.config.json`;**信任来源只有内置包里的这份**,下载的 manifest 里的只用于诊断 | | `version` | `tinyui build` | 包标识,目录名,只求唯一:`-`,如 `20260918T100212Z-3f2a1c9e04b7`;`--version` 可覆盖 | | `createdAt` | `tinyui build` | ISO 8601 UTC,新旧比较只看它 | -| `engine` | `tinyui build` | 字节码文件头里的引擎 commit(40 位 hex,所有 `.qjsb` 一致,取第一个);客户端要求等于宿主 | -| `hashes` | `tinyui build` | 模块名 → 该模块 `.qjsb` 的 sha256 hex,键与 `files` 一致;另含每个 i18n 文件,键为其相对路径(`i18n/en.json`) | +| `engine` | `tinyui build` | 字节码文件头里的引擎 commit(40 位 hex,所有 `.jsb` 一致,取第一个);客户端要求等于宿主 | +| `hashes` | `tinyui build` | 模块名 → 该模块 `.jsb` 的 sha256 hex,键与 `files` 一致;另含每个 i18n 文件,键为其相对路径(`i18n/en.json`) | | `i18n` | `tinyui build` | `{ "default": 默认语言, "files": { 语言: 相对路径 } }`,包没有 i18n 资源时省略(build-chain.md §8) | | `tinyui` | `tinyui build` | 页面需要的最低运行时:JS 工程构建时的 `tinyui-core` 版本(能用哪些内置组件与运行时 API)与 `tinyui-cli` 版本(产物布局由同版本起的库读取)取较新者;`publish` 核对目标宿主版本的 tinyui 下限与它兼容(§1.3),客户端核对它与 `TinyUI.version` 兼容(§4.3)。**兼容 = major 相同,且宿主的版本不低于包的**(semver `^`;0.x 之间按版本号全序比较,1.0 之前不允许破坏公开面)。成立的前提是运行时公开面只增不删(runtime-api.md §11);升 major 时宿主加 `hostVersion`(§4.1),旧 major 的包因不兼容被拒收 | | `requires` | `tinyui build` | 页面模块名 → `{ components, capabilities, channels }`:该页用到的带点宿主组件名、`host.call` 能力名与 `http.client` 通道名,各自排序;内置组件与 `default` 通道不列。`publish` 据此核对目标宿主版本(§1.3),客户端不读 | @@ -53,7 +53,7 @@ tinyui bundle --host-version --signing-key <私钥 PEM> [--rollout ``` dist/ota///current.json 指针:{ "version", "rollout", "signature" } dist/ota////manifest.json build 的 manifest + hostVersion,写定后不再变 -dist/ota////pages/**/*.qjsb +dist/ota////pages/**/*.jsb dist/ota////i18n/.json manifest 的 i18n 列出的文件(有才写) ``` @@ -102,7 +102,7 @@ tinyui pull --channel production --host-version --out <宿主资源目录>/< |---|---|---| | `//current.json` | 可变指针 | `Cache-Control: no-store` | | `///manifest.json` | 不可变,签名覆盖其原始字节 | `Cache-Control: public, max-age=31536000, immutable` | -| `///.qjsb` | 不可变内容 | 同上 | +| `///.jsb` | 不可变内容 | 同上 | | `///` | 不可变内容(包内 i18n 资源,build-chain.md §8) | 同上 | - `pkg` 库从内置 manifest 读,`hostVersion` 宿主给;宿主只拼 base,一个 App 不管几个包都是一个 base URL、一个 `fetch` @@ -134,7 +134,7 @@ base = https://updates.tinyui.app// 把宿主现在手写的"读 manifest → 读页面与 map"收进库;运行时字节码随库(ADR-006 §2.11),不从包里读: ```kotlin -fun interface BundleFiles { suspend fun read(path: String): ByteArray? } // path 如 pages/home.qjsb、manifest.json +fun interface BundleFiles { suspend fun read(path: String): ByteArray? } // path 如 pages/home.jsb、manifest.json class Bundle(val manifest: BuildManifest, files: BundleFiles) { val name: String get() = manifest.name @@ -247,7 +247,7 @@ fetch //current.json ─解析失败──────── │ / engine ≠ QuickJs.upstreamCommit / tinyui 与 TinyUI.version 不兼容 ▶ Skipped(incompatible) // 发错目录,上报 ├─ createdAt ≤ embedded.createdAt(内置可用)────────────────────────▶ Skipped(older-than-embedded) │ - ▼ 逐文件 fetch ///(files 的 .qjsb 与 i18n.files)→ /staging//,每个核对 sha256 + ▼ 逐文件 fetch ///(files 的 .jsb 与 i18n.files)→ /staging//,每个核对 sha256 ├─ 任一失败 ─删 staging──────────────────────────────────────────────▶ Failed(download | integrity) ▼ manifest 原始字节写入 staging//manifest.json,staging/ 改名 installed/,state.installed = version,删其他 installed ▼ Installed(version) // 下次启动生效 diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 575b612..5359deb 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -50,7 +50,7 @@ export interface BuildResult { /** `manifest.json` as `tinyui build` writes it (docs/updates.md §1.1). */ export interface Manifest { pages: string[]; - /** Module name → output path without extension (`pages/home`); hosts locate `.qjsb` / `.js.map` through it. */ + /** Module name → output path without extension (`pages/home`); hosts locate `.jsb` / `.js.map` through it. */ files: Record; buildIds: Record; name: string; @@ -61,7 +61,7 @@ export interface Manifest { engine: string; /** The oldest tinyui the pages run on: the `tinyui-core` they were built against or the CLI that wrote them, whichever is newer (docs/updates.md §1.1). */ tinyui: string; - /** Module name → sha256 hex of its `.qjsb`; empty when built with `jsOnly`. */ + /** Module name → sha256 hex of its `.jsb`; empty when built with `jsOnly`. */ hashes: Record; /** Page module name → the host capabilities, http channels and host components it uses (docs/updates.md §1.3). */ requires: Record; @@ -142,7 +142,7 @@ async function finish(root: string, modules: BuiltModule[], qjsc: string | undef m.buildId = createHash("sha256").update(await readFile(m.js)).digest("hex").slice(0, 8); await rootRelativeSources(root, m.map); if (!qjsc) continue; - m.bin = m.js.replace(/\.js$/, ".qjsb"); + m.bin = m.js.replace(/\.js$/, ".jsb"); await compileModule({ qjsc, input: m.js, output: m.bin, name: m.name }); } } diff --git a/packages/cli/src/bundle.ts b/packages/cli/src/bundle.ts index 2222f5e..921540a 100644 --- a/packages/cli/src/bundle.ts +++ b/packages/cli/src/bundle.ts @@ -60,7 +60,7 @@ export async function bundle(options: BundleOptions): Promise { if (publicKeyOf(privateKeyPem) !== manifest.publicKey) { throw new Error(`${options.signingKey} does not match the publicKey in ${dist}/manifest.json (tinyui.config.json)`); } - if (!manifest.engine) throw new Error(`${dist} has no bytecode (built with --js-only); a package needs .qjsb files`); + if (!manifest.engine) throw new Error(`${dist} has no bytecode (built with --js-only); a package needs .jsb files`); // everything is checked before anything is written const files: { source: string; target: string }[] = []; diff --git a/packages/cli/src/payload.ts b/packages/cli/src/payload.ts index 9443a96..a51d90e 100644 --- a/packages/cli/src/payload.ts +++ b/packages/cli/src/payload.ts @@ -11,7 +11,7 @@ export interface PayloadManifest { /** Every file of the package: its path inside the package, and its key into `hashes`. */ export function payload(manifest: PayloadManifest): { path: string; hashKey: string }[] { return [ - ...manifest.pages.map((module) => ({ path: `${manifest.files[module]}.qjsb`, hashKey: module })), + ...manifest.pages.map((module) => ({ path: `${manifest.files[module]}.jsb`, hashKey: module })), ...Object.values(manifest.i18n?.files ?? {}).map((path) => ({ path, hashKey: path })), ]; } diff --git a/packages/cli/test/admin.test.ts b/packages/cli/test/admin.test.ts index dec673c..eacf48e 100644 --- a/packages/cli/test/admin.test.ts +++ b/packages/cli/test/admin.test.ts @@ -85,7 +85,7 @@ describe("management commands", () => { it("encodes each segment and holds names to the rule the server enforces", () => { assert.equal(segments("demo", "a/b", "1"), "/demo/a%2Fb/1"); // page paths are ASCII by construction (tinyui build refuses the rest); encoding is the last line of defence - assert.equal(segments("demo", "fixture", "1", "订单.qjsb"), "/demo/fixture/1/%E8%AE%A2%E5%8D%95.qjsb"); + assert.equal(segments("demo", "fixture", "1", "订单.jsb"), "/demo/fixture/1/%E8%AE%A2%E5%8D%95.jsb"); // encodeURIComponent leaves dots alone, so a traversing value is refused rather than escaped for (const traversal of ["..", ".", ""]) assert.throws(() => segments("demo", traversal, "1"), /cannot be a path segment/); assert.throws(() => requireName("--app", ".."), /--app must match \[a-z0-9-\]\+/); diff --git a/packages/cli/test/bundle.test.ts b/packages/cli/test/bundle.test.ts index 351eb2d..fe9e798 100644 --- a/packages/cli/test/bundle.test.ts +++ b/packages/cli/test/bundle.test.ts @@ -36,8 +36,8 @@ describe("tinyui bundle", () => { const versionDir = join(result.dir, result.version); assert.deepEqual((await readdir(versionDir, { recursive: true })).filter((f) => f.includes(".")).sort(), [ "manifest.json", - "pages/home.qjsb", - "pages/orders.qjsb", + "pages/home.jsb", + "pages/orders.jsb", ]); const pointer = JSON.parse(await readFile(result.pointer, "utf8")) as Pointer; assert.deepEqual(Object.keys(pointer), ["version", "rollout", "signature"]); @@ -89,7 +89,7 @@ describe("tinyui bundle", () => { it("refuses a build whose bytecode changed since manifest.json was written, touching nothing", async () => { const dist = await fakeDist(join(tmp, "dist-stale")); - await writeFile(join(dist, "pages", "home.qjsb"), "tampered"); + await writeFile(join(dist, "pages", "home.jsb"), "tampered"); await assert.rejects(bundle({ dist, hostVersion: "1", signingKey }), /does not match manifest.hashes/); await assert.rejects(readdir(join(dist, "ota")), /ENOENT/, "nothing was written"); }); @@ -123,7 +123,7 @@ describe("tinyui bundle", () => { await assert.rejects(bundle({ dist: name, hostVersion: "1", signingKey }), /manifest name must match/); const files = await fakeDist(join(tmp, "dist-files"), (m) => { m.files["fixture/home"] = "../outside/core"; }); await mkdir(join(tmp, "outside"), { recursive: true }); - await writeFile(join(tmp, "outside", "core.qjsb"), "x"); + await writeFile(join(tmp, "outside", "core.jsb"), "x"); await assert.rejects(bundle({ dist: files, hostVersion: "1", signingKey }), /points outside/); await assert.rejects(readdir(join(tmp, "escape")), /ENOENT/); }); @@ -164,7 +164,7 @@ describe("tinyui bundle", () => { assert.ok(verify(config.publicKey, bytes, pointer.signature)); const manifest = JSON.parse(bytes.toString("utf8")) as Manifest; for (const m of built.pages) { - const copied = await readFile(join(result.dir, result.version, manifest.files[m.name]! + ".qjsb")); + const copied = await readFile(join(result.dir, result.version, manifest.files[m.name]! + ".jsb")); assert.equal(createHash("sha256").update(copied).digest("hex"), manifest.hashes[m.name]); } }); diff --git a/packages/cli/test/helpers.ts b/packages/cli/test/helpers.ts index 2769757..f3da51a 100644 --- a/packages/cli/test/helpers.ts +++ b/packages/cli/test/helpers.ts @@ -4,14 +4,14 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } import { join } from "node:path"; import type { Manifest } from "../src/build.ts"; -/** A `tinyui build` output written by hand: bundle only needs the manifest and the `.qjsb` files it lists. */ +/** A `tinyui build` output written by hand: bundle only needs the manifest and the `.jsb` files it lists. */ export async function fakeDist(dir: string, publicKey: string, edit: (m: Manifest) => void = () => {}): Promise { const files = { "fixture/home": "pages/home", "fixture/orders": "pages/orders" }; const hashes: Record = {}; for (const [module, path] of Object.entries(files)) { const bytes = Buffer.concat([Buffer.from("QJKB"), Buffer.alloc(8), Buffer.from("a".repeat(40)), Buffer.from(module)]); await mkdir(join(dir, path, ".."), { recursive: true }); - await writeFile(join(dir, path + ".qjsb"), bytes); + await writeFile(join(dir, path + ".jsb"), bytes); hashes[module] = createHash("sha256").update(bytes).digest("hex"); } const manifest: Manifest = { diff --git a/packages/cli/test/publish.test.ts b/packages/cli/test/publish.test.ts index c7e9745..0ad76f4 100644 --- a/packages/cli/test/publish.test.ts +++ b/packages/cli/test/publish.test.ts @@ -13,7 +13,7 @@ import { fakeDist, startFakeServer, type FakeServer } from "./helpers.ts"; const pair = generateKeyPair(); const VERSION = "20260922T090000Z-3f2a1c"; const HOST_1 = "hostVersion 1\ntinyui 0.3.0\n\ncomponents\n ta.Icon name: string\n\ncapabilities\n billing.prices\n"; -const FILES = ["pages/home.qjsb", "pages/orders.qjsb", "manifest.json"]; +const FILES = ["pages/home.jsb", "pages/orders.jsb", "manifest.json"]; describe("tinyui publish", () => { let tmp: string; @@ -173,8 +173,8 @@ describe("tinyui publish", () => { const dir = await bundled("symlink"); const secret = join(tmp, "secret.txt"); await writeFile(secret, "not this package's to publish"); - await rm(join(dir, "fixture", "1", VERSION, "pages", "home.qjsb")); - await symlink(secret, join(dir, "fixture", "1", VERSION, "pages", "home.qjsb")); + await rm(join(dir, "fixture", "1", VERSION, "pages", "home.jsb")); + await symlink(secret, join(dir, "fixture", "1", VERSION, "pages", "home.jsb")); const mark = server.requests.length; // lexical checks pass here; only resolving the link catches it await assert.rejects(publish({ client, dir, app: "demo", channel: "staging" }), /leaves .*a bundle only publishes its own files/); diff --git a/packages/cli/test/pull.test.ts b/packages/cli/test/pull.test.ts index c2bce97..9ee2ead 100644 --- a/packages/cli/test/pull.test.ts +++ b/packages/cli/test/pull.test.ts @@ -62,7 +62,7 @@ describe("tinyui pull", () => { await publish(NEW, "2026-09-23T09:00:00Z"); assert.deepEqual(await pull(into(out)), { version: NEW, changed: true }); - assert.deepEqual((await readdir(join(out, "pages"))).sort(), ["home.qjsb", "orders.qjsb"]); + assert.deepEqual((await readdir(join(out, "pages"))).sort(), ["home.jsb", "orders.jsb"]); }); it("keeps the embedded package while the channel rolls out, or when it is not newer", async () => { @@ -89,9 +89,9 @@ describe("tinyui pull", () => { await assert.rejects(pull(into(out)), (e: Error) => e.message.includes(`pass --accept-key ${rotated.publicKey}`)); assert.equal(JSON.parse(await readFile(join(out, "manifest.json"), "utf8")).version, OLD); - const page = join(hostDir, ROTATED, "pages", "home.qjsb"); + const page = join(hostDir, ROTATED, "pages", "home.jsb"); await writeFile(page, "tampered"); - await assert.rejects(pull(into(out, { acceptKey: rotated.publicKey })), /pages\/home\.qjsb does not match manifest\.hashes/); + await assert.rejects(pull(into(out, { acceptKey: rotated.publicKey })), /pages\/home\.jsb does not match manifest\.hashes/); assert.equal(JSON.parse(await readFile(join(out, "manifest.json"), "utf8")).version, OLD); }); }); diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index 54c4d78..d52afc0 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -38,7 +38,7 @@ kotlin { val updateHostSnapshot = providers.gradleProperty("tinyui.updateHostSnapshot") -// JS 侧:pnpm 编三个包 → tinyui build 把每个示例包的页面编成字节码 → 只把 .qjsb 与清单打成 Compose 资源(运行时随库) +// JS 侧:pnpm 编三个包 → tinyui build 把每个示例包的页面编成字节码 → 只把 .jsb 与清单打成 Compose 资源(运行时随库) // 两个包演示多包模型(docs/updates.md §0):各自的 JS 工程、密钥、资源子目录 val jsPackages = mapOf("sample" to rootDir.resolve("sample/js"), "sample-extra" to rootDir.resolve("sample/js-extra")) val cliOut = layout.buildDirectory.dir("tinyui-cli") @@ -89,7 +89,7 @@ val collectTinyUIResources by tasks.registering(Sync::class) { jsPackages.keys.forEachIndexed { i, pkg -> from(packageBuilds[i]) { into("files/tinyui/$pkg") - include("pages/**/*.qjsb", "i18n/*.json", "manifest.json") + include("pages/**/*.jsb", "i18n/*.json", "manifest.json") if (providers.gradleProperty("tinyui.maps").orNull != "false") include("pages/**/*.js.map") } } diff --git a/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt b/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt index 02b97ef..724a117 100644 --- a/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt +++ b/updates/src/commonTest/kotlin/app/tinyui/updates/Fixture.kt @@ -33,5 +33,5 @@ object Fixture { """.trimIndent() + "\n" /** The bytes whose sha256 the fixture manifest lists. */ - val FILES = mapOf("pages/home.qjsb" to "HOME2") + val FILES = mapOf("pages/home.jsb" to "HOME2") } diff --git a/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt b/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt index c07d899..c0148d4 100644 --- a/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt +++ b/updates/src/commonTest/kotlin/app/tinyui/updates/UpdatesTest.kt @@ -43,17 +43,17 @@ class UpdatesTest { engine: String = Fixture.ENGINE, tinyui: String = Fixture.TINYUI, publicKey: String = Fixture.PUBLIC_KEY, - files: Map = mapOf("pages/home.qjsb" to "HOME-$version"), + files: Map = mapOf("pages/home.jsb" to "HOME-$version"), hostVersion: String? = "1", ): String { val modules = mapOf("$name/home" to "pages/home") - val hashes = modules.entries.joinToString(",") { (m, p) -> "\"$m\":\"${files.getValue("$p.qjsb").encodeUtf8().sha256().hex()}\"" } + val hashes = modules.entries.joinToString(",") { (m, p) -> "\"$m\":\"${files.getValue("$p.jsb").encodeUtf8().sha256().hex()}\"" } val hostVersion = if (hostVersion == null) "" else ",\"hostVersion\":\"$hostVersion\"" return """{"pages":["$name/home"],"files":{${modules.entries.joinToString(",") { "\"${it.key}\":\"${it.value}\"" }}},"buildIds":{},"name":"$name","publicKey":"$publicKey","version":"$version","createdAt":"$createdAt","engine":"$engine","tinyui":"$tinyui","hashes":{$hashes}$hostVersion}""" } private fun embedded(name: String = "shop", version: String = "v0", createdAt: String = "2026-09-22T09:00:00Z"): Bundle { - val files = mapOf("pages/home.qjsb" to "HOME-$version") + val files = mapOf("pages/home.jsb" to "HOME-$version") val json = manifest(name, version, createdAt, files = files, hostVersion = null) return Bundle(BuildManifest.parse(json)) { path -> files[path]?.encodeToByteArray() } } @@ -64,7 +64,7 @@ class UpdatesTest { val version = pointerVersion ?: m.version server["$pkg/$hostVersion/current.json"] = """{"version":"$version","rollout":$rollout,"signature":"$signature"}""".encodeToByteArray() server["$pkg/$hostVersion/$version/manifest.json"] = manifestJson.encodeToByteArray() - val content = files ?: mapOf("pages/home.qjsb" to "HOME-${m.version}") + val content = files ?: mapOf("pages/home.jsb" to "HOME-${m.version}") for ((path, bytes) in content) server["$pkg/$hostVersion/$version/$path"] = bytes.encodeToByteArray() } @@ -82,7 +82,7 @@ class UpdatesTest { assertEquals(UpdateEvent.Installed("shop", "v1"), events.last()) assertEquals("HOME-v0", pageBytes(first.current("shop")), "the process keeps what it started with") assertTrue(fs.exists(dir / "shop/installed/v1/manifest.json")) - assertTrue(fs.exists(dir / "shop/installed/v1/pages/home.qjsb")) + assertTrue(fs.exists(dir / "shop/installed/v1/pages/home.jsb")) assertFalse(fs.exists(dir / "shop/staging")) assertContains(fs.read(dir / "shop/state.json") { readUtf8() }, "\"installed\":\"v1\"") @@ -187,11 +187,11 @@ class UpdatesTest { @Test fun aFileThatDoesNotMatchItsHashOrCannotBeFetchedLeavesNothingBehind() = runTest { - publish(manifest(), files = mapOf("pages/home.qjsb" to "tampered")) + publish(manifest(), files = mapOf("pages/home.jsb" to "tampered")) val integrity = assertIs(updates(embedded()).check("shop")) assertEquals(FailStage.INTEGRITY, integrity.stage) - assertContains(integrity.message, "pages/home.qjsb") - server.remove("shop/1/v1/pages/home.qjsb") + assertContains(integrity.message, "pages/home.jsb") + server.remove("shop/1/v1/pages/home.jsb") publish(manifest(), files = emptyMap()) val download = assertIs(updates(embedded()).check("shop")) assertEquals(FailStage.DOWNLOAD, download.stage) @@ -240,7 +240,7 @@ class UpdatesTest { fun startupChecksTheInstalledBytesAndBlacklistsACorruptedPackage() = runTest { publish(manifest()) updates(embedded()).check("shop") - fs.write(dir / "shop/installed/v1/pages/home.qjsb") { writeUtf8("HOME-v1 ") } + fs.write(dir / "shop/installed/v1/pages/home.jsb") { writeUtf8("HOME-v1 ") } events.clear() val u = updates(embedded()) assertEquals("HOME-v0", pageBytes(u.current("shop")))