1/**2 * Astro and Vite integration used by generated lofi applications.3 *4 * {@link prepareLofiAstroConfig} materializes version-matched runtime aliases5 * and Jazz configuration into the ignored `.lofi/` directory.6 *7 * @module8 */910import { join, resolve } from "node:path";1112/** Options for materializing the package-owned Astro configuration. */13export type LofiAstroOptions = {14 /** Project root. Defaults to the directory from which Astro was invoked. */15 root?: string;16 /** Directory containing the Jazz schema and permissions. */17 schemaDir?: string;18};1920/**21 * Runtime modules vendored into a project's `.lofi/` directory. Static because22 * the published package cannot list directories over JSR; the manifest test23 * fails when this list and `package/runtime/` drift apart.24 */25export const runtimeFiles = [26 "app.ts",27 "auth.ts",28 "boot.ts",29 "config.ts",30 "data-sink.ts",31 "device-capabilities.ts",32 "diagnostics.ts",33 "durability.ts",34 "env.d.ts",35 "envelope.ts",36 "foreground-recovery.ts",37 "inspector.ts",38 "lifecycle.ts",39 "live-query-store.ts",40 "mod.ts",41 "namespace-state.ts",42 "passkey-recovery.ts",43 "probe.ts",44 "provision.ts",45 "pwa.ts",46 "recovery.ts",47 "resource-lifecycle.ts",48 "runtime.ts",49 "session.ts",50 "startup-recovery.ts",51 "table-mutations.ts",52 "table-store.ts",53 "transport-gate.ts",54 "ui-mutation.ts",55] as const;5657/** Access modules vendored into `.lofi/`; kept in lockstep by the manifest test. */58export const accessFiles = [59 "errors.ts",60 "identity.ts",61 "mod.ts",62 "operations.ts",63 "policies.ts",64 "schema.ts",65] as const;6667/** Schema facade modules vendored into `.lofi/`; kept in lockstep by the manifest test. */68export const schemaFiles = [69 "mod.ts",70 "nested.ts",71 "store.ts",72] as const;7374/** Preact modules vendored into `.lofi/`; kept in lockstep by the manifest test. */75export const preactFiles = [76 "DeviceStatus.tsx",77 "live-data.ts",78 "mod.ts",79 "PwaActions.tsx",80 "RuntimeRecovery.tsx",81 "TicketEnrollForm.tsx",82 "use-device-capabilities.ts",83] as const;8485/** Recipe modules vendored into `.lofi/`; kept in lockstep by the manifest test. */86export const recipeFiles = [87 "file-handler.ts",88 "launch-handler.ts",89 "protocol-handler.ts",90 "related-app-discovery.ts",91 "scope-extension.ts",92 "web-share.ts",93 "window-controls-overlay.ts",94] as const;9596async function readPackageFile(path: string): Promise<string> {97 const url = new URL(path, import.meta.url);98 if (url.protocol === "file:") return await Deno.readTextFile(url);99 const response = await fetch(url);100 if (!response.ok) {101 throw new Error(`failed to read package module ${path}: HTTP ${response.status}`);102 }103 return await response.text();104}105106function renderConfig(projectRoot: string): string {107 const runtimeEntry = join(projectRoot, ".lofi", "package", "runtime", "mod.ts");108 const accessEntry = join(projectRoot, ".lofi", "package", "access", "mod.ts");109 const preactEntry = join(projectRoot, ".lofi", "package", "preact", "mod.ts");110 const schemaEntry = join(projectRoot, ".lofi", "package", "schema", "mod.ts");111 const schemaStoreEntry = join(projectRoot, ".lofi", "package", "schema", "store.ts");112 const fileHandlerRecipe = join(projectRoot, ".lofi", "package", "recipes", "file-handler.ts");113 const launchHandlerRecipe = join(114 projectRoot,115 ".lofi",116 "package",117 "recipes",118 "launch-handler.ts",119 );120 const protocolHandlerRecipe = join(121 projectRoot,122 ".lofi",123 "package",124 "recipes",125 "protocol-handler.ts",126 );127 const relatedAppDiscoveryRecipe = join(128 projectRoot,129 ".lofi",130 "package",131 "recipes",132 "related-app-discovery.ts",133 );134 const scopeExtensionRecipe = join(135 projectRoot,136 ".lofi",137 "package",138 "recipes",139 "scope-extension.ts",140 );141 const webShareRecipe = join(projectRoot, ".lofi", "package", "recipes", "web-share.ts");142 const windowControlsOverlayRecipe = join(143 projectRoot,144 ".lofi",145 "package",146 "recipes",147 "window-controls-overlay.ts",148 );149 return `import preact from "@astrojs/preact";150import { defineConfig } from "astro/config";151import { jazzPlugin } from "jazz-tools/dev/vite";152import { resolve } from "node:path";153154const projectRoot = Deno.cwd();155const schemaDir = resolve(projectRoot, "src");156const cloudAppId = process.env.JAZZ_APP_ID?.trim();157const cloudServerUrl = process.env.JAZZ_SERVER_URL?.trim();158const managedJazzServer = process.env.LOFI_SKIP_JAZZ_MANAGED === "1"159 ? false160 : { allowLocalFirstAuth: true };161const deploymentBase = process.env.LOFI_BASE_PATH || "/";162163if (Boolean(cloudAppId) !== Boolean(cloudServerUrl)) {164 throw new Error(165 "Cloud mode requires both JAZZ_APP_ID and JAZZ_SERVER_URL; set the missing name or remove the partial pair.",166 );167}168169if (cloudAppId && cloudServerUrl) {170 process.env.VITE_JAZZ_APP_ID = cloudAppId;171 process.env.VITE_JAZZ_SERVER_URL = cloudServerUrl;172}173174export default defineConfig({175 base: deploymentBase,176 output: "static",177 trailingSlash: "always",178 integrations: [preact({ compat: true, devtools: true })],179 vite: {180 plugins: [jazzPlugin({ schemaDir, inspector: false, server: managedJazzServer })],181 server: { fs: { allow: [projectRoot] } },182 // The worker sub-build falls back to Vite's default asset template183 // ("[name]-[hash][extname]") while Astro names client assets184 // "[name].[hash][extname]", so the Jazz WASM binary — imported by both the185 // runtime and the jazz worker — lands in dist twice under two names.186 // Matching Astro's template collapses the copies into one file.187 worker: {188 rollupOptions: {189 output: { assetFileNames: "_astro/[name].[hash][extname]" },190 },191 },192 define: {193 __LOFI_JAZZ_APP_ID__: JSON.stringify(cloudAppId ?? ""),194 __LOFI_JAZZ_SERVER_URL__: JSON.stringify(cloudServerUrl ?? ""),195 },196 resolve: {197 alias: [198 { find: /^jsr:@nzip\\/lofi@[^/]+\\/recipes\\/file-handler$/, replacement: ${199 JSON.stringify(fileHandlerRecipe)200 } },201 { find: /^jsr:@nzip\\/lofi@[^/]+\\/recipes\\/launch-handler$/, replacement: ${202 JSON.stringify(launchHandlerRecipe)203 } },204 { find: /^jsr:@nzip\\/lofi@[^/]+\\/recipes\\/protocol-handler$/, replacement: ${205 JSON.stringify(protocolHandlerRecipe)206 } },207 { find: /^jsr:@nzip\\/lofi@[^/]+\\/recipes\\/related-app-discovery$/, replacement: ${208 JSON.stringify(relatedAppDiscoveryRecipe)209 } },210 { find: /^jsr:@nzip\\/lofi@[^/]+\\/recipes\\/scope-extension$/, replacement: ${211 JSON.stringify(scopeExtensionRecipe)212 } },213 { find: /^jsr:@nzip\\/lofi@[^/]+\\/recipes\\/web-share$/, replacement: ${214 JSON.stringify(webShareRecipe)215 } },216 { find: /^jsr:@nzip\\/lofi@[^/]+\\/recipes\\/window-controls-overlay$/, replacement: ${217 JSON.stringify(windowControlsOverlayRecipe)218 } },219 { find: /^jsr:@nzip\\/lofi@[^/]+\\/preact$/, replacement: ${JSON.stringify(preactEntry)} },220 { find: /^jsr:@nzip\\/lofi@[^/]+\\/access$/, replacement: ${JSON.stringify(accessEntry)} },221 { find: /^jsr:@nzip\\/lofi@[^/]+\\/schema\\/store$/, replacement: ${222 JSON.stringify(schemaStoreEntry)223 } },224 { find: /^jsr:@nzip\\/lofi@[^/]+\\/schema$/, replacement: ${JSON.stringify(schemaEntry)} },225 { find: /^jsr:@nzip\\/lofi@[^/]+$/, replacement: ${JSON.stringify(runtimeEntry)} },226 { find: /^npm:preact@[^/]+\\/hooks$/, replacement: "preact/hooks" },227 { find: /^npm:preact@[^/]+\\/jsx-dev-runtime$/, replacement: "preact/jsx-dev-runtime" },228 { find: /^npm:preact@[^/]+\\/jsx-runtime$/, replacement: "preact/jsx-runtime" },229 { find: /^npm:preact@[^/]+$/, replacement: "preact" },230 { find: /^npm:jazz-tools@[^/]+\\/passkey-backup$/, replacement: "jazz-tools/passkey-backup" },231 { find: /^npm:jazz-tools@[^/]+\\/passphrase$/, replacement: "jazz-tools/passphrase" },232 { find: /^npm:jazz-tools@[^/]+$/, replacement: "jazz-tools" },233 { find: "@nzip/lofi/recipes/launch-handler", replacement: ${234 JSON.stringify(launchHandlerRecipe)235 } },236 { find: "@nzip/lofi/recipes/protocol-handler", replacement: ${237 JSON.stringify(protocolHandlerRecipe)238 } },239 { find: "@nzip/lofi/recipes/related-app-discovery", replacement: ${240 JSON.stringify(relatedAppDiscoveryRecipe)241 } },242 { find: "@nzip/lofi/recipes/scope-extension", replacement: ${243 JSON.stringify(scopeExtensionRecipe)244 } },245 { find: "@nzip/lofi/recipes/file-handler", replacement: ${246 JSON.stringify(fileHandlerRecipe)247 } },248 { find: "@nzip/lofi/recipes/web-share", replacement: ${JSON.stringify(webShareRecipe)} },249 { find: "@nzip/lofi/recipes/window-controls-overlay", replacement: ${250 JSON.stringify(windowControlsOverlayRecipe)251 } },252 { find: "@nzip/lofi/preact", replacement: ${JSON.stringify(preactEntry)} },253 { find: "@nzip/lofi/access", replacement: ${JSON.stringify(accessEntry)} },254 { find: "@nzip/lofi/schema", replacement: ${JSON.stringify(schemaEntry)} },255 { find: "@nzip/lofi", replacement: ${JSON.stringify(runtimeEntry)} },256 ],257 noExternal: ["@astrojs/preact"],258 },259 },260});261`;262}263264/**265 * Materializes the package-owned Astro/Jazz integration into an ignored tooling266 * directory so Astro's Node-compatible config loader can consume it.267 */268export async function prepareLofiAstroConfig(options: LofiAstroOptions = {}): Promise<string> {269 const projectRoot = resolve(options.root ?? Deno.cwd());270 if (options.schemaDir && options.schemaDir !== "src") {271 throw new Error("custom schemaDir is not supported by the generated lofi Astro integration");272 }273 const toolingDir = join(projectRoot, ".lofi");274 const path = join(toolingDir, "astro.config.ts");275 const packageDir = join(toolingDir, "package");276 await Deno.mkdir(join(packageDir, "runtime"), { recursive: true });277 await Deno.mkdir(join(packageDir, "access"), { recursive: true });278 await Deno.mkdir(join(packageDir, "preact"), { recursive: true });279 await Deno.mkdir(join(packageDir, "recipes"), { recursive: true });280 await Deno.mkdir(join(packageDir, "schema"), { recursive: true });281 for (const file of runtimeFiles) {282 await Deno.writeTextFile(283 join(packageDir, "runtime", file),284 await readPackageFile(`../runtime/${file}`),285 );286 }287 for (const file of preactFiles) {288 await Deno.writeTextFile(289 join(packageDir, "preact", file),290 await readPackageFile(`../preact/${file}`),291 );292 }293 for (const file of accessFiles) {294 await Deno.writeTextFile(295 join(packageDir, "access", file),296 await readPackageFile(`../access/${file}`),297 );298 }299 for (const file of recipeFiles) {300 await Deno.writeTextFile(301 join(packageDir, "recipes", file),302 await readPackageFile(`../recipes/${file}`),303 );304 }305 for (const file of schemaFiles) {306 await Deno.writeTextFile(307 join(packageDir, "schema", file),308 await readPackageFile(`../schema/${file}`),309 );310 }311 const configSource = renderConfig(projectRoot);312 let current = "";313 try {314 current = await Deno.readTextFile(path);315 } catch (error) {316 if (!(error instanceof Deno.errors.NotFound)) throw error;317 }318 if (current !== configSource) await Deno.writeTextFile(path, configSource);319 return path;320}