clone

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

docs: adopt @nzip/lofi package naming

Dami committed 6e5ebab
commit
6e5ebabf25d1286803471fb7a2c5bc770bcd69d0
parent
697bebdbb0db61b113dd4dbc6516731edf5e0a60
author
Dami <dami.etoile@gmail.com>
4 files changed, +83 -52
Mapps/reference/README.md7++-
Adocs/decisions/0005-publish-one-nzip-lofi-package.md29++++++++++++
Mdocs/devx-contract.md25++++++----
Minit.md74++++++++++++++----------------
M apps/reference/README.md +4 -3
A docs/decisions/0005-publish-one-nzip-lofi-package.md +29 -0
M docs/devx-contract.md +15 -10
@@ -30,11 +30,16 @@ This is the eventual product experience. The generator itself is M2 work and is
3030 deliverable.3131 3232 ```sh33-deno run -A jsr:@lofi/create my-app33+deno run -A jsr:@nzip/lofi/create my-app3434 cd my-app3535 deno task dev3636 ```3737 38+The planned JSR surface is one package, `@nzip/lofi`, with one version and publish operation.39+Framework and tooling boundaries are subpath exports—starting with `./create`, `./core`, `./sync`,40+`./auth`, `./ui`, `./pwa`, and `./testing`—rather than lockstep packages. See41+[ADR 0005](decisions/0005-publish-one-nzip-lofi-package.md).42+3843 From there the developer makes a retained local write, reloads it, works offline, runs checks,3944 builds, previews, and opens the same stable secure origin on a physical device.4045
@@ -116,15 +121,15 @@ These are product outputs, not claims that every command is implemented at Contr
116121 prototype must implement `dev`, `check`, `test`, `build`, and `preview`; `create` and the complete117122 `doctor` experience graduate in M2.118123 119-| Command | Success output must contain | Failure output must contain |120-| ------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------- |121-| `deno run -A jsr:@lofi/create <name>` | Created path and exact `cd`/`deno task dev` next steps. | Conflicting input/path, unchanged-files guarantee, and corrective command. |122-| `deno task dev` | Usable URL plus storage, identity, sync, and PWA states. | Failed subsystem, developer impact, and one remediation. |123-| `deno task doctor` | Versioned capability/configuration table without values. | Invalid/unsupported item plus remediation; nonzero exit for blockers. |124-| `deno task check` | Checks run and concise pass summary. | First failing check and rerun command. |125-| `deno task test` | Suites, durations, and retained failure-artifact location. | Failing scenario and artifact location without fixed-sleep advice. |126-| `deno task build` | Output path, route count, and secret-scan result. | Failed internal tool/adapter and supported fallback action. |127-| `deno task preview` | Production URL and build identity. | Missing/stale build and exact build command. |124+| Command | Success output must contain | Failure output must contain |125+| ------------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------- |126+| `deno run -A jsr:@nzip/lofi/create <name>` | Created path and exact `cd`/`deno task dev` next steps. | Conflicting input/path, unchanged-files guarantee, and corrective command. |127+| `deno task dev` | Usable URL plus storage, identity, sync, and PWA states. | Failed subsystem, developer impact, and one remediation. |128+| `deno task doctor` | Versioned capability/configuration table without values. | Invalid/unsupported item plus remediation; nonzero exit for blockers. |129+| `deno task check` | Checks run and concise pass summary. | First failing check and rerun command. |130+| `deno task test` | Suites, durations, and retained failure-artifact location. | Failing scenario and artifact location without fixed-sleep advice. |131+| `deno task build` | Output path, route count, and secret-scan result. | Failed internal tool/adapter and supported fallback action. |132+| `deno task preview` | Production URL and build identity. | Missing/stale build and exact build command. |128133 129134 Example successful development output:130135
M init.md +35 -39
@@ -4,7 +4,7 @@
44 > contract lives in `docs/devx-contract.md`. M1 spikes are expected to correct or reject claims in55 > this draft as Jazz 2 and target-browser behavior are verified.66 7-Working name **`lofi`** (import paths like `@lofi/core`). Goal: validate that Jazz + Preact7+Working name **`lofi`** (import paths like `@nzip/lofi/core`). Goal: validate that Jazz + Preact88 islands + Astro + Deno can be wrapped into an ergonomic framework where the UI always hydrates from99 local data, identity is passkey-only, and **the only network surface an app developer ever sees is1010 the sync module's primitives/hooks**.
@@ -29,8 +29,8 @@ targeting Android Chrome 148+ / iOS Safari 16.4+ as hard floors**.
2929 ## Platform floors (enforced, not advisory)3030 3131 Jazz 2.0's only fallback below these floors is memory-only storage, which silently loses data — so32-`@lofi/core`'s `detect()` is a **boot gate**: unsupported browser → explicit "unsupported" screen33-(or opt-in memory mode with a "nothing will persist" banner). Never a silent fallback.32+`@nzip/lofi/core`'s `detect()` is a **boot gate**: unsupported browser → explicit "unsupported"33+screen (or opt-in memory mode with a "nothing will persist" banner). Never a silent fallback.3434 3535 | Feature | iOS Safari tab | iOS installed PWA | Android Chrome 148+ | Framework behavior |3636 | ----------------------------------------------- | ----------------------------------------------- | ----------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
@@ -44,35 +44,31 @@ Jazz 2.0's only fallback below these floors is memory-only storage, which silent
4444 | WebAuthn passkeys | ✅ iCloud Keychain | ✅ | ✅ | Primary auth |4545 | WebAuthn PRF | Safari 18+, no external keys | same | robust | Stretch phase only; feature-detect, never require |4646 47-App code never sniffs user agents — `@lofi/pwa` and `@lofi/sync` branch on the `Capabilities` object48-from `detect()`.47+App code never sniffs user agents — `@nzip/lofi/pwa` and `@nzip/lofi/sync` branch on the48+`Capabilities` object from `detect()`.4949 5050 ## Monorepo layout (Deno workspace)5151 5252 ```5353 lofi/54-├── deno.json # workspace: ./packages/*, ./apps/demo; shared tasks54+├── deno.json # workspace: ./packages/lofi, ./apps/demo; shared tasks5555 ├── packages/56-│ ├── core/ # @lofi/core — schema (re-export co/z + defineSchema), createApp()57-│ │ # singleton client (lazy, auth-gated), platform.ts detect() boot gate58-│ ├── auth/ # @lofi/auth — registerPasskey/signInWithPasskey/recoverWithPassphrase,59-│ │ # authState signal: "loading" | "anonymous" | "signedIn"60-│ ├── sync/ # @lofi/sync — THE ONLY NETWORK-AWARE MODULE: syncState signal61-│ │ # (connected/syncing/pendingOps/lastSyncedAt), pauseSync/resumeSync,62-│ │ # lifecycle.ts: visibility/online reconnect manager (iOS WS-drop fix)63-│ ├── ui/ # @lofi/ui — Preact hooks: useCoState, useAccount, useAuthState,64-│ │ # useSyncStatus, useOnline; adapter-compat.ts (Plan A) / adapter-vanilla.ts (Plan B)65-│ ├── pwa/ # @lofi/pwa — lofiPwa() @vite-pwa/astro config preset, install-prompt66-│ │ # orchestration (beforeinstallprompt / iOS coach-mark / EU degrade),67-│ │ # requestPersistentStorage() incl. Safari notification-permission dance68-│ └── testing/ # @lofi/testing — Playwright virtual-authenticator + offline/sync test helpers56+│ └── lofi/ # @nzip/lofi — one JSR package, one version, subpath exports57+│ ├── core/ # ./core — schema (re-export co/z + defineSchema), createApp(), detect()58+│ ├── auth/ # ./auth — passkey registration/sign-in/recovery and authState59+│ ├── sync/ # ./sync — THE ONLY NETWORK-AWARE MODULE and lifecycle manager60+│ ├── ui/ # ./ui — context-free Preact hooks and adapter implementation61+│ ├── pwa/ # ./pwa — PWA config, install orchestration, persistent-storage UX62+│ ├── testing/ # ./testing — virtual-authenticator and offline/sync test helpers63+│ └── create/ # ./create — non-interactive project creation command6964 └── apps/demo/ # Astro app: "Ticktick-lite" checklist; schema.ts, app.ts, islands/7065 ```7166 72-Dependency direction (strict): `demo → ui,pwa,auth,sync,core` · `ui → sync,auth,core` · `pwa → core`73-· `sync → core` · `auth → core` · `core → npm:jazz-tools` and nothing else. **Only `sync` may74-construct or configure a network peer** — the enforcement point for the "sync is the only network75-surface" constraint.67+The public modules are subpath exports of `@nzip/lofi`, not separately versioned packages.68+Dependency direction remains strict inside that package: `demo → ui,pwa,auth,sync,core` ·69+`ui → sync,auth,core` · `pwa → core` · `sync → core` · `auth → core` · `core → npm:jazz-tools` and70+nothing else. **Only `sync` may construct or configure a network peer** — the enforcement point for71+the "sync is the only network surface" constraint.7672 7773 Multi-tab coordination comes free: the SharedWorker Jazz 2.0 already requires is the single owner of7874 storage (and possibly sync) across tabs. Open question for Spike 0: does Jazz 2.0 run the sync
@@ -83,7 +79,7 @@ manager.
8379 8480 ```ts8581 // schema.ts86-import { co, defineSchema, z } from "@lofi/core";82+import { co, defineSchema, z } from "@nzip/lofi/core";8783 export const Task = co.map({ text: z.string(), done: z.boolean(), createdAt: z.date() });8884 export const TaskList = co.list(Task);8985 export const schema = defineSchema({
@@ -95,19 +91,19 @@ export const schema = defineSchema({
9591 });9692 9793 // app.ts — module scope, imported by every island (Vite guarantees one instance per page)98-import { createApp } from "@lofi/core";94+import { createApp } from "@nzip/lofi/core";9995 export const app = createApp({10096 schema,10197 sync: { peer: `wss://cloud.jazz.tools/?key=${import.meta.env.PUBLIC_JAZZ_KEY}` },10298 storage: "opfs",10399 });104100 // createApp() registers config only; the client boots lazily on first hook/auth call,105-// and the sync peer is attached exclusively by @lofi/sync.101+// and the sync peer is attached exclusively by @nzip/lofi/sync.106102 ```107103 108104 ```tsx109105 // islands/TaskListView.tsx110-import { useCoState } from "@lofi/ui";106+import { useCoState } from "@nzip/lofi/ui";111107 export default function TaskListView({ listId }) {112108 const list = useCoState(TaskList, listId, { resolve: { $each: true } });113109 if (list === undefined) return <Spinner />; // still loading from OPFS
@@ -155,15 +151,15 @@ exists, which is what makes multi-island apps work.
155151 156152 ## Risk-ordered roadmap157153 158-| # | Phase | Duration | Learn / kill criteria |159-| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |160-| 0 | **Spike: preact/compat × jazz-tools 2.0** — bare Vite+Preact page (no Astro, no Deno), Plan A then Plan B, anonymous account, `useCoState` round-trip to Jazz Cloud; measure gzipped bundle; find where the sync socket lives (worker vs tab) | 2 days | Which adapter plan; bundle cost. **Kill:** both plans fail to subscribe reliably → Jazz choice is dead, re-evaluate data layer |161-| 0.5 | **Spike: OPFS persistence on real devices** — same page with `storage: "opfs"` on a real iPhone (Safari tab + installed) and a real Chrome-148 Android; kill/relaunch, check data survives; multi-tab open | 2 days | Does Jazz 2.0 persistent mode actually work on WebKit's SharedWorker. **Kill:** broken on iOS → fall back to jazz-tools 0.20.x/IndexedDB (architecture unchanged — `storage:` is a named seam) |162-| 1 | **Spike: PasskeyAuth on real devices** — register/sign-in/passphrase-recovery over real HTTPS on iOS Safari tab, installed PWA, Android Chrome, WebAPK; verify the credential survives install | 3 days | Does Jazz's passkey custody work on mobile Safari (its PRF/largeBlob mechanism is undocumented — this is where we find out). **Kill:** fails on iOS → passphrase-primary UX or reconsider |163-| 2 | **Astro shell + islands + Deno toolchain** — scaffold workspace, port spikes into `@lofi/*`, two _separate_ islands sharing one client and live-updating each other (acceptance test for the singleton design); `deno task dev/build/serve` end-to-end with @deno/astro-adapter | 1 wk | Island-boundary state sharing works; npm-compat friction. **Soft kill:** Astro misbehaves under Deno → Node for build, Deno for serve; cap at 1 day |164-| 3 | **PWA hardening** — Workbox shell precache, manifest, Android install prompt, iOS coach-mark, EU-iOS degrade, `storage.persist()` incl. Safari notification-permission dance, reconnect-on-foreground verified on devices | 1 wk | Offline cold-start (airplane mode → installed PWA → shell + data render); WS reconnect reliability. Failures here are degradable UX, not fatal |165-| 4 | **Demo app "Ticktick-lite"** — checklists, shared lists via Group invite links (two accounts, two devices), sync badge, offline edits, conflict-free merge test | 1 wk | Is the API pleasant; where the abstraction leaks; invite-revocation gap surfaces concretely |166-| 5 | **(Stretch) PRF-derived E2E-at-rest** — WebAuthn PRF → HKDF → field-level encryption before data enters Jazz | optional | Only if a real threat model demands it; Safari 18 iCloud-Keychain-only; feature-detect, never require |154+| # | Phase | Duration | Learn / kill criteria |155+| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |156+| 0 | **Spike: preact/compat × jazz-tools 2.0** — bare Vite+Preact page (no Astro, no Deno), Plan A then Plan B, anonymous account, `useCoState` round-trip to Jazz Cloud; measure gzipped bundle; find where the sync socket lives (worker vs tab) | 2 days | Which adapter plan; bundle cost. **Kill:** both plans fail to subscribe reliably → Jazz choice is dead, re-evaluate data layer |157+| 0.5 | **Spike: OPFS persistence on real devices** — same page with `storage: "opfs"` on a real iPhone (Safari tab + installed) and a real Chrome-148 Android; kill/relaunch, check data survives; multi-tab open | 2 days | Does Jazz 2.0 persistent mode actually work on WebKit's SharedWorker. **Kill:** broken on iOS → fall back to jazz-tools 0.20.x/IndexedDB (architecture unchanged — `storage:` is a named seam) |158+| 1 | **Spike: PasskeyAuth on real devices** — register/sign-in/passphrase-recovery over real HTTPS on iOS Safari tab, installed PWA, Android Chrome, WebAPK; verify the credential survives install | 3 days | Does Jazz's passkey custody work on mobile Safari (its PRF/largeBlob mechanism is undocumented — this is where we find out). **Kill:** fails on iOS → passphrase-primary UX or reconsider |159+| 2 | **Astro shell + islands + Deno toolchain** — scaffold workspace, port spikes into `@nzip/lofi` subpath modules, two _separate_ islands sharing one client and live-updating each other (acceptance test for the singleton design); `deno task dev/build/serve` end-to-end with @deno/astro-adapter | 1 wk | Island-boundary state sharing works; npm-compat friction. **Soft kill:** Astro misbehaves under Deno → Node for build, Deno for serve; cap at 1 day |160+| 3 | **PWA hardening** — Workbox shell precache, manifest, Android install prompt, iOS coach-mark, EU-iOS degrade, `storage.persist()` incl. Safari notification-permission dance, reconnect-on-foreground verified on devices | 1 wk | Offline cold-start (airplane mode → installed PWA → shell + data render); WS reconnect reliability. Failures here are degradable UX, not fatal |161+| 4 | **Demo app "Ticktick-lite"** — checklists, shared lists via Group invite links (two accounts, two devices), sync badge, offline edits, conflict-free merge test | 1 wk | Is the API pleasant; where the abstraction leaks; invite-revocation gap surfaces concretely |162+| 5 | **(Stretch) PRF-derived E2E-at-rest** — WebAuthn PRF → HKDF → field-level encryption before data enters Jazz | optional | Only if a real threat model demands it; Safari 18 iCloud-Keychain-only; feature-detect, never require |167163 168164 ## Verification169165
@@ -174,7 +170,7 @@ exists, which is what makes multi-island apps work.
174170 - **Per-phase device pass:** one-page manual checklist on a physical iPhone (tab + installed) and175171 Android (tab + WebAPK); debug via Safari Web Inspector over USB and `chrome://inspect`.176172 - **CI-able WebAuthn:** Playwright CDP virtual authenticator (resident key + user verification) in177- `@lofi/testing` for register/sign-in/recovery. Real-device passkey behavior still verified173+ `@nzip/lofi/testing` for register/sign-in/recovery. Real-device passkey behavior still verified178174 manually in Spike 1 (virtual authenticators don't model iCloud Keychain or PRF quirks).179175 - **Offline:** Playwright `context.setOffline(true)` for automated shell+data cold-start; manual180176 airplane mode on devices for the installed path.
@@ -190,8 +186,8 @@ exists, which is what makes multi-island apps work.
190186 2. **Jazz 2.0 OPFS persistence on WebKit** — SharedWorker on iOS Safari exists but is quirk-prone;191187 Spike 0.5 decides. Fallback to 0.20.x/IndexedDB costs nothing architecturally (`storage:` seam).192188 3. **Alpha API churn** — pin the exact 2.0-alpha version; expect breakage on every bump;193- `@lofi/core` is the single jazz-tools chokepoint containing migration cost. 2.0 docs are sparse —194- read the changelog and source when docs fail.189+ `@nzip/lofi/core` is the single jazz-tools chokepoint containing migration cost. 2.0 docs are190+ sparse — read the changelog and source when docs fail.195191 4. **Pre-148 Android installed base** — SharedWorker only returned in Chrome 148 (stable ~May–June196192 2026); older devices hit the boot gate. Acceptable for a prototype; revisit for any real release.197193 5. **Astro-under-Deno friction** — pre-agreed fallback: Node for `astro build`, Deno for everything