diff --git a/README.md b/README.md index 1e16b22..acdf38c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Two kinds of `GET`, relative to `https:////`: | Path | Content | Cache | |---|---|---| | `//current.json` | mutable pointer: `{ version, rollout, signature }` | `no-store` | -| `///…` | immutable content: `manifest.json`, `pages/**/*.bin` (the runtime ships inside the App, not in packages) | `immutable` | +| `///…` | immutable content: `manifest.json`, `pages/**/*.jsb` (the runtime ships inside the App, not in packages) | `immutable` | `` is the positive integer a host App declares for what it promises to pages: it goes up when host components or capabilities change, when pages may use a newer TinyUI runtime, or when the JS engine changes. It is not the TinyUI version: upgrading TinyUI alone leaves it as is, and the manifest's `engine` / `tinyui` are checked on their own; it plays the role of Expo's `runtimeVersion` but is always a positive integer that counts host changes, never an App version such as `1.2.0`; see the TinyUI repo's `docs/updates.md`. diff --git a/README.zh-CN.md b/README.zh-CN.md index b5b76e7..0ac97a2 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -13,7 +13,7 @@ | 路径 | 内容 | 缓存 | |---|---|---| | `//current.json` | 可变指针:`{ version, rollout, signature }` | `no-store` | -| `///…` | 不可变内容:`manifest.json`、`pages/**/*.bin`(运行时随 App,不在包里) | `immutable` | +| `///…` | 不可变内容:`manifest.json`、`pages/**/*.jsb`(运行时随 App,不在包里) | `immutable` | `` 是宿主 App 为"它对页面承诺了什么"声明的正整数:宿主组件、宿主能力增删或改动,页面要用更新的 TinyUI 运行时,或者换了 JS 引擎,都加 1。它不是 TinyUI 版本:单纯升级 TinyUI 不动它,manifest 的 `engine` / `tinyui` 另行校验,作用同 Expo 的 `runtimeVersion`,但取值只能是正整数、数的是宿主的变化,不是 `1.2.0` 这类 App 版本号,见 TinyUI 仓的 `docs/updates.md`。 diff --git a/src/ids.ts b/src/ids.ts index 5c53be1..83a2d98 100644 --- a/src/ids.ts +++ b/src/ids.ts @@ -13,7 +13,7 @@ export function isSegment(value: string): boolean { return /^[A-Za-z0-9._-]+$/.test(value) && value !== "." && value !== ".."; } -/** A file path inside a version directory: `pages/home.bin`, `manifest.json`. */ +/** A file path inside a version directory: `pages/home.jsb`, `manifest.json`. */ export function isObjectPath(value: string): boolean { const segments = value.split("/"); return segments.length > 0 && segments.every(isSegment); diff --git a/src/manifest.ts b/src/manifest.ts index fb8e0af..7415547 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -18,7 +18,7 @@ export interface Manifest { /** Every file of the version besides `manifest.json`, with its key into `hashes`. */ export function payload(manifest: Manifest): { path: string; hashKey: string }[] { return [ - ...manifest.pages.map((module) => ({ path: `${manifest.files[module]}.bin`, hashKey: module })), + ...manifest.pages.map((module) => ({ path: `${manifest.files[module]}.jsb`, hashKey: module })), ...Object.values(manifest.i18n?.files ?? {}).map((path) => ({ path, hashKey: path })), ]; } @@ -82,7 +82,7 @@ export function parseManifest(text: string): Manifest { if (keys.join("\n") !== [...expected[key]].sort().join("\n")) throw new Error(`manifest.json ${key} does not cover exactly the modules in pages${key === "hashes" ? " and the i18n files" : ""}`); } for (const path of Object.values(manifest.files)) { - if (!isObjectPath(path + ".bin")) throw new Error(`manifest.json files entry ${path} is not a file path`); + if (!isObjectPath(path + ".jsb")) throw new Error(`manifest.json files entry ${path} is not a file path`); } for (const path of strings) { if (!isObjectPath(path)) throw new Error(`manifest.json i18n file ${path} is not a file path`); diff --git a/test/helpers.ts b/test/helpers.ts index 22cfd26..70a2e3c 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -34,11 +34,11 @@ export async function signedPackage(options: { const version = options.version ?? "20260922T100000Z-abcdef1"; const key = options.key ?? (await newKeyPair()); const strings = Object.fromEntries(Object.entries(options.i18n ?? {}).map(([language, text]) => [`i18n/${language}.json`, text])); - const contents = { ...(options.files ?? { "pages/home.bin": `HOME-${version}` }), ...strings }; + const contents = { ...(options.files ?? { "pages/home.jsb": `HOME-${version}` }), ...strings }; const files = Object.fromEntries(Object.entries(contents).map(([p, text]) => [p, new TextEncoder().encode(text)])); const modules: Record = { [`${name}/home`]: "pages/home" }; const hashes: Record = {}; - for (const [module, path] of Object.entries(modules)) hashes[module] = await sha256Hex(files[`${path}.bin`]!); + for (const [module, path] of Object.entries(modules)) hashes[module] = await sha256Hex(files[`${path}.jsb`]!); for (const path of Object.keys(strings)) hashes[path] = await sha256Hex(files[path]!); const manifest: Record = { pages: [`${name}/home`], diff --git a/test/publish.test.ts b/test/publish.test.ts index f60edd1..26e382d 100644 --- a/test/publish.test.ts +++ b/test/publish.test.ts @@ -22,10 +22,10 @@ describe("publishing and delivery", () => { expect(manifest.status).toBe(200); expect(manifest.headers.get("cache-control")).toBe("public, max-age=31536000, immutable"); expect(new Uint8Array(await manifest.arrayBuffer())).toEqual(p.manifest); - const home = await request(`/${app}/production/${pkg}/1/${p.version}/pages/home.bin`); + const home = await request(`/${app}/production/${pkg}/1/${p.version}/pages/home.jsb`); expect(new TextDecoder().decode(await home.arrayBuffer())).toBe(`HOME-${p.version}`); // the channel is a pointer, not a place: another channel reads the same bytes even before it points there - expect((await request(`/${app}/staging/${pkg}/1/${p.version}/pages/home.bin`)).status).toBe(200); + expect((await request(`/${app}/staging/${pkg}/1/${p.version}/pages/home.jsb`)).status).toBe(200); expect((await request(`/${app}/staging/${pkg}/1/current.json`)).status).toBe(404); }); @@ -39,9 +39,9 @@ describe("publishing and delivery", () => { await request(`/${app}/${pkg}/1/${p.version}/manifest.json`, { method: "PUT", token, body: p.manifest as BodyInit }); const missing = await publishPointer(app, "production", pkg, "1", p, token); expect(missing.status).toBe(409); - expect(await missing.text()).toContain("pages/home.bin is not uploaded yet"); + expect(await missing.text()).toContain("pages/home.jsb is not uploaded yet"); - const tampered = await signedPackage({ files: { "pages/home.bin": "HOME-x" }, key: { publicKey: p.publicKey, privateKey: p.privateKey } }); + const tampered = await signedPackage({ files: { "pages/home.jsb": "HOME-x" }, key: { publicKey: p.publicKey, privateKey: p.privateKey } }); for (const [path, bytes] of Object.entries(tampered.files)) { await request(`/${app}/${pkg}/1/${p.version}/${path}`, { method: "PUT", token, body: bytes as BodyInit }); } @@ -54,7 +54,7 @@ describe("publishing and delivery", () => { it("publishes a package with strings only once they are uploaded and match, and serves them", async () => { const p = await signedPackage({ i18n: { en: '{"hi":"Hi"}', zh: '{"hi":"你好"}' } }); const { app, pkg, token } = await setup({ publicKey: p.publicKey }); - await request(`/${app}/${pkg}/1/${p.version}/pages/home.bin`, { method: "PUT", token, body: p.files["pages/home.bin"] as BodyInit }); + await request(`/${app}/${pkg}/1/${p.version}/pages/home.jsb`, { method: "PUT", token, body: p.files["pages/home.jsb"] as BodyInit }); await request(`/${app}/${pkg}/1/${p.version}/manifest.json`, { method: "PUT", token, body: p.manifest as BodyInit }); const missing = await publishPointer(app, "production", pkg, "1", p, token); expect(missing.status).toBe(409); @@ -88,14 +88,14 @@ describe("publishing and delivery", () => { it("keeps a version immutable: the same bytes again are fine, different ones are refused", async () => { const p = await signedPackage(); const { app, pkg, token } = await setup({ publicKey: p.publicKey }); - const first = await request(`/${app}/${pkg}/1/${p.version}/pages/home.bin`, { method: "PUT", token, body: p.files["pages/home.bin"] as BodyInit }); + const first = await request(`/${app}/${pkg}/1/${p.version}/pages/home.jsb`, { method: "PUT", token, body: p.files["pages/home.jsb"] as BodyInit }); expect(first.status).toBe(201); - const again = await request(`/${app}/${pkg}/1/${p.version}/pages/home.bin`, { method: "PUT", token, body: p.files["pages/home.bin"] as BodyInit }); + const again = await request(`/${app}/${pkg}/1/${p.version}/pages/home.jsb`, { method: "PUT", token, body: p.files["pages/home.jsb"] as BodyInit }); expect(again.status).toBe(200); expect(await again.json()).toMatchObject({ existing: true }); - const other = await request(`/${app}/${pkg}/1/${p.version}/pages/home.bin`, { method: "PUT", token, body: "something else" }); + const other = await request(`/${app}/${pkg}/1/${p.version}/pages/home.jsb`, { method: "PUT", token, body: "something else" }); expect(other.status).toBe(409); - const tooBig = await request(`/${app}/${pkg}/1/${p.version}/pages/big.bin`, { method: "PUT", token, body: new Uint8Array(1048577) as BodyInit }); + const tooBig = await request(`/${app}/${pkg}/1/${p.version}/pages/big.jsb`, { method: "PUT", token, body: new Uint8Array(1048577) as BodyInit }); expect(tooBig.status).toBe(413); }); @@ -246,7 +246,7 @@ describe("publishing and delivery", () => { "", ].join("\n"); const { app, pkg, token } = await setup({ publicKey }); - for (const [path, text] of Object.entries({ "pages/home.bin": "HOME2" })) { + for (const [path, text] of Object.entries({ "pages/home.jsb": "HOME2" })) { expect((await request(`/${app}/${pkg}/1/${version}/${path}`, { method: "PUT", token, body: text })).status).toBe(201); } expect((await request(`/${app}/${pkg}/1/${version}/manifest.json`, { method: "PUT", token, body: manifest })).status).toBe(201); @@ -263,7 +263,7 @@ describe("publishing and delivery", () => { const { app, pkg, token } = await setup({ publicKey: v1.publicKey }); // two different bodies race for one path: exactly one wins, the other is told so - const race = await Promise.all(["one", "two"].map((body) => request(`/${app}/${pkg}/1/v1/pages/race.bin`, { method: "PUT", token, body }))); + const race = await Promise.all(["one", "two"].map((body) => request(`/${app}/${pkg}/1/v1/pages/race.jsb`, { method: "PUT", token, body }))); expect(race.map((r) => r.status).sort()).toEqual([201, 409]); await uploadContent(app, pkg, "1", v1, token); @@ -280,7 +280,7 @@ describe("publishing and delivery", () => { const p = await signedPackage(); const { app, pkg, token } = await setup({ publicKey: p.publicKey }); expect((await request(`/${app}/${pkg}/1/../manifest.json`, { method: "PUT", token, body: "x" })).status).not.toBe(201); - expect((await request(`/${app}/${pkg}/1/v1/..%2Fescape.bin`, { method: "PUT", token, body: "x" })).status).toBe(404); + expect((await request(`/${app}/${pkg}/1/v1/..%2Fescape.jsb`, { method: "PUT", token, body: "x" })).status).toBe(404); expect((await request(`/${app}/Prod/${pkg}/1/current.json`)).status).toBe(404); // `apps` is reserved: never served as an app expect((await request(`/apps/production/${pkg}/1/current.json`)).status).toBe(404);