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());