Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ name: ios

on:
push:
tags: ["v*"]
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:

jobs:
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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 }}
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 编成字节码打进资源)
Expand All @@ -22,6 +22,13 @@ build-logic/ Gradle convention plugins

目录与命名依据见 [docs/README.md](./docs/README.md)。

## 安装

- Kotlin:`implementation("wang.harlon:tinyui:<version>")`(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
Expand Down
5 changes: 4 additions & 1 deletion compose/api/compose.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init>(kotlin.collections/List<kotlin/String>, kotlin.collections/List<kotlin/String>, kotlin.collections/Map<kotlin/String, kotlin/String>) // wang.harlon.tinyui/BuildManifest.<init>|<init>(kotlin.collections.List<kotlin.String>;kotlin.collections.List<kotlin.String>;kotlin.collections.Map<kotlin.String,kotlin.String>){}[0]
constructor <init>(kotlin.collections/List<kotlin/String>, kotlin.collections/List<kotlin/String>, kotlin.collections/Map<kotlin/String, kotlin/String>, kotlin.collections/Map<kotlin/String, kotlin/String>) // wang.harlon.tinyui/BuildManifest.<init>|<init>(kotlin.collections.List<kotlin.String>;kotlin.collections.List<kotlin.String>;kotlin.collections.Map<kotlin.String,kotlin.String>;kotlin.collections.Map<kotlin.String,kotlin.String>){}[0]

final val buildIds // wang.harlon.tinyui/BuildManifest.buildIds|{}buildIds[0]
final fun <get-buildIds>(): kotlin.collections/Map<kotlin/String, kotlin/String> // wang.harlon.tinyui/BuildManifest.buildIds.<get-buildIds>|<get-buildIds>(){}[0]
final val files // wang.harlon.tinyui/BuildManifest.files|{}files[0]
final fun <get-files>(): kotlin.collections/Map<kotlin/String, kotlin/String> // wang.harlon.tinyui/BuildManifest.files.<get-files>|<get-files>(){}[0]
final val pages // wang.harlon.tinyui/BuildManifest.pages|{}pages[0]
final fun <get-pages>(): kotlin.collections/List<kotlin/String> // wang.harlon.tinyui/BuildManifest.pages.<get-pages>|<get-pages>(){}[0]
final val runtime // wang.harlon.tinyui/BuildManifest.runtime|{}runtime[0]
final fun <get-runtime>(): kotlin.collections/List<kotlin/String> // wang.harlon.tinyui/BuildManifest.runtime.<get-runtime>|<get-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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand All @@ -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<PageError>()
val sink = object : PageSink {
override fun error(error: PageError) { errors += error }
Expand Down
16 changes: 12 additions & 4 deletions compose/src/commonMain/kotlin/wang/harlon/tinyui/BuildManifest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>, val pages: List<String>, val buildIds: Map<String, String>) {
/** `manifest.json` as `tinyui build` writes it: module names, the output path and the build id of each. */
class BuildManifest(
val runtime: List<String>,
val pages: List<String>,
val files: Map<String, String>,
val buildIds: Map<String, String>,
) {
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"))
}
}
}
6 changes: 3 additions & 3 deletions compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>) {
Expand Down
14 changes: 7 additions & 7 deletions compose/src/commonTest/kotlin/wang/harlon/tinyui/PageHostTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>()
private val errors = mutableListOf<PageError>()
Expand All @@ -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,
Expand Down Expand Up @@ -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}""") }
Expand Down Expand Up @@ -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()
Expand All @@ -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() }
Expand Down Expand Up @@ -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 }
Expand Down
Loading
Loading