From 9dec8110694454ef35cfc70d1cad08f2a7162d7e Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 20 Aug 2026 07:11:53 +0530 Subject: [PATCH] fix(compiler): route the generated api object through the real buildApiRequest Root-cause fix for the fix-round-1 review: the inlined query/body assembly in __wrnexusCallApi was a third, unguarded copy of buildApiRequest's rules. Restore the import of buildApiRequest from @wrnexus/core in the generated module and delete the inline copy. The four api-block-ssr.test.ts tests (and three in compiler.test.ts) that dynamically import a generated module from an OS tmpdir were failing against a stale globally-installed @wrnexus/core (v0.8.8, predates buildApiRequest) because that tmpdir has no node_modules of its own and bare-specifier resolution walked out of the workspace. Fixed at the source: symlink the workspace @wrnexus/core into each tmpdir root before the dynamic import, the same way every in-repo package already resolves it. --- editors/vscode/src/compiler.cjs | 24 +++++-------------- editors/vscode/src/extension.bundle.cjs | 2 +- packages/compiler/src/codegen.ts | 22 ++++------------- packages/compiler/test/api-block-ssr.test.ts | 25 +++++++++++++++++++- packages/compiler/test/compiler.test.ts | 21 +++++++++++++++- 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index 8bf41486..0286a5e7 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -1,6 +1,6 @@ "use strict"; // Generated by scripts/build-editor-compiler.mjs. Do not edit directly. -// WRN editor compiler source hash: 03f56cb1555bb66c503f2ec4caf75aa149dd7142637593a07c78d1f1baab8e2b +// WRN editor compiler source hash: 843d73711698bc31e874370cb28bf0cb6940d3d4a6c761d00921b5f35806ab98 // WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8 // Generated with TypeScript: 6.0.3 const __nodeRequire = require; @@ -1691,28 +1691,15 @@ async function __wrnexusCallApi( return await ctx.__wrnexusCallApi(path, method, input); } - const verb = String(method || "GET").toUpperCase(); - const values = (input ?? {}) as Record; + const built = __wrnexusBuildApiRequest(path, method, input as Record | undefined); + const url = new URL(built.url, ctx.req.url); const headers = new Headers(ctx.req.headers); - let requestPath = path; - let body: string | undefined; - if (verb === "GET" || verb === "HEAD") { - const query: string[] = []; - for (const [key, value] of Object.entries(values)) { - if (value === undefined || value === null || value === "") continue; - query.push(\`\${encodeURIComponent(key)}=\${encodeURIComponent(String(value))}\`); - } - if (query.length) requestPath = \`\${path}?\${query.join("&")}\`; - } else { - body = JSON.stringify(values); - headers.set("content-type", "application/json"); - } - const url = new URL(requestPath, ctx.req.url); + if (built.contentType) headers.set("content-type", built.contentType); const res = await fetch( new Request(url, { method, headers, - ...(body === undefined ? {} : { body }), + ...(built.body === undefined ? {} : { body: built.body }), }), ); const type = res.headers.get("content-type") || ""; @@ -2203,6 +2190,7 @@ function generateInner(ast) { const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0; const needsRuntimeHelpers = needsSsrRuntime || hasServerApis; if (needsRuntimeHelpers) { + out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`); out.push(ssrRuntimeSource()); out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`); } diff --git a/editors/vscode/src/extension.bundle.cjs b/editors/vscode/src/extension.bundle.cjs index f727f2f2..2bb266e7 100644 --- a/editors/vscode/src/extension.bundle.cjs +++ b/editors/vscode/src/extension.bundle.cjs @@ -1,4 +1,4 @@ -// WRN editor extension source hash: ae7dab4aff1ce30f0d01fa0b9172651e526408916351862e2644a85c25df03ac +// WRN editor extension source hash: 52dfa294ba556cb26807ce112cc6826a690aa4a80721520d0c39bd7cfd8aa33e // WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728 "use strict"; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index 3321ce22..c0f5d653 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -1068,28 +1068,15 @@ async function __wrnexusCallApi( return await ctx.__wrnexusCallApi(path, method, input); } - const verb = String(method || "GET").toUpperCase(); - const values = (input ?? {}) as Record; + const built = __wrnexusBuildApiRequest(path, method, input as Record | undefined); + const url = new URL(built.url, ctx.req.url); const headers = new Headers(ctx.req.headers); - let requestPath = path; - let body: string | undefined; - if (verb === "GET" || verb === "HEAD") { - const query: string[] = []; - for (const [key, value] of Object.entries(values)) { - if (value === undefined || value === null || value === "") continue; - query.push(\`\${encodeURIComponent(key)}=\${encodeURIComponent(String(value))}\`); - } - if (query.length) requestPath = \`\${path}?\${query.join("&")}\`; - } else { - body = JSON.stringify(values); - headers.set("content-type", "application/json"); - } - const url = new URL(requestPath, ctx.req.url); + if (built.contentType) headers.set("content-type", built.contentType); const res = await fetch( new Request(url, { method, headers, - ...(body === undefined ? {} : { body }), + ...(built.body === undefined ? {} : { body: built.body }), }), ); const type = res.headers.get("content-type") || ""; @@ -1663,6 +1650,7 @@ function generateInner(ast: PageAst): string { const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0; const needsRuntimeHelpers = needsSsrRuntime || hasServerApis; if (needsRuntimeHelpers) { + out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`); out.push(ssrRuntimeSource()); out.push( `const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`, diff --git a/packages/compiler/test/api-block-ssr.test.ts b/packages/compiler/test/api-block-ssr.test.ts index b111619c..24eb2578 100644 --- a/packages/compiler/test/api-block-ssr.test.ts +++ b/packages/compiler/test/api-block-ssr.test.ts @@ -1,10 +1,29 @@ import { afterEach, expect, test } from "bun:test"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { parse } from "@wrnexus/syntax"; import { generate } from "../src/codegen.ts"; +// The generated module dynamically imported below is written to an OS +// tmpdir with no node_modules of its own, so Node's bare-specifier +// resolution for "@wrnexus/core" would otherwise walk up to whatever +// (possibly stale, globally-installed) copy happens to sit outside the +// workspace. Symlink the workspace package in so it resolves to the real, +// currently-built `@wrnexus/core` — the same one every other package in +// this repo gets via its own `node_modules/@wrnexus/core` symlink. +const WORKSPACE_CORE = join(import.meta.dir, "../../core"); + +function linkWorkspaceCore(root: string): void { + const scopeDir = join(root, "node_modules", "@wrnexus"); + mkdirSync(scopeDir, { recursive: true }); + symlinkSync( + WORKSPACE_CORE, + join(scopeDir, "core"), + process.platform === "win32" ? "junction" : "dir", + ); +} + const ROOT_TSCONFIG = join(import.meta.dir, "../../../tsconfig.json").replace(/\\/g, "/"); // The repo's own tsc, not a `bunx`-fetched one — `bunx tsc` can resolve an // unrelated TypeScript version that doesn't understand this repo's tsconfig @@ -150,6 +169,7 @@ test("an ssr block used in {#each} with an error section runs the error body on const root = mkdtempSync(join(tmpdir(), "wrnexus-ssr-each-")); roots.push(root); mkdirSync(root, { recursive: true }); + linkWorkspaceCore(root); const file = join(root, "page.ts"); writeFileSync(file, generated); @@ -189,6 +209,7 @@ test("an ssr block's response body error is not swallowed by the error section", const root = mkdtempSync(join(tmpdir(), "wrnexus-ssr-response-throws-")); roots.push(root); mkdirSync(root, { recursive: true }); + linkWorkspaceCore(root); const file = join(root, "page.ts"); writeFileSync(file, generated); @@ -227,6 +248,7 @@ test("an ssr block still runs the error body on a genuine transport failure", as const root = mkdtempSync(join(tmpdir(), "wrnexus-ssr-transport-fails-")); roots.push(root); mkdirSync(root, { recursive: true }); + linkWorkspaceCore(root); const file = join(root, "page.ts"); writeFileSync(file, generated); @@ -263,6 +285,7 @@ test("an ssr block used in {#each} without an error section still propagates a f const root = mkdtempSync(join(tmpdir(), "wrnexus-ssr-each-propagate-")); roots.push(root); mkdirSync(root, { recursive: true }); + linkWorkspaceCore(root); const file = join(root, "page.ts"); writeFileSync(file, generated); diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index 5daeda1d..88514fe7 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; -import { writeFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { writeFileSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -7,6 +7,23 @@ import { generate, parse } from "../src/index.ts"; import { compileWrnFile } from "../src/index.ts"; import { mountHtml } from "@wrnexus/test"; +// The compiled module below is written to an OS tmpdir with no node_modules +// of its own, so Node's bare-specifier resolution for "@wrnexus/core" would +// otherwise walk up to whatever (possibly stale, globally-installed) copy +// happens to sit outside the workspace. Symlink the workspace package in so +// it resolves to the real, currently-built `@wrnexus/core` — the same one +// every other package in this repo gets via its own +// `node_modules/@wrnexus/core` symlink. +const WORKSPACE_CORE = join(import.meta.dir, "../../core"); + +function linkWorkspaceCore(root: string): void { + const scopeDir = join(root, "node_modules", "@wrnexus"); + const linkPath = join(scopeDir, "core"); + if (existsSync(linkPath)) return; + mkdirSync(scopeDir, { recursive: true }); + symlinkSync(WORKSPACE_CORE, linkPath, process.platform === "win32" ? "junction" : "dir"); +} + test("explicit static rendering disables hydration metadata", () => { const output = compileWrnFile(`page StaticPage { render = "static" @@ -33,6 +50,7 @@ let seq = 0; async function compileAndImport(src: string): Promise> { const dir = join(tmpdir(), "wrn-compiler-test"); mkdirSync(dir, { recursive: true }); + linkWorkspaceCore(dir); const file = join(dir, `m${seq++}.ts`); writeFileSync(file, compileWrnFile(src)); return import(pathToFileURL(file).href); @@ -1351,6 +1369,7 @@ test("page state SSR condition renders without ReferenceError", async () => { const generatedFile = join(root, "pricing.generated.ts"); try { + linkWorkspaceCore(root); const source = ` page Pricing { state billingCycle = "monthly"