clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat(starter): enforce the author boundary everywhere, fix vendoring drift

Dami committed 5d8d2ea
An exemplar pass over the starter, closing review findings CONV-1 and
CONV-2 and a latent build break.

Author boundary (CONV-1/2):
- @nzip/lofi exports a RowOf<Table> type helper, so use-tasks.ts derives
  its row type from the declared schema without importing jazz-tools.
- AccountGate drops its navigator.clipboard copy button - the phrase block
  already instructs writing the words down, and author UI stays free of
  browser branching.
- The author-boundary test now scans EVERY author UI file (index.astro,
  Shell.astro, AccountGate.tsx, TaskList.tsx, use-tasks.ts) with the full
  forbidden-pattern set, and scans schema.ts/permissions.ts - the two
  deliberate raw-Jazz surfaces - with every rule except the raw-Jazz one.
  Previously the loop deliberately covered only index.astro and
  TaskList.tsx, which is how the violations shipped.

Vendoring drift (found while building the starter):
- package/astro/mod.ts's hand-maintained .lofi manifests were missing
  namespace-state.ts (#83) and transport-gate.ts (#86), breaking
  `deno task build`/`dev` for every generated project since those merged;
  `deno task check` never runs the Astro build, so CI missed it. The
  manifest gains both files plus a contract test that fails whenever the
  lists and the package directories drift again.

Exemplar touches:
- schema.ts drops the redundant explicit App annotation in favor of
  inference and documents the raw-Jazz-surface split.
- AccountGate's run() helper types its actions as Promise<Session | void>
  instead of duck-typing the result.
- Shell.astro explains the side-effect app.ts import; index.astro hero
  copy re-indented.
- docs/data-and-ui.md aligned with the inferred defineApp shape and now
  documents RowOf and the boundary split.

Reference build verified end to end (deno task build: 1 route, secret
scan passed). Starter snapshot regenerated.
commit
5d8d2eae4c7fad11d612860fa8ce911782ccd222
parent
efa9e7b5032335ca0c6574ba2c97c2b842ac1411
author
Dami <dami.etoile@gmail.com>
12 files changed, +126 -34
Mapps/reference/src/islands/AccountGate.tsx15++------
Mapps/reference/src/islands/use-tasks.ts6++--
Mapps/reference/src/layouts/Shell.astro1+
Mapps/reference/src/pages/index.astro3+-
Mapps/reference/src/schema.ts6++-
Mapps/reference/tests/author-boundary_test.ts28+++++++++++++---
Mdocs/data-and-ui.md8+++-
Mpackage/astro/mod.ts18++++++++--
Apackage/astro/vendored_manifest_test.ts55++++++++++++++++++++++++++++++
Mpackage/runtime/mod.ts1+
Mpackage/runtime/table-store.ts7++++
Mpackage/testdata/starter.snapshot.json12+++---
M apps/reference/src/islands/AccountGate.tsx +4 -11
M apps/reference/src/islands/use-tasks.ts +3 -3
M apps/reference/src/layouts/Shell.astro +1 -0
M apps/reference/src/pages/index.astro +2 -1
M apps/reference/src/schema.ts +4 -2
M apps/reference/tests/author-boundary_test.ts +23 -5
@@ -2,18 +2,27 @@ const test = (globalThis as unknown as {
22 Deno: { test(name: string, body: () => void | Promise<void>): void };33 }).Deno.test;44 5+// Every author UI file is scanned with the full rule set — no exceptions, so6+// a violation cannot hide in a file the loop forgot to list.57 const authorUiFiles = [68 new URL("../src/pages/index.astro", import.meta.url),9+ new URL("../src/layouts/Shell.astro", import.meta.url),10+ new URL("../src/islands/AccountGate.tsx", import.meta.url),711 new URL("../src/islands/TaskList.tsx", import.meta.url),12+ new URL("../src/islands/use-tasks.ts", import.meta.url),13+];14+15+// schema.ts and permissions.ts are the starter's two deliberate raw-Jazz16+// surfaces; every other rule still applies to them.17+const schemaSurfaceFiles = [18+ new URL("../src/schema.ts", import.meta.url),19+ new URL("../src/permissions.ts", import.meta.url),820 ];921 1022 const packageConsumers = [1123 new URL("../src/app.ts", import.meta.url),12- new URL("../src/pages/index.astro", import.meta.url),13- new URL("../src/layouts/Shell.astro", import.meta.url),14- new URL("../src/islands/AccountGate.tsx", import.meta.url),15- new URL("../src/islands/TaskList.tsx", import.meta.url),16- new URL("../src/islands/use-tasks.ts", import.meta.url),24+ ...authorUiFiles,25+ ...schemaSurfaceFiles,1726 ];1827 1928 const forbidden = [
@@ -23,6 +32,7 @@ const forbidden = [
2332 { pattern: /workbox/i, name: "Workbox configuration" },2433 { pattern: /navigator\.|globalThis\.(?:isSecureContext|SharedWorker)/, name: "browser branch" },2534 ];35+const forbiddenOnSchemaSurface = forbidden.filter((rule) => rule.name !== "raw Jazz import");2636 2737 test("author source consumes public lofi package seams and hides plumbing from product UI", async () => {2838 for (
@@ -53,4 +63,12 @@ test("author source consumes public lofi package seams and hides plumbing from p
5363 }5464 }5565 }66+ for (const file of schemaSurfaceFiles) {67+ const source = await Deno.readTextFile(file);68+ for (const rule of forbiddenOnSchemaSurface) {69+ if (rule.pattern.test(source)) {70+ throw new Error(`${file.pathname} exposes forbidden ${rule.name}`);71+ }72+ }73+ }5674 });
M docs/data-and-ui.md +6 -2
M package/astro/mod.ts +14 -4
@@ -17,7 +17,12 @@ export type LofiAstroOptions = {
1717 schemaDir?: string;1818 };1919 20-const runtimeFiles = [20+/**21+ * Runtime modules vendored into a project's `.lofi/` directory. Static because22+ * the published package cannot list directories over JSR; the manifest test23+ * fails when this list and `package/runtime/` drift apart.24+ */25+export const runtimeFiles = [2126 "app.ts",2227 "auth.ts",2328 "boot.ts",
@@ -30,6 +35,7 @@ const runtimeFiles = [
3035 "lifecycle.ts",3136 "live-query-store.ts",3237 "mod.ts",38+ "namespace-state.ts",3339 "passkey-recovery.ts",3440 "probe.ts",3541 "pwa.ts",
@@ -40,10 +46,12 @@ const runtimeFiles = [
4046 "startup-recovery.ts",4147 "table-mutations.ts",4248 "table-store.ts",49+ "transport-gate.ts",4350 "ui-mutation.ts",4451 ] as const;4552 46-const accessFiles = [53+/** Access modules vendored into `.lofi/`; kept in lockstep by the manifest test. */54+export const accessFiles = [4755 "errors.ts",4856 "identity.ts",4957 "mod.ts",
@@ -52,7 +60,8 @@ const accessFiles = [
5260 "schema.ts",5361 ] as const;5462 55-const preactFiles = [63+/** Preact modules vendored into `.lofi/`; kept in lockstep by the manifest test. */64+export const preactFiles = [5665 "DeviceStatus.tsx",5766 "live-data.ts",5867 "mod.ts",
@@ -61,7 +70,8 @@ const preactFiles = [
6170 "use-device-capabilities.ts",6271 ] as const;6372 64-const recipeFiles = [73+/** Recipe modules vendored into `.lofi/`; kept in lockstep by the manifest test. */74+export const recipeFiles = [6575 "file-handler.ts",6676 "launch-handler.ts",6777 "protocol-handler.ts",
A package/astro/vendored_manifest_test.ts +55 -0
@@ -0,0 +1,55 @@
1+// Repo contract test: the static `.lofi/` vendoring manifests must track the2+// actual package directories. The lists are static because the published3+// package cannot enumerate directories over JSR — this test is what fails4+// when a new runtime module lands without being added to the manifest5+// (namespace-state.ts and transport-gate.ts shipped exactly that way and6+// broke every generated project's build until vendoring caught up).7+import { accessFiles, preactFiles, recipeFiles, runtimeFiles } from "./mod.ts";8+9+function assert(condition: unknown, message: string): asserts condition {10+ if (!condition) throw new Error(message);11+}12+13+// Files that live in the source directories but are deliberately not vendored.14+const notVendored = new Set([15+ "sw.js", // copied by the build command with its revision stamped in16+ "test-assert.ts", // package test helper17+]);18+19+async function moduleEntries(directory: string): Promise<string[]> {20+ const names: string[] = [];21+ for await (const entry of Deno.readDir(new URL(`../${directory}`, import.meta.url))) {22+ if (!entry.isFile) continue;23+ if (entry.name.endsWith("_test.ts") || entry.name.endsWith("_test.tsx")) continue;24+ if (notVendored.has(entry.name)) continue;25+ if (!/\.(ts|tsx|d\.ts)$/.test(entry.name)) continue;26+ names.push(entry.name);27+ }28+ return names.sort();29+}30+31+const manifests: ReadonlyArray<[string, readonly string[]]> = [32+ ["runtime", runtimeFiles],33+ ["access", accessFiles],34+ ["preact", preactFiles],35+ ["recipes", recipeFiles],36+];37+38+Deno.test("the .lofi vendoring manifests match the package directories", async () => {39+ for (const [directory, manifest] of manifests) {40+ const actual = await moduleEntries(directory);41+ const listed = [...manifest].sort();42+ const missing = actual.filter((name) => !listed.includes(name));43+ const stale = listed.filter((name) => !actual.includes(name));44+ assert(45+ missing.length === 0,46+ `package/${directory} contains modules missing from the vendoring manifest: ${47+ missing.join(", ")48+ } — generated projects cannot build without them`,49+ );50+ assert(51+ stale.length === 0,52+ `the vendoring manifest lists modules absent from package/${directory}: ${stale.join(", ")}`,53+ );54+ }55+});
M package/runtime/mod.ts +1 -0
M package/runtime/table-store.ts +7 -0
M package/testdata/starter.snapshot.json +6 -6
@@ -14,16 +14,16 @@
1414 "README.md": "10d47c681ae44a3d70b790e3952e0ac361e0eeec4a433f0ff35cfe9db61a7778",1515 "src/app.ts": "0f508004dfb12456d65f3ee2231199b932fe239bef6977405fb93135972c0f33",1616 "src/env.d.ts": "b44daed05ec5cdfacfd8d8acf7866974b5f6b8db923ab53bd244419093c719da",17- "src/islands/AccountGate.tsx": "9375e8393fadeb621244cccd14234ede017fbf1f2014f28cbddf55ea412a9094",17+ "src/islands/AccountGate.tsx": "31e74120f882dc9c1ff44e7ff41c0aa6e816ad8a4e9f937e611a3c19fb69c5f0",1818 "src/islands/TaskList.tsx": "cf559f8529e3836730abcdafab8184032c8ac662a1e794e20f0879161bee37be",19- "src/islands/use-tasks.ts": "6a4ef635bda5fafb57965fe680dd4d87b46fa487f3eb2db6bb6bd39c4b6c266b",20- "src/layouts/Shell.astro": "12144355def66c7bf6ed5b0d611ba3a5454cf7499b667936aa6c04494807653a",21- "src/pages/index.astro": "de9ad86f9b25f7aff1e293931a785dbe393e4d0a35464ffbb649a0ebb4518281",19+ "src/islands/use-tasks.ts": "22400693c9b7197adb8af858b0af13456459823229908c87e3af4fd4f6336554",20+ "src/layouts/Shell.astro": "57f5c144813fad7d6c7223b0e68498665f41dcd8b0855f2e8bf020d3bb072711",21+ "src/pages/index.astro": "884afb177743ccef0d996b8f60158f948b8bf38c966281f29564a9b4e767fa4c",2222 "src/permissions.ts": "dcb00b7f0576d04ad527b8892b8096573b107c3130ed6d835243251c406a38b7",23- "src/schema.ts": "f2c555b088ded8eea2653f5d955fc9aae69dfd588e891adf509e99052f1e9949",23+ "src/schema.ts": "9f88050f6ece01d28242a6d93ed32d32c1c5a71cff35e0dc421ab3715d591748",2424 "src/styles/global.css": "da8594026ea8420d00c58549b9b38040b4456f75615959ca2e94997b18be1997",2525 "tests/auth_e2e_test.ts": "87dc3eb441eeaa96fc82b15df90bf4108096506217e5bfae907b483a3e75b4bf",26- "tests/author-boundary_test.ts": "303691da91fd4d13ed2dca195b7745c5c106bcd0837338e632469dfec032d71b",26+ "tests/author-boundary_test.ts": "9fdb6d94180cef044b1d2e74601a01d8f0b26253700bb494bbb45d4127dc728a",2727 "tests/convergence_e2e_test.ts": "71ec0ac935c25ff0722905e5b195c730f32a3b886dd4172e11d9e0e80f19cb38",2828 "tests/testing-contract_test.ts": "4257306060d42566b879d92b3a6c395805b20f53717bee55d670c0ef7a117eec",2929 "tsconfig.json": "c632473513c623f3cc69d7476159fb9421624521220029755695defa265c6cab"