diff --git a/packages/cli/package.json b/packages/cli/package.json index a7c91971..63b90e8f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.8.31", + "version": "0.8.32", "type": "module", "main": "src/index.ts", "exports": { @@ -10,6 +10,9 @@ "bin": { "wrnexus": "src/index.ts" }, + "files": [ + "src" + ], "dependencies": { "@wrnexus/core": "workspace:*", "@wrnexus/router": "workspace:*", diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index b1dcecbc..60047eac 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -961,7 +961,11 @@ async function emitPluginAssets( runtime: true, immutable: true, }); - runtimes.push({ ...runtime, entry: undefined, source: undefined, publicPath }); + // The emitted runtime is served from its content-addressed public path. + // Keep an empty source marker so @wrnexus/plugin can normalize this + // already-emitted descriptor during production HTML rendering without + // trying to resolve the original build-time source on the deploy host. + runtimes.push({ ...runtime, entry: undefined, source: "", publicPath }); } for (const asset of contributions.assets) { diff --git a/packages/cli/test/plugin-runtime-production.test.ts b/packages/cli/test/plugin-runtime-production.test.ts new file mode 100644 index 00000000..3096f792 --- /dev/null +++ b/packages/cli/test/plugin-runtime-production.test.ts @@ -0,0 +1,88 @@ +import { afterAll, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { runBuild } from "../src/build.ts"; + +const scratchRoot = join(import.meta.dir, ".tmp-plugin-runtime-production"); +mkdirSync(scratchRoot, { recursive: true }); + +afterAll(() => rmSync(scratchRoot, { recursive: true, force: true })); + +test("production renders and serves a content-addressed plugin runtime", async () => { + const root = mkdtempSync(join(scratchRoot, "app-")); + mkdirSync(join(root, "app", "pages"), { recursive: true }); + mkdirSync(join(root, "app", "components"), { recursive: true }); + writeFileSync(join(root, "package.json"), JSON.stringify({ name: "plugin-runtime-fixture" })); + writeFileSync( + join(root, "wrnexus.config.ts"), + `import { definePlugin } from "@wrnexus/plugin"; + +export default { + plugins: [definePlugin({ + name: "runtime-fixture", + clientRuntimes: [{ + id: "runtime-fixture", + source: "window.__runtimeFixture = true;", + type: "script", + bundle: false, + }], + })], +}; +`, + ); + writeFileSync( + join(root, "app", "components", "RuntimeProbe.wrn"), + `component RuntimeProbe { view {
Runtime fixture
} }\n`, + ); + writeFileSync( + join(root, "app", "pages", "index.wrn"), + `import RuntimeProbe from "../components/RuntimeProbe.wrn"\npage Home { view { } }\n`, + ); + + await runBuild(root); + + const proc = Bun.spawn({ + cmd: ["bun", join(root, "dist", "server.js")], + env: { ...process.env, PORT: "0" }, + stdout: "pipe", + stderr: "pipe", + cwd: root, + }); + + let port: number | undefined; + try { + const reader = proc.stdout.getReader(); + const decoder = new TextDecoder(); + let output = ""; + const giveUp = setTimeout(() => proc.kill(), 20_000); + try { + while (port === undefined) { + const { value, done } = await reader.read(); + if (done) break; + output += decoder.decode(value, { stream: true }); + const match = /listening on http:\/\/[^:]+:(\d+)/.exec(output); + if (match) port = Number(match[1]); + } + } finally { + clearTimeout(giveUp); + reader.releaseLock(); + } + + if (port === undefined) { + throw new Error(`production server failed to start: ${await new Response(proc.stderr).text()}`); + } + + const page = await fetch(`http://127.0.0.1:${port}/`); + expect(page.status).toBe(200); + const html = await page.text(); + const assetPath = html.match(/(\/__wrnexus\/assets\/runtime-fixture\.[a-f0-9]+\.js)/)?.[1]; + expect(assetPath).toBeTruthy(); + + const asset = await fetch(`http://127.0.0.1:${port}${assetPath}`); + expect(asset.status).toBe(200); + expect(await asset.text()).toContain("window.__runtimeFixture = true"); + } finally { + proc.kill(); + await proc.exited; + } +}, 30_000); diff --git a/packages/db/package.json b/packages/db/package.json index 0530bf84..93933978 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.8.11", + "version": "0.8.12", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/db/src/generate.ts b/packages/db/src/generate.ts index 60b7e320..a14f29fb 100644 --- a/packages/db/src/generate.ts +++ b/packages/db/src/generate.ts @@ -188,6 +188,7 @@ export function generateQueriesFile( ): string { const byTable = new Map(models.map((m) => [m.model.name, m])); const usedModels = new Set(); + let usesExecResult = false; const blocks: string[] = []; for (const q of queries) { @@ -202,6 +203,7 @@ export function generateQueriesFile( const sig = argFields.length ? `db: Db, args: { ${argFields.join("; ")} }` : "db: Db"; if (q.kind === "exec") { + usesExecResult = true; blocks.push( `export async function ${q.name}(${sig}): Promise {\n return db.exec(${sqlLit}, ${positional});\n}`, ); @@ -224,7 +226,9 @@ export function generateQueriesFile( ); } - const imports = [`import type { Db, ExecResult } from "@wrnexus/db";`]; + const imports = [ + `import type { Db${usesExecResult ? ", ExecResult" : ""} } from "@wrnexus/db";`, + ]; if (usedModels.size > 0) { imports.push(`import { ${[...usedModels].sort().join(", ")} } from "./schema.ts";`); } diff --git a/packages/db/test/db.test.ts b/packages/db/test/db.test.ts index bf7b2ce6..325a5217 100644 --- a/packages/db/test/db.test.ts +++ b/packages/db/test/db.test.ts @@ -213,6 +213,13 @@ test("query generator infers params and result types", () => { ); }); +test("query generator omits ExecResult when there are no exec queries", () => { + const queries = parseQueries("-- name: ListUsers :many\nSELECT * FROM users;"); + const code = generateQueriesFile(queries, [{ varName: "users", model: users }], "sqlite"); + expect(code).toContain('import type { Db } from "@wrnexus/db";'); + expect(code).not.toContain("ExecResult"); +}); + test("paginate returns a page window with correct metadata", async () => { const { paginate } = await import("../src/index.ts"); const db = createDb(sqlite());