Move storage, sync, identity, PWA, and diagnostics into @nzip/lofi exports; shrink generated apps to author-owned source and package-backed integration.
- commit
- e9f0f6da511728592378aa8dccf43f3af8e3424b
- parent
- 110e0af90b9c9f607f7d95fcc29edc0b65126e59
- author
- FelineStateMachine <dami.etoile@gmail.com>
@@ -83,23 +83,26 @@ Keep these current constraints in mind:
8383 8484 ## Project anatomy8585 86-A generated project separates the files you own from the framework runtime:86+A generated project keeps application source separate from the versioned framework package:8787 8888 ```mermaid8989 flowchart TB90- Root["my-app/"] --> Config["deno.json + astro.config.ts"]91- Root --> Public["public/<br/>manifest · service worker · icons"]90+ Root["my-app/"] --> Config["deno.json"]91+ Root --> Public["public/<br/>manifest · icons"]9292 Root --> Src["src/"]9393 Root --> Tests["tests/<br/>your tests + lofi testing contract"]94+ Root --> Framework["@nzip/lofi<br/>versioned framework package"]9495 9596 Src --> Model["schema.ts + permissions.ts<br/>your model and access policy"]9697 Src --> App["app.ts<br/>your application config"]9798 Src --> UI["pages + islands + styles<br/>your product experience"]98- Src --> Runtime["_lofi/<br/>generated runtime—do not edit"]99+ Src --> Layout["layouts/<br/>your document shell"]100+ Framework --> Runtime["storage · identity · sync<br/>PWA · lifecycle · diagnostics"]99101 ```100102 101-Most product work stays in `schema.ts`, `permissions.ts`, `app.ts`, `pages/`, `islands/`, and102-`styles/`. Everything under `src/_lofi/` is generated runtime code.103+Product work stays in `schema.ts`, `permissions.ts`, `app.ts`, `pages/`, `layouts/`, `islands/`, and104+`styles/`. Framework behavior is imported from `@nzip/lofi`; upgrading the package updates runtime105+code without copying it into your source tree.103106 104107 ## Testing local-first behavior105108 @@ -1,8 +1,7 @@
11 # lofi reference application22 3-This application proves the author-facing lofi workflow while the public `@nzip/lofi` package is4-graduated in layers. M2 extracts the generated-project commands; broader framework API extraction5-remains M4 work. The application is deliberately split into two ownership zones.3+This application proves the author-facing lofi workflow against the public `@nzip/lofi` runtime.4+Framework implementation stays package-owned while the application retains its product source.65 76 ## Author-owned files87 @@ -14,15 +13,16 @@ remains M4 work. The application is deliberately split into two ownership zones.
1413 - `src/styles/` — product styling1514 - `tests/` — product behavior and author-boundary tests1615 17-## Generated and runtime-owned files16+## Package-owned framework1817 19-- `astro.config.ts`, `tsconfig.json`, and `src/env.d.ts` — compatible toolchain wiring20-- `public/manifest.webmanifest`, `public/sw.js`, and `public/favicon.svg` — PWA plumbing21-- `src/_lofi/` — storage, identity, synchronization, lifecycle, diagnostics, and framework adapter18+- `@nzip/lofi` — storage, identity, synchronization, lifecycle, diagnostics, and framework adapter19+- `@nzip/lofi/preact` — optional framework UI such as `DeviceStatus`20+- `.lofi/` — ignored Astro/Jazz integration materialized by framework commands21+- `dist/sw.js` — service worker generated by `deno task build`2222 23-Application work should not require edits in `src/_lofi/`. The M2 generator copies these validated24-runtime-owned files without turning them into public framework APIs. M4 may replace the local seam25-with proven `@nzip/lofi` subpath imports without changing product UI.23+Application work imports supported package APIs and does not carry framework implementation files.24+`public/manifest.webmanifest`, `public/favicon.svg`, `src/layouts/`, and `src/env.d.ts` remain25+application-owned integration and branding surfaces.2626 2727 Browser journeys import readiness, offline, two-client, and sanitized failure-artifact helpers from2828 `@nzip/lofi/testing`. `tests/convergence_e2e_test.ts` is a worked example: two clients add a task@@ -1,51 +0,0 @@
1-import { fileURLToPath } from "node:url";2-import preact from "@astrojs/preact";3-import { defineConfig } from "astro/config";4-import { jazzPlugin } from "jazz-tools/dev/vite";5-6-const workspaceRoot = fileURLToPath(new URL("../../", import.meta.url));7-const schemaDir = fileURLToPath(new URL("./src", import.meta.url));8-const cloudAppId = process.env.JAZZ_APP_ID?.trim();9-const cloudServerUrl = process.env.JAZZ_SERVER_URL?.trim();10-const managedJazzServer = process.env.LOFI_SKIP_JAZZ_MANAGED === "1"11- ? false12- : { allowLocalFirstAuth: true };13-14-if (Boolean(cloudAppId) !== Boolean(cloudServerUrl)) {15- throw new Error(16- "Cloud mode requires both JAZZ_APP_ID and JAZZ_SERVER_URL; set the missing name or remove the partial pair.",17- );18-}19-20-// Jazz's managed Vite plugin reads the VITE_ pair when selecting the server21-// that receives schema, migration, and permission publication. The reference22-// runtime still receives explicit build constants below, so only this public23-// pair enters Vite's client-visible namespace and server secrets remain private.24-if (cloudAppId && cloudServerUrl) {25- process.env.VITE_JAZZ_APP_ID = cloudAppId;26- process.env.VITE_JAZZ_SERVER_URL = cloudServerUrl;27-}28-29-export default defineConfig({30- output: "static",31- integrations: [preact({ compat: true, devtools: true })],32- vite: {33- plugins: [34- jazzPlugin({35- schemaDir,36- inspector: false,37- server: managedJazzServer,38- }),39- ],40- server: {41- fs: { allow: [workspaceRoot] },42- },43- define: {44- __LOFI_JAZZ_APP_ID__: JSON.stringify(cloudAppId ?? ""),45- __LOFI_JAZZ_SERVER_URL__: JSON.stringify(cloudServerUrl ?? ""),46- },47- resolve: {48- noExternal: ["@astrojs/preact"],49- },50- },51-});@@ -2,11 +2,14 @@
22 "imports": {33 "@astrojs/check": "npm:@astrojs/check@0.9.9",44 "@astrojs/preact": "npm:@astrojs/preact@6.0.1",5+ "@nzip/lofi": "jsr:@nzip/lofi@0.2.0",56 "@nzip/lofi/": "jsr:@nzip/lofi@0.2.0/",7+ "@nzip/lofi/astro": "jsr:@nzip/lofi@0.2.0/astro",68 "@nzip/lofi/build": "jsr:@nzip/lofi@0.2.0/build",79 "@nzip/lofi/dev": "jsr:@nzip/lofi@0.2.0/dev",810 "@nzip/lofi/doctor": "jsr:@nzip/lofi@0.2.0/doctor",911 "@nzip/lofi/preview": "jsr:@nzip/lofi@0.2.0/preview",12+ "@nzip/lofi/preact": "jsr:@nzip/lofi@0.2.0/preact",1013 "@nzip/lofi/provision": "jsr:@nzip/lofi@0.2.0/provision",1114 "@nzip/lofi/test": "jsr:@nzip/lofi@0.2.0/test",1215 "@nzip/lofi/testing": "jsr:@nzip/lofi@0.2.0/testing",@@ -3101,7 +3101,7 @@
31013101 },31023102 "workspace": {31033103 "dependencies": [3104- "jsr:@nzip/lofi@0.1.1",3104+ "jsr:@nzip/lofi@0.2.0",31053105 "npm:@astrojs/check@0.9.9",31063106 "npm:@astrojs/preact@6.0.1",31073107 "npm:astro@7.0.9",@@ -3111,7 +3111,7 @@
31113111 "npm:vite@8.0.1"31123112 ],31133113 "links": {3114- "jsr:@nzip/lofi@0.1.1": {3114+ "jsr:@nzip/lofi@0.2.0": {31153115 "dependencies": [31163116 "npm:@astrojs/check@0.9.9",31173117 "npm:@astrojs/preact@6.0.1",@@ -1,7 +1,8 @@
1+import { defineLofiApp } from "@nzip/lofi";12 import { app as schema } from "./schema.ts";23 3-/** Author-owned composition. Vendor URLs and browser drivers remain in generated runtime code. */4-export const referenceApp = {4+/** Author-owned composition. Vendor URLs and browser drivers remain in package runtime code. */5+export const app = defineLofiApp({56 name: "lofi-prototype",67 databaseName: "lofi-prototype",78 schema,@@ -13,11 +14,11 @@ export const referenceApp = {
1314 // `docs/sync-and-recovery.md` guide.1415 //1516 // Hostnames you have committed to keeping stable, for the optional device-credential16- // primitive in `src/_lofi/auth.ts` (WebAuthn/PRF at-rest encryption). A credential17+ // primitive in `@nzip/lofi` (WebAuthn/PRF at-rest encryption). A credential1718 // binds to its origin; left empty, the served origin is trusted. Pin your permanent1819 // hostname(s) here before relying on a credential across a host change.1920 credentialOrigins: [] as readonly string[],2021 sync: { adapter: "jazz" as const },2122 // Source/home link shown in the starter footer. Point it at your own repo.2223 repositoryUrl: "https://github.com/FelineStateMachine/lofi",23-};24+});@@ -10,14 +10,14 @@ import {
1010 revealRecoveryPhrase,1111 type Session,1212 stopSyncBackup,13-} from "../_lofi/session.ts";13+} from "@nzip/lofi";1414 1515 /**1616 * Author-owned account example. lofi is local-first: the app already opened on a1717 * device-local account with no sign-in. This island offers the *opt-in* upgrade —1818 * back up and sync the account, and recover it elsewhere — using only the19- * primitives in `src/_lofi/session.ts`. Delete it if your app is local-only, or20- * restyle it freely; nothing under `src/_lofi/` needs to change.19+ * public `@nzip/lofi` session primitives. Delete it if your app is local-only,20+ * or restyle it freely; framework implementation remains package-owned.2121 *2222 * It renders only when a managed Jazz app is configured (`JAZZ_APP_ID` /2323 * `JAZZ_SERVER_URL`, e.g. via `deno task jazz:provision`), because a recovery@@ -1,11 +1,11 @@
11 import { useState } from "preact/hooks";2-import { settleUiMutation } from "../_lofi/ui-mutation.ts";2+import { settleUiMutation } from "@nzip/lofi";33 import { useTasks } from "./use-tasks.ts";44 55 /**66 * Author-owned starter UI. One island over one table: add a row, list rows, and7- * toggle a boolean. Replace it with your own schema and components — the runtime8- * under `src/_lofi/` stays the same.7+ * toggle a boolean. Replace it with your own schema and components — the8+ * package-owned runtime stays the same.99 */1010 export default function TaskList() {1111 const { status, error, durability, tasks, create, setCompleted } = useTasks();@@ -1,16 +1,15 @@
11 import { useCallback, useEffect, useState } from "preact/hooks";22 import { schema as s } from "jazz-tools";3-import { referenceApp } from "../app.ts";4-import { getRuntime, runtimeRecreatedEvent } from "../_lofi/runtime.ts";5-import type { TableSnapshot } from "../_lofi/table-store.ts";3+import { getRuntime, runtimeRecreatedEvent, type TableSnapshot } from "@nzip/lofi";4+import { app } from "../app.ts";65 76 /**87 * Author-owned example. This is the whole binding between a declared table and98 * the UI: pick a table from your schema, derive its row type, and wrap the109 * generic store's writes in domain verbs. Replace `tasks` and the methods below11- * with your own model — nothing under `src/_lofi/` needs to change.10+ * with your own model — framework runtime code remains in `@nzip/lofi`.1211 */13-const tasksTable = referenceApp.schema.tasks;12+const tasksTable = app.schema.tasks;1413 1514 /** Row and insert types come straight from the declared schema. */1615 export type Task = s.RowOf<typeof tasksTable>;@@ -1,4 +1,5 @@
11 ---2+// Author-owned application shell.23 export interface Props {34 title: string;45 }@@ -19,7 +20,10 @@ const { title } = Astro.props;
1920 <body>2021 <main><slot /></main>2122 <script>22- import "./boot.ts";23+ import { bootLofi } from "@nzip/lofi";24+ import { app } from "../app.ts";25+ void app;26+ void bootLofi();2327 </script>2428 </body>2529 </html>@@ -1,9 +1,9 @@
11 ---22 import AccountGate from "../islands/AccountGate";33 import TaskList from "../islands/TaskList";4-import DeviceStatus from "../_lofi/DeviceStatus";5-import Shell from "../_lofi/Shell.astro";6-import { referenceApp } from "../app.ts";4+import { DeviceStatus } from "@nzip/lofi/preact";5+import Shell from "../layouts/Shell.astro";6+import { app } from "../app.ts";77 import "../styles/global.css";88 ---99 @@ -20,6 +20,6 @@ import "../styles/global.css";
2020 <TaskList client:load />2121 <DeviceStatus client:load />2222 <footer>23- <a href={referenceApp.repositoryUrl} rel="noreferrer">lofi on GitHub →</a>23+ <a href={app.repositoryUrl} rel="noreferrer">lofi on GitHub →</a>2424 </footer>2525 </Shell>@@ -7,19 +7,36 @@ const authorUiFiles = [
77 new URL("../src/islands/TaskList.tsx", import.meta.url),88 ];99 10+const packageConsumers = [11+ 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),17+];18+1019 const forbidden = [1120 { pattern: /jazz-tools/, name: "raw Jazz import" },1221 { pattern: /(?:Shared|Service|Web)?Worker\s*\(/, name: "worker construction" },1322 { pattern: /(?:wss?|https?):\/\//, name: "transport URL" },1423 { pattern: /workbox/i, name: "Workbox configuration" },1524 { pattern: /navigator\.|globalThis\.(?:isSecureContext|SharedWorker)/, name: "browser branch" },16- {17- pattern: /_lofi\/(?:boot|config|device-capabilities|probe|pwa|runtime)/,18- name: "runtime plumbing import",19- },2025 ];2126 22-test("author UI stays behind the lofi checklist boundary", async () => {27+test("author source consumes public lofi package seams and hides plumbing from product UI", async () => {28+ try {29+ await Deno.stat(new URL("../src/_lofi", import.meta.url));30+ throw new Error("generated project still contains a vendored src/_lofi runtime");31+ } catch (error) {32+ if (!(error instanceof Deno.errors.NotFound)) throw error;33+ }34+ for (const file of packageConsumers) {35+ const source = await Deno.readTextFile(file);36+ if (/_lofi\//.test(source)) {37+ throw new Error(`${file.pathname} imports vendored framework code`);38+ }39+ }2340 for (const file of authorUiFiles) {2441 const source = await Deno.readTextFile(file);2542 for (const rule of forbidden) {@@ -4,11 +4,14 @@
44 "license": "MIT",55 "description": "A local-first mobile PWA meta-framework for Deno: declare a schema, build Preact islands, and ship an installable offline-capable app whose UI always hydrates from local data.",66 "exports": {7+ ".": "./package/runtime/mod.ts",8+ "./astro": "./package/astro/mod.ts",79 "./build": "./package/commands/build.ts",810 "./create": "./package/create.ts",911 "./dev": "./package/commands/dev.ts",1012 "./doctor": "./package/commands/doctor.ts",1113 "./preview": "./package/commands/preview.ts",14+ "./preact": "./package/preact/mod.ts",1215 "./provision": "./package/commands/provision.ts",1316 "./testing": "./package/testing/mod.ts",1417 "./test": "./package/commands/run_tests.ts"@@ -38,7 +41,7 @@
3841 },3942 "tasks": {4043 "check": "deno fmt --check . && deno lint && deno task test:unit",41- "test:unit": "deno test --allow-env=LOFI_UPDATE_SNAPSHOT,CI --allow-net=127.0.0.1 --allow-read --allow-write=. --allow-run tools package apps/reference/src/_lofi apps/reference/tests",44+ "test:unit": "deno test --allow-env=LOFI_UPDATE_SNAPSHOT,CI --allow-net=127.0.0.1 --allow-read --allow-write=. --allow-run tools package apps/reference/tests",4245 "test:update:create": "LOFI_UPDATE_SNAPSHOT=1 deno test --allow-env=LOFI_UPDATE_SNAPSHOT --allow-read --allow-write=. --allow-run package/create_core_test.ts",4346 "test": "deno task test:unit",4447 "test:browser:install": "deno run -A npm:playwright@1.61.1 install chromium",@@ -69,7 +72,6 @@
6972 "docs/sync-and-recovery.md",7073 "docs/testing.md",7174 "docs/troubleshooting.md",72- "apps/reference/astro.config.ts",7375 "apps/reference/deno.json",7476 "apps/reference/public",7577 "apps/reference/src",@@ -42,7 +42,7 @@ The root [README](../README.md) provides the shortest product overview and comma
4242 4343 ## The author boundary4444 45-Generated projects intentionally divide application code from framework runtime code:45+Generated projects intentionally divide application source from versioned framework code:4646 4747 ```mermaid4848 flowchart TB@@ -56,7 +56,7 @@ flowchart TB
5656 end5757 5858 subgraph Framework["lofi owns"]59- Runtime["src/_lofi"]59+ Runtime["@nzip/lofi package"]6060 Storage["storage and subscriptions"]6161 Identity["identity and recovery"]6262 Lifecycle["sync, PWA, lifecycle"]@@ -70,9 +70,10 @@ flowchart TB
7070 7171 - Change `src/schema.ts`, `src/permissions.ts`, `src/app.ts`, `src/pages/`, `src/islands/`, and7272 `src/styles/`.73-- Treat `src/_lofi/` as generated runtime code. Application hooks may import its documented seams in74- the same way as the starter, but ordinary product work should not edit those files.75-- Change files under `public/` when customizing the icon, manifest, or service worker assets.73+- Import documented runtime seams from `@nzip/lofi`; no framework implementation is copied into74+ `src/`.75+- Change files under `public/` when customizing the icon or manifest. The package build generates76+ the service worker.7677 - Keep application tests under `tests/`.7778 7879 ## Framework contributor material@@ -65,7 +65,7 @@ is not handed out without confirming the person is present. Stated honestly: thi
6565 **confirmation**, not encryption. The account secret still lives in device storage so the app can6666 open instantly; the passkey does not encrypt it. A device where WebAuthn is unavailable can still6767 back up, and the UI says the reveal was unconfirmed. (For real at-rest encryption of the secret, the68-PRF primitive in `src/_lofi/auth.ts` is available — see below.)68+PRF primitive exported by `@nzip/lofi` is available — see below.)6969 7070 ### Boot flow7171 @@ -77,7 +77,7 @@ PRF primitive in `src/_lofi/auth.ts` is available — see below.)
7777 - **Stop syncing** → detaches the network and returns to local-only; the account and data are7878 untouched, and electing again resumes against the same account.7979 80-The framework side lives in `src/_lofi/session.ts` (`readSession`, `createBackupPasskey`,80+The framework side is exported by `@nzip/lofi` (`readSession`, `createBackupPasskey`,8181 `confirmPhraseAccess`, `revealRecoveryPhrase`, `enableSyncBackup`, `stopSyncBackup`,8282 `restoreFromRecoveryPhrase`); the gate is the author-owned example you can restyle or replace.8383 @@ -106,7 +106,7 @@ it expires. `.env` holds the server-only secrets and is git-ignored.
106106 107107 ## Optional: protecting the secret at rest108108 109-`src/_lofi/auth.ts` is a standalone device-credential primitive: enroll a WebAuthn passkey and109+The package auth runtime is a standalone device-credential primitive: enroll a WebAuthn passkey and110110 derive a credential-bound key (PRF) to encrypt data — including the account secret — **at rest**. It111111 is feature-detected and never faked. It is _not_ the account identity (deriving the account from a112112 credential was removed, since a derived key is a different account and cannot carry local-only data@@ -3,8 +3,8 @@
33 Status: **primitive validated in reference source; `./auth` subpath deferred to a later milestone**\44 Scope: **device-local WebAuthn + PRF at-rest key derivation**55 6-> **Advanced/internal seam.** Generated applications do not expose this as a supported package7-> subpath, and normal product work should not edit `src/_lofi/auth.ts`. Use this document only when6+> **Advanced runtime seam.** Generated applications consume this through the supported root package7+> export, and normal product work should not reimplement it. Use this document only when88 > deliberately evaluating the optional device-credential primitive. Account sync and recovery are99 > documented separately in [Sync and recovery](sync-and-recovery.md).1010 @@ -14,7 +14,7 @@ against — so this module does exactly three things: enroll a device passkey, a
1414 and derive a credential-bound key to encrypt data **at rest**. The generated session runtime handles1515 accounts, recovery phrases, and optional multi-device sync separately.1616 17-The runtime lives at `apps/reference/src/_lofi/auth.ts`.17+The runtime lives at `package/runtime/auth.ts` and is exposed through `@nzip/lofi`.1818 1919 ## What it guarantees2020 @@ -43,7 +43,7 @@ import {
4343 encryptAtRest,4444 enrollDeviceCredential,4545 getAuthCapability,46-} from "./_lofi/auth.ts";46+} from "@nzip/lofi";4747 4848 // 1. Feature-detect before offering enrollment.4949 const capability = await getAuthCapability();@@ -68,7 +68,7 @@ console.log(credential.id, blob.iv.length, blob.ciphertext.length);
6868 6969 ## Testing7070 71-- **Unit** (`src/_lofi/auth_test.ts`): origin classification, enroll/authenticate, PRF result71+- **Unit** (`package/runtime/auth_test.ts`): origin classification, enroll/authenticate, PRF result7272 handling, error mapping, and the HKDF -> AES-GCM round-trip run without a browser by injecting a7373 fake `CredentialsContainer` and an explicit `rpId`.7474 - **Browser** (`tests/auth_e2e_test.ts` + `@nzip/lofi/testing`'s `withVirtualAuthenticator`): a CDP@@ -6,7 +6,7 @@ The generated task list demonstrates lofi's main authoring pattern:
66 flowchart LR77 Schema["schema.ts<br/>table and row types"] --> Permissions["permissions.ts<br/>read and write policy"]88 Schema --> Hook["domain hook<br/>useTasks"]9- Runtime["src/_lofi runtime<br/>storage and subscriptions"] --> Hook9+ Runtime["@nzip/lofi runtime<br/>storage and subscriptions"] --> Hook1010 Hook --> Island["Preact island<br/>product UI"]1111 Island -- "domain mutation" --> Hook1212 Hook -- "insert, update, delete" --> Runtime@@ -51,10 +51,10 @@ The generated `src/islands/use-tasks.ts` is the canonical example. Its important
5151 5252 ```ts5353 import { schema as s } from "jazz-tools";54-import { referenceApp } from "../app.ts";55-import { getRuntime } from "../_lofi/runtime.ts";54+import { getRuntime } from "@nzip/lofi";55+import { app } from "../app.ts";5656 57-const notesTable = referenceApp.schema.notes;57+const notesTable = app.schema.notes;5858 export type Note = s.RowOf<typeof notesTable>;5959 ```6060 @@ -40,8 +40,8 @@ deno task dev
4040 ```4141 4242 The JSR surface is one package, `@nzip/lofi`, with one version and publish operation. Its supported43-public surface is the generator, command subpaths, and `./testing`. Runtime modules remain generated44-under `src/_lofi/` until another application proves a stable package seam.43+public surface includes the configured browser runtime, Preact integration, Astro integration,44+generator, command subpaths, and `./testing`. Framework implementations remain package-owned.4545 4646 From there the developer makes a retained local write, reloads it, works offline, runs checks,4747 builds, previews, and opens the same stable secure origin on a physical device.@@ -156,7 +156,6 @@ the partial JAZZ_APP_ID/JAZZ_SERVER_URL pair to run local-only.
156156 ```text157157 my-app/158158 ├── deno.json159-├── astro.config.ts160159 ├── .env.example161160 ├── public/162161 ├── src/@@ -164,25 +163,26 @@ my-app/
164163 │ ├── permissions.ts165164 │ ├── app.ts166165 │ ├── pages/166+│ ├── layouts/167167 │ ├── islands/168-│ ├── styles/169-│ └── _lofi/ # generated; not author-edited168+│ └── styles/170169 └── tests/171170 ```172171 173172 The exact layout may change only when verified toolchain behavior requires it or the change removes174-an author-facing concept. `src/_lofi/` is generated machinery, not an author-editing surface.173+an author-facing concept. Ignored `.lofi/` tooling files and `dist/sw.js` are regenerated from the174+pinned package; they are not author-editing surfaces.175175 176176 ## Author boundary177177 178-| Location | Default allowance | M1 qualification |179-| -------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |180-| Product UI/pages/islands | lofi components/hooks and application types only. | Raw Jazz, worker, transport, Workbox, and capability branching are forbidden. |181-| `schema.ts` | Application schema declarations. | Direct pinned Jazz 2 schema declarations are allowed in the M1 prototype until #7 determines whether a lofi-owned schema facade is honest and affordable. |182-| `permissions.ts` | Application access policy next to the schema. | Direct pinned Jazz 2 permission declarations are allowed; Jazz tooling watches this location. |183-| `app.ts` | Named lofi configuration and composition. | Raw vendor setup is allowed only inside spike controls, not the graduated prototype. |184-| Generated/internal runtime | Vendor clients, workers, storage, sync, auth, and PWA wiring. | May change across alpha pins without changing product UI. |185-| Escape-hatch module | Explicitly isolated unsupported vendor access. | Never generated by default; no compatibility promise before framework extraction. |178+| Location | Default allowance | M1 qualification |179+| ------------------------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |180+| Product UI/pages/islands | lofi components/hooks and application types only. | Raw Jazz, worker, transport, Workbox, and capability branching are forbidden. |181+| `schema.ts` | Application schema declarations. | Direct pinned Jazz 2 schema declarations are allowed in the M1 prototype until #7 determines whether a lofi-owned schema facade is honest and affordable. |182+| `permissions.ts` | Application access policy next to the schema. | Direct pinned Jazz 2 permission declarations are allowed; Jazz tooling watches this location. |183+| `app.ts` | Named lofi configuration and composition. | Raw vendor setup is allowed only inside spike controls, not the graduated prototype. |184+| Package runtime | Vendor clients, workers, storage, sync, auth, and PWA wiring. | May change across package pins without changing product UI. |185+| Escape-hatch module | Explicitly isolated unsupported vendor access. | Never generated by default; package upgrades provide no compatibility promise for vendor internals. |186186 187187 ## Safe defaults188188 @@ -52,13 +52,27 @@ output at `http://127.0.0.1:4321/` by default.
5252 | `src/pages/` | Astro pages and shell composition |5353 | `src/islands/` | Preact hooks and interactive UI |5454 | `src/styles/` | Application styling |55-| `public/` | Manifest, icon, and service-worker assets |55+| `public/` | Manifest, icon, and other product assets |5656 | `tests/` | Application and local-first browser tests |57-| `src/_lofi/` | Generated framework runtime; do not edit |5857 59-Application hooks can import runtime seams from `src/_lofi/`, as the generated `use-tasks.ts` does.60-The boundary means you should not modify the runtime implementation or import Jazz networking and61-storage machinery directly into product components.58+Application hooks import supported runtime seams from `@nzip/lofi`, as the generated `use-tasks.ts`59+does. The boundary keeps Jazz networking and storage machinery out of product components while60+allowing framework fixes to arrive through a package upgrade.61+62+The final package map is intentionally small:63+64+| Import | Supported use |65+| ----------------------------- | --------------------------------------------------- |66+| `@nzip/lofi` | App configuration, runtime, session, stores, PWA |67+| `@nzip/lofi/preact` | Package-owned Preact hooks and diagnostic UI |68+| `@nzip/lofi/astro` | Package-owned Astro/Vite integration |69+| `@nzip/lofi/testing` | Local-first Playwright helpers |70+| `@nzip/lofi/{create,dev,...}` | Generator and generated-project command entrypoints |71+72+All of these imports resolve through the single version pinned in `deno.json`. Upgrade that pin to73+receive framework fixes; do not copy package files into `src/` or import unpublished internal paths.74+Direct vendor access is an unsupported escape hatch: isolate it from ordinary product files and do75+not expect lofi compatibility guarantees for it.6276 6377 ## Customize the application identity first6478 @@ -3,9 +3,9 @@
33 ```mermaid44 flowchart TB55 Root["my-app/"]6- Root --> Config["Project config<br/>deno.json · astro.config.ts · tsconfig.json"]6+ Root --> Config["Project config<br/>deno.json · tsconfig.json"]77 Root --> Env[".env.example"]8- Root --> Public["public/<br/>favicon · manifest · service worker"]8+ Root --> Public["public/<br/>favicon · manifest"]99 Root --> Src["src/"]1010 Root --> Tests["tests/"]1111 @@ -15,7 +15,8 @@ flowchart TB
1515 Src --> Pages["pages/"]1616 Src --> Islands["islands/"]1717 Src --> Styles["styles/"]18- Src --> Runtime["_lofi/<br/>generated runtime"]18+ Src --> Layouts["layouts/"]19+ Root --> Runtime["@nzip/lofi<br/>versioned runtime"]1920 ```2021 2122 ## Author-owned files@@ -27,12 +28,13 @@ flowchart TB
2728 - `public/` contains install and static shell assets.2829 - `tests/` contains application tests and worked local-first browser examples.2930 30-## Generated runtime files31+## Versioned framework runtime3132 32-`src/_lofi/` owns durable storage, the Jazz client, account sessions, recovery, lifecycle handling,33-PWA capability gates, diagnostics, and table stores. Do not edit these files during normal product34-work.33+`@nzip/lofi` owns durable storage, the Jazz client, account sessions, recovery, lifecycle handling,34+PWA capability gates, diagnostics, and table stores. These modules are installed once through the35+pinned package version instead of copied into application source.3536 36-The generated domain hook imports selected `_lofi` runtime seams. Follow that pattern when binding a37+The generated domain hook imports selected public package seams. Follow that pattern when binding a3738 new table, but keep vendor setup, storage selection, transports, service-worker logic, and38-capability branching out of product components.39+capability branching out of product components. `deno task dev` and `build` materialize ignored40+Astro integration files under `.lofi/`; `build` emits the service worker into `dist/`.@@ -20,8 +20,9 @@ flowchart TB
2020 deno task test2121 ```2222 23-The default suite covers the generated runtime contracts and application tests without launching a24-browser. Keep domain logic and permission-shape checks in this fast path when possible.23+The default suite covers application tests without launching a browser. Framework runtime contracts24+run once in the `@nzip/lofi` package suite instead of being copied into every application. Keep25+domain logic and permission-shape checks in this fast path when possible.2526 2627 ## Test the production build manually2728 @@ -0,0 +1,128 @@
1+import { join, resolve } from "node:path";2+3+export type LofiAstroOptions = {4+ /** Project root. Defaults to the directory from which Astro was invoked. */5+ root?: string;6+ /** Directory containing the Jazz schema and permissions. */7+ schemaDir?: string;8+};9+10+const runtimeFiles = [11+ "app.ts",12+ "auth.ts",13+ "boot.ts",14+ "config.ts",15+ "device-capabilities.ts",16+ "diagnostics.ts",17+ "env.d.ts",18+ "foreground-recovery.ts",19+ "inspector.ts",20+ "lifecycle.ts",21+ "mod.ts",22+ "probe.ts",23+ "pwa.ts",24+ "recovery.ts",25+ "resource-lifecycle.ts",26+ "runtime.ts",27+ "session.ts",28+ "table-store.ts",29+ "ui-mutation.ts",30+] as const;31+32+const preactFiles = ["DeviceStatus.tsx", "mod.ts", "use-device-capabilities.ts"] as const;33+34+async function readPackageFile(path: string): Promise<string> {35+ const url = new URL(path, import.meta.url);36+ if (url.protocol === "file:") return await Deno.readTextFile(url);37+ const response = await fetch(url);38+ if (!response.ok) {39+ throw new Error(`failed to read package module ${path}: HTTP ${response.status}`);40+ }41+ return await response.text();42+}43+44+function renderConfig(projectRoot: string): string {45+ const runtimeEntry = join(projectRoot, ".lofi", "package", "runtime", "mod.ts");46+ const preactEntry = join(projectRoot, ".lofi", "package", "preact", "mod.ts");47+ return `import preact from "@astrojs/preact";48+import { defineConfig } from "astro/config";49+import { jazzPlugin } from "jazz-tools/dev/vite";50+import { resolve } from "node:path";51+52+const projectRoot = Deno.cwd();53+const schemaDir = resolve(projectRoot, "src");54+const cloudAppId = process.env.JAZZ_APP_ID?.trim();55+const cloudServerUrl = process.env.JAZZ_SERVER_URL?.trim();56+const managedJazzServer = process.env.LOFI_SKIP_JAZZ_MANAGED === "1"57+ ? false58+ : { allowLocalFirstAuth: true };59+60+if (Boolean(cloudAppId) !== Boolean(cloudServerUrl)) {61+ throw new Error(62+ "Cloud mode requires both JAZZ_APP_ID and JAZZ_SERVER_URL; set the missing name or remove the partial pair.",63+ );64+}65+66+if (cloudAppId && cloudServerUrl) {67+ process.env.VITE_JAZZ_APP_ID = cloudAppId;68+ process.env.VITE_JAZZ_SERVER_URL = cloudServerUrl;69+}70+71+export default defineConfig({72+ output: "static",73+ integrations: [preact({ compat: true, devtools: true })],74+ vite: {75+ plugins: [jazzPlugin({ schemaDir, inspector: false, server: managedJazzServer })],76+ server: { fs: { allow: [projectRoot] } },77+ define: {78+ __LOFI_JAZZ_APP_ID__: JSON.stringify(cloudAppId ?? ""),79+ __LOFI_JAZZ_SERVER_URL__: JSON.stringify(cloudServerUrl ?? ""),80+ },81+ resolve: {82+ alias: [83+ { find: "@nzip/lofi/preact", replacement: ${JSON.stringify(preactEntry)} },84+ { find: "@nzip/lofi", replacement: ${JSON.stringify(runtimeEntry)} },85+ ],86+ noExternal: ["@astrojs/preact"],87+ },88+ },89+});90+`;91+}92+93+/**94+ * Materializes the package-owned Astro/Jazz integration into an ignored tooling95+ * directory so Astro's Node-compatible config loader can consume it.96+ */97+export async function prepareLofiAstroConfig(options: LofiAstroOptions = {}): Promise<string> {98+ const projectRoot = resolve(options.root ?? Deno.cwd());99+ if (options.schemaDir && options.schemaDir !== "src") {100+ throw new Error("custom schemaDir is not supported by the generated lofi Astro integration");101+ }102+ const toolingDir = join(projectRoot, ".lofi");103+ const path = join(toolingDir, "astro.config.ts");104+ const packageDir = join(toolingDir, "package");105+ await Deno.mkdir(join(packageDir, "runtime"), { recursive: true });106+ await Deno.mkdir(join(packageDir, "preact"), { recursive: true });107+ for (const file of runtimeFiles) {108+ await Deno.writeTextFile(109+ join(packageDir, "runtime", file),110+ await readPackageFile(`../runtime/${file}`),111+ );112+ }113+ for (const file of preactFiles) {114+ await Deno.writeTextFile(115+ join(packageDir, "preact", file),116+ await readPackageFile(`../preact/${file}`),117+ );118+ }119+ const configSource = renderConfig(projectRoot);120+ let current = "";121+ try {122+ current = await Deno.readTextFile(path);123+ } catch (error) {124+ if (!(error instanceof Deno.errors.NotFound)) throw error;125+ }126+ if (current !== configSource) await Deno.writeTextFile(path, configSource);127+ return path;128+}@@ -0,0 +1,23 @@
1+import { prepareLofiAstroConfig } from "./mod.ts";2+3+Deno.test("prepareLofiAstroConfig materializes package-owned vendor integration", async () => {4+ const root = await Deno.makeTempDir({ dir: ".", prefix: ".lofi-astro-test-" });5+ try {6+ const path = await prepareLofiAstroConfig({ root });7+ const source = await Deno.readTextFile(path);8+ if (!path.endsWith("/.lofi/astro.config.ts")) throw new Error(`unexpected path: ${path}`);9+ for (10+ const expected of [11+ "@astrojs/preact",12+ "jazz-tools/dev/vite",13+ "JAZZ_APP_ID",14+ ".lofi/package/runtime/mod.ts",15+ ]16+ ) {17+ if (!source.includes(expected)) throw new Error(`generated config omitted ${expected}`);18+ }19+ await Deno.stat(`${root}/.lofi/package/preact/DeviceStatus.tsx`);20+ } finally {21+ await Deno.remove(root, { recursive: true });22+ }23+});@@ -8,8 +8,9 @@
88 * 99 */1010 11-import { extname, join } from "node:path";11+import { extname, join, relative } from "node:path";1212 import { LOFI_VERSION } from "../version.ts";13+import { prepareLofiAstroConfig } from "../astro/mod.ts";1314 import { loadEnvironment, serverEnvironmentNames } from "../tooling/environment.ts";1415 import { precacheUrls, scanSecrets, sourceFingerprint, walkFiles } from "../tooling/project.ts";1516 import { runDeno } from "../tooling/process.ts";@@ -20,9 +21,23 @@ environment.LOFI_SKIP_JAZZ_MANAGED = "1";
2021 environment.JAZZ_ADMIN_SECRET = "";2122 environment.BACKEND_SECRET = "";2223 const sourceHash = (await sourceFingerprint()).slice(0, 12);24+const astroConfig = relative(Deno.cwd(), await prepareLofiAstroConfig());25+26+async function readPackageAsset(path: string): Promise<string> {27+ const url = new URL(path, import.meta.url);28+ if (url.protocol === "file:") return await Deno.readTextFile(url);29+ const response = await fetch(url);30+ if (!response.ok) {31+ throw new Error(`failed to read package asset ${path}: HTTP ${response.status}`);32+ }33+ return await response.text();34+}2335 2436 exitOnFailure(25- await runDeno(["run", "-A", "npm:astro@7.0.9", "build"], environment),37+ await runDeno(38+ ["run", "-A", "npm:astro@7.0.9", "build", "--config", astroConfig],39+ environment,40+ ),2641 "production build",2742 );2843 @@ -33,7 +48,7 @@ await Deno.writeTextFile(
3348 }\n`,3449 );3550 const serviceWorkerPath = "dist/sw.js";36-const serviceWorker = await Deno.readTextFile(serviceWorkerPath);51+const serviceWorker = await readPackageAsset("../runtime/sw.js");3752 await Deno.writeTextFile(3853 serviceWorkerPath,3954 serviceWorker.replace("__LOFI_BUILD_REVISION__", sourceHash),@@ -7,7 +7,9 @@
77 * 88 */99 10+import { relative } from "node:path";1011 import { runDenoStatus } from "../tooling/process.ts";12+import { prepareLofiAstroConfig } from "../astro/mod.ts";1113 import { validatedCommandEnvironment } from "./shared.ts";1214 1315 const environment = await validatedCommandEnvironment();@@ -30,8 +32,9 @@ console.log("PWA: development service worker disabled");
3032 console.log("Device: stable HTTPS URL not configured; device preview graduates in M3");3133 3234 const forwarded = Deno.args[0] === "--" ? Deno.args.slice(1) : Deno.args;35+const astroConfig = relative(Deno.cwd(), await prepareLofiAstroConfig());3336 const status = await runDenoStatus(34- ["run", "-A", "npm:astro@7.0.9", "dev", ...forwarded],37+ ["run", "-A", "npm:astro@7.0.9", "dev", "--config", astroConfig, ...forwarded],3538 environment,3639 );3740 if (status.forwardedSignal) {@@ -13,7 +13,7 @@ import { exitOnFailure, validatedCommandEnvironment } from "./shared.ts";
1313 const environment = await validatedCommandEnvironment();1414 const started = performance.now();1515 const exitCode = await runDeno(16- ["test", "--allow-read", "--allow-write=.", "src/_lofi", "tests"],16+ ["test", "--allow-read", "--allow-write=.", "tests"],1717 environment,1818 );1919 exitOnFailure(exitCode, "local-first test suite; retained browser artifacts use test-results/");@@ -21,6 +21,7 @@ const generatedGitignore = `.env
2121 node_modules/2222 .astro/2323 .vite/24+.lofi/2425 dist/2526 coverage/2627 playwright-report/@@ -61,7 +62,7 @@ on-device account with no sign-in. To enable managed sync and account backup, ru
6162 6263 With a Jazz app configured, the \`AccountGate\` island lets a user back up and sync their local account6364 and recover it from a 24-word recovery phrase. The account they already have carries over — electing64-to sync never changes its identity. See the framework side in \`src/_lofi/session.ts\`.65+to sync never changes its identity. The framework implementation is provided by \`@nzip/lofi\`.6566 6667 Public tasks: \`dev\`, \`doctor\`, \`test\`, \`build\`, and \`preview\`. Sync/backup and schema tasks:6768 \`jazz:provision\`, \`schema:validate\`, \`schema:deploy\`, \`migrations:create\`, and \`migrations:push\`.@@ -88,10 +89,13 @@ Point them at any other static host by editing those two tasks.
8889 // — npm imports, tasks (including schema/migration/deploy), fmt, lint — is copied8990 // verbatim, so the template is the single source of truth for project config.9091 const lofiImportTargets: Record<string, { subpath: string; localPath: string }> = {92+ "@nzip/lofi": { subpath: "", localPath: "runtime/mod.ts" },93+ "@nzip/lofi/astro": { subpath: "astro", localPath: "astro/mod.ts" },9194 "@nzip/lofi/build": { subpath: "build", localPath: "commands/build.ts" },9295 "@nzip/lofi/dev": { subpath: "dev", localPath: "commands/dev.ts" },9396 "@nzip/lofi/doctor": { subpath: "doctor", localPath: "commands/doctor.ts" },9497 "@nzip/lofi/preview": { subpath: "preview", localPath: "commands/preview.ts" },98+ "@nzip/lofi/preact": { subpath: "preact", localPath: "preact/mod.ts" },9599 "@nzip/lofi/provision": { subpath: "provision", localPath: "commands/provision.ts" },96100 "@nzip/lofi/test": { subpath: "test", localPath: "commands/run_tests.ts" },97101 "@nzip/lofi/testing": { subpath: "testing", localPath: "testing/mod.ts" },@@ -110,7 +114,11 @@ async function rewritePortableDenoConfig(root: string, packagePrefix: string): P
110114 if (typeof imports[key] !== "string") {111115 throw new Error(`starter deno.json is missing the ${key} import`);112116 }113- imports[key] = isFileOverride ? `${packagePrefix}${localPath}` : `${packagePrefix}${subpath}`;117+ imports[key] = isFileOverride118+ ? `${packagePrefix}${localPath}`119+ : subpath120+ ? `${packagePrefix}${subpath}`121+ : packagePrefix.slice(0, -1);114122 }115123 // The reference app links the local @nzip/lofi package for its dev loop; a116124 // generated project resolves the pinned JSR version directly.@@ -179,17 +187,6 @@ async function writeTemplate(destination: string): Promise<void> {
179187 }180188 }181189 182-async function rewritePortableAstroConfig(root: string): Promise<void> {183- const path = join(root, "astro.config.ts");184- const source = await Deno.readTextFile(path);185- const portable = source.replace(186- 'const workspaceRoot = fileURLToPath(new URL("../../", import.meta.url));',187- 'const workspaceRoot = fileURLToPath(new URL("./", import.meta.url));',188- );189- if (portable === source) throw new Error("starter Astro workspace-root marker was not found");190- await Deno.writeTextFile(path, portable);191-}192-193190 export async function createProject(options: CreateProjectOptions): Promise<CreateProjectResult> {194191 const segments = validateName(options.name);195192 const cwd = resolve(options.cwd);@@ -217,7 +214,6 @@ export async function createProject(options: CreateProjectOptions): Promise<Crea
217214 });218215 try {219216 await writeTemplate(staging);220- await rewritePortableAstroConfig(staging);221217 await rewritePortableDenoConfig(staging, options.packagePrefix ?? LOFI_PACKAGE_PREFIX);222218 await Deno.writeTextFile(join(staging, ".gitignore"), generatedGitignore);223219 await Deno.writeTextFile(join(staging, ".env.example"), generatedEnvironment);@@ -99,14 +99,15 @@ Deno.test("createProject materializes the complete starter snapshot", async () =
9999 );100100 }101101 const config = JSON.parse(await Deno.readTextFile(join(result.destination, "deno.json")));102+ assertEquals(config.imports["@nzip/lofi"], "jsr:@nzip/lofi@0.2.0");102103 assertEquals(config.imports["@nzip/lofi/"], "jsr:@nzip/lofi@0.2.0/");103104 assertEquals(config.imports["@nzip/lofi/testing"], "jsr:@nzip/lofi@0.2.0/testing");104105 const lofiSpecifiers = Object.entries(config.imports)105106 .filter(([name]) => name.startsWith("@nzip/lofi/"))106107 .map(([, specifier]) => String(specifier));107108 assert(108- lofiSpecifiers.length === 8,109- `expected one package prefix, six commands, and testing, received ${lofiSpecifiers.length}`,109+ lofiSpecifiers.length === 10,110+ `expected one package prefix, two integrations, six commands, and testing, received ${lofiSpecifiers.length}`,110111 );111112 assert(112113 lofiSpecifiers.every((specifier) => specifier.startsWith("jsr:@nzip/lofi@0.2.0/")),@@ -120,6 +121,22 @@ Deno.test("createProject materializes the complete starter snapshot", async () =
120121 Deno.test("package manifest and generated version stay coupled", () => {121122 assertEquals(packageManifest.name, "@nzip/lofi");122123 assertEquals(packageManifest.version, LOFI_VERSION);124+ assertEquals(Object.keys(packageManifest.exports).sort(), [125+ ".",126+ "./astro",127+ "./build",128+ "./create",129+ "./dev",130+ "./doctor",131+ "./preact",132+ "./preview",133+ "./provision",134+ "./test",135+ "./testing",136+ ]);137+ assertEquals(packageManifest.exports["."], "./package/runtime/mod.ts");138+ assertEquals(packageManifest.exports["./astro"], "./package/astro/mod.ts");139+ assertEquals(packageManifest.exports["./preact"], "./package/preact/mod.ts");123140 assertEquals(packageManifest.exports["./testing"], "./package/testing/mod.ts");124141 });125142 @@ -169,6 +186,7 @@ Deno.test("createProject accepts an existing empty directory and a local package
169186 packagePrefix: "file:///clean-room/package/",170187 });171188 const config = JSON.parse(await Deno.readTextFile(join(result.destination, "deno.json")));189+ assertEquals(config.imports["@nzip/lofi"], "file:///clean-room/package/runtime/mod.ts");172190 assertEquals(config.imports["@nzip/lofi/"], "file:///clean-room/package/");173191 assert(174192 !await Array.fromAsync(Deno.readDir(result.destination)).then((entries) =>@@ -1,10 +1,12 @@
1+import type { JSX } from "preact";12 import { useEffect, useState } from "preact/hooks";2-import { serverUrl } from "./config.ts";3+// Package-owned optional diagnostics UI.4+import { serverUrl } from "../runtime/config.ts";35 import { useDeviceCapabilities } from "./use-device-capabilities.ts";4-import { settleUiMutation } from "./ui-mutation.ts";5-import { getPwaState, type PwaState, subscribePwaState } from "./pwa.ts";6-import { runtimeRecreatedEvent } from "./runtime.ts";7-import { readSession, type Session } from "./session.ts";6+import { settleUiMutation } from "../runtime/ui-mutation.ts";7+import { getPwaState, type PwaState, subscribePwaState } from "../runtime/pwa.ts";8+import { runtimeRecreatedEvent } from "../runtime/runtime.ts";9+import { readSession, type Session } from "../runtime/session.ts";810 911 /**1012 * The Device gate: every subsystem's live status, grouped by the system it@@ -14,7 +16,7 @@ import { readSession, type Session } from "./session.ts";
1416 */1517 1618 // One label/value row inside a category's definition list.17-function Row({ label, value }: { label: string; value: string }) {19+function Row({ label, value }: { label: string; value: string }): JSX.Element {1820 return (1921 <div>2022 <dt>{label}</dt>@@ -25,7 +27,7 @@ function Row({ label, value }: { label: string; value: string }) {
2527 2628 const available = (present: boolean) => (present ? "available" : "missing");2729 28-export default function DeviceStatus() {30+export default function DeviceStatus(): JSX.Element {2931 const { report, requestPersistence } = useDeviceCapabilities();3032 const [pwa, setPwa] = useState<PwaState>(getPwaState());3133 const [session, setSession] = useState<Session | null>(null);@@ -63,7 +65,7 @@ export default function DeviceStatus() {
6365 <div class="device-category">6466 <h3>Shared worker & durable storage</h3>6567 <p class="device-hook">66- Hook in: <code>src/_lofi/device-capabilities.ts</code>, <code>runtime.ts</code>68+ Hook in: <code>@nzip/lofi</code> device capabilities and runtime6769 </p>6870 <dl>6971 <Row@@ -89,7 +91,7 @@ export default function DeviceStatus() {
8991 <div class="device-category">9092 <h3>Data sync</h3>9193 <p class="device-hook">92- Hook in: <code>src/_lofi/config.ts</code>, <code>runtime.ts</code>94+ Hook in: <code>@nzip/lofi</code> session and runtime9395 </p>9496 <dl>9597 <Row label="Sync" value={syncState} />@@ -106,7 +108,7 @@ export default function DeviceStatus() {
106108 <div class="device-category">107109 <h3>Auth</h3>108110 <p class="device-hook">109- Hook in: <code>src/_lofi/auth.ts</code>, <code>session.ts</code>111+ Hook in: <code>@nzip/lofi</code> auth and session110112 </p>111113 <dl>112114 <Row label="Identity" value="local-first account" />@@ -119,7 +121,7 @@ export default function DeviceStatus() {
119121 <div class="device-category">120122 <h3>PWA</h3>121123 <p class="device-hook">122- Hook in: <code>src/_lofi/pwa.ts</code>124+ Hook in: <code>@nzip/lofi</code> PWA state123125 </p>124126 <dl>125127 <Row label="Display mode" value={report.displayMode} />@@ -1,11 +1,17 @@
11 import { useCallback, useEffect, useState } from "preact/hooks";2+// Package-owned Preact capability hook.23 import {34 type DeviceCapabilityReport,45 readDeviceCapabilityReport,56 requestPersistentStorage,6-} from "./device-capabilities.ts";7+} from "../runtime/device-capabilities.ts";78 8-export function useDeviceCapabilities() {9+export type DeviceCapabilitiesHook = {10+ report: DeviceCapabilityReport | null;11+ requestPersistence(): Promise<void>;12+};13+14+export function useDeviceCapabilities(): DeviceCapabilitiesHook {915 const [report, setReport] = useState<DeviceCapabilityReport | null>(null);1016 useEffect(() => {1117 let active = true;@@ -0,0 +1,46 @@
1+/** Author-owned configuration consumed by the package runtime. */2+export type LofiAppConfig<Schema = unknown> = {3+ name: string;4+ databaseName: string;5+ schema: Schema;6+ storage: "durable";7+ credentialOrigins?: readonly string[];8+ sync: { adapter: "jazz" };9+ repositoryUrl?: string;10+};11+12+type AppSlot = { config?: LofiAppConfig };13+14+const slotName = "__LOFI_APP_DEFINITION__";15+const browserGlobal = globalThis as typeof globalThis & { [slotName]?: AppSlot };16+17+function slot(): AppSlot {18+ browserGlobal[slotName] ??= {};19+ return browserGlobal[slotName];20+}21+22+/**23+ * Defines the application-facing values the versioned runtime needs.24+ *25+ * The returned value retains the exact schema type so author code can select26+ * tables without importing or editing framework internals.27+ */28+export function defineLofiApp<const Schema>(29+ config: LofiAppConfig<Schema>,30+): LofiAppConfig<Schema> {31+ if (!config.name.trim()) throw new Error("lofi app name must not be empty");32+ if (!config.databaseName.trim()) throw new Error("lofi database name must not be empty");33+ slot().config = config;34+ return config;35+}36+37+/** Package-internal lookup used when browser resources are opened lazily. */38+export function getLofiApp(): LofiAppConfig {39+ const config = slot().config;40+ if (!config) {41+ throw new Error(42+ "lofi app is not defined; export defineLofiApp({...}) from src/app.ts before booting the runtime",43+ );44+ }45+ return config;46+}@@ -0,0 +1,38 @@
1+import { defineLofiApp, getLofiApp } from "./app.ts";2+import { assert } from "./test-assert.ts";3+4+Deno.test("defineLofiApp retains schema types and registers package runtime configuration", () => {5+ const schema = { notes: { name: "notes" } } as const;6+ const app = defineLofiApp({7+ name: "notes",8+ databaseName: "notes-db",9+ schema,10+ storage: "durable",11+ sync: { adapter: "jazz" },12+ });13+14+ assert(app.schema === schema, "defineLofiApp replaced the author schema");15+ assert(getLofiApp().databaseName === "notes-db", "runtime config was not registered");16+});17+18+Deno.test("defineLofiApp rejects empty identity and database names", () => {19+ for (20+ const config of [21+ { name: "", databaseName: "db" },22+ { name: "app", databaseName: "" },23+ ]24+ ) {25+ let message = "";26+ try {27+ defineLofiApp({28+ ...config,29+ schema: {},30+ storage: "durable",31+ sync: { adapter: "jazz" },32+ });33+ } catch (error) {34+ message = error instanceof Error ? error.message : String(error);35+ }36+ assert(message.includes("must not be empty"), "empty app configuration was accepted");37+ }38+});@@ -1,4 +1,5 @@
11 /**2+// Package-owned identity and device-credential runtime.23 * Device credential auth for local-first apps.34 *45 * Local-first identity is device-local and cryptographic — there is no central@@ -14,7 +15,7 @@
1415 * 1516 */1617 17-import { referenceApp } from "../app.ts";18+import { getLofiApp } from "./app.ts";1819 1920 /** Whether the WebAuthn PRF extension can be used on this client. */2021 export type PrfSupport = "available" | "not-reported" | "unknown" | "unavailable";@@ -180,7 +181,7 @@ function matchesTrustedOrigin(hostname: string, pattern: string): boolean {
180181 */181182 export function classifyCredentialOrigin(182183 url: URL,183- trustedOrigins: readonly string[] = referenceApp.credentialOrigins ?? [],184+ trustedOrigins: readonly string[] = getLofiApp().credentialOrigins ?? [],184185 ): CredentialOriginReport {185186 const rpId = url.hostname;186187 const hostname = canonicalHostname(rpId);@@ -219,7 +220,7 @@ function currentOrigin(trustedOrigins?: readonly string[]): CredentialOriginRepo
219220 return { status: "blocked", rpId: "", action: "open the application in a browser" };220221 }221222 const url = new URL(location.href);222- const declared = trustedOrigins ?? referenceApp.credentialOrigins ?? [];223+ const declared = trustedOrigins ?? getLofiApp().credentialOrigins ?? [];223224 // Default trust is the origin you are actually served from: the passkey binds224225 // to this hostname (its RP ID) regardless, so trusting it lets the app work225226 // wherever it is deployed. Pin explicit hosts in `app.ts` `credentialOrigins`@@ -278,13 +279,14 @@ export async function getAuthCapability(
278279 export async function enrollDeviceCredential(279280 options: EnrollOptions = {},280281 ): Promise<DeviceCredential> {282+ const app = getLofiApp();281283 const rpId = requireStableRpId(options);282284 const credentials = options.credentials ?? browserCredentials();283- const label = options.label ?? referenceApp.name;285+ const label = options.label ?? app.name;284286 try {285287 const credential = await credentials.create({286288 publicKey: {287- rp: { id: rpId, name: referenceApp.name },289+ rp: { id: rpId, name: app.name },288290 user: { id: randomBytes(), name: label, displayName: label },289291 challenge: randomBytes(),290292 pubKeyCredParams: [@@ -1,4 +1,5 @@
11 import {2+ // Package contract tests.23 authenticateDeviceCredential,34 AuthError,45 classifyCredentialOrigin,@@ -8,8 +9,18 @@ import {
89 encryptAtRest,910 enrollDeviceCredential,1011 } from "./auth.ts";12+import { defineLofiApp } from "./app.ts";1113 import { assert } from "./test-assert.ts";1214 15+defineLofiApp({16+ name: "lofi-test",17+ databaseName: "lofi-test",18+ schema: {},19+ storage: "durable",20+ credentialOrigins: [],21+ sync: { adapter: "jazz" },22+});23+1324 const test = (globalThis as unknown as {1425 Deno: { test(name: string, body: () => void | Promise<void>): void };1526 }).Deno.test;@@ -0,0 +1,13 @@
1+// Package-owned browser boot orchestration.2+import { registerProductionServiceWorker } from "./pwa.ts";3+4+let booted = false;5+6+/** Starts lofi's browser lifecycle once for the current document. */7+export async function bootLofi(): Promise<void> {8+ if (booted || typeof document === "undefined") return;9+ booted = true;10+ await import("./lifecycle.ts");11+ registerProductionServiceWorker();12+ if (import.meta.env.DEV) void import("./probe.ts");13+}@@ -1,7 +1,10 @@
1-/// <reference path="../env.d.ts" />1+// Package-owned environment and adapter configuration.22 33 import type { DbConfig } from "jazz-tools";4-import { referenceApp } from "../app.ts";4+import { getLofiApp } from "./app.ts";5+6+declare const __LOFI_JAZZ_APP_ID__: string;7+declare const __LOFI_JAZZ_SERVER_URL__: string;58 69 const LOCAL_APP_ID = "00000000-0000-0000-0000-00000000f153";710 const configuredAppId = typeof __LOFI_JAZZ_APP_ID__ === "string" ? __LOFI_JAZZ_APP_ID__ : "";@@ -47,12 +50,13 @@ export function syncing(): boolean {
4750 }4851 4952 export function databaseConfig(secret: string): DbConfig {53+ const app = getLofiApp();5054 return {5155 appId,5256 // The same local-first secret opens the same account whether or not a server5357 // is attached, so electing to sync later preserves all existing data.5458 ...(syncing() && serverUrl ? { serverUrl } : {}),5559 secret,56- driver: { type: "persistent", dbName: `${referenceApp.databaseName}-${appId}` },60+ driver: { type: "persistent", dbName: `${app.databaseName}-${appId}` },5761 };5862 }@@ -1,4 +1,5 @@
11 export type DeviceCapabilityReport = {2+ // Package-owned device capability checks.23 secureContext: boolean;34 opfs: boolean;45 sharedWorker: boolean;@@ -99,7 +100,7 @@ export class DurableStorageUnsupportedError extends Error {
99100 override name = "DurableStorageUnsupportedError";100101 readonly report: ReturnType<typeof durableCapabilityReport>;101102 102- constructor(report = durableCapabilityReport()) {103+ constructor(report: ReturnType<typeof durableCapabilityReport> = durableCapabilityReport()) {103104 const missing = Object.entries({104105 secureContext: report.secureContext,105106 opfs: report.opfs,@@ -0,0 +1,66 @@
1+/**2+ * The supported `@nzip/lofi` browser/runtime surface.3+ *4+ * Application code composes these APIs; storage, identity, sync, PWA, and5+ * lifecycle implementations remain versioned package code.6+ *7+ * @module8+ */9+10+export { defineLofiApp, type LofiAppConfig } from "./app.ts";11+export {12+ type AuthCapability,13+ authenticateDeviceCredential,14+ AuthError,15+ classifyCredentialOrigin,16+ type CredentialOriginReport,17+ decryptAtRest,18+ deriveAtRestKey,19+ derivePrfSecret,20+ encryptAtRest,21+ enrollDeviceCredential,22+ getAuthCapability,23+} from "./auth.ts";24+export { bootLofi } from "./boot.ts";25+export {26+ assertDurableBrowser,27+ type DeviceCapabilityReport,28+ DurableStorageUnsupportedError,29+ readDeviceCapabilityReport,30+ requestPersistentStorage,31+} from "./device-capabilities.ts";32+export {33+ applyPwaUpdate,34+ getPwaState,35+ type PwaState,36+ requestPwaInstall,37+ subscribePwaState,38+} from "./pwa.ts";39+export {40+ getRuntime,41+ getRuntimeDiagnostics,42+ type LofiRuntime,43+ recreateRuntime,44+ runtimeRecreatedEvent,45+ shutdownRuntime,46+ subscribeRuntimeDiagnostics,47+} from "./runtime.ts";48+export {49+ confirmPhraseAccess,50+ createBackupPasskey,51+ enableSyncBackup,52+ isAuthError,53+ isRecoveryError,54+ readSession,55+ restoreFromRecoveryPhrase,56+ revealRecoveryPhrase,57+ type Session,58+ stopSyncBackup,59+} from "./session.ts";60+export {61+ type TableHandle,62+ type TableRow,63+ type TableSnapshot,64+ type TableStore,65+} from "./table-store.ts";66+export { settleUiMutation } from "./ui-mutation.ts";@@ -1,4 +1,5 @@
11 import { serverUrl, syncing } from "./config.ts";2+// Package-owned development probe.23 import { readDeviceCapabilityReport } from "./device-capabilities.ts";34 import { type InspectorAdapter, type InspectorSnapshot, mountInspector } from "./inspector.ts";45 import { getForegroundRecoveryState, subscribeForegroundRecovery } from "./lifecycle.ts";@@ -1,4 +1,5 @@
1-/// <reference path="../env.d.ts" />1+/// <reference path="./env.d.ts" />2+// Package-owned PWA lifecycle.23 export type PwaWorkerState =34 | "development-disabled"45 | "unsupported"@@ -41,11 +42,7 @@ type InstallEnvironment = {
4142 maxTouchPoints: number;4243 };4344 44-declare global {45- interface Window {46- __LOFI_PWA_STATE__?: PwaState;47- }48-}45+type LofiPwaGlobal = typeof globalThis & { __LOFI_PWA_STATE__?: PwaState };4946 5047 const subscribers = new Set<(state: PwaState) => void>();5148 let deferredInstallPrompt: InstallPromptEvent | undefined;@@ -61,7 +58,7 @@ let state: PwaState = {
6158 function publish(next: PwaState) {6259 state = next;6360 if (typeof document !== "undefined") {64- (globalThis as typeof globalThis & Window).__LOFI_PWA_STATE__ = state;61+ (globalThis as LofiPwaGlobal).__LOFI_PWA_STATE__ = state;6562 }6663 for (const subscriber of subscribers) subscriber(state);6764 }@@ -1,4 +1,5 @@
11 import { BrowserAuthSecretStore, createDb, type Db } from "jazz-tools";2+// Package-owned Jazz runtime.23 import { createDiagnostics, type RuntimeDiagnostics } from "./diagnostics.ts";34 import { appId, databaseConfig, syncing } from "./config.ts";45 import { assertDurableBrowser } from "./device-capabilities.ts";@@ -1,4 +1,5 @@
11 import type { Db, MutationErrorEvent } from "jazz-tools";2+// Package contract tests.23 import { createDiagnostics, type RuntimeDiagnostics } from "./diagnostics.ts";34 import { createTableStore, type TableHandle } from "./table-store.ts";45 import { assert, assertCount } from "./test-assert.ts";@@ -6,11 +6,9 @@
66 */77 export const STARTER_FILES: readonly string[] = [88 "deno.json",9- "astro.config.ts",109 "tsconfig.json",1110 "public/favicon.svg",1211 "public/manifest.webmanifest",13- "public/sw.js",1412 "src/app.ts",1513 "src/env.d.ts",1614 "src/permissions.ts",@@ -20,35 +18,7 @@ export const STARTER_FILES: readonly string[] = [
2018 "src/islands/AccountGate.tsx",2119 "src/islands/TaskList.tsx",2220 "src/islands/use-tasks.ts",23- "src/_lofi/DeviceStatus.tsx",24- "src/_lofi/Shell.astro",25- "src/_lofi/auth.ts",26- "src/_lofi/auth_test.ts",27- "src/_lofi/boot.ts",28- "src/_lofi/config.ts",29- "src/_lofi/session.ts",30- "src/_lofi/diagnostics.ts",31- "src/_lofi/device-capabilities.ts",32- "src/_lofi/device-capabilities_test.ts",33- "src/_lofi/foreground-recovery.ts",34- "src/_lofi/foreground-recovery_test.ts",35- "src/_lofi/inspector.ts",36- "src/_lofi/inspector_test.ts",37- "src/_lofi/lifecycle.ts",38- "src/_lofi/probe.ts",39- "src/_lofi/pwa.ts",40- "src/_lofi/pwa_test.ts",41- "src/_lofi/recovery.ts",42- "src/_lofi/recovery_test.ts",43- "src/_lofi/resource-lifecycle.ts",44- "src/_lofi/resource-lifecycle_test.ts",45- "src/_lofi/runtime.ts",46- "src/_lofi/table-store.ts",47- "src/_lofi/table-store_test.ts",48- "src/_lofi/test-assert.ts",49- "src/_lofi/ui-mutation.ts",50- "src/_lofi/ui-mutation_test.ts",51- "src/_lofi/use-device-capabilities.ts",21+ "src/layouts/Shell.astro",5222 "tests/auth_e2e_test.ts",5323 "tests/author-boundary_test.ts",5424 "tests/convergence_e2e_test.ts",@@ -1,53 +1,23 @@
11 {22 ".env.example": "9b21194b6be854a0c3c14ce03e664a81d7102924c3bc3db00e51464df44a1c84",3- ".gitignore": "9efb15c6a9bfc20f296ed712e74273b6015a0dd9a40f7e861b8711dcf61825f1",4- "astro.config.ts": "8a19cae348af7fcbd9db119d2423dd2e464f0e5ace6e5723d06cd3a1cc9a0d4b",5- "deno.json": "74841f3f60de9ea13ae218818a90e79043db2805cdebabbc02224e5b40ee3765",3+ ".gitignore": "9b9fa4b95f8f728ce80e93fc224d430328786b620096b80f067a3190c410a00e",4+ "deno.json": "663cd027bee884c7e5ac6f5607c0a9ef2a0fac922ab94fe228be4f9eeb15c92d",65 "public/favicon.svg": "23efdb888dae688715faafe3ef94f7b5ad1976afcc715c0a742218b54732c6ad",76 "public/manifest.webmanifest": "7d492285fd982e6e68fa04f12d78cd4f0bef4343f0408e314f1582029f3ea407",8- "public/sw.js": "6fa9f4c4b5ef7baabd48256a91520c766a4ffaf75c261686384d6d13e663d0c2",9- "README.md": "51d08f08312c9a555368e1e57a265dad7cc540016d8d4031b7429e6d577e5471",10- "src/_lofi/auth_test.ts": "771789f0bc48f27c95340e7d8dac452cf87a63e536736a00ac006433aa16e83b",11- "src/_lofi/auth.ts": "19c0cfb08f0c00a05421bdcfcbd01ad099e52997793cbf0e89de0e25d385fc4f",12- "src/_lofi/boot.ts": "0f813fbf75d0ba420dcf893e2762ec84be75869b70ef4f197a367c691773474b",13- "src/_lofi/config.ts": "374e8eba1c08cf80bec19cdb35c6ef242ae15cc859700013fec9c92d900c70fe",14- "src/_lofi/device-capabilities_test.ts": "be099ef5f0b3623dc2768b5b588f7e0eb313fea8cf31e4d86b3a52ed1f85cd5a",15- "src/_lofi/device-capabilities.ts": "6911fbe8ce93cdd79531982fdafd505202141985baec874e4dd965b02e6115d3",16- "src/_lofi/DeviceStatus.tsx": "0b5f4480ae544f3183080dbdcaf7d7b0a1030e5ac3433ace78639412b46c98d6",17- "src/_lofi/diagnostics.ts": "abb2a5f8e4d6b8ab1a86d475db6c374aecf4580b534ba9d7d850e1dbb29bb7f8",18- "src/_lofi/foreground-recovery_test.ts": "a5081aecaa70b71c63d721a0875f4d2b3898a49879eb8045c8e74baf0d867444",19- "src/_lofi/foreground-recovery.ts": "a5ed7a65b9454770de62a4ba8cdac7b0d1a867ec3c703049d2c103791c4d0049",20- "src/_lofi/inspector_test.ts": "cc0d5514cf792cf1627f277794dcb6fd511e2d2c8f1e79bda47b24833ce53394",21- "src/_lofi/inspector.ts": "1638b01913c97e01faf86d721128a8d6e1c8df0fbf446c04b25e0e53f944b424",22- "src/_lofi/lifecycle.ts": "0652ce9d4564b830771522986e9d3d6a2afe0849661b43f2d8b4e47ff16c1a1d",23- "src/_lofi/probe.ts": "20011497e8f6f14825fe76348e5b1e96b5e42bc466d8b9a5598cf8e65653ec16",24- "src/_lofi/pwa_test.ts": "bc9c04ff61e2f5a0dfd4601fbab929aa3815f4b174fad600d68b1f74f070d346",25- "src/_lofi/pwa.ts": "9ee6958a63857bb87f9afb58595eb138ef0865fd7c8a4014ac8f90f36813f39a",26- "src/_lofi/recovery_test.ts": "4fffd6ba780389e1411985377f7037e9007b78174fea5e036dc201575a0aa87c",27- "src/_lofi/recovery.ts": "59b5e0a77dae3650611b7b9b807d1e75f749db934f8b5aae7fc34cda72ec56ea",28- "src/_lofi/resource-lifecycle_test.ts": "9a43053351b8b90f3b67a2953c3ff159e0337ab8009f1d061e9e2bf7382abf43",29- "src/_lofi/resource-lifecycle.ts": "4e017f4391f18854486044709041f163308fa3fc84e08d5ec7f45c39e3ae3cb5",30- "src/_lofi/runtime.ts": "a33d7a802d4ae0a5d34efc11f958a25f55f84db23fa3732a468ed427c74eb3ac",31- "src/_lofi/session.ts": "783dc6d4fff855f7e8486b5a2e22db881b02faac809631cc2c509f31daa4a368",32- "src/_lofi/Shell.astro": "a2817146d1fe62cfdc399669709996b468823a8828257abf63b445c4c8eac912",33- "src/_lofi/table-store_test.ts": "1b8cfd06192b9c0daacc8a8c7811b691338f4ef459b209b3830fb87a0a039955",34- "src/_lofi/table-store.ts": "7ad445cc34c11e0cab3afcf1cde097345f1a53f2583da6c7fd00c7cb3e082df8",35- "src/_lofi/test-assert.ts": "38549889964a4e92a0d63ee514c746cae5a43e6160cea4f2075a9e61543d3744",36- "src/_lofi/ui-mutation_test.ts": "16153da95e080570af254eb0c56433f2db8abacec542984f59f6afd4ebf1c501",37- "src/_lofi/ui-mutation.ts": "3273053a796160556045d06d672f4c79d9597e1f822928f1ee71a7d31865dc32",38- "src/_lofi/use-device-capabilities.ts": "f7c7b2321b400d8076a5347e0f1ff2924623535979d48a2d94a69f825b93841e",39- "src/app.ts": "4b4a5c77b92d59e38649e87c28db5890520c31290de15a438b2b67ff8608553f",7+ "README.md": "777d1e5a00bf4501ac83d584b779467910b26dd8fdfc0e15f37c11c968cadb67",8+ "src/app.ts": "be92b21b00f9b737aef6676928573c02f058700058cdf7d8838df5a4933dd26f",409 "src/env.d.ts": "b44daed05ec5cdfacfd8d8acf7866974b5f6b8db923ab53bd244419093c719da",41- "src/islands/AccountGate.tsx": "7355d496180ee45a6144f10b96d1ec39e2d4427ffeab6e574c9b78a601480138",42- "src/islands/TaskList.tsx": "a3d054078819ea8b5609a014e94878bf1b21b2908c340237ed029e80e58f2349",43- "src/islands/use-tasks.ts": "1df5748b14427755352b8438977ce8720abc42f1e2f69bf1e927ceaa52166246",44- "src/pages/index.astro": "6bcbcbd1c1aad8d55855e5f52473b7a7711c439d4877ef54fe7f6f5d4f88ec3c",10+ "src/islands/AccountGate.tsx": "8dcb98ba3f2f7cb1b0a7494b7ac6cec02d65bab1f78fe6e4be88349e6a8227d2",11+ "src/islands/TaskList.tsx": "b38d32ecc08c60e1bef3c70e3e96914df1a2037419253324c4ac48443531f7e7",12+ "src/islands/use-tasks.ts": "2ad1649b64f677bb042861d988f54736e5442bae8f4e335fc0e50f51b2a260f1",13+ "src/layouts/Shell.astro": "50203c979679cfa1f9af461f053db29d8af3e4705497c80233fb94e4ba99c9c5",14+ "src/pages/index.astro": "de9ad86f9b25f7aff1e293931a785dbe393e4d0a35464ffbb649a0ebb4518281",4515 "src/permissions.ts": "dcb00b7f0576d04ad527b8892b8096573b107c3130ed6d835243251c406a38b7",4616 "src/schema.ts": "f2c555b088ded8eea2653f5d955fc9aae69dfd588e891adf509e99052f1e9949",4717 "src/styles/global.css": "27e98368f3fa1c57befc038cc9bf36f800fef192e368f4c393327ac96bfdd448",4818 "tests/auth_e2e_test.ts": "87dc3eb441eeaa96fc82b15df90bf4108096506217e5bfae907b483a3e75b4bf",49- "tests/author-boundary_test.ts": "529bbd81977294398f4c48a3616d10cdcf0fe5f7c83b705365ad80124ab1a0f1",19+ "tests/author-boundary_test.ts": "3f0a605d1b334577d1ec02cf43073ee0c612cad739a16d6f82360919e5de63fb",5020 "tests/convergence_e2e_test.ts": "71ec0ac935c25ff0722905e5b195c730f32a3b886dd4172e11d9e0e80f19cb38",5121 "tests/testing-contract_test.ts": "4257306060d42566b879d92b3a6c395805b20f53717bee55d670c0ef7a117eec",52- "tsconfig.json": "fe2e5acc66101ed42d893d7e7063d07d298927621b351adcfb1ebde6c3a0a2ac"22+ "tsconfig.json": "c632473513c623f3cc69d7476159fb9421624521220029755695defa265c6cab"5323 }@@ -3,6 +3,7 @@ import { extname, join, relative, resolve } from "node:path";
33 const ignoredDirectories = new Set([44 ".astro",55 ".git",6+ ".lofi",67 ".playwright-cli",78 ".vite",89 "coverage",@@ -25,15 +26,12 @@ export async function projectChecks(root = Deno.cwd()): Promise<ProjectCheck[]>
2526 // example is replaced.2627 const required = [2728 "deno.json",28- "astro.config.ts",2929 "src/schema.ts",3030 "src/permissions.ts",3131 "src/app.ts",3232 "src/pages/index.astro",33- "src/_lofi/boot.ts",34- "src/_lofi/runtime.ts",33+ "src/layouts/Shell.astro",3534 "public/manifest.webmanifest",36- "public/sw.js",3735 ];3836 const missing: string[] = [];3937 for (const path of required) {@@ -34,14 +34,14 @@ Deno.test("source fingerprint is stable across file creation order and changes w
3434 }3535 });3636 37-Deno.test("source fingerprint includes authored service worker JavaScript", async () => {37+Deno.test("source fingerprint includes authored public JavaScript", async () => {3838 const root = await makeTestRoot();3939 try {4040 await Deno.mkdir(join(root, "public"));41- await Deno.writeTextFile(join(root, "public", "sw.js"), "const revision = 'one';\n");41+ await Deno.writeTextFile(join(root, "public", "app.js"), "const revision = 'one';\n");4242 const first = await sourceFingerprint(root);43- await Deno.writeTextFile(join(root, "public", "sw.js"), "const revision = 'two';\n");44- assert(first !== await sourceFingerprint(root), "fingerprint ignored service worker source");43+ await Deno.writeTextFile(join(root, "public", "app.js"), "const revision = 'two';\n");44+ assert(first !== await sourceFingerprint(root), "fingerprint ignored authored public source");4545 } finally {4646 await Deno.remove(root, { recursive: true });4747 }