finish CSS delivery and generated type remediation
Quality / quality (ubuntu-latest) (push) Failing after 12m54s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-10 00:17:25 +05:30
parent 232d8e6734
commit bca9549f3a
11 changed files with 277 additions and 18 deletions
+6
View File
@@ -16,6 +16,12 @@ showcase-generation, typecheck, lint, formatting, public-API, visual-contract,
security, and runtime-size gates. The only skipped test is the explicitly security, and runtime-size gates. The only skipped test is the explicitly
environment-dependent live PostgreSQL/MySQL test. environment-dependent live PostgreSQL/MySQL test.
Production CSS delivery now links the selected palette/accent sheet (6,632
decoded bytes for dark/violet) instead of the complete 74,764-byte theme
matrix. The compatibility `theme.css` endpoint remains available but is not
linked by generated pages. Generated application declarations are committed
and checked for drift by `check:production`.
Written 2026-08-09, after the 0.8.6 release. Every number here was measured Written 2026-08-09, after the 0.8.6 release. Every number here was measured
against the completed tree, not estimated. Where a cause is not yet against the completed tree, not estimated. Where a cause is not yet
proven the item says so and makes proving it step one — nothing in this document proven the item says so and makes proving it step one — nothing in this document
+5 -4
View File
@@ -13,15 +13,14 @@ declare namespace WRNexusGenerated {
: never; : never;
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown; type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown; type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
type RouteName = "about" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "language.tools" | "login" | "modal" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "test" | "ui"; type RouteName = "about" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "language.tools" | "layout" | "login" | "modal" | "navigation" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "table" | "test" | "ui";
type ApiRoute = "/api/echo" | "/api/graphql-example" | "/api/hello" | "/api/login" | "/api/logout" | "/api/me" | "/api/typed-user" | "/api/users/csr" | "/api/users/ssr" | "/api/webhooks/payment"; type ApiRoute = "/api/accounts" | "/api/echo" | "/api/graphql-example" | "/api/hello" | "/api/invite" | "/api/login" | "/api/logout" | "/api/me" | "/api/typed-user" | "/api/users/csr" | "/api/users/ssr" | "/api/webhooks/payment";
type RealtimeRoute = "/realtime/chat" | "/realtime/hello"; type RealtimeRoute = "/realtime/chat" | "/realtime/hello";
type EnvironmentKey = "APP_LABEL" | "DATABASE_URL" | "DEMO_SHARED" | "HOST" | "NODE_ENV" | "PORT" | "SESSION_SECRET" | "UAT_ONLY"; type EnvironmentKey = "APP_LABEL" | "DATABASE_URL" | "DEMO_SHARED" | "HOST" | "NODE_ENV" | "PORT" | "SESSION_SECRET" | "UAT_ONLY";
type TranslationKey = "api.greeting" | "home.intro" | "home.title" | "nav.about" | "nav.chat" | "nav.dashboard" | "nav.home" | "nav.ui"; type TranslationKey = "api.greeting" | "home.intro" | "home.title" | "nav.about" | "nav.chat" | "nav.dashboard" | "nav.home" | "nav.layout" | "nav.navigation" | "nav.ui";
type QueueName = "welcome-email"; type QueueName = "welcome-email";
type CacheKey = "users"; type CacheKey = "users";
interface Components { interface Components {
"Modal": { props: Record<string, never>; outputs: Record<string, never> };
"Counter": { props: { "start"?: unknown; "label"?: unknown }; outputs: Record<string, never> }; "Counter": { props: { "start"?: unknown; "label"?: unknown }; outputs: Record<string, never> };
} }
interface ApiContracts { interface ApiContracts {
@@ -30,6 +29,8 @@ declare namespace WRNexusGenerated {
"/api/users/ssr": { GET: ApiContract<typeof import("../api/users/ssr.ts")["GET"]> }; "/api/users/ssr": { GET: ApiContract<typeof import("../api/users/ssr.ts")["GET"]> };
"/api/graphql-example": { POST: ApiContract<typeof import("../api/graphql-example.ts")["POST"]> }; "/api/graphql-example": { POST: ApiContract<typeof import("../api/graphql-example.ts")["POST"]> };
"/api/typed-user": { POST: ApiContract<typeof import("../api/typed-user.ts")["POST"]> }; "/api/typed-user": { POST: ApiContract<typeof import("../api/typed-user.ts")["POST"]> };
"/api/accounts": { GET: ApiContract<typeof import("../api/accounts.ts")["GET"]> };
"/api/invite": { POST: ApiContract<typeof import("../api/invite.ts")["POST"]> };
"/api/logout": { POST: ApiContract<typeof import("../api/logout.ts")["POST"]> }; "/api/logout": { POST: ApiContract<typeof import("../api/logout.ts")["POST"]> };
"/api/hello": { GET: ApiContract<typeof import("../api/hello.ts")["GET"]> }; "/api/hello": { GET: ApiContract<typeof import("../api/hello.ts")["GET"]> };
"/api/login": { POST: ApiContract<typeof import("../api/login.ts")["POST"]> }; "/api/login": { POST: ApiContract<typeof import("../api/login.ts")["POST"]> };
+3 -1
View File
@@ -43,12 +43,14 @@
"security:asvs": "node scripts/check-security-asvs.mjs", "security:asvs": "node scripts/check-security-asvs.mjs",
"generate:security-report": "node scripts/security-performance-audit.mjs --write", "generate:security-report": "node scripts/security-performance-audit.mjs --write",
"check:public-api": "node scripts/check-public-api.mjs", "check:public-api": "node scripts/check-public-api.mjs",
"check:generated-types": "bun run scripts/check-generated-types.ts",
"generate:example-types": "bun run packages/cli/src/index.ts generate types examples/basic-app",
"generate:public-api": "node scripts/check-public-api.mjs --write", "generate:public-api": "node scripts/check-public-api.mjs --write",
"check:ui-visual": "node scripts/check-ui-visual-contract.mjs", "check:ui-visual": "node scripts/check-ui-visual-contract.mjs",
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write", "generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
"sbom": "node scripts/generate-sbom.mjs", "sbom": "node scripts/generate-sbom.mjs",
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write", "benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write",
"check:production": "bun run check:workspace && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples", "check:production": "bun run check:workspace && bun run check:generated-types && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples",
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs", "validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
"stage:packages": "bun run scripts/publish-packages.ts", "stage:packages": "bun run scripts/publish-packages.ts",
"test:staged-consumers": "node scripts/test-staged-consumers.mjs", "test:staged-consumers": "node scripts/test-staged-consumers.mjs",
@@ -0,0 +1,130 @@
import { describe, expect, test } from "bun:test";
import { Database } from "bun:sqlite";
import {
RedisCaptchaStore,
SqliteCaptchaStore,
managedCaptchaProvider,
type CaptchaChallengeRecord,
type CaptchaResponseTokenRecord,
type RedisCaptchaClient,
type SqliteDatabaseLike,
} from "../src/index.ts";
const challenge = (id: string, expiresAt = Date.now() + 60_000): CaptchaChallengeRecord => ({
id,
provider: "self-hosted",
type: "text",
presentation: "visual",
action: "signup",
publicChallenge: {
id,
provider: "self-hosted",
type: "text",
presentation: "visual",
action: "signup",
prompt: "Type the text",
createdAt: Date.now(),
expiresAt,
responseField: "wrn-captcha-response",
},
answerDigest: "digest",
answerSalt: "salt",
answerKind: "text",
caseSensitive: false,
attempts: 0,
maxAttempts: 3,
metadata: {},
createdAt: Date.now(),
expiresAt,
});
const token = (tokenHash: string): CaptchaResponseTokenRecord => ({
tokenHash,
provider: "self-hosted",
action: "signup",
createdAt: Date.now(),
expiresAt: Date.now() + 60_000,
});
describe("CAPTCHA durable stores", () => {
test("SQLite atomically mutates, consumes, deletes, and collects records", async () => {
const db = new Database(":memory:");
const store = new SqliteCaptchaStore(db as unknown as SqliteDatabaseLike);
await store.createChallenge(challenge("c1"));
expect((await store.incrementAttempts("c1", Date.now()))?.attempts).toBe(1);
expect(await store.consumeChallenge("c1", Date.now())).toMatchObject({
consumedAt: expect.any(Number),
});
expect(await store.consumeChallenge("c1", Date.now())).toBeUndefined();
await store.createToken(token("t1"));
expect(await store.consumeToken("t1", Date.now())).toMatchObject({
consumedAt: expect.any(Number),
});
expect(await store.consumeToken("t1", Date.now())).toBeUndefined();
await store.deleteChallenge("c1");
await store.deleteToken("t1");
await store.createChallenge(challenge("expired", Date.now() - 1));
await store.gc(Date.now());
expect(await store.getChallenge("expired")).toBeUndefined();
db.close();
});
test("Redis fallback locking preserves one-time consumption without Lua", async () => {
const values = new Map<string, string>();
const redis: RedisCaptchaClient = {
get: (key) => values.get(key) ?? null,
set: (key, value) => void values.set(key, value),
del: (key) => (values.delete(key) ? 1 : 0),
};
const store = new RedisCaptchaStore(redis, { prefix: "test:" });
await store.createChallenge(challenge("c1"));
expect((await store.incrementAttempts("c1", Date.now()))?.attempts).toBe(1);
const consumed = await Promise.all([
store.consumeChallenge("c1", Date.now()),
store.consumeChallenge("c1", Date.now()),
]);
expect(consumed.filter(Boolean)).toHaveLength(1);
await store.createToken(token("t1"));
const tokens = await Promise.all([
store.consumeToken("t1", Date.now()),
store.consumeToken("t1", Date.now()),
]);
expect(tokens.filter(Boolean)).toHaveLength(1);
await store.deleteChallenge("c1");
await store.deleteToken("t1");
expect(await store.getChallenge("c1")).toBeUndefined();
});
test("managed provider separates public creation from secret verification", async () => {
const requests: RequestInit[] = [];
const provider = managedCaptchaProvider({
baseUrl: "https://captcha.test/",
siteKey: "site",
secretKey: "secret",
fetch: (async (_url, init) => {
requests.push(init ?? {});
return Response.json(
requests.length === 1
? {
id: "c1",
provider: "managed",
type: "text",
presentation: "visual",
action: "signup",
prompt: "Type",
createdAt: 1,
expiresAt: 2,
responseField: "token",
}
: { success: true, provider: "managed", action: "signup" },
);
}) as typeof fetch,
});
await provider.createChallenge({ action: "signup" });
await provider.verify({ action: "signup", providerToken: "answer" });
expect(new Headers(requests[0]?.headers).has("authorization")).toBe(false);
expect(new Headers(requests[1]?.headers).get("authorization")).toBe("Bearer secret");
});
});
+11 -8
View File
@@ -566,14 +566,17 @@ export async function runBuild(appRoot: string): Promise<void> {
}, },
config.styles, config.styles,
); );
const combinedSource = `${frameworkStyles}\n${css}`; // Theme tokens are request-selected and loaded separately. Keep only the
// shared UI primitives with application CSS so the full theme/accent
// matrix cannot leak back into the blocking stylesheet.
const combinedSource = `${uiStyles}\n${css}`;
const combinedInput = join(distDir, ".wrnexus-combined.css"); const combinedInput = join(distDir, ".wrnexus-combined.css");
writeFileSync(combinedInput, combinedSource, "utf8"); writeFileSync(combinedInput, combinedSource, "utf8");
const combinedCss = await bundleCss(combinedInput, "production"); const combinedCss = await bundleCss(combinedInput, "production");
rmSync(combinedInput, { force: true }); rmSync(combinedInput, { force: true });
assetHash.update(combinedCss); assetHash.update(combinedCss);
// One blocking CSS request in production: tokens → UI application CSS. // One shared blocking request for UI + application CSS. The small active
// The standalone framework.css remains available for apps without global CSS. // theme stylesheet is selected from the request cookie and loaded first.
writeFileSync(join(distDir, "styles.css"), combinedCss, "utf8"); writeFileSync(join(distDir, "styles.css"), combinedCss, "utf8");
hasStyles = true; hasStyles = true;
if (Buffer.byteLength(combinedCss, "utf8") <= INLINE_CSS_LIMIT_BYTES) { if (Buffer.byteLength(combinedCss, "utf8") <= INLINE_CSS_LIMIT_BYTES) {
@@ -655,8 +658,8 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
const componentsLit = router.components const componentsLit = router.components
.map((c) => { .map((c) => {
const v = `c${counter++}`; const v = `c${counter++}`;
imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(c.file))};`); imports.push(`import { render as ${v} } from ${JSON.stringify(importPathFor(c.file))};`);
return `{ name: ${JSON.stringify(c.name)}, mod: ${v} }`; return `{ name: ${JSON.stringify(c.name)}, mod: { render: ${v} } }`;
}) })
.join(", "); .join(", ");
console.log(`✓ Components: ${router.components.length}`); console.log(`✓ Components: ${router.components.length}`);
@@ -665,8 +668,8 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
const layoutsLit = router.layouts const layoutsLit = router.layouts
.map((l) => { .map((l) => {
const v = `c${counter++}`; const v = `c${counter++}`;
imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(l.file))};`); imports.push(`import { render as ${v} } from ${JSON.stringify(importPathFor(l.file))};`);
return `{ name: ${JSON.stringify(l.name)}, mod: ${v} }`; return `{ name: ${JSON.stringify(l.name)}, mod: { render: ${v} } }`;
}) })
.join(", "); .join(", ");
if (router.layouts.length) console.log(`✓ Layouts: ${router.layouts.length}`); if (router.layouts.length) console.log(`✓ Layouts: ${router.layouts.length}`);
@@ -748,7 +751,7 @@ await createProductionServer(
realtime: ${config.realtime ? JSON.stringify(config.realtime) : "undefined"}, realtime: ${config.realtime ? JSON.stringify(config.realtime) : "undefined"},
publicDir: join(import.meta.dir, "public"), publicDir: join(import.meta.dir, "public"),
${hasStyles ? `stylesPath: join(import.meta.dir, "styles.css"),` : ""} ${hasStyles ? `stylesPath: join(import.meta.dir, "styles.css"),` : ""}
${hasStyles ? `stylesIncludeFramework: true,` : ""} ${hasStyles ? `stylesIncludeUi: true,` : ""}
${inlineStyles ? `inlineStyles: ${JSON.stringify(inlineStyles)},` : ""} ${inlineStyles ? `inlineStyles: ${JSON.stringify(inlineStyles)},` : ""}
assetVersion: ${JSON.stringify(assetVersion)}, assetVersion: ${JSON.stringify(assetVersion)},
clientRuntimes: ${JSON.stringify(emittedPluginAssets.runtimes)}, clientRuntimes: ${JSON.stringify(emittedPluginAssets.runtimes)},
+1 -2
View File
@@ -75,7 +75,7 @@ function writePluginArtifacts(root: string, contributions?: PluginContributions)
}); });
writeFileSync( writeFileSync(
join(typeDir, "wrnexus.plugins.generated.d.ts"), join(typeDir, "wrnexus.plugins.generated.d.ts"),
`// AUTO-GENERATED plugin type aggregation - do not edit.\n${references.join("\n")}\n`, `// AUTO-GENERATED plugin type aggregation - do not edit.\n${references.length > 0 ? `${references.join("\n")}\n` : ""}`,
"utf8", "utf8",
); );
const docsDir = join(root, ".wrnexus", "documentation"); const docsDir = join(root, ".wrnexus", "documentation");
@@ -182,7 +182,6 @@ export function generateApplicationTypes(
.map((name) => join(root, name)) .map((name) => join(root, name))
.find(existsSync); .find(existsSync);
const code = `// AUTO-GENERATED by \`wrnexus generate types\` - do not edit. const code = `// AUTO-GENERATED by \`wrnexus generate types\` - do not edit.
/* eslint-disable @typescript-eslint/no-empty-object-type */
declare namespace WRNexusGenerated { declare namespace WRNexusGenerated {
type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O> type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O>
? { input: I; output: O } ? { input: I; output: O }
@@ -0,0 +1,38 @@
import { afterAll, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { runBuild } from "../src/build.ts";
const scratchRoot = join(import.meta.dir, ".tmp-css-delivery");
mkdirSync(scratchRoot, { recursive: true });
afterAll(() => rmSync(scratchRoot, { recursive: true, force: true }));
test("production ships active themes separately and retains one component style copy", async () => {
const root = mkdtempSync(join(scratchRoot, "app-"));
mkdirSync(join(root, "app", "pages"), { recursive: true });
mkdirSync(join(root, "app", "components"), { recursive: true });
mkdirSync(join(root, "app", "styles"), { recursive: true });
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "css-delivery-fixture" }));
writeFileSync(
join(root, "wrnexus.config.ts"),
`export default { theme: { default: "dark", palette: "violet" }, styles: { entry: "app/styles/global.css" } };\n`,
);
writeFileSync(join(root, "app", "styles", "global.css"), ".app-shell{display:block}\n");
writeFileSync(
join(root, "app", "components", "Probe.wrn"),
`component Probe { style { .unique-server-style-marker{color:red} } view { <p class="unique-server-style-marker">Probe</p> } }\n`,
);
writeFileSync(
join(root, "app", "pages", "index.wrn"),
`import Probe from "../components/Probe.wrn"\npage Home { view { <main class="app-shell"><Probe /></main> } }\n`,
);
await runBuild(root);
const styles = readFileSync(join(root, "dist", "styles.css"), "utf8");
const activeTheme = readFileSync(join(root, "dist", "theme", "dark", "violet.css"), "utf8");
const server = readFileSync(join(root, "dist", "server.js"), "utf8");
expect(styles).toContain(".app-shell");
expect(styles).not.toContain("[data-theme=");
expect(activeTheme.length).toBeLessThan(10_000);
expect(server.match(/unique-server-style-marker/g)).toHaveLength(2);
});
+3
View File
@@ -91,6 +91,8 @@ export interface ProdOptions {
inlineStyles?: string; inlineStyles?: string;
/** `stylesPath` contains theme + UI + app CSS in cascade order. */ /** `stylesPath` contains theme + UI + app CSS in cascade order. */
stylesIncludeFramework?: boolean; stylesIncludeFramework?: boolean;
/** `stylesPath` already contains the shared Wire UI stylesheet. */
stylesIncludeUi?: boolean;
/** Absolute path to the pre-built reactive runtime. */ /** Absolute path to the pre-built reactive runtime. */
reactivePath?: string; reactivePath?: string;
/** Absolute path to the on-demand component controller runtime. */ /** Absolute path to the on-demand component controller runtime. */
@@ -501,6 +503,7 @@ export function createProductionHandlers(
i18n: opts.i18n, i18n: opts.i18n,
inlineStyles: opts.inlineStyles, inlineStyles: opts.inlineStyles,
stylesIncludeFramework: opts.stylesIncludeFramework, stylesIncludeFramework: opts.stylesIncludeFramework,
stylesIncludeUi: opts.stylesIncludeUi,
assetVersion: opts.assetVersion, assetVersion: opts.assetVersion,
clientRuntimes: opts.clientRuntimes, clientRuntimes: opts.clientRuntimes,
head: opts.head, head: opts.head,
+4 -3
View File
@@ -160,6 +160,7 @@ export interface RuntimeDeps {
hasFrameworkStyles?: boolean; hasFrameworkStyles?: boolean;
/** App stylesheet already contains theme + UI CSS and is the only CSS request needed. */ /** App stylesheet already contains theme + UI CSS and is the only CSS request needed. */
stylesIncludeFramework?: boolean; stylesIncludeFramework?: boolean;
stylesIncludeUi?: boolean;
/** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */ /** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */
theme?: ResolvedTheme; theme?: ResolvedTheme;
/** Resolved i18n bundle: enables `ctx.t`, `<html lang>`, and `{t:key}` markers. */ /** Resolved i18n bundle: enables `ctx.t`, `<html lang>`, and `{t:key}` markers. */
@@ -940,12 +941,12 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
// Order: theme tokens, then Wire UI, then the app stylesheet — so the app's // Order: theme tokens, then Wire UI, then the app stylesheet — so the app's
// own CSS (loaded last) can override both the tokens and the UI classes. // own CSS (loaded last) can override both the tokens and the UI classes.
const headParts: string[] = []; const headParts: string[] = [];
if (deps.hasFrameworkStyles && !deps.stylesIncludeFramework) { if (!deps.theme && deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
headParts.push( headParts.push(
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/framework.css", deps.assetVersion)}" />`, `<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/framework.css", deps.assetVersion)}" />`,
); );
} }
if (deps.hasUi && !deps.hasFrameworkStyles && !deps.stylesIncludeFramework) { if (deps.hasUi && !deps.stylesIncludeUi && !deps.stylesIncludeFramework) {
headParts.push( headParts.push(
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/ui.css", deps.assetVersion)}" />`, `<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/ui.css", deps.assetVersion)}" />`,
); );
@@ -1847,7 +1848,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
if (accentName) { if (accentName) {
attrs.push(`data-accent="${accentName}"`); attrs.push(`data-accent="${accentName}"`);
} }
if (!deps.hasFrameworkStyles && !deps.stylesIncludeFramework) { if (!deps.stylesIncludeFramework) {
const themeHref = versionAssetUrl( const themeHref = versionAssetUrl(
activeThemeCssHref(themeName, accentName), activeThemeCssHref(themeName, accentName),
deps.assetVersion, deps.assetVersion,
@@ -38,4 +38,39 @@ describe("syntax contract and security diagnostics", () => {
expect(codes).toContain("WRN-SEC-STRING-TIMER"); expect(codes).toContain("WRN-SEC-STRING-TIMER");
expect(codes).toContain("WRN-PERSIST-SENSITIVE"); expect(codes).toContain("WRN-PERSIST-SENSITIVE");
}); });
test.each([
[
"server output call",
`component Bad { outputs { save() } functions { server function run(): void { output.save() } } view { <div></div> } }`,
"WRN-OUTPUT-SERVER-CALL",
],
[
"unknown output call",
`component Bad { functions { client function run(): void { output.missing() } } view { <button @click="run()">Run</button> } }`,
"WRN-OUTPUT-UNKNOWN",
],
[
"server API in client function",
`component Bad { functions { client function run(): void { process.cwd() } } view { <button @click="run()">Run</button> } }`,
"WRN-CLIENT-SERVER-API",
],
[
"browser API in server function",
`component Bad { functions { server function run(): void { document.title = "bad" } } view { <div></div> } }`,
"WRN-SERVER-BROWSER-API",
],
[
"non-serializable shared state",
`global store Bad { state { values: unknown = new Map() } }`,
"WRN-STATE-NON-SERIALIZABLE",
],
[
"unknown persisted state",
`global store Bad { state { value: string = "x" } persist { storage = "local" include = ["missing"] version = 1 } }`,
"WRN-PERSIST-UNKNOWN-FIELD",
],
])("diagnoses %s", (_name, source, expectedCode) => {
expect(diagnose(source).map((diagnostic) => diagnostic.code)).toContain(expectedCode);
});
}); });
+41
View File
@@ -0,0 +1,41 @@
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { generateApplicationTypesWithPlugins } from "../packages/cli/src/types.ts";
const root = process.cwd();
const source = join(root, "examples", "basic-app");
const scratchParent = join(root, ".wrnexus-type-check");
const scratch = mkdtempSync(`${scratchParent}-`);
const generated = [
join("app", "types", "wrnexus.generated.d.ts"),
join("app", "types", "wrnexus.plugins.generated.d.ts"),
];
try {
cpSync(source, scratch, {
recursive: true,
filter(path) {
const relative = path.slice(source.length).replaceAll("\\", "/");
return !/(?:^|\/)(?:dist|node_modules|\.wrnexus(?:-[^/]*)?)(?:\/|$)/.test(relative);
},
});
await generateApplicationTypesWithPlugins(scratch);
const stale = generated.filter((relative) => {
const committed = join(source, relative);
const expected = join(scratch, relative);
return (
!existsSync(committed) ||
!existsSync(expected) ||
readFileSync(committed, "utf8") !== readFileSync(expected, "utf8")
);
});
if (stale.length) {
throw new Error(
`Generated application types are stale: ${stale.join(", ")}. Run \`bun run generate:example-types\`.`,
);
}
console.log(`Generated application types match ${generated.length} committed artifacts.`);
} finally {
rmSync(scratch, { recursive: true, force: true });
}