diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 4c2f11e..d052936 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -2,7 +2,8 @@ name: ios on: push: - tags: ["v*"] + tags: + - '[0-9]+.[0-9]+.[0-9]+*' workflow_dispatch: jobs: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e04f1f5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,46 @@ +name: publish + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + +# Maven only; the npm packages are published from a maintainer's machine (README「安装」) +jobs: + maven: + runs-on: macos-15 + timeout-minutes: 60 + permissions: + contents: write # gh release create + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 25 + - uses: gradle/actions/setup-gradle@v5 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: quickjs-kmp host tools (qjsc-kmp for bytecode, host JNI for Android host tests) + run: | + version=$(sed -n 's/^quickjsKmp = "\(.*\)"/\1/p' gradle/libs.versions.toml) + git clone --depth 1 --branch "$version" https://github.com/HarlonWang/quickjs-kmp.git "$RUNNER_TEMP/quickjs-kmp" + (cd "$RUNNER_TEMP/quickjs-kmp" && ./gradlew :library:buildHostTools :library:buildNativeHostJni --no-daemon) + echo "TINYUI_QJSC=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-tools/bin/qjsc-kmp" >> "$GITHUB_ENV" + echo "TINYUI_QUICKJS_HOST_JNI=$RUNNER_TEMP/quickjs-kmp/library/build/native/host-jni/lib" >> "$GITHUB_ENV" + - run: ./gradlew :compose:testAndroidHostTest :compose:apiCheck --no-daemon + - name: Publish to Maven Central + run: ./gradlew :compose:publishAndReleaseToMavenCentral --no-configuration-cache --no-daemon + env: + ORG_GRADLE_PROJECT_VERSION_NAME: ${{ github.ref_name }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + - run: gh release create "$GITHUB_REF_NAME" --generate-notes + env: + GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 4332468..634a215 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ JS 写声明式组件、Compose Multiplatform 渲染的 UI 框架,引擎为 Qu ``` docs/ 设计文档、ADR、roadmap -packages/ npm 包:@tiny-ui/core · @tiny-ui/native · @tiny-ui/cli +packages/ npm 包:tinyui-core · tinyui-native · tinyui-cli compose/ KMP 库 wang.harlon:tinyui(Compose Multiplatform 侧) schema/ 内置组件 schema(TS DSL),两侧契约的唯一真值,`pnpm schema` 生成 sample/ 示例 App(sample/js 是页面源码,Gradle 构建时经 CLI 编成字节码打进资源) @@ -22,6 +22,13 @@ build-logic/ Gradle convention plugins 目录与命名依据见 [docs/README.md](./docs/README.md)。 +## 安装 + +- Kotlin:`implementation("wang.harlon:tinyui:")`(Maven Central;自带 `wang.harlon:quickjs-kmp`) +- JS:`pnpm add tinyui-core tinyui-native` 与 `pnpm add -D tinyui-cli`,三包同版本号;`tinyui build` 另需 quickjs-kmp 的宿主工具 `qjsc-kmp`(见下) + +版本号与 git tag 一致(`0.1.0`),Maven 与 npm 三包同号同 tag 发。 + ## 构建 ```sh diff --git a/compose/api/compose.klib.api b/compose/api/compose.klib.api index e97a210..525328a 100644 --- a/compose/api/compose.klib.api +++ b/compose/api/compose.klib.api @@ -231,16 +231,19 @@ final class wang.harlon.tinyui.schema/ComponentSchema { // wang.harlon.tinyui.sc } final class wang.harlon.tinyui/BuildManifest { // wang.harlon.tinyui/BuildManifest|null[0] - constructor (kotlin.collections/List, kotlin.collections/List, kotlin.collections/Map) // wang.harlon.tinyui/BuildManifest.|(kotlin.collections.List;kotlin.collections.List;kotlin.collections.Map){}[0] + constructor (kotlin.collections/List, kotlin.collections/List, kotlin.collections/Map, kotlin.collections/Map) // wang.harlon.tinyui/BuildManifest.|(kotlin.collections.List;kotlin.collections.List;kotlin.collections.Map;kotlin.collections.Map){}[0] final val buildIds // wang.harlon.tinyui/BuildManifest.buildIds|{}buildIds[0] final fun (): kotlin.collections/Map // wang.harlon.tinyui/BuildManifest.buildIds.|(){}[0] + final val files // wang.harlon.tinyui/BuildManifest.files|{}files[0] + final fun (): kotlin.collections/Map // wang.harlon.tinyui/BuildManifest.files.|(){}[0] final val pages // wang.harlon.tinyui/BuildManifest.pages|{}pages[0] final fun (): kotlin.collections/List // wang.harlon.tinyui/BuildManifest.pages.|(){}[0] final val runtime // wang.harlon.tinyui/BuildManifest.runtime|{}runtime[0] final fun (): kotlin.collections/List // wang.harlon.tinyui/BuildManifest.runtime.|(){}[0] final fun buildId(kotlin/String): kotlin/String // wang.harlon.tinyui/BuildManifest.buildId|buildId(kotlin.String){}[0] + final fun file(kotlin/String): kotlin/String // wang.harlon.tinyui/BuildManifest.file|file(kotlin.String){}[0] final object Companion { // wang.harlon.tinyui/BuildManifest.Companion|null[0] final fun parse(kotlin/String): wang.harlon.tinyui/BuildManifest // wang.harlon.tinyui/BuildManifest.Companion.parse|parse(kotlin.String){}[0] diff --git a/compose/src/androidHostTest/kotlin/wang/harlon/tinyui/BundleSmokeTest.kt b/compose/src/androidHostTest/kotlin/wang/harlon/tinyui/BundleSmokeTest.kt index e400045..ab1ed2a 100644 --- a/compose/src/androidHostTest/kotlin/wang/harlon/tinyui/BundleSmokeTest.kt +++ b/compose/src/androidHostTest/kotlin/wang/harlon/tinyui/BundleSmokeTest.kt @@ -24,9 +24,9 @@ class BundleSmokeTest { if (!out.isDirectory) return JsEngine(JsEngineConfig(moduleScheme = PageHost.MODULE_SCHEME)).use { engine -> for (fn in listOf("__host_apply", "__host_query", "__host_call", "__host_send", "__host_report")) engine.registerFunction(fn) { JsValue.Undefined } - assertEquals("@tiny-ui/core", engine.registerModule(out.resolve("runtime/core.bin").readBytes())) - assertEquals("@tiny-ui/native", engine.registerModule(out.resolve("runtime/native.bin").readBytes())) - engine.evaluateModule("import \"@tiny-ui/core\"; export const p = globalThis.__tinyui.protocol;").use { + assertEquals("tinyui-core", engine.registerModule(out.resolve("runtime/core.bin").readBytes())) + assertEquals("tinyui-native", engine.registerModule(out.resolve("runtime/native.bin").readBytes())) + engine.evaluateModule("import \"tinyui-core\"; export const p = globalThis.__tinyui.protocol;").use { assertEquals(JsValue.Num(PageHost.PROTOCOL), it.get("p")) } engine.runBytecode(out.resolve("pages/counter.bin").readBytes()).let { if (it is AutoCloseable) it.close() } @@ -38,7 +38,7 @@ class BundleSmokeTest { fun stacksFromABuiltPageMapBackToTheSource() = runBlocking { if (!out.isDirectory) return@runBlocking val manifest = BuildManifest.parse(out.resolve("manifest.json").readText()) - val maps = SourceMaps(mapOf("pages/todos" to out.resolve("pages/todos.js.map").readText(), "@tiny-ui/core" to out.resolve("runtime/core.js.map").readText())) + val maps = SourceMaps(mapOf("pages/todos" to out.resolve("pages/todos.js.map").readText(), "tinyui-core" to out.resolve("runtime/core.js.map").readText())) val errors = mutableListOf() val sink = object : PageSink { override fun error(error: PageError) { errors += error } diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/BuildManifest.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/BuildManifest.kt index 19fb7c5..44cabd0 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/BuildManifest.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/BuildManifest.kt @@ -5,16 +5,24 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -/** `manifest.json` as `tinyui build` writes it: module names and the build id of each. */ -class BuildManifest(val runtime: List, val pages: List, val buildIds: Map) { +/** `manifest.json` as `tinyui build` writes it: module names, the output path and the build id of each. */ +class BuildManifest( + val runtime: List, + val pages: List, + val files: Map, + val buildIds: Map, +) { fun buildId(module: String): String = buildIds[module] ?: "" + /** Output path of [module] without extension (`runtime/core`, `pages/home`): append `.bin` or `.js.map`. */ + fun file(module: String): String = files[module] ?: module + companion object { fun parse(json: String): BuildManifest { val root = Json.parseToJsonElement(json).jsonObject val names = { key: String -> root[key]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList() } - val ids = root["buildIds"]?.jsonObject?.mapValues { it.value.jsonPrimitive.content } ?: emptyMap() - return BuildManifest(names("runtime"), names("pages"), ids) + val strings = { key: String -> root[key]?.jsonObject?.mapValues { it.value.jsonPrimitive.content } ?: emptyMap() } + return BuildManifest(names("runtime"), names("pages"), strings("files"), strings("buildIds")) } } } diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt index 1283106..04307d3 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt @@ -116,10 +116,10 @@ class PageHost( // K0 + K1 mount under the same timeout as every other entry: a page that spins on load fails as E6 withTimeout(entryTimeoutMs) { runtime.withEngine { registerHost(this) - step("registering @tiny-ui/core") { registerModule(runtimeBundle.core) } - step("registering @tiny-ui/native") { registerModule(runtimeBundle.native) } + step("registering tinyui-core") { registerModule(runtimeBundle.core) } + step("registering tinyui-native") { registerModule(runtimeBundle.native) } // a registered module only runs on its first import; the runtime must be up before the page - evaluateModule("import \"@tiny-ui/core\"; import \"@tiny-ui/native\";").close() + evaluateModule("import \"tinyui-core\"; import \"tinyui-native\";").close() val namespace = runBytecode(page.bytecode, ObjectTransport.REF) as JsRef if (namespace.isPromise) { namespace.close(); error("page module is still pending after microtasks were drained") } val self = evaluate("__tinyui", objects = ObjectTransport.REF) as JsRef diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/SourceMaps.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/SourceMaps.kt index e381711..1b8073c 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/SourceMaps.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/SourceMaps.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive /** - * The `.js.map` files `tinyui build` wrote, keyed by module name (`pages/todos`, `@tiny-ui/core`), parsed on first use. + * The `.js.map` files `tinyui build` wrote, keyed by module name (`pages/todos`, `tinyui-core`), parsed on first use. * Ship them in debug builds only (docs/build-chain.md); release stacks are mapped offline with the build id. */ class SourceMaps(maps: Map) { diff --git a/compose/src/commonTest/kotlin/wang/harlon/tinyui/PageHostTest.kt b/compose/src/commonTest/kotlin/wang/harlon/tinyui/PageHostTest.kt index fbaed1f..1556023 100644 --- a/compose/src/commonTest/kotlin/wang/harlon/tinyui/PageHostTest.kt +++ b/compose/src/commonTest/kotlin/wang/harlon/tinyui/PageHostTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull -/** Drives PageHost with a hand-written stand-in for `@tiny-ui/core`: the K/J entry shapes, not the real runtime. */ +/** Drives PageHost with a hand-written stand-in for `tinyui-core`: the K/J entry shapes, not the real runtime. */ class PageHostTest { private val log = mutableListOf() private val errors = mutableListOf() @@ -23,7 +23,7 @@ class PageHostTest { override fun log(line: String) { log += line } } - private val core = module("@tiny-ui/core", """ + private val core = module("tinyui-core", """ let patches = [], count = 0, handler = null, page = null, emits = 0, settled = []; const settle = (line) => { settled.push(line); patches.push(["p",1,"text", settled.slice().sort().join(";")]); }; globalThis.__tinyui = { protocol: 1, @@ -52,7 +52,7 @@ class PageHostTest { }; export const VERSION = "stub"; """) - private val native = module("@tiny-ui/native", "export const NATIVE = 1;") + private val native = module("tinyui-native", "export const NATIVE = 1;") private val page = PageModule("pages/counter", module("pages/counter", "export default function Counter() { return 'page'; }"), "abcd1234") private val store = InMemoryStore().apply { set("cart", """{"n":2}""") } @@ -128,7 +128,7 @@ class PageHostTest { assertEquals("abcd1234", e.buildId) val frame = e.frames.first() assertEquals("dispatch", frame.function) - assertEquals("@tiny-ui/core", frame.file, "engine frames name the module; stack=${e.jsStack}") + assertEquals("tinyui-core", frame.file, "engine frames name the module; stack=${e.jsStack}") assertEquals(16, frame.line) assertNotNull(frame.column, "bytecode compiled with --strip-source keeps columns; stack=${e.jsStack}") host.close() @@ -137,7 +137,7 @@ class PageHostTest { @Test fun framesAreMappedThroughTheModuleSourceMap() = runTest { // generated line 16 → src/core.ts line 3 column 7: fifteen empty groups, then one segment [0, 0, 2, 6] - val map = SourceMaps(mapOf("@tiny-ui/core" to """{"version":3,"sources":["src/core.ts"],"mappings":";;;;;;;;;;;;;;;AAEM"}""")) + val map = SourceMaps(mapOf("tinyui-core" to """{"version":3,"sources":["src/core.ts"],"mappings":";;;;;;;;;;;;;;;AAEM"}""")) val host = host(maps = map) host.start() host.await { host.tree.root.children.isNotEmpty() } @@ -203,14 +203,14 @@ class PageHostTest { val failure = assertNotNull(host.failure) assertEquals("E2", failure.kind) assertEquals(true, "render exploded" in failure.message) - assertEquals(true, failure.error.frames.any { it.function == "flush" && it.file == "@tiny-ui/core" }, "stack=${failure.error.jsStack}") + assertEquals(true, failure.error.frames.any { it.function == "flush" && it.file == "tinyui-core" }, "stack=${failure.error.jsStack}") assertEquals(1, errors.count { it.kind == "E2" }, "E2 goes through the sink once") host.close() } @Test fun protocolMismatchIsE6() = runTest { - val badCore = module("@tiny-ui/core", "globalThis.__tinyui = { protocol: 99 }; export const VERSION = 'x';") + val badCore = module("tinyui-core", "globalThis.__tinyui = { protocol: 99 }; export const VERSION = 'x';") val host = PageHost(RuntimeBundle(badCore, native), page, ComponentRegistry().registerBuiltins(), sink) host.start() host.await { host.failure != null } diff --git a/docs/README.md b/docs/README.md index cdb92cb..c38831e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,7 +18,7 @@ | 有微任务队列,但没有事件循环;JS 的每次执行都是宿主调进来的 | 宿主调用返回前排空微任务再 flush,"一次 K 入口 = 一个事务"的边界不变 | | malloc 分配 + 引用计数 + 环检测 GC;`JS_SetMemoryLimit` 限额;一个 Runtime 基线约 4~8 MB(含 1000 行页面) | 每页一 Runtime,返回栈深页要有回收策略 | | 原生 ESM,loader 回调 | 运行时与页面都是模块字节码,引擎侧只解析预注册的裸说明符 | -| 没有 `Intl`、DOM、`fetch` | 格式化、网络、存储走 `@tiny-ui/native` | +| 没有 `Intl`、DOM、`fetch` | 格式化、网络、存储走 `tinyui-native` | | 值跨界只走原始类型 / 字符串 / JSON,Kotlin 永不持有 JSValue(句柄表) | JS 侧节点就是整数 id;树 / patch 以 JSON 字符串过桥 | ## 总体运行模型 @@ -53,9 +53,9 @@ JS 桥 Kotlin |---|---|---| | **TinyUI** | 品牌与行文:文档标题、README 首行、日志前缀、对外提及 | 大小写体现 Tiny + UI 两个词 | | **tinyui** | 所有机器标识符:GitHub 仓 `HarlonWang/tinyui`、Maven `wang.harlon:tinyui`、Kotlin 包 `wang.harlon.tinyui`、CLI 命令、环境变量前缀 `TINYUI_`、目录名 | 无连字符是唯一在每种标识符里都合法的形态;避开 npm 上他人的 `tiny-ui` React 库 | -| **@tiny-ui** | 仅 npm scope:`@tiny-ui/core`、`@tiny-ui/native`、`@tiny-ui/cli` | scope 已归项目所有且不可改名;scope 下包名回到无连字符 | +| **tinyui-core / tinyui-native / tinyui-cli** | 仅 npm 包名(也是引擎里的运行时模块名) | 无 scope;`tinyui-` 前缀粘连、功能词用连字符接 | -npm 裸包 `tinyui` 不可用:npm 防仿冒规则判定其与已有的 `tiny-ui` 过于相似(2026-09-15 实测 E403)。域名 **tinyui.app** 已于 2026-09-15 在 Cloudflare Registrar(个人账号)注册,自动续费 $14.20/年,DNS 在 Cloudflare。 +npm 裸包 `tinyui` 不可用:npm 防仿冒规则判定其与已有的 `tiny-ui` 过于相似(2026-09-15 实测 E403);scope `@tiny-ui` 是他人 2017 年起的用户 scope(`@tiny-ui/components`),`@tinyui` 也已被占,2026-09-18 定为无 scope 的 `tinyui-*`。域名 **tinyui.app** 已于 2026-09-15 在 Cloudflare Registrar(个人账号)注册,自动续费 $14.20/年,DNS 在 Cloudflare。 业界对照(2026-09-16 核对 24 个带 UI 的库,仓名 / 包名取自 GitHub API 与各包管理器,品牌拼写取官网 ``):机器标识符粘连 13 个(headlessui、fluentui、onsenui、gioui、heroui、daisyui、tamagui、nicegui、imgui、egui、baseui、primeng、SwiftUI)、连字符 8 个(material-ui、chakra-ui、radix-ui、semantic-ui、jquery-ui、kendo-ui、element-plus、ant-design)、其余为 `nuxt/ui` 这类单词形。连字符一派集中在老项目,近几年新库几乎都粘连。"品牌分词 `Xxx UI`、标识符粘连 `xxxui`"与本项目完全相同的组合有 Headless UI、Fluent UI、Onsen UI、Gio UI 四例;品牌与标识符脱钩更是常态(Material UI ↔ `@mui`,Base Web ↔ `baseweb` 仓 ↔ `baseui` 包),所以 npm scope 单独带连字符不算异类。 @@ -67,9 +67,9 @@ npm 裸包 `tinyui` 不可用:npm 防仿冒规则判定其与已有的 `tiny-u tinyui/ ├── docs/ README、ADR、roadmap ├── packages/ 所有 npm 包,目录名 = 包名去掉 scope(pnpm workspace) -│ ├── core/ @tiny-ui/core JS 运行时(signal / effect / owner / h / For / Show / ref + cmd)+ 内置组件的 TS 类型(由 schema/ 生成,无运行时代码);作为 ES 模块字节码内置 -│ ├── native/ @tiny-ui/native 业务调用的宿主能力 API(http / storage / toast / navigation / i18n …,即 ADR-002 的 J2 / J3) -│ └── cli/ @tiny-ui/cli 构建工具:TSX → h()(ES2025)→ 每页一个 ESM 模块字节码,`tinyui build` +│ ├── core/ tinyui-core JS 运行时(signal / effect / owner / h / For / Show / ref + cmd)+ 内置组件的 TS 类型(由 schema/ 生成,无运行时代码);作为 ES 模块字节码内置 +│ ├── native/ tinyui-native 业务调用的宿主能力 API(http / storage / toast / navigation / i18n …,即 ADR-002 的 J2 / J3) +│ └── cli/ tinyui-cli 构建工具:TSX → h()(ES2025)→ 每页一个 ESM 模块字节码,`tinyui build` ├── compose/ wang.harlon:tinyui KMP 库(Compose Multiplatform 侧):节点表、注册表、桥、内置组件;依赖 quickjs-kmp ├── schema/ 内置组件 schema 的唯一真值(TS DSL)→ `pnpm schema` 生成 packages/core 的类型与 compose/ 的注册 schema ├── sample/ 示例 App,M1 Counter / M2 列表页在这里跑 @@ -83,7 +83,7 @@ tinyui/ 命名依据: - `packages/`:npm 生态惯例,`pnpm -r` / changesets 零配置;目录名与包名一一对应 -- `core`:主包,业务 `import from "@tiny-ui/core"`;内置组件类型并入而不单独成包(只有 `.d.ts`,不值得多一个依赖;宿主 App 自己扩展的组件类型由它自己的 schema 生成到它自己的包里,不经过本仓) +- `core`:主包,业务 `import from "tinyui-core"`;内置组件类型并入而不单独成包(只有 `.d.ts`,不值得多一个依赖;宿主 App 自己扩展的组件类型由它自己的 schema 生成到它自己的包里,不经过本仓) - `native`:回答业务的问题"怎么调原生";与 `core` 互补——core 是 JS 世界里的东西,native 是伸到 JS 外面的手。否掉 `host`(与运行时视角的"宿主"一词混)、`platform`(泛)、`capabilities`(长) - `compose/`:直说它是 Compose Multiplatform 那一侧,将来若有第二渲染端可并列扩展;否掉 `host/`(运行时视角的词不适合做目录)、`library/`(多生态仓里"library of what"不清)、`container/`(国内直觉好但海外联想 Docker)、`kmp/`(说构建方式不说职责) - `schema/`:两侧共同真值,独立顶层目录以体现"契约在中间" @@ -102,11 +102,11 @@ tinyui/ | [build-chain.md](./build-chain.md) | 构建链:TSX → ESM 模块字节码,esbuild 选型,字节码由 CLI 经 `qjsc-kmp` 生成;§7 错误上报与 source map 回映射 | 已完成(2026-09-16;§7 2026-09-17) | | [app-model.md](./app-model.md) | 应用模型:路由、生命周期、跨页状态在 Kotlin;页面之间经 Kotlin 中转的四种通信 | 已对齐(2026-09-16),API 面归 C 组 | | [js-runtime.html](./js-runtime.html) | **JS 层总览**:七样运行时对象、四个时期、响应式闭环、一个页面的一生、规则表。先读这份(浏览器打开) | 已定(2026-09-16) | -| [runtime-api.md](./runtime-api.md) | `@tiny-ui/core` v1 的 API 定义、页面模块契约、桥入口 | 已定(2026-09-16) | +| [runtime-api.md](./runtime-api.md) | `tinyui-core` v1 的 API 定义、页面模块契约、桥入口 | 已定(2026-09-16) | | [jsx-transform.md](./jsx-transform.md) | CLI 的 JSX 变换:thunk 包裹规则、编译期报错、source map | 已定(2026-09-16) | | [patch-protocol.md](./patch-protocol.md) | patch 协议 v1:六种 op、id、值、顺序保证、版本、E5 | 已定(2026-09-16) | | [components.md](./components.md) | 内置组件:schema 真值与生成链、公共布局 prop 与 Modifier 顺序、首批八个组件、命令消费、Placeholder | 已定(2026-09-16) | -| [native-api.md](./native-api.md) | `@tiny-ui/native` 与 `HostServices`:J2 白名单、navigation / store / events / http、E3 错误码与阈值 | 已定(2026-09-16) | +| [native-api.md](./native-api.md) | `tinyui-native` 与 `HostServices`:J2 白名单、navigation / store / events / http、E3 错误码与阈值 | 已定(2026-09-16) | ## 下一阶段 diff --git a/docs/adr-002-bridge-communication.md b/docs/adr-002-bridge-communication.md index 196706f..73ea890 100644 --- a/docs/adr-002-bridge-communication.md +++ b/docs/adr-002-bridge-communication.md @@ -8,8 +8,8 @@ > **2026-09-15 修订(ADR-005)**:事实基础中的 mquickjs-kmp 换为 quickjs-kmp(shim ABI 与 `JsRuntime` 形态同形,句柄表保留)。逐条复盘后的结论: > - **事务机制加一步**:每次 K 入口的顺序是"执行入口 → 排空微任务(`JS_ExecutePendingJob` 至队列空)→ flush → 返回"。K3 回送因此是:resolve Promise → `.then` 链在排空期执行 → 其中的 signal 写入 → flush,全在同一事务内。事务边界与原子性结论不变 -> - **每页一 Runtime 的论证重写**:§3.4 表中"后续页面只能走源码 evaluate"是 MicroQuickJS 特有问题,作废。QuickJS 下真正的替代是"一 Runtime 多 Context"(共享堆、原子表、shape),但限额与 interrupt 是 Runtime 级、卸载 = 关 Runtime 仍是最干净的回收,结论不变。2026-09-16 在 quickjs-kmp 侧实测后不翻:Runtime + Context 基线仅 155 KB,多 Context 每页只省约 100 KB;模块缓存挂在 JSContext 上,运行时模块每页仍要各编译一遍,共享堆换不来共享 `@tiny-ui/core`;微任务队列也是 Runtime 级,多页共享会打破"一次 K 入口 = 一个事务"的页级边界。quickjs-kmp 已定为不暴露多 Context(其 docs/decisions.md「Runtime / Context」条) -> - K0 改为"注册运行时模块(`@tiny-ui/core` / `@tiny-ui/native`)→ 求值页面模块(Promise,排空后取 namespace)→ 调 `default` 导出挂载";"每引擎一个程序"的硬限制消失 +> - **每页一 Runtime 的论证重写**:§3.4 表中"后续页面只能走源码 evaluate"是 MicroQuickJS 特有问题,作废。QuickJS 下真正的替代是"一 Runtime 多 Context"(共享堆、原子表、shape),但限额与 interrupt 是 Runtime 级、卸载 = 关 Runtime 仍是最干净的回收,结论不变。2026-09-16 在 quickjs-kmp 侧实测后不翻:Runtime + Context 基线仅 155 KB,多 Context 每页只省约 100 KB;模块缓存挂在 JSContext 上,运行时模块每页仍要各编译一遍,共享堆换不来共享 `tinyui-core`;微任务队列也是 Runtime 级,多页共享会打破"一次 K 入口 = 一个事务"的页级边界。quickjs-kmp 已定为不暴露多 Context(其 docs/decisions.md「Runtime / Context」条) +> - K0 改为"注册运行时模块(`tinyui-core` / `tinyui-native`)→ 求值页面模块(Promise,排空后取 namespace)→ 调 `default` 导出挂载";"每引擎一个程序"的硬限制消失 > - Promise 原生,J3 / K3 的对接不再需要 polyfill > - **sink 的形态(2026-09-17)**:七类错误统一为一个 `PageError`(kind / 页面 / buildId / message / 入口 / 原始栈 / 映射后的帧 / op)经 `PageSink.error()` 交出,`log()` 只收 `console.*`;栈经 source map 回映射到 `.tsx`,dev 进包实时映射、release 离线,见 build-chain.md §7 > - **新增 E7:未处理的 Promise rejection**——业务 `async` 里抛出且无人 catch,既不经入口 catch(非 E1)也非宿主失败(非 E3);由 quickjs-kmp 接 `JS_SetHostPromiseRejectionTracker` 上报,页面继续。**E6 增加栈溢出**:`JS_SetMaxStackSize` 触发归入引擎级失败 diff --git a/docs/adr-005-engine.md b/docs/adr-005-engine.md index 0092dc8..b0ea982 100644 --- a/docs/adr-005-engine.md +++ b/docs/adr-005-engine.md @@ -70,12 +70,12 @@ Lynx、Kuikly、Weex 2 三个头部动态化框架独立选择了 QuickJS(或 | 引擎 | QuickJS,bellard/quickjs main,锁 commit `04be246`(版本 2026-06-04) | | 接入 | 新建 **quickjs-kmp**(`wang.harlon:quickjs-kmp`,包 `wang.harlon.quickjs`),复制 mquickjs-kmp 的骨架,Kotlin 公共 API 与之同形;实现思路见 quickjs-kmp 仓 `docs/` | | 语言目标 | **ES2025**,CLI 只做 TS 擦除与 JSX → `h()`,不降级 | -| 模块 | **原生 ESM 必选**:运行时(`@tiny-ui/core` / `@tiny-ui/native`)与页面都是模块字节码;引擎侧"名字 → 预编译模块"表,CLI 保证模块图里只剩表内的裸说明符(业务内部 import 构建期合并进页面模块,`external: ["@tiny-ui/*"]`) | +| 模块 | **原生 ESM 必选**:运行时(`tinyui-core` / `tinyui-native`)与页面都是模块字节码;引擎侧"名字 → 预编译模块"表,CLI 保证模块图里只剩表内的裸说明符(业务内部 import 构建期合并进页面模块,运行时模块 `external`) | | 页面加载 | 每页一 Runtime(ADR-002 不变):注册运行时模块 → 求值页面模块(Promise,排空微任务)→ 调 `default` 导出挂载;页面模块允许顶层 `await`,但只用于同步依赖(等运行时初始化之类),数据获取走 `createResource` | | 运行时 API 调整(复盘 ADR-001) | 编译器自动 thunk(v1);`createResource` + "组件函数必须同步"(v1);`createStore` v1.1 | | 错误分类调整(复盘 ADR-002) | 新增 E7 未处理 Promise rejection;E6 增加栈溢出 | | source map | CLI 输出 source map,E1 / E2 上报的行列号映射回 TSX 源;ES5 降级时代不可行,现在只是转译 | -| 模块名 | 即说明符:`@tiny-ui/core`、`pages/home`,无后缀无路径前缀;`import.meta.url` 为 `tinyui:pages/home` | +| 模块名 | 即说明符:`tinyui-core`、`pages/home`,无后缀无路径前缀;`import.meta.url` 为 `tinyui:pages/home` | | 热下发 | **不在本期**;本期字节码全部内置 App。Kotlin 回调式 loader(动态取源码)留给热下发那一期 | | mquickjs-kmp | 独立 SDK 继续存在,与 TinyUI 无关;不做"低端档位"抽象 | diff --git a/docs/app-model.md b/docs/app-model.md index 034d26b..f9f0664 100644 --- a/docs/app-model.md +++ b/docs/app-model.md @@ -1,11 +1,11 @@ # 应用模型:页面是 JS 的世界,应用是 Kotlin 的世界 -- 状态:已对齐(2026-09-16);API 面(`@tiny-ui/native` 的 `navigation` / `store` / `events`)归 roadmap C 组与 J2 白名单一起定 +- 状态:已对齐(2026-09-16);API 面(`tinyui-native` 的 `navigation` / `store` / `events`)归 roadmap C 组与 J2 白名单一起定 - 来源:ADR-002 "跨页共享状态走 Kotlin(K5 推送)"、"多页共享引擎被否:JS 全局正是隔离问题的来源"、`navigation.push` 为 J4;ADR-003 "组件注册表 App 级、不可变" ## 1. 前提 -JS 侧不存在长生命周期的"应用对象":没有 `app.tsx`、没有 `onLaunch`、没有 `globalData`。每个页面模块是一个孤岛,出口只有 `@tiny-ui/native` 的宿主能力,入口只有 K1 生命周期、K2 事件、K3 回送、K5 推送。页面对另一个页面的存在一无所知。 +JS 侧不存在长生命周期的"应用对象":没有 `app.tsx`、没有 `onLaunch`、没有 `globalData`。每个页面模块是一个孤岛,出口只有 `tinyui-native` 的宿主能力,入口只有 K1 生命周期、K2 事件、K3 回送、K5 推送。页面对另一个页面的存在一无所知。 ## 2. 四类应用级事务的归属 @@ -31,7 +31,7 @@ JS 侧不存在长生命周期的"应用对象":没有 `app.tsx`、没有 `onL | 直接调用另一页 | 无 | 无 | 不支持;需要它说明那个状态该进 store | - **向前传参**:props 是 JSON,函数不过桥。"完成后回调我"只能靠回传或事件 -- **向后回传**:原语是 K5 topic,结果投给"处在那个栈位置的页面"而不是某个闭包,页面被回收重建后照样收到。`push` 返回 Promise 的写法(J3 + cbId)与栈深回收冲突——被回收页的闭包已不存在;可之后作为糖加在 `@tiny-ui/native`,文档写明回收语义 +- **向后回传**:原语是 K5 topic,结果投给"处在那个栈位置的页面"而不是某个闭包,页面被回收重建后照样收到。`push` 返回 Promise 的写法(J3 + cbId)与栈深回收冲突——被回收页的闭包已不存在;可之后作为糖加在 `tinyui-native`,文档写明回收语义 - **共享状态**:真值在 Kotlin,原生页与 TinyUI 页共用同一份;内存 key-value 所以 J2 同步读合法;JS 侧包成 signal,K5 到达时更新 signal,绑定了它的 prop 自动重算,与页内响应式同一套。持久化由宿主决定 - **事件广播**:与宿主事件同一条总线,网络状态、主题是 Kotlin 往总线发,业务事件是 JS 往总线发,消费方不区分来源。订阅登记到 Kotlin,只推给感兴趣的页面(每次 K5 = 一次线程切换 + 一个事务);订阅随引擎关闭消失 diff --git a/docs/build-chain.md b/docs/build-chain.md index 133c55c..bbc2d3c 100644 --- a/docs/build-chain.md +++ b/docs/build-chain.md @@ -2,11 +2,11 @@ - 状态:已完成(2026-09-16,PR #2;2026-09-17 加 §7 错误上报与 source map);sample 在 Android 模拟器与 iOS 模拟器上显示 `tinyui:pages/home` - 来源:[ADR-005](./adr-005-engine.md) §4 "语言目标 / 模块" 两行的展开;roadmap A 组"构建链打通" -- 范围:`@tiny-ui/cli` 的 `tinyui build`、`compose/` 的 K0 加载序列、sample 的接线与 CI。自动 thunk(roadmap C 组)、`qjsc-kmp` 二进制分发、页面级配置文件不在本期;字节码行号回映射见 §7 +- 范围:`tinyui-cli` 的 `tinyui build`、`compose/` 的 K0 加载序列、sample 的接线与 CI。自动 thunk(roadmap C 组)、`qjsc-kmp` 二进制分发、页面级配置文件不在本期;字节码行号回映射见 §7 ## 1. 目标与验收 -`tinyui build` 把每个页面的 TSX 编成一个 ESM 模块字节码,`@tiny-ui/core` / `@tiny-ui/native` 各编成一个运行时模块字节码,App 内置这些字节码,每页一个 Runtime 按 ADR-002 K0 的顺序加载。 +`tinyui build` 把每个页面的 TSX 编成一个 ESM 模块字节码,`tinyui-core` / `tinyui-native` 各编成一个运行时模块字节码,App 内置这些字节码,每页一个 Runtime 按 ADR-002 K0 的顺序加载。 验收:sample 的 `src/pages/home.tsx` 经 CLI 产出 `pages/home.bin`,Android / iOS 上 App 启动后显示页面 `default` 导出的返回值,其中含 `import.meta.url` 的值 `tinyui:pages/home`——模块名、external、注册顺序三件事一次验完。CI 的 gradle 与 pnpm 两条 job 都经过这条链。 @@ -14,20 +14,20 @@ ``` src/pages/home.tsx ──esbuild──▶ dist/pages/home.js + .map ──qjsc-kmp -m -n pages/home──▶ dist/pages/home.bin -@tiny-ui/core 包入口 ──esbuild──▶ dist/runtime/core.js ──qjsc-kmp -m -n @tiny-ui/core──▶ dist/runtime/core.bin -@tiny-ui/native 同上(对它而言 @tiny-ui/core 是 external) +tinyui-core 包入口 ──esbuild──▶ dist/runtime/core.js ──qjsc-kmp -m -n tinyui-core──▶ dist/runtime/core.bin +tinyui-native 同上(对它而言 tinyui-core 是 external) ``` | 步骤 | 做什么 | 不做什么 | |---|---|---| -| esbuild | TS 类型擦除;JSX → `h()`(`jsxFactory: "h"`,`inject` 一个内存模块自动引入 `h` / `Fragment`,页面不手写);业务内部相对 import 合并进页面模块;`@tiny-ui/*` 在插件 `onResolve` 里标 `external` + `sideEffects: false`,保持裸说明符且未用到时整条 import 被去掉;`format: "esm"`,`target: "esnext"`;输出 source map | 任何语法降级;对 `@tiny-ui/*` 的解析 | +| 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` 保留行号去源码 | 校验模块图(引擎加载时查表) | -**页面名即模块名**:`src/pages/home.tsx` → `pages/home`,子目录保留路径,无后缀无前缀(ADR-005 §4)。运行时模块名固定 `@tiny-ui/core`、`@tiny-ui/native`。产物目录 `runtime/*.bin`、`pages/**/*.bin`、`manifest.json`(页面与运行时模块清单,Kotlin 侧路由表的来源,见 [app-model.md](./app-model.md))。 +**页面名即模块名**:`src/pages/home.tsx` → `pages/home`,子目录保留路径,无后缀无前缀(ADR-005 §4)。运行时模块名固定 `tinyui-core`、`tinyui-native`,产物文件是 `runtime/core`、`runtime/native`。产物目录 `runtime/*.bin`、`pages/**/*.bin`、`manifest.json`(页面与运行时模块清单、每个模块的产物路径 `files` 与 `buildIds`;Kotlin 侧路由表的来源,宿主用 `BuildManifest.file(name)` 定位 `.bin` / `.js.map`,见 [app-model.md](./app-model.md))。 -业务工程的 tsconfig 用 `jsx: "react-jsx"` + `jsxImportSource: "@tiny-ui/core"` 做类型检查([jsx-transform.md](./jsx-transform.md) §4),而 esbuild 会读到这份 tsconfig 并按它产出 `import { jsx } from "@tiny-ui/core/jsx-runtime"`,即使 `build()` 显式传了 `jsx: "transform"`;引擎里没有这个模块,加载报 `module '@tiny-ui/core/jsx-runtime' is not registered`(2026-09-16 CI 实测)。页面构建要传 `tsconfigRaw` 覆盖 tsconfig 的 jsx 三项。 +业务工程的 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 三项。 -`inject` 的 import 不能只写 `external` 配置项:esbuild 对 `external` 列表里的模块一律保留 import(当作有副作用),无 JSX 的页面也会 `import { h } from "@tiny-ui/core"`,引擎链接模块时因 core 没有该导出而报 `SyntaxError`(2026-09-16 Android 模拟器实测)。走插件 `onResolve` 返回 `sideEffects: false` 才会被摇掉。 +`inject` 的 import 不能只写 `external` 配置项:esbuild 对 `external` 列表里的模块一律保留 import(当作有副作用),无 JSX 的页面也会 `import { h } from "tinyui-core"`,引擎链接模块时因 core 没有该导出而报 `SyntaxError`(2026-09-16 Android 模拟器实测)。走插件 `onResolve` 返回 `sideEffects: false` 才会被摇掉。 ## 3. 页面即构建单元 @@ -37,7 +37,7 @@ src/pages/home.tsx ──esbuild──▶ dist/pages/home.js + .map ──qjsc-k 实现上"一页一次 build()"是对产物形态的描述,不是对 esbuild 调用次数的要求:所有页面作为 `entryPoints` 放进一次 `build()`、`splitting: false`,每个入口的输出仍各自自包含,且共享解析缓存。 -扩展出口:业务共享代码大到安装包体积成问题时,把它编成 App 级共享模块(名字进 external 列表,与 `@tiny-ui/*` 一样注册进每个引擎的模块表),页面对它保持裸 import。这只是 external 列表与模块表多一项,管线形态不变;触发条件出现前不做。 +扩展出口:业务共享代码大到安装包体积成问题时,把它编成 App 级共享模块(名字进 external 列表,与 `tinyui-core` / `tinyui-native` 一样注册进每个引擎的模块表),页面对它保持裸 import。这只是 external 列表与模块表多一项,管线形态不变;触发条件出现前不做。 ## 4. 工具选型:esbuild @@ -45,7 +45,7 @@ src/pages/home.tsx ──esbuild──▶ dist/pages/home.js + .map ──qjsc-k 1. TS 擦除,不做任何降级:顶层 `await`、`import.meta.url` 原样保留 2. JSX → `h()`,工厂名可指定 -3. 打包:合并业务内部 import;`@tiny-ui/*` 不解析 +3. 打包:合并业务内部 import;运行时模块不解析 4. ESM 单文件输出 + source map 5. 以库形式被 CLI 调用 diff --git a/docs/components.md b/docs/components.md index 4329da4..c8c8010 100644 --- a/docs/components.md +++ b/docs/components.md @@ -13,7 +13,7 @@ | `packages/core/src/generated/components.ts` | `XxxProps`(JSX 属性类型)、`XxxYyyPayload`(事件 payload)、`XxxCommands`(命令参数表,配 `ref<XxxCommands>()`)、`IntrinsicElements`、组件名常量 | 业务的类型检查 | | `compose/.../components/generated/BuiltinSchemas.kt` | 每个组件一个 `ComponentSchema`,prop 的类型 / 默认值 / `required` / `initial`,事件与命令的字段表 | Kotlin 注册表:写入时转换、E5 校验、清单下发 | -Kotlin 侧的 composable 仍手写,`register(BuiltinSchemas.Text) { scope -> … }`。宿主扩展组件(`pp.KycCard`)用同一套 DSL(`import … from "@tiny-ui/cli/schema"`)生成到宿主自己的包里,本仓不含。 +Kotlin 侧的 composable 仍手写,`register(BuiltinSchemas.Text) { scope -> … }`。宿主扩展组件(`pp.KycCard`)用同一套 DSL(`import … from "tinyui-cli/schema"`)生成到宿主自己的包里,本仓不含。 DSL: diff --git a/docs/js-runtime.html b/docs/js-runtime.html index d6256dd..4b3c0a9 100644 --- a/docs/js-runtime.html +++ b/docs/js-runtime.html @@ -241,8 +241,8 @@ <h2 id="loop">3. 响应式闭环</h2> <h2 id="life">4. 一个页面的一生</h2> <p>页面:拉一个待办列表,加载中显示占位,每行点击切换完成态。</p> <pre><code>// pages/todos.tsx -import { signal, resource, Column, Row, Text, Show, For } from "@tiny-ui/core"; -import { http } from "@tiny-ui/native"; +import { signal, resource, Column, Row, Text, Show, For } from "tinyui-core"; +import { http } from "tinyui-native"; interface Todo { id: number; title: string; done: boolean } diff --git a/docs/jsx-transform.md b/docs/jsx-transform.md index cd516b9..ec5180a 100644 --- a/docs/jsx-transform.md +++ b/docs/jsx-transform.md @@ -2,7 +2,7 @@ - 状态:已定(2026-09-16) - 来源:ADR-001 修订"thunk 写法改由编译器生成"、ADR-005 §4"CLI 只做 TS 擦除与 JSX → `h()`,source map";运行时侧的接收形态见 [runtime-api.md](./runtime-api.md) §3 -- 位置:`@tiny-ui/cli` 在 esbuild 的 `onLoad` 里对每个 `.tsx` 先跑本文的 pass,再交给 esbuild 做 JSX → `h()`、TS 擦除与合并([build-chain.md](./build-chain.md) §4.4 预留的出口) +- 位置:`tinyui-cli` 在 esbuild 的 `onLoad` 里对每个 `.tsx` 先跑本文的 pass,再交给 esbuild 做 JSX → `h()`、TS 擦除与合并([build-chain.md](./build-chain.md) §4.4 预留的出口) ## 1. 目标形态 @@ -16,7 +16,7 @@ h(Text, { text: thunk(() => "Count: " + count()), color: thunk(() => theme.primary), onClick: () => inc() }) ``` -`thunk` 与 `h` / `Fragment` 一样由 CLI 注入 import(`@tiny-ui/core`),业务文件里不出现。类型层面 `thunk<T>(fn: () => T): T`,所以 `.d.ts` 里 prop 类型按值声明(`text: string`),业务与组件作者都看不到包装。 +`thunk` 与 `h` / `Fragment` 一样由 CLI 注入 import(`tinyui-core`),业务文件里不出现。类型层面 `thunk<T>(fn: () => T): T`,所以 `.d.ts` 里 prop 类型按值声明(`text: string`),业务与组件作者都看不到包装。 ## 2. 属性表达式的包裹规则 @@ -64,8 +64,8 @@ h(Text, { text: thunk(() => "Count: " + count()), color: thunk(() => theme.prima ## 4. 元素类型 -- 小写标签 `<text>` 不支持(不是 DOM),首字母大写视为标识符:`<Text>` 变成 `h(Text, …)`,`Text` 由 `@tiny-ui/core` 导出,是值为 `"Text"` 的字符串常量(TS 允许字符串字面量类型的标识符做元素名,prop 类型从 `JSX.IntrinsicElements["Text"]` 查,由 schema 生成,roadmap C 组第 4 项);宿主扩展 `<pp.KycCard>` 变成 `h(pp.KycCard, …)`,`pp` 由宿主 App 自己的类型包导出 -- 类型检查用 `"jsx": "react-jsx"` + `"jsxImportSource": "@tiny-ui/core"`:TS 从 `@tiny-ui/core/jsx-runtime` 取 `JSX` 命名空间,业务文件不用 import `h`;产物仍由 esbuild 按 `jsxFactory: "h"` 生成(TS 只管类型,不参与产出) +- 小写标签 `<text>` 不支持(不是 DOM),首字母大写视为标识符:`<Text>` 变成 `h(Text, …)`,`Text` 由 `tinyui-core` 导出,是值为 `"Text"` 的字符串常量(TS 允许字符串字面量类型的标识符做元素名,prop 类型从 `JSX.IntrinsicElements["Text"]` 查,由 schema 生成,roadmap C 组第 4 项);宿主扩展 `<pp.KycCard>` 变成 `h(pp.KycCard, …)`,`pp` 由宿主 App 自己的类型包导出 +- 类型检查用 `"jsx": "react-jsx"` + `"jsxImportSource": "tinyui-core"`:TS 从 `tinyui-core/jsx-runtime` 取 `JSX` 命名空间,业务文件不用 import `h`;产物仍由 esbuild 按 `jsxFactory: "h"` 生成(TS 只管类型,不参与产出) - `<>…</>` 变成 `h(Fragment, null, …)` - 组件 `<Row title={x()} />` 与内置元素规则相同:属性照 §2 包裹,运行时把 thunk 转成 getter 交给组件 diff --git a/docs/native-api.md b/docs/native-api.md index 1e8f12d..3b41dc3 100644 --- a/docs/native-api.md +++ b/docs/native-api.md @@ -1,8 +1,8 @@ -# `@tiny-ui/native` 与宿主服务 +# `tinyui-native` 与宿主服务 - 状态:已定(2026-09-16);M2 的实现依据 - 来源:ADR-002 §3.1(J2 白名单、J3 / J4)、[app-model.md](./app-model.md)(路由、store、事件)、ADR-004 §3.1(连续事件阈值) -- 两侧:JS 的 `@tiny-ui/native` 是 `__host_*` 的类型化封装;Kotlin 的 `HostServices` 是宿主 App 实现的接口组,交给 `TinyUIPage` +- 两侧:JS 的 `tinyui-native` 是 `__host_*` 的类型化封装;Kotlin 的 `HostServices` 是宿主 App 实现的接口组,交给 `TinyUIPage` ## 1. J2 白名单 diff --git a/docs/roadmap.md b/docs/roadmap.md index 7b468dd..3149565 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -10,7 +10,7 @@ | 仓库结构 | 已定单仓,目录见 README"仓库结构"一节 | 已定 | | 构建链骨架 | Gradle(build-logic / compose / sample 含 iOS 壳)+ pnpm workspace(core / native / cli)+ CI(PR 门禁 Android / host / apiCheck,iOS 全量在 ios.yml tag / 手动);quickjs-kmp 本地 composite、CI 走 Central | 已完成(PR #1,2026-09-16) | | **quickjs-kmp** | 新建 SDK 接入 QuickJS(ADR-005),复制 mquickjs-kmp 骨架,API 同形;M1 shim + 三端构建,M2 句柄表 + Runtime + 微任务 + **ESM 模块表**,M3 字节码 + 宿主编译工具 + 发布。TinyUI 的 `compose/` 依赖它 | 已完成(M1~M3,`wang.harlon:quickjs-kmp:0.1.0` 2026-09-16 发 Central;桥形验收见 `bench/results/2026-09-16-quickjs-kmp.md`) | -| 构建链打通 | CLI:TSX → `h()`(ES2025)→ 每页一个 ESM 模块字节码,`external: ["@tiny-ui/*"]`;运行时两个模块字节码内置 | 已完成(PR #2,2026-09-16;方案与踩坑见 [build-chain.md](./build-chain.md)) | +| 构建链打通 | CLI:TSX → `h()`(ES2025)→ 每页一个 ESM 模块字节码,运行时模块 `external`;运行时两个模块字节码内置 | 已完成(PR #2,2026-09-16;方案与踩坑见 [build-chain.md](./build-chain.md)) | ## B. 验证实验(数据驱动,互相独立,可并行) @@ -35,7 +35,7 @@ | 公共布局 prop 清单与 `Modifier` 合成顺序 | ADR-003 | 已定(components.md §2) | | 内置组件集首批(`Column` / `Row` / `Box` / `Text` / `Image` / `Button` / `TextField` / `LazyColumn` / `Spacer`)及各自 prop / 事件 / 命令清单 | ADR-003 / 004 | 已定(components.md §3);`Image` 推迟到图片加载管线选型 | | J2 白名单清单与注册方式 | ADR-002 | 已定([native-api.md](./native-api.md) §1) | -| `@tiny-ui/native` 的 `navigation` / `store` / `events` API 面与 `Navigator` 接口([app-model.md](./app-model.md));`push` 的 Promise 糖做不做 | app-model.md | 已定(native-api.md §2–§5;Promise 糖不做) | +| `tinyui-native` 的 `navigation` / `store` / `events` API 面与 `Navigator` 接口([app-model.md](./app-model.md));`push` 的 Promise 糖做不做 | app-model.md | 已定(native-api.md §2–§5;Promise 糖不做) | | E3 错误 code 表、K 入口超时阈值、连续事件节流阈值 | ADR-002 / 004 | 已定(native-api.md §6) | | `Placeholder` 在 release 的表现 | ADR-003 | 已定(components.md §5:零尺寸空 Box) | | 错误上报:`PageError` 单 sink、E7 接入、栈经 source map 回映射到 `.tsx`、buildId | ADR-002 §3.5 | 已完成(2026-09-17,[build-chain.md](./build-chain.md) §7) | @@ -65,7 +65,7 @@ 1. **A 立项骨架**——先 quickjs-kmp(M1~M3),再 TinyUI 构建链,否则 B、C 都没有落点 2. **B 前三项并行**——半天到一天的实验,结果决定 C 里几个数值和 ADR-003 的退路 3. **C 前三项定稿**(运行时 API、patch 协议、schema DSL)——两侧代码的契约,定了才能分头写 -4. ~~**M1:Counter 端到端**——一个引擎、一页、一个 `Text` + 一个 `Button`,J1 / K1 / K2 全链路跑通,验证 ADR-001~004 主干~~ 已完成(PR #3,2026-09-16):`@tiny-ui/core` 运行时(30 测试)、CLI JSX 变换(19 测试)、compose 节点表 / schema DSL / 注册表 / `PageHost` / `TinyUIPage`(9 测试),Counter 在 Android 与 iOS 模拟器上跑通。顺带修了 quickjs-kmp 的字节码注册入口不刷新栈顶的 bug(quickjs-kmp PR #12),**真机需要 quickjs-kmp ≥ 0.1.1**,发版后 bump catalog -5. ~~**C 剩余项 + 内置组件集,M2:列表页**——`For` / `LazyColumn` / `TextField` / J3 网络,覆盖所有权、命令、流式输入~~ 已完成(PR #4,2026-09-16):schema 生成链、八个内置组件、`HostServices`、`@tiny-ui/native`;todos 页在 Android 与 iOS 模拟器上跑通输入提交、行增删改、命令滚动、分页 +4. ~~**M1:Counter 端到端**——一个引擎、一页、一个 `Text` + 一个 `Button`,J1 / K1 / K2 全链路跑通,验证 ADR-001~004 主干~~ 已完成(PR #3,2026-09-16):`tinyui-core` 运行时(30 测试)、CLI JSX 变换(19 测试)、compose 节点表 / schema DSL / 注册表 / `PageHost` / `TinyUIPage`(9 测试),Counter 在 Android 与 iOS 模拟器上跑通。顺带修了 quickjs-kmp 的字节码注册入口不刷新栈顶的 bug(quickjs-kmp PR #12),**真机需要 quickjs-kmp ≥ 0.1.1**,发版后 bump catalog +5. ~~**C 剩余项 + 内置组件集,M2:列表页**——`For` / `LazyColumn` / `TextField` / J3 网络,覆盖所有权、命令、流式输入~~ 已完成(PR #4,2026-09-16):schema 生成链、八个内置组件、`HostServices`、`tinyui-native`;todos 页在 Android 与 iOS 模拟器上跑通输入提交、行增删改、命令滚动、分页 6. **实战接入:TrendingAI 订阅页**(2026-09-17 起,代替多页业务样例)——页面源码在 `~/TrendingProjects/trendingai-tinyui`,产物经 `pnpm sync` 入 TrendingAI;框架缺什么以这一页为准补(第一批:主题 token、weight / border / scroll、`host.call`、`Button` children、`RadioButton`、宿主 include)。已在 Android 模拟器跑通拉价、选档、深浅色(TrendingAI 分支 `feat/tinyui-subscription`);合入 TrendingAI main 的前提是 tinyui 0.1.0(Maven + npm)与 quickjs-kmp 0.1.1 发版 diff --git a/docs/runtime-api.md b/docs/runtime-api.md index a8c36b9..1f93f85 100644 --- a/docs/runtime-api.md +++ b/docs/runtime-api.md @@ -1,4 +1,4 @@ -# JS 运行时 API:`@tiny-ui/core` v1 +# JS 运行时 API:`tinyui-core` v1 - 状态:已定(2026-09-16;2026-09-17 加 §2.6 `observable`、§1 命名规则,`createResource` 改名 `resource`、`host()` 改名 `manifest()`);M1 的实现依据。系统说明见 [js-runtime.html](./js-runtime.html),本文只放定义 - 来源:ADR-001(Signal、所有权)、ADR-002(桥入口、事务、错误)、ADR-004(ref + cmd、事件 payload)、ADR-005(组件函数必须同步、`resource`、原生 Promise) @@ -26,11 +26,11 @@ | 页面 | `pageVisible()` | 页面是否可见(K1 `visible`) | | | `manifest()` | Kotlin 下发的组件 / 能力清单 | | 版本 | `VERSION`、`PROTOCOL` | 包版本;patch 协议版本号 | -| 内部 | `internal.{call, query, send, onEmit}` | core 与 `@tiny-ui/native` 之间的桥契约(§9),业务不用 | +| 内部 | `internal.{call, query, send, onEmit}` | core 与 `tinyui-native` 之间的桥契约(§9),业务不用 | 不在 v1:`createContext`、`ErrorBoundary`、`Suspense`、`Switch/Match`、`Index`、`Portal`、`lazy`、`setInterval`。页内跨组件共享状态直接用模块顶层的 `signal` / `observable`(每页一个引擎,模块作用域就是页面作用域)。 -**命名规则**(2026-09-17 定):状态与异步原语一律无前缀小写(`signal` / `memo` / `effect` / `resource` / `observable`),不用 `create*`、`use*` 前缀——`use*` 会被读成 React hooks 的"每次渲染重跑",而组件只跑一次;组件与结构原语首字母大写。`observable` 原拟名 `createStore`(Solid),改名因 "store" 留给 `@tiny-ui/native` 的跨页 KV(业界 store 的主流含义)。 +**命名规则**(2026-09-17 定):状态与异步原语一律无前缀小写(`signal` / `memo` / `effect` / `resource` / `observable`),不用 `create*`、`use*` 前缀——`use*` 会被读成 React hooks 的"每次渲染重跑",而组件只跑一次;组件与结构原语首字母大写。`observable` 原拟名 `createStore`(Solid),改名因 "store" 留给 `tinyui-native` 的跨页 KV(业界 store 的主流含义)。 ## 2. 响应式原语 @@ -99,7 +99,7 @@ function unwrap<T>(value: T): T - 存进去的对象被直接持有(不拷贝):绕过 Proxy 改原对象不会触发 - 与 `For` 的配合:`each={state.list}` 时行 accessor `item()` 返回缓存的代理,行内绑定落到属性级——改一行的一个字段只重跑读了它的绑定,`For` 不重算;`push` / `splice` 触发一次 reconcile - `unwrap(value)`:返回代理背后的原对象,并把嵌套的代理原地换回原对象;发请求体、打日志时用。非代理原样返回 -- 分工:原始值用 `signal`;有独立变化字段的对象 / 数组用 `observable`;跨页共享走 `@tiny-ui/native` 的 `store`(native-api.md §3);`signal<T[]>` 整体替换仍合法。不做 `reconcile` / `produce` / 只读视图(roadmap D 组) +- 分工:原始值用 `signal`;有独立变化字段的对象 / 数组用 `observable`;跨页共享走 `tinyui-native` 的 `store`(native-api.md §3);`signal<T[]>` 整体替换仍合法。不做 `reconcile` / `produce` / 只读视图(roadmap D 组) ## 3. 节点与组件 @@ -235,7 +235,7 @@ export default function Home(props: HomeProps): Node { … } ## 9. 桥入口(运行时 ↔ Kotlin 的内部契约) -业务不可见,是 `@tiny-ui/core` 与 `compose/` 之间的接口;改动两侧同一 PR。 +业务不可见,是 `tinyui-core` 与 `compose/` 之间的接口;改动两侧同一 PR。 **Kotlin → JS**:运行时模块求值时挂 `globalThis.__tinyui`,Kotlin 在 K0 之后取它一次并持有。 @@ -264,7 +264,7 @@ export default function Home(props: HomeProps): Node { … } | J4 | `__host_send` | `(name: string, argsJson: string) => void` | 即发即忘 | | J5 | `__host_report` | `(kind: "E1", detailJson: string) => void` | 运行时捕获但不中断事务的业务错误 `{ entry, message, stack }`;`console.*` 走引擎 logger | -`@tiny-ui/native` 是这五个全局的类型化封装(`http.get` = `__host_call("http.get", …)` 包成 Promise),经 core 的 `internal.{call, query, send, onEmit}` 调用;业务不直接碰 `__host_*` 也不碰 `internal`。`internal` 留在主入口而不是子路径 `@tiny-ui/core/internal`:运行时模块按名字注册进引擎的模块表,子路径会成为第三个模块名,要么再注册一个模块、要么被打进 native 的字节码而复制一份 core 的模块状态(pending 表分裂)。 +`tinyui-native` 是这五个全局的类型化封装(`http.get` = `__host_call("http.get", …)` 包成 Promise),经 core 的 `internal.{call, query, send, onEmit}` 调用;业务不直接碰 `__host_*` 也不碰 `internal`。`internal` 留在主入口而不是子路径 `tinyui-core/internal`:运行时模块按名字注册进引擎的模块表,子路径会成为第三个模块名,要么再注册一个模块、要么被打进 native 的字节码而复制一份 core 的模块状态(pending 表分裂)。 错误分类的运行时侧行为(ADR-002 §3.5): diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d248b1..18fcd03 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ android-targetSdk = "37" androidxActivityCompose = "1.13.0" kotlinxCoroutines = "1.11.0" kotlinxSerialization = "1.11.0" -quickjsKmp = "0.1.0" +quickjsKmp = "0.1.1" vanniktechMavenPublish = "0.37.0" binaryCompatibilityValidator = "0.18.2" diff --git a/packages/cli/package.json b/packages/cli/package.json index be16d07..8b5b017 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { - "name": "@tiny-ui/cli", - "version": "0.0.0", + "name": "tinyui-cli", + "version": "0.1.0", "description": "TinyUI build tool: TSX -> h() (ES2025) -> ESM module bytecode.", "license": "MIT", "type": "module", @@ -36,8 +36,8 @@ "oxc-parser": "0.150.0" }, "devDependencies": { - "@tiny-ui/core": "workspace:*", - "@tiny-ui/native": "workspace:*", + "tinyui-core": "workspace:*", + "tinyui-native": "workspace:*", "@types/node": "latest" } } diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 9c64fbb..6957987 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -19,7 +19,7 @@ schema --entry TS module whose default export is the component list --ts write JSX prop types here --kt write Kotlin schemas here (with --package, optional --object, default BuiltinSchemas) - --node-import where the generated TS imports Node / Ref from (default @tiny-ui/core) + --node-import where the generated TS imports Node / Ref from (default tinyui-core) --check exit 1 if a target is not already up to date, write nothing `; @@ -37,7 +37,7 @@ async function main(argv: string[]): Promise<number> { kt: { type: "string" }, package: { type: "string" }, object: { type: "string", default: "BuiltinSchemas" }, - "node-import": { type: "string", default: "@tiny-ui/core" }, + "node-import": { type: "string", default: "tinyui-core" }, check: { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false }, }, diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index b7ed76e..5234634 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -5,7 +5,8 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { compileModule, findQjsc } from "./qjsc.ts"; import { TransformError, transformJsx } from "./transform.ts"; -export const RUNTIME_MODULES = ["@tiny-ui/core", "@tiny-ui/native"] as const; +/** Runtime module name as the engine sees it → output file under `runtime/`. */ +export const RUNTIME_MODULES = { "tinyui-core": "core", "tinyui-native": "native" } as const; export interface BuildOptions { /** Project root: pages are discovered under `<root>/src/pages`, packages resolved from `<root>/node_modules`. */ @@ -19,7 +20,7 @@ export interface BuildOptions { } export interface BuiltModule { - /** Module name as the engine sees it: `pages/home`, `@tiny-ui/core`. */ + /** Module name as the engine sees it: `pages/home`, `tinyui-core`. */ name: string; js: string; map: string; @@ -37,6 +38,8 @@ export interface BuildResult { export interface Manifest { runtime: string[]; pages: string[]; + /** Module name → output path without extension (`runtime/core`, `pages/home`); hosts locate `.bin` / `.js.map` through it. */ + files: Record<string, string>; buildIds: Record<string, string>; } @@ -72,6 +75,7 @@ export async function build(options: BuildOptions): Promise<BuildResult> { const content: Manifest = { runtime: runtime.map((m) => m.name), pages: pages.map((m) => m.name), + files: Object.fromEntries([...runtime, ...pages].map((m) => [m.name, relative(out, m.js).replace(/\.js$/, "").split(sep).join("/")])), buildIds: Object.fromEntries([...runtime, ...pages].map((m) => [m.name, m.buildId])), }; await writeFile(manifest, JSON.stringify(content, null, 2) + "\n"); @@ -99,14 +103,14 @@ async function discoverPages(pagesDir: string): Promise<Map<string, string>> { async function bundleRuntime(root: string, out: string): Promise<BuiltModule[]> { const built: BuiltModule[] = []; - for (const name of RUNTIME_MODULES) { - const outfile = join(out, "runtime", name.replace("@tiny-ui/", "") + ".js"); + for (const [name, file] of Object.entries(RUNTIME_MODULES)) { + const outfile = join(out, "runtime", file + ".js"); await mkdir(dirname(outfile), { recursive: true }); await esbuild({ ...common(root), entryPoints: [name], outfile, - external: RUNTIME_MODULES.filter((m) => m !== name), + external: Object.keys(RUNTIME_MODULES).filter((m) => m !== name), }); built.push({ name, js: outfile, map: outfile + ".map", buildId: "" }); } @@ -154,9 +158,9 @@ const pagePlugin: Plugin = { setup(api) { api.onResolve({ filter: /^tinyui:jsx-shim$/ }, (args) => ({ path: args.path, namespace: "tinyui" })); // Runtime modules stay bare specifiers and are pure, so a page that never uses JSX keeps no import of h - api.onResolve({ filter: /^@tiny-ui\// }, (args) => ({ path: args.path, external: true, sideEffects: false })); + api.onResolve({ filter: /^tinyui-(core|native)$/ }, (args) => ({ path: args.path, external: true, sideEffects: false })); api.onLoad({ filter: /.*/, namespace: "tinyui" }, () => ({ - contents: 'export { h, Fragment, thunk } from "@tiny-ui/core";', + contents: 'export { h, Fragment, thunk } from "tinyui-core";', loader: "js", })); // docs/jsx-transform.md: wrap dynamic attributes before esbuild turns JSX into h() diff --git a/packages/cli/src/schema/generate.ts b/packages/cli/src/schema/generate.ts index c5aa26c..fbc278c 100644 --- a/packages/cli/src/schema/generate.ts +++ b/packages/cli/src/schema/generate.ts @@ -4,11 +4,11 @@ import { COLOR_TOKENS, TEXT_STYLES } from "./tokens.ts"; const HEADER = "// Generated by `tinyui schema` from schema/; do not edit."; -/** [nodeImport] is where `Node` and `Ref` come from: `@tiny-ui/core` for host packages, `../node.ts` inside core itself. */ -export function generateTs(components: ComponentDef[], nodeImport = "@tiny-ui/core"): string { +/** [nodeImport] is where `Node` and `Ref` come from: `tinyui-core` for host packages, `../node.ts` inside core itself. */ +export function generateTs(components: ComponentDef[], nodeImport = "tinyui-core"): string { const out: string[] = [HEADER]; - if (nodeImport === "@tiny-ui/core") { - out.push(`import type { ColorValue, Node, Ref } from "@tiny-ui/core";`, ""); + if (nodeImport === "tinyui-core") { + out.push(`import type { ColorValue, Node, Ref } from "tinyui-core";`, ""); } else { out.push(`import type { Node, Ref } from ${JSON.stringify(nodeImport)};`, ""); out.push(`export type ColorToken = ${COLOR_TOKENS.map((t) => JSON.stringify(t)).join(" | ")};`); @@ -44,11 +44,11 @@ export function generateTs(components: ComponentDef[], nodeImport = "@tiny-ui/co for (const c of components) if (!c.name.includes(".")) out.push(`export const ${c.name} = ${JSON.stringify(c.name)};`); // host packages: one object per prefix so pages write <ta.Icon>, and the JSX namespace learns the tags const prefixed = components.filter((c) => c.name.includes(".")); - if (nodeImport === "@tiny-ui/core" && prefixed.length > 0) { + if (nodeImport === "tinyui-core" && prefixed.length > 0) { const byPrefix = new Map<string, ComponentDef[]>(); for (const c of prefixed) { const [prefix] = c.name.split("."); byPrefix.set(prefix!, [...(byPrefix.get(prefix!) ?? []), c]); } for (const [prefix, cs] of byPrefix) out.push(`export const ${prefix} = { ${cs.map((c) => `${c.name.split(".")[1]}: ${JSON.stringify(c.name)}`).join(", ")} } as const;`); - out.push("", `declare module "@tiny-ui/core/jsx-runtime" {`, " namespace JSX {", " interface IntrinsicElements {"); + out.push("", `declare module "tinyui-core/jsx-runtime" {`, " namespace JSX {", " interface IntrinsicElements {"); for (const c of prefixed) out.push(` ${JSON.stringify(c.name)}: ${tsIdent(c.name)}Props;`); out.push(" }", " }", "}"); } diff --git a/packages/cli/test/build.test.ts b/packages/cli/test/build.test.ts index 635eb0b..164f70d 100644 --- a/packages/cli/test/build.test.ts +++ b/packages/cli/test/build.test.ts @@ -22,12 +22,12 @@ describe("tinyui build", () => { it("names pages after their path under src/pages", () => { assert.deepEqual(result.pages.map((m) => m.name), ["pages/home", "pages/nested/detail"]); - assert.deepEqual(result.runtime.map((m) => m.name), ["@tiny-ui/core", "@tiny-ui/native"]); + assert.deepEqual(result.runtime.map((m) => m.name), ["tinyui-core", "tinyui-native"]); }); - it("turns JSX into h() calls with the factory imported from @tiny-ui/core", async () => { + it("turns JSX into h() calls with the factory imported from tinyui-core", async () => { const js = await readFile(join(out, "pages", "home.js"), "utf8"); - assert.match(js, /import \{ h, Fragment, thunk \} from "@tiny-ui\/core"/); + assert.match(js, /import \{ h, Fragment, thunk \} from "tinyui-core"/); assert.match(js, /h\(Column, null, .*h\(Text, \{ text: label \}\)/s); assert.match(js, /h\(Text, \{ text: thunk\(\(\) => title\(name\)\) \}\)/, "call expressions are wrapped"); assert.doesNotMatch(js, /interface Props|: Props/, "types are erased"); @@ -39,16 +39,16 @@ describe("tinyui build", () => { assert.match(js, /h\(Column/); }); - it("inlines relative imports and keeps @tiny-ui/* as bare specifiers", async () => { + it("inlines relative imports and keeps runtime modules as bare specifiers", async () => { const js = await readFile(join(out, "pages", "home.js"), "utf8"); assert.match(js, /function title\(name\)/, "../lib/format.ts is bundled in"); assert.doesNotMatch(js, /from "\.\.?\//, "no relative imports survive"); - assert.match(js, /from "@tiny-ui\/core"/); + assert.match(js, /from "tinyui-core"/); }); it("imports the JSX factory only from pages that use JSX", async () => { const js = await readFile(join(out, "pages", "nested", "detail.js"), "utf8"); - assert.doesNotMatch(js, /@tiny-ui\/core/, "no JSX, no factory import: the runtime module does not export h yet"); + assert.doesNotMatch(js, /tinyui-core/, "no JSX, no factory import: the runtime module does not export h yet"); }); it("keeps ES2025 syntax as is", async () => { @@ -71,7 +71,7 @@ describe("tinyui build", () => { it("bundles each runtime module on its own", async () => { const core = await readFile(join(out, "runtime", "core.js"), "utf8"); assert.match(core, /export \{/); - assert.doesNotMatch(core, /from "@tiny-ui\//); + assert.doesNotMatch(core, /from "tinyui-/); }); it("rejects two sources for one page name", async () => { @@ -88,9 +88,15 @@ describe("tinyui build", () => { it("lists everything in manifest.json", async () => { const manifest = JSON.parse(await readFile(result.manifest, "utf8")); - assert.deepEqual(manifest.runtime, ["@tiny-ui/core", "@tiny-ui/native"]); + assert.deepEqual(manifest.runtime, ["tinyui-core", "tinyui-native"]); assert.deepEqual(manifest.pages, ["pages/home", "pages/nested/detail"]); assert.deepEqual(Object.keys(manifest.buildIds), [...manifest.runtime, ...manifest.pages]); + assert.deepEqual(manifest.files, { + "tinyui-core": "runtime/core", + "tinyui-native": "runtime/native", + "pages/home": "pages/home", + "pages/nested/detail": "pages/nested/detail", + }); for (const id of Object.values(manifest.buildIds)) assert.match(id as string, /^[0-9a-f]{8}$/); assert.equal(manifest.buildIds["pages/home"], result.pages[0]!.buildId); }); diff --git a/packages/cli/test/fixtures/app/src/pages/home.tsx b/packages/cli/test/fixtures/app/src/pages/home.tsx index a4a7f40..d07584e 100644 --- a/packages/cli/test/fixtures/app/src/pages/home.tsx +++ b/packages/cli/test/fixtures/app/src/pages/home.tsx @@ -1,4 +1,4 @@ -import { VERSION } from "@tiny-ui/core"; +import { VERSION } from "tinyui-core"; import { title } from "../lib/format.ts"; interface Props { name: string } diff --git a/packages/cli/test/fixtures/app/tsconfig.json b/packages/cli/test/fixtures/app/tsconfig.json index 84e7d78..13f1e25 100644 --- a/packages/cli/test/fixtures/app/tsconfig.json +++ b/packages/cli/test/fixtures/app/tsconfig.json @@ -1,3 +1,3 @@ { - "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@tiny-ui/core", "strict": true } + "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "tinyui-core", "strict": true } } diff --git a/packages/cli/test/schema.test.ts b/packages/cli/test/schema.test.ts index d7f3379..b3dd87b 100644 --- a/packages/cli/test/schema.test.ts +++ b/packages/cli/test/schema.test.ts @@ -11,7 +11,7 @@ const card = defineComponent("pp.Card", { describe("schema generator", () => { it("types color props as ColorValue; host packages import it from core, core defines it", () => { const host = generateTs([card]); - assert.match(host, /import type \{ ColorValue, Node, Ref \} from "@tiny-ui\/core"/); + assert.match(host, /import type \{ ColorValue, Node, Ref \} from "tinyui-core"/); assert.match(host, /tint\?: ColorValue;/); assert.doesNotMatch(host, /export type ColorValue/); const core = generateTs([card], "../node.ts"); @@ -22,7 +22,7 @@ describe("schema generator", () => { it("groups host components under their prefix and augments the JSX namespace", () => { const host = generateTs([card]); assert.match(host, /export const pp = \{ Card: "pp\.Card" \} as const;/); - assert.match(host, /declare module "@tiny-ui\/core\/jsx-runtime" \{\n namespace JSX \{\n interface IntrinsicElements \{\n "pp\.Card": PpCardProps;/); + assert.match(host, /declare module "tinyui-core\/jsx-runtime" \{\n namespace JSX \{\n interface IntrinsicElements \{\n "pp\.Card": PpCardProps;/); assert.doesNotMatch(generateTs([card], "../node.ts"), /declare module/); }); diff --git a/packages/core/package.json b/packages/core/package.json index 6ce3e72..013b13d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { - "name": "@tiny-ui/core", - "version": "0.0.0", + "name": "tinyui-core", + "version": "0.1.0", "description": "TinyUI runtime: signals, ownership, h/For/Show, patch emission.", "license": "MIT", "type": "module", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a7d67a5..adccd12 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -export const VERSION = "0.0.0"; +export const VERSION = "0.1.0"; export { PROTOCOL, pageVisible, manifest } from "./page.ts"; export type { HostManifest } from "./page.ts"; export { signal, memo, effect, onCleanup, untrack } from "./reactive.ts"; diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index a4cfc33..b11d193 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -1,3 +1,3 @@ -// The contract between @tiny-ui/core and @tiny-ui/native (docs/runtime-api.md §9); page code has no business here. +// The contract between tinyui-core and tinyui-native (docs/runtime-api.md §9); page code has no business here. export { call, query, send } from "./host.ts"; export { onEmit } from "./page.ts"; diff --git a/packages/core/src/jsx-runtime.ts b/packages/core/src/jsx-runtime.ts index 56f61ce..d30e24e 100644 --- a/packages/core/src/jsx-runtime.ts +++ b/packages/core/src/jsx-runtime.ts @@ -1,4 +1,4 @@ -// Type-check entry for `jsx: "react-jsx"` + `jsxImportSource: "@tiny-ui/core"`; the CLI still emits h() calls +// Type-check entry for `jsx: "react-jsx"` + `jsxImportSource: "tinyui-core"`; the CLI still emits h() calls // (docs/jsx-transform.md). The runtime functions exist so a react-jsx transform would also work. import { Fragment as FragmentImpl, h, type Node, type Props } from "./node.ts"; import type { IntrinsicElements as Generated } from "./generated/components.ts"; diff --git a/packages/core/test/version.test.ts b/packages/core/test/version.test.ts new file mode 100644 index 0000000..73847b1 --- /dev/null +++ b/packages/core/test/version.test.ts @@ -0,0 +1,11 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { describe, it } from "node:test"; +import { VERSION } from "../src/index.ts"; + +describe("VERSION", () => { + it("matches package.json, the version the bytecode ships under", () => { + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); + assert.equal(VERSION, pkg.version); + }); +}); diff --git a/packages/native/package.json b/packages/native/package.json index 41c9807..82ac535 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { - "name": "@tiny-ui/native", - "version": "0.0.0", + "name": "tinyui-native", + "version": "0.1.0", "description": "TinyUI host capability API for business code: http, storage, toast, navigation, i18n.", "license": "MIT", "type": "module", @@ -25,7 +25,7 @@ }, "sideEffects": false, "dependencies": { - "@tiny-ui/core": "workspace:*" + "tinyui-core": "workspace:*" }, "devDependencies": { "@types/node": "latest" diff --git a/packages/native/src/events.ts b/packages/native/src/events.ts index 3d1ee9b..c681170 100644 --- a/packages/native/src/events.ts +++ b/packages/native/src/events.ts @@ -1,5 +1,5 @@ // docs/native-api.md §4: one bus for host events and business events. -import { internal } from "@tiny-ui/core"; +import { internal } from "tinyui-core"; /** J4: delivered to every page subscribed to `topic`, including native listeners. */ export function emit(topic: string, payload: Record<string, unknown> = {}): void { diff --git a/packages/native/src/host.ts b/packages/native/src/host.ts index 2305c6e..e069a56 100644 --- a/packages/native/src/host.ts +++ b/packages/native/src/host.ts @@ -1,5 +1,5 @@ // docs/native-api.md §7: J3 for capabilities the host App registers under its own names. -import { internal } from "@tiny-ui/core"; +import { internal } from "tinyui-core"; /** Rejects with HostError E_UNSUPPORTED when the host did not register [name]. */ export function call<T = void>(name: string, args: Record<string, unknown> = {}): Promise<T> { diff --git a/packages/native/src/http.ts b/packages/native/src/http.ts index f7f9289..1653f1c 100644 --- a/packages/native/src/http.ts +++ b/packages/native/src/http.ts @@ -1,5 +1,5 @@ // docs/native-api.md §5: J3 `http.request`, errors are HostError with the E3 codes. -import { internal } from "@tiny-ui/core"; +import { internal } from "tinyui-core"; export interface HttpOptions { headers?: Record<string, string>; diff --git a/packages/native/src/index.ts b/packages/native/src/index.ts index 84cc91f..76fed77 100644 --- a/packages/native/src/index.ts +++ b/packages/native/src/index.ts @@ -1,8 +1,8 @@ -export const VERSION = "0.0.0"; +export const VERSION = "0.1.0"; export * as navigation from "./navigation.ts"; export * as store from "./store.ts"; export * as events from "./events.ts"; export * as http from "./http.ts"; export * as host from "./host.ts"; export type { HttpResponse, HttpOptions } from "./http.ts"; -export { HostError } from "@tiny-ui/core"; +export { HostError } from "tinyui-core"; diff --git a/packages/native/src/navigation.ts b/packages/native/src/navigation.ts index 6c6df8b..c6f4ce6 100644 --- a/packages/native/src/navigation.ts +++ b/packages/native/src/navigation.ts @@ -1,5 +1,5 @@ // docs/native-api.md §2; the host implements Navigator (docs/app-model.md). -import { internal } from "@tiny-ui/core"; +import { internal } from "tinyui-core"; export type Params = Record<string, string | number | boolean | null>; diff --git a/packages/native/src/store.ts b/packages/native/src/store.ts index f511a3a..42eb569 100644 --- a/packages/native/src/store.ts +++ b/packages/native/src/store.ts @@ -1,5 +1,5 @@ // docs/native-api.md §3: the truth lives in Kotlin; pages read snapshots and subscribe to changes. -import { internal, signal } from "@tiny-ui/core"; +import { internal, signal } from "tinyui-core"; /** J2: the current value, or undefined. */ export function get<T = unknown>(key: string): T | undefined { diff --git a/packages/native/test/native.test.ts b/packages/native/test/native.test.ts index af3533e..25f9c95 100644 --- a/packages/native/test/native.test.ts +++ b/packages/native/test/native.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { afterEach, describe, it } from "node:test"; import { bridge } from "./host-stub.ts"; -import { Column, h, Text, thunk } from "@tiny-ui/core"; +import { Column, h, Text, thunk } from "tinyui-core"; import { events, host, http, navigation, store } from "../src/index.ts"; type Entries = { mount(page: unknown, props: string, host: string): void; unmount(): void; flush(): void; resolve(id: number, json: string): void; emit(topic: string, json: string): void }; @@ -10,7 +10,7 @@ const HOST = JSON.stringify({ components: {}, capabilities: [] }); const drain = () => new Promise((r) => setTimeout(r, 0)); afterEach(() => tinyui().unmount()); -describe("@tiny-ui/native", () => { +describe("tinyui-native", () => { it("http.get is a J3 http.request that resolves through K3", async () => { const p = http.get<{ ok: boolean }>("/me", { timeout: 100 }); const call = bridge.calls.at(-1)!; diff --git a/packages/native/test/version.test.ts b/packages/native/test/version.test.ts new file mode 100644 index 0000000..73847b1 --- /dev/null +++ b/packages/native/test/version.test.ts @@ -0,0 +1,11 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { describe, it } from "node:test"; +import { VERSION } from "../src/index.ts"; + +describe("VERSION", () => { + it("matches package.json, the version the bytecode ships under", () => { + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); + assert.equal(VERSION, pkg.version); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2be0ea4..89bdf22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,21 +24,21 @@ importers: specifier: 0.150.0 version: 0.150.0 devDependencies: - '@tiny-ui/core': + '@types/node': + specifier: latest + version: 22.20.3 + tinyui-core: specifier: workspace:* version: link:../core - '@tiny-ui/native': + tinyui-native: specifier: workspace:* version: link:../native - '@types/node': - specifier: latest - version: 22.20.3 packages/core: {} packages/native: dependencies: - '@tiny-ui/core': + tinyui-core: specifier: workspace:* version: link:../core devDependencies: @@ -48,20 +48,20 @@ importers: sample/js: dependencies: - '@tiny-ui/core': + tinyui-core: specifier: workspace:* version: link:../../packages/core - '@tiny-ui/native': + tinyui-native: specifier: workspace:* version: link:../../packages/native devDependencies: - '@tiny-ui/cli': + tinyui-cli: specifier: workspace:* version: link:../../packages/cli schema: devDependencies: - '@tiny-ui/cli': + tinyui-cli: specifier: workspace:* version: link:../packages/cli diff --git a/sample/js/package.json b/sample/js/package.json index 5323a0e..5e43bd8 100644 --- a/sample/js/package.json +++ b/sample/js/package.json @@ -6,10 +6,10 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@tiny-ui/core": "workspace:*", - "@tiny-ui/native": "workspace:*" + "tinyui-core": "workspace:*", + "tinyui-native": "workspace:*" }, "devDependencies": { - "@tiny-ui/cli": "workspace:*" + "tinyui-cli": "workspace:*" } } diff --git a/sample/js/src/pages/counter.tsx b/sample/js/src/pages/counter.tsx index 6a5a577..a4a0af1 100644 --- a/sample/js/src/pages/counter.tsx +++ b/sample/js/src/pages/counter.tsx @@ -1,4 +1,4 @@ -import { Button, Column, signal, Text } from "@tiny-ui/core"; +import { Button, Column, signal, Text } from "tinyui-core"; export default function Counter() { const [count, setCount] = signal(0); diff --git a/sample/js/src/pages/todos.tsx b/sample/js/src/pages/todos.tsx index b4371cb..c8e591d 100644 --- a/sample/js/src/pages/todos.tsx +++ b/sample/js/src/pages/todos.tsx @@ -1,5 +1,5 @@ -import { Button, Column, effect, For, LazyColumn, observable, ref, resource, Row, Show, Text, TextField, untrack, type LazyColumnCommands, type TextFieldCommands } from "@tiny-ui/core"; -import { http } from "@tiny-ui/native"; +import { Button, Column, effect, For, LazyColumn, observable, ref, resource, Row, Show, Text, TextField, untrack, type LazyColumnCommands, type TextFieldCommands } from "tinyui-core"; +import { http } from "tinyui-native"; interface Todo { id: number; title: string; done: boolean } interface Page { items: Todo[]; next: number | null } diff --git a/sample/js/tsconfig.json b/sample/js/tsconfig.json index 18b2fa3..cf35637 100644 --- a/sample/js/tsconfig.json +++ b/sample/js/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "noEmit": true, "jsx": "react-jsx", - "jsxImportSource": "@tiny-ui/core" + "jsxImportSource": "tinyui-core" }, "include": ["src"] } diff --git a/sample/shared/src/commonMain/kotlin/wang/harlon/tinyui/sample/App.kt b/sample/shared/src/commonMain/kotlin/wang/harlon/tinyui/sample/App.kt index c99212e..6db69e5 100644 --- a/sample/shared/src/commonMain/kotlin/wang/harlon/tinyui/sample/App.kt +++ b/sample/shared/src/commonMain/kotlin/wang/harlon/tinyui/sample/App.kt @@ -68,17 +68,16 @@ fun App() { val manifest = BuildManifest.parse(Res.readBytes("files/tinyui/manifest.json").decodeToString()) // debug builds ship the maps; without them (-Ptinyui.maps=false) stacks stay as the engine printed them val maps = (manifest.runtime + manifest.pages).mapNotNull { name -> - val file = if (name.startsWith("@tiny-ui/")) "runtime/" + name.removePrefix("@tiny-ui/") else name - runCatching { Res.readBytes("files/tinyui/$file.js.map").decodeToString() }.getOrNull()?.let { name to it } + runCatching { Res.readBytes("files/tinyui/${manifest.file(name)}.js.map").decodeToString() }.getOrNull()?.let { name to it } }.toMap() // stacks on the failure screen only when the maps came along, i.e. the same switch as -Ptinyui.maps TinyUI.debug = maps.isNotEmpty() bundle = Bundle( RuntimeBundle( - core = Res.readBytes("files/tinyui/runtime/core.bin"), - native = Res.readBytes("files/tinyui/runtime/native.bin"), + core = Res.readBytes("files/tinyui/${manifest.file("tinyui-core")}.bin"), + native = Res.readBytes("files/tinyui/${manifest.file("tinyui-native")}.bin"), ), - page = PageModule("pages/todos", Res.readBytes("files/tinyui/pages/todos.bin"), manifest.buildId("pages/todos")), + page = PageModule("pages/todos", Res.readBytes("files/tinyui/${manifest.file("pages/todos")}.bin"), manifest.buildId("pages/todos")), maps = SourceMaps(maps), ) } diff --git a/schema/components/box.ts b/schema/components/box.ts index 29435bc..09c6201 100644 --- a/schema/components/box.ts +++ b/schema/components/box.ts @@ -1,4 +1,4 @@ -import { defineComponent, enumOf } from "@tiny-ui/cli/schema"; +import { defineComponent, enumOf } from "tinyui-cli/schema"; export default defineComponent("Box", { doc: "Children stacked on top of each other.", diff --git a/schema/components/button.ts b/schema/components/button.ts index c87345c..20ea088 100644 --- a/schema/components/button.ts +++ b/schema/components/button.ts @@ -1,4 +1,4 @@ -import { boolean, defineComponent, enumOf, string } from "@tiny-ui/cli/schema"; +import { boolean, defineComponent, enumOf, string } from "tinyui-cli/schema"; export default defineComponent("Button", { doc: "Label is `text`, or the children when given (an indicator, an icon and a label…); children win.", diff --git a/schema/components/column.ts b/schema/components/column.ts index 10cb76f..293c11d 100644 --- a/schema/components/column.ts +++ b/schema/components/column.ts @@ -1,4 +1,4 @@ -import { boolean, defineComponent, dp, enumOf } from "@tiny-ui/cli/schema"; +import { boolean, defineComponent, dp, enumOf } from "tinyui-cli/schema"; export default defineComponent("Column", { doc: "Vertical stack.", diff --git a/schema/components/lazy-column.ts b/schema/components/lazy-column.ts index 4feb8b9..78a9237 100644 --- a/schema/components/lazy-column.ts +++ b/schema/components/lazy-column.ts @@ -1,4 +1,4 @@ -import { defineComponent, dp, field } from "@tiny-ui/cli/schema"; +import { defineComponent, dp, field } from "tinyui-cli/schema"; export default defineComponent("LazyColumn", { doc: "Virtualised vertical list; children are usually a <For>.", diff --git a/schema/components/radio-button.ts b/schema/components/radio-button.ts index 7a0ee95..54fb7a7 100644 --- a/schema/components/radio-button.ts +++ b/schema/components/radio-button.ts @@ -1,4 +1,4 @@ -import { boolean, defineComponent } from "@tiny-ui/cli/schema"; +import { boolean, defineComponent } from "tinyui-cli/schema"; export default defineComponent("RadioButton", { doc: "Selection lives in JS: `onClick` reports the tap, `selected` is a plain prop.", diff --git a/schema/components/row.ts b/schema/components/row.ts index 4e27df2..1691d6d 100644 --- a/schema/components/row.ts +++ b/schema/components/row.ts @@ -1,4 +1,4 @@ -import { boolean, defineComponent, dp, enumOf } from "@tiny-ui/cli/schema"; +import { boolean, defineComponent, dp, enumOf } from "tinyui-cli/schema"; export default defineComponent("Row", { doc: "Horizontal stack.", diff --git a/schema/components/spacer.ts b/schema/components/spacer.ts index c117423..69429cb 100644 --- a/schema/components/spacer.ts +++ b/schema/components/spacer.ts @@ -1,4 +1,4 @@ -import { defineComponent } from "@tiny-ui/cli/schema"; +import { defineComponent } from "tinyui-cli/schema"; export default defineComponent("Spacer", { doc: "Empty space; size it with width / height.", diff --git a/schema/components/text-field.ts b/schema/components/text-field.ts index 6fc3713..367286c 100644 --- a/schema/components/text-field.ts +++ b/schema/components/text-field.ts @@ -1,4 +1,4 @@ -import { boolean, defineComponent, enumOf, field, string } from "@tiny-ui/cli/schema"; +import { boolean, defineComponent, enumOf, field, string } from "tinyui-cli/schema"; export default defineComponent("TextField", { doc: "Text and cursor live on the Kotlin side; JS gets notified and issues commands (docs/adr-004 §3.3).", diff --git a/schema/components/text.ts b/schema/components/text.ts index 3403ee2..3fa0194 100644 --- a/schema/components/text.ts +++ b/schema/components/text.ts @@ -1,4 +1,4 @@ -import { color, defineComponent, enumOf, number, sp, string, TEXT_STYLES } from "@tiny-ui/cli/schema"; +import { color, defineComponent, enumOf, number, sp, string, TEXT_STYLES } from "tinyui-cli/schema"; export default defineComponent("Text", { props: { diff --git a/schema/package.json b/schema/package.json index 4f54875..5e0fedb 100644 --- a/schema/package.json +++ b/schema/package.json @@ -6,6 +6,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "devDependencies": { - "@tiny-ui/cli": "workspace:*" + "tinyui-cli": "workspace:*" } }