- add a package description to deno.json (hasDescription) - add module-level @module docs to all 7 JSR entrypoints (allEntrypointsDocs) - add JSDoc to the exported symbols across package/testing/* (documented symbols was ~10%) - rewrite the root README to lead with `create` and what a generated project looks like, drop the stale 'nothing published yet' + repo-internal map, and fix links to removed files Provenance was already recorded for 0.1.0 (sigstore transparency log); the score API just lagged. multipleRuntimesCompatible is left false on purpose — this is a Deno-only CLI/toolkit and claiming Node/browser support would be dishonest.
- commit
- 8397e65635abc9e798962c242693e3424b2cd4b8
- parent
- 57718ef68ac0a21ac3517ff12688101b523919a5
- author
- Dami <dami.etoile@gmail.com>
@@ -1,33 +1,13 @@
1-# lofi1+# @nzip/lofi22 3-**A local-first mobile PWA meta-framework, built as an honest prototype.**4-5-Write a schema, build Preact islands, ship an installable offline-capable app. The UI always6-hydrates from local data, identity never waits on a server, and the sync module is the only network7-surface an app developer ever sees.3+**A local-first mobile PWA meta-framework for Deno.** Declare a schema, build Preact islands, ship4+an installable offline-capable app. The UI always hydrates from local data, identity never waits on5+a server, and the sync module is the only network surface you touch.86 97 Built on [Jazz 2](https://jazz.tools) (CRDT data + sync) · [Preact](https://preactjs.com) islands in108 [Astro](https://astro.build) · the [Deno](https://deno.com) toolchain.119 12-> **Status: working prototype, pre-release.** Nothing is published yet. M0 (DevX contract) and M113-> (feasibility) are complete; all three M2 source layers are merged on `m2` and undergoing combined14-> promotion review. See the [roadmap](#roadmap).15-16-## Why lofi17-18-- **Local-first without the plumbing.** Product code touches schema, config, islands, styles, and19- tests — never providers, workers, transports, or service-worker internals20- ([DX-AUTHOR-01, DX-LEAK-01](docs/devx-contract.md#measurable-promises)).21-- **Honest by construction.** Durable storage never silently degrades to memory; diagnostics only22- claim what the underlying APIs can actually observe; identity wording matches real custody and23- recovery semantics.24-- **Mobile PWA first.** Platform floors (Android Chrome 148+, iOS Safari 16.4+) are enforced boot25- gates, not advisory footnotes. Unsupported browsers get an explicit answer, never silent data26- loss.27-28-## The north star29-30-When every milestone below has landed, this is the entire onboarding experience:10+## Quick start3111 3212 ```sh3313 deno run -A jsr:@nzip/lofi/create my-app@@ -35,105 +15,89 @@ cd my-app
3515 deno task dev3616 ```3717 38-From there a developer makes a retained local write, reloads it, works offline, runs checks, builds,39-previews, and opens the same stable secure origin on a physical phone — with Deno as the only40-required global runtime and a command surface of exactly seven verbs: `create`, `dev`, `doctor`,41-`check`, `test`, `build`, `preview`.18+That's the whole onboarding. You now have an installable, offline-capable local-first PWA — make a19+write, reload it, go offline, and the UI keeps working because it reads from durable local storage,20+not the network.4221 43-## Try it today (checkout journey)22+> Requires **Deno 2.9+**, the only global runtime. With no `.env` the app runs local-only (stated23+> explicitly at boot); for managed sync, copy `.env.example` and set the public `JAZZ_APP_ID` /24+> `JAZZ_SERVER_URL` pair. Server-only secrets never reach client output.4425 45-The `m2` branch contains the accepted source generator, package-owned commands, development46-inspector, and browser-testing helpers. The package is not published until explicitly authorized and47-the registry-backed smoke passes, so the currently available journey runs from a checkout:26+## What a generated project looks like4827 49-```sh50-git clone https://github.com/FelineStateMachine/lofi.git51-cd lofi52-deno task dev53-```54-55-- **Deno 2.9+** is the only required global runtime.56-- **No `.env` means local-only mode**, stated explicitly at boot. For cloud sync, copy57- `.env.example` and set the public `JAZZ_APP_ID`/`JAZZ_SERVER_URL` pair. Server-only secrets never58- reach client output — this is tested, not hoped.59-- From `deno task dev` you should reach a retained local write in under 60 seconds60- ([DX-PROTOTYPE-01](docs/devx-contract.md#measurable-promises)).61-62-### Command surface63-64-| Command | What it does today |65-| ------------------- | ------------------------------------------------------------------------ |66-| `deno task dev` | Serves the prototype; prints URL plus storage, identity, and sync state. |67-| `deno task check` | Format, lint, type, Astro, env-contract, and secret-leak checks. |68-| `deno task test` | Deterministic local-first tests (no hand-timed sleeps). |69-| `deno task build` | Production build through the supported Deno path. |70-| `deno task preview` | Serves the production build. |71-72-`create` and `doctor` have graduated through the source-backed generated-project golden journey and73-clean-room review. The public JSR invocation remains unavailable until publication and its74-registry-backed smoke pass; the output contracts are specified in the75-[DevX contract](docs/devx-contract.md#canonical-command-surface-and-output-contract).76-77-## How this repository works78-79-Development here is **contract-driven**: the product is a set of falsifiable promises, and code80-graduates only by producing evidence against them.81-82-1. **[The DevX contract](docs/devx-contract.md)** defines measurable promises with IDs (like83- `DX-LOCAL-01`), budgets, and statuses (`proposed → validated / revised / rejected`).84-2. **[Decision records](docs/decisions/)** capture each experiment: question, exact versions,85- control, procedure, retained evidence, and the resulting contract delta. A spike is complete when86- it supports a decision — including a negative one — not when a demo renders once.87-3. **[Milestones](https://github.com/FelineStateMachine/lofi/milestones)** gate scope. Issues carry88- `area:*`, `type:*`, and `priority:p0–p2` labels; p0 blocks the golden path.89-90-Notable honest outcomes so far: the Jazz 2 alpha passkey-backup ceremony was **rejected** on91-security grounds rather than demoed92-([decision 0004](docs/decisions/0004-reject-alpha53-passkey-backup.md)), and live transport state is93-reported as _unavailable_ rather than faked, because the pinned API exposes no supported signal.94-95-## Roadmap96-97-| Milestone | Goal | Status |98-| ------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------- |99-| **M0 — DevX contract** | Define the golden path, command surface, environment contract, and measurable acceptance criteria. | ✅ Done |100-| **M1 — Feasibility spikes** | Prove Jazz 2, Preact, OPFS, identity, Astro islands, and Deno behavior with go/no-go decisions. | ✅ Done |101-| **M2 — DevX vertical slice** | `create`, `dev`, `doctor`, diagnostics, inspector, and testing workflows around a reference app. | 🧪 Promotion review |102-| **M3 — Mobile PWA hardening** | Install, persistence, offline cold start, lifecycle recovery, and physical-device validation. | ⏳ Queued |103-| **M4 — Runtime API extraction** | Extract remaining proven runtime seams as `@nzip/lofi` subpaths; validate them through a second app. | ⏳ Queued |104-105-The definition of done for the whole prototype is the [north star](#the-north-star) journey passing106-its contract budgets end-to-end, on real devices.107-108-## Repository map28+A new project is split into two zones — what you own, and the runtime you don't touch:10929 11030 ```text111-lofi/112-├── docs/113-│ ├── devx-contract.md # The product contract: promises, budgets, statuses114-│ └── decisions/ # Evidence-backed decision records (ADR-style)115-├── apps/reference/ # The integrated app graduated from M1 and reshaped in M2116-├── package/ # The single @nzip/lofi package source and generated-project commands117-├── spikes/ # M1 feasibility experiments, kept for evidence118-├── tools/ # Deno tasks: env contract, secret scanning, runners119-├── init.md # Original research plan — input, not the contract120-├── deno.json # Workspace tasks and pinned toolchain121-└── jsr.json # One package name, version, export map, and publish manifest31+my-app/32+├── deno.json # tasks + pinned toolchain33+├── astro.config.ts34+├── public/ # manifest, service worker, icons35+├── src/36+│ ├── schema.ts # ← your data model37+│ ├── permissions.ts # ← your access policy38+│ ├── app.ts # ← your app configuration39+│ ├── pages/ # ← your Astro shell40+│ ├── islands/ # ← your Preact UI (starts as a minimal task list to replace)41+│ ├── styles/42+│ └── _lofi/ # runtime: storage, identity, sync, lifecycle, diagnostics — don't edit43+└── tests/ # your tests + the @nzip/lofi/testing contract12244 ```12345 46+You write `schema.ts`, `permissions.ts`, islands, and styles. Everything under `src/_lofi/` is the47+framework runtime; product work never needs to touch it.48+49+## Commands50+51+Every generated project ships this task surface (`deno task <name>`):52+53+| Command | What it does |54+| --------------------------------------- | ----------------------------------------------------------- |55+| `dev` | Astro dev server; prints storage, identity, and sync state. |56+| `doctor` | Value-free readiness report — no secrets, no faked claims. |57+| `test` | Deterministic local-first tests (no hand-timed sleeps). |58+| `build` | Static production build into `dist/`. |59+| `preview` | Serves the production build locally. |60+| `deploy` / `deploy:create` | Host the static build on Deno Deploy. |61+| `schema:validate` / `schema:deploy` | Validate and publish your Jazz schema. |62+| `migrations:create` / `migrations:push` | Author and push schema migrations. |63+64+## Testing local-first behavior65+66+`@nzip/lofi/testing` provides Playwright-backed helpers for the parts that are genuinely hard to67+test — offline and multi-client behavior: a two-client fixture, concurrent-offline convergence,68+readiness waits without arbitrary sleeps, and value-free (secret-free) failure artifacts. Every69+project includes a worked example at `tests/convergence_e2e_test.ts`.70+71+## Why lofi72+73+- **Local-first without the plumbing.** Product code touches schema, config, islands, styles, and74+ tests — never providers, workers, transports, or service-worker internals.75+- **Honest by construction.** Durable storage never silently degrades to memory; diagnostics only76+ claim what the underlying APIs can actually observe; identity wording matches real custody and77+ recovery semantics.78+- **Mobile PWA first.** Platform floors (Android Chrome 148+, iOS Safari 16.4+) are enforced boot79+ gates, not advisory footnotes. Unsupported browsers get an explicit answer, never silent data80+ loss.81+12482 ## Stack12583 12684 | Layer | Choice | Pinned at |12785 | ---------- | --------------------------------------------- | ------------------- |128-| Data/sync | jazz-tools 2.0 alpha (exact pin), OPFS | `2.0.0-alpha.53` |86+| Data/sync | jazz-tools 2.0 alpha, OPFS | `2.0.0-alpha.53` |12987 | UI runtime | Preact islands, `client:only`, thin adapter | Preact 10 / Astro 7 |13088 | Shell | Astro, fully prerendered, static-through-Deno | Astro 7 |13189 | Toolchain | Deno tasks + npm-compat | Deno 2.9 |13290 133-Version pins are deliberate: the data layer is an alpha, so every bump is a reviewed decision, and134-`docs/decisions/` records what each pin was verified to do.91+Version pins are deliberate: the data layer is an alpha, so every bump is a reviewed decision.92+93+## Status94+95+Early release, pre-1.0. Passkey backup and recovery are intentionally absent — the pinned Jazz alpha96+exposes a rejected credential design — so a fresh project uses a device-local identity and does not97+imply recoverability.13598 136-## Contributing99+---137100 138-See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow: how promises, evidence, and decision139-records fit together, what `deno task check` enforces, and how issues and milestones are organized.101+This repository is also the framework's development monorepo: `package/` is the `@nzip/lofi` source102+and `apps/reference/` is the reference app the generator is validated against. See103+[CONTRIBUTING.md](CONTRIBUTING.md) and the [DevX contract](docs/devx-contract.md).@@ -2,6 +2,7 @@
22 "name": "@nzip/lofi",33 "version": "0.1.0",44 "license": "MIT",5+ "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.",56 "exports": {67 "./build": "./package/commands/build.ts",78 "./create": "./package/create.ts",@@ -1,5 +1,13 @@
11 22 3+/**4+ * The `deno task build` command: builds the static PWA into `dist/` with a5+ * source-hash build id, a service-worker precache manifest, and a scan that6+ * fails the build if server secrets leak into client output.7+ *8+ * @module9+ */10+311 import { extname, join } from "node:path";412 import { LOFI_VERSION } from "../version.ts";513 import { loadEnvironment, serverEnvironmentNames } from "../tooling/environment.ts";@@ -1,5 +1,13 @@
11 22 3+/**4+ * The `deno task doctor` command: prints a value-free readiness report covering5+ * the package, Deno, project layout, environment, storage, identity, sync, and6+ * PWA, and exits non-zero when the project is blocked.7+ *8+ * @module9+ */10+311 import { doctorReport, printDoctorReport } from "../tooling/diagnostics.ts";412 513 const report = await doctorReport();@@ -1,5 +1,12 @@
11 22 3+/**4+ * The `deno task preview` command: serves the built `dist/` output locally over5+ * HTTP for previewing the production PWA.6+ *7+ * @module8+ */9+310 import { extname, join, normalize } from "node:path";411 512 const portFlag = Deno.args.findIndex((argument) => argument === "--port");@@ -1,5 +1,12 @@
11 22 3+/**4+ * The `deno task test` command: runs the generated project's local-first test5+ * suite, retaining browser artifacts under `test-results/`.6+ *7+ * @module8+ */9+310 import { runDeno } from "../tooling/process.ts";411 import { exitOnFailure, validatedCommandEnvironment } from "./shared.ts";512 @@ -1,3 +1,4 @@
1+/** Thrown when Playwright's Chromium browser is not installed, with install guidance. */12 export class BrowserUnavailableError extends Error {23 override readonly name = "BrowserUnavailableError";34 @@ -9,6 +10,10 @@ export class BrowserUnavailableError extends Error {
910 }1011 }1112 13+/**14+ * Rethrow a browser launch failure as a {@link BrowserUnavailableError} when the15+ * message indicates a missing Chromium install; otherwise rethrow it unchanged.16+ */1217 export function rethrowBrowserLaunchError(error: unknown): never {1318 const message = error instanceof Error ? error.message : String(error);1419 if (@@ -1,12 +1,14 @@
11 import type { BrowserTestClient } from "./fixture.ts";22 import type { ValueFreeState } from "./safety.ts";33 4+/** Minimal client contract the convergence runner needs: offline state and toggles. */45 export interface OfflineTestClient {56 readonly offline: boolean;67 goOffline(): Promise<void>;78 goOnline(): Promise<void>;89 }910 11+/** Minimal two-client fixture contract required to drive an offline scenario. */1012 export interface OfflineTestFixture<Client extends OfflineTestClient> {1113 readonly clients: readonly [Client, Client];1214 readonly first: Client;@@ -19,6 +21,11 @@ export interface OfflineTestFixture<Client extends OfflineTestClient> {
1921 ): Promise<unknown>;2022 }2123 24+/**25+ * App-owned definition of a concurrent-offline convergence test: the two edits26+ * plus the hooks that assert readiness, apply and locally verify each edit, and27+ * confirm convergence after reconnection.28+ */2229 export interface ConcurrentOfflineScenario<2330 Edit,2431 Client extends OfflineTestClient = BrowserTestClient,@@ -44,9 +51,11 @@ export interface ConcurrentOfflineScenario<
4451 readonly failureLabel?: string;4552 }4653 54+/** Thrown when a convergence scenario fails, naming the stage that failed via {@link stage}. */4755 export class ConvergenceScenarioError extends Error {4856 override readonly name = "ConvergenceScenarioError";4957 58+ /** @param stage the lifecycle stage that was in progress when the scenario failed */5059 constructor(readonly stage: string, options?: ErrorOptions) {5160 super(`Concurrent offline convergence failed during ${stage}`, options);5261 }@@ -18,13 +18,19 @@ import {
1818 import { sanitizeTraceArchive } from "./trace.ts";1919 2020 type MemoryStorageState = Awaited<ReturnType<BrowserContext["storageState"]>>;21+/**22+ * Playwright context options with `storageState` forbidden, so identity never23+ * leaves memory via an on-disk state file.24+ */2125 export type SafeContextOptions = Omit<BrowserContextOptions, "storageState"> & {2226 /** Identity state is managed in memory by the fixture. File paths are forbidden. */2327 storageState?: never;2428 };2529 30+/** Identifies which of the fixture's two clients a value belongs to. */2631 export type ClientName = "first" | "second";2732 33+/** A redacted console, page-error, or failed-request record captured from a client. */2834 export interface BrowserDiagnostic {2935 readonly client: ClientName;3036 readonly kind: "console" | "page-error" | "request-failed";@@ -33,6 +39,11 @@ export interface BrowserDiagnostic {
3339 readonly url?: string;3440 }3541 42+/**43+ * How the two clients obtain identity: `shared` clones the primary's prepared44+ * state (in memory) into the second client, `isolated` prepares each client on45+ * its own.46+ */3647 export type IdentityOptions =3748 | {3849 readonly mode: "shared";@@ -45,6 +56,7 @@ export type IdentityOptions =
4556 readonly prepare?: (client: BrowserTestClient) => Promise<void>;4657 };4758 59+/** Configures where and how redacted failure artifacts are written on capture. */4860 export interface FailureArtifactOptions {4961 readonly directory: string;5062 /** Literal credential values to remove from retained diagnostics. */@@ -53,6 +65,7 @@ export interface FailureArtifactOptions {
5365 readonly mask?: (client: BrowserTestClient) => readonly Locator[];5466 }5567 68+/** Options for constructing a {@link TwoClientFixture} via {@link createTwoClientFixture}. */5669 export interface TwoClientFixtureOptions {5770 readonly baseURL: string;5871 readonly identity: IdentityOptions;@@ -63,6 +76,7 @@ export interface TwoClientFixtureOptions {
6376 readonly traceOnFailure?: boolean;6477 }6578 79+/** Re-exported so callers can catch a missing-Chromium launch failure. */6680 export { BrowserUnavailableError };6781 6882 function validateContextOptions(options: SafeContextOptions | undefined): void {@@ -73,6 +87,11 @@ function validateContextOptions(options: SafeContextOptions | undefined): void {
7387 }7488 }7589 90+/**91+ * One browser client (context + page) whose identity and IndexedDB stay in92+ * memory. Records redacted diagnostics and can go offline/online, restart, and93+ * capture a sanitized trace.94+ */7695 export class BrowserTestClient {7796 readonly #diagnostics: BrowserDiagnostic[] = [];7897 #context: BrowserContext;@@ -94,22 +113,27 @@ export class BrowserTestClient {
94113 this.#page = page;95114 }96115 116+ /** The client's current Playwright browser context. */97117 get context(): BrowserContext {98118 return this.#context;99119 }100120 121+ /** The client's current Playwright page. */101122 get page(): Page {102123 return this.#page;103124 }104125 126+ /** Whether the client's network is currently forced offline. */105127 get offline(): boolean {106128 return this.#offline;107129 }108130 131+ /** Redacted diagnostics collected from the client's pages so far. */109132 get diagnostics(): readonly BrowserDiagnostic[] {110133 return this.#diagnostics;111134 }112135 136+ /** Attach diagnostic listeners and, if enabled, begin failure tracing. */113137 async startRecording(): Promise<void> {114138 if (this.#recording || this.#closed) return;115139 this.#attachDiagnostics(this.#page);@@ -119,11 +143,13 @@ export class BrowserTestClient {
119143 this.#recording = true;120144 }121145 146+ /** Force the client's network offline. */122147 async goOffline(): Promise<void> {123148 await this.#context.setOffline(true);124149 this.#offline = true;125150 }126151 152+ /** Restore the client's network to online. */127153 async goOnline(): Promise<void> {128154 await this.#context.setOffline(false);129155 this.#offline = false;@@ -168,6 +194,10 @@ export class BrowserTestClient {
168194 return this.#page;169195 }170196 197+ /**198+ * Stop tracing and write a sanitized trace archive to `path`. Returns false199+ * when tracing was not active; removes the archive if sanitization fails.200+ */171201 async captureTrace(path: string): Promise<boolean> {172202 if (!this.#recording || !this.traceOnFailure) return false;173203 await this.#context.tracing.stop({ path });@@ -181,6 +211,7 @@ export class BrowserTestClient {
181211 return true;182212 }183213 214+ /** Discard any pending trace and close the client's context. Idempotent. */184215 async close(): Promise<void> {185216 if (this.#closed) return;186217 this.#closed = true;@@ -222,12 +253,19 @@ export class BrowserTestClient {
222253 }223254 }224255 256+/** The directory and file paths produced by a failure capture. */225257 export interface FailureArtifacts {226258 readonly directory: string;227259 readonly files: readonly string[];228260 }229261 262+/**263+ * Coordinates a pair of {@link BrowserTestClient}s sharing one browser, with264+ * helpers to take both clients offline/online and to capture redacted,265+ * value-free failure artifacts.266+ */230267 export class TwoClientFixture {268+ /** The fixture's two clients, ordered `[first, second]`. */231269 readonly clients: readonly [BrowserTestClient, BrowserTestClient];232270 #closed = false;233271 @@ -241,22 +279,31 @@ export class TwoClientFixture {
241279 this.clients = [first, second];242280 }243281 282+ /** The first client. */244283 get first(): BrowserTestClient {245284 return this.clients[0];246285 }247286 287+ /** The second client. */248288 get second(): BrowserTestClient {249289 return this.clients[1];250290 }251291 292+ /** Take both clients offline. */252293 async goOffline(): Promise<void> {253294 await Promise.all(this.clients.map((client) => client.goOffline()));254295 }255296 297+ /** Restore both clients to online. */256298 async goOnline(): Promise<void> {257299 await Promise.all(this.clients.map((client) => client.goOnline()));258300 }259301 302+ /**303+ * Write redacted screenshots, traces, diagnostics, and (optionally) a304+ * value-free state-shape snapshot under a label-named directory. Returns305+ * undefined when no artifact directory was configured.306+ */260307 async captureFailure(261308 label: string,262309 snapshot?: (client: BrowserTestClient) => Promise<ValueFreeState>,@@ -299,6 +346,10 @@ export class TwoClientFixture {
299346 return { directory, files };300347 }301348 349+ /**350+ * Close both clients and, when the fixture owns it, the browser. Idempotent;351+ * rethrows the first client close failure.352+ */302353 async close(): Promise<void> {303354 if (this.#closed) return;304355 this.#closed = true;@@ -309,6 +360,11 @@ export class TwoClientFixture {
309360 }310361 }311362 363+/**364+ * Launch (or reuse) a browser and build a {@link TwoClientFixture}: opens both365+ * clients at `baseURL`, applies the chosen identity mode with state kept in366+ * memory, and starts recording. Cleans up on any setup failure.367+ */312368 export async function createTwoClientFixture(313369 options: TwoClientFixtureOptions,314370 ): Promise<TwoClientFixture> {@@ -1,3 +1,11 @@
1+/**2+ * The `@nzip/lofi/testing` public surface: Playwright-backed helpers for testing3+ * local-first behavior, including two-client fixtures, concurrent offline4+ * convergence, app-owned readiness waits, and value-free failure artifacts.5+ *6+ * @module7+ */8+19 export {210 type BrowserDiagnostic,311 BrowserTestClient,@@ -1,5 +1,6 @@
11 import type { Page } from "npm:playwright@1.61.1";22 3+/** Options controlling {@link waitForReady}'s description, timeout, and polling. */34 export interface ReadinessOptions {45 /** A useful name for the condition, included when Playwright times out. */56 description?: string;@@ -9,6 +10,7 @@ export interface ReadinessOptions {
910 polling?: "raf" | number;1011 }1112 13+/** Thrown when the readiness predicate does not become true before the timeout. */1214 export class ReadinessError extends Error {1315 override readonly name = "ReadinessError";1416 @@ -1,3 +1,8 @@
1+/**2+ * A JSON-like value restricted to booleans, finite numbers, arrays, and plain3+ * objects. Strings are excluded so state snapshots record shape and counts but4+ * never user values or credentials.5+ */16 export type ValueFreeState =27 | null38 | boolean@@ -67,6 +72,10 @@ export function assertValueFreeState(
6772 throw new TypeError(`${path} contains an unsupported value`);6873 }6974 75+/**76+ * Normalize a label into a filesystem-safe artifact directory name, falling77+ * back to `"failure"` when nothing usable remains.78+ */7079 export function artifactName(value: string): string {7180 const safe = value.toLowerCase().replaceAll(/[^a-z0-9._-]+/g, "-").replaceAll(/^-+|-+$/g, "");7281 return safe || "failure";@@ -45,6 +45,11 @@ async function inflateRaw(bytes: Uint8Array): Promise<Uint8Array> {
4545 return new Uint8Array(await new Response(output).arrayBuffer());4646 }4747 48+/**49+ * Read a Playwright trace ZIP at `path` into a map of entry name to50+ * decompressed bytes, supporting stored and deflate entries. Throws on51+ * encrypted, malformed, or unsupported-compression archives.52+ */4853 export async function readTraceArchive(path: string): Promise<Record<string, Uint8Array>> {4954 const bytes = await Deno.readFile(path);5055 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);@@ -81,6 +86,10 @@ export async function readTraceArchive(path: string): Promise<Record<string, Uin
8186 return entries;8287 }8388 89+/**90+ * Serialize the given entries into a stored (uncompressed) ZIP archive with91+ * CRC-32 checksums, suitable for rewriting a sanitized trace.92+ */8493 export function writeTraceArchive(entries: Readonly<Record<string, Uint8Array>>): Uint8Array {8594 const localParts: Uint8Array[] = [];8695 const centralParts: Uint8Array[] = [];