finish CSS delivery and generated type remediation
This commit is contained in:
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
+5
-4
@@ -13,15 +13,14 @@ declare namespace WRNexusGenerated {
|
||||
: never;
|
||||
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 RouteName = "about" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "language.tools" | "login" | "modal" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "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 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/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 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 CacheKey = "users";
|
||||
interface Components {
|
||||
"Modal": { props: Record<string, never>; outputs: Record<string, never> };
|
||||
"Counter": { props: { "start"?: unknown; "label"?: unknown }; outputs: Record<string, never> };
|
||||
}
|
||||
interface ApiContracts {
|
||||
@@ -30,6 +29,8 @@ declare namespace WRNexusGenerated {
|
||||
"/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/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/hello": { GET: ApiContract<typeof import("../api/hello.ts")["GET"]> };
|
||||
"/api/login": { POST: ApiContract<typeof import("../api/login.ts")["POST"]> };
|
||||
|
||||
+3
-1
@@ -43,12 +43,14 @@
|
||||
"security:asvs": "node scripts/check-security-asvs.mjs",
|
||||
"generate:security-report": "node scripts/security-performance-audit.mjs --write",
|
||||
"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",
|
||||
"check:ui-visual": "node scripts/check-ui-visual-contract.mjs",
|
||||
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
||||
"sbom": "node scripts/generate-sbom.mjs",
|
||||
"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",
|
||||
"stage:packages": "bun run scripts/publish-packages.ts",
|
||||
"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");
|
||||
});
|
||||
});
|
||||
@@ -566,14 +566,17 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
},
|
||||
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");
|
||||
writeFileSync(combinedInput, combinedSource, "utf8");
|
||||
const combinedCss = await bundleCss(combinedInput, "production");
|
||||
rmSync(combinedInput, { force: true });
|
||||
assetHash.update(combinedCss);
|
||||
// One blocking CSS request in production: tokens → UI → application CSS.
|
||||
// The standalone framework.css remains available for apps without global CSS.
|
||||
// One shared blocking request for UI + application CSS. The small active
|
||||
// theme stylesheet is selected from the request cookie and loaded first.
|
||||
writeFileSync(join(distDir, "styles.css"), combinedCss, "utf8");
|
||||
hasStyles = true;
|
||||
if (Buffer.byteLength(combinedCss, "utf8") <= INLINE_CSS_LIMIT_BYTES) {
|
||||
@@ -655,8 +658,8 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
|
||||
const componentsLit = router.components
|
||||
.map((c) => {
|
||||
const v = `c${counter++}`;
|
||||
imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(c.file))};`);
|
||||
return `{ name: ${JSON.stringify(c.name)}, mod: ${v} }`;
|
||||
imports.push(`import { render as ${v} } from ${JSON.stringify(importPathFor(c.file))};`);
|
||||
return `{ name: ${JSON.stringify(c.name)}, mod: { render: ${v} } }`;
|
||||
})
|
||||
.join(", ");
|
||||
console.log(`✓ Components: ${router.components.length}`);
|
||||
@@ -665,8 +668,8 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
|
||||
const layoutsLit = router.layouts
|
||||
.map((l) => {
|
||||
const v = `c${counter++}`;
|
||||
imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(l.file))};`);
|
||||
return `{ name: ${JSON.stringify(l.name)}, mod: ${v} }`;
|
||||
imports.push(`import { render as ${v} } from ${JSON.stringify(importPathFor(l.file))};`);
|
||||
return `{ name: ${JSON.stringify(l.name)}, mod: { render: ${v} } }`;
|
||||
})
|
||||
.join(", ");
|
||||
if (router.layouts.length) console.log(`✓ Layouts: ${router.layouts.length}`);
|
||||
@@ -748,7 +751,7 @@ await createProductionServer(
|
||||
realtime: ${config.realtime ? JSON.stringify(config.realtime) : "undefined"},
|
||||
publicDir: join(import.meta.dir, "public"),
|
||||
${hasStyles ? `stylesPath: join(import.meta.dir, "styles.css"),` : ""}
|
||||
${hasStyles ? `stylesIncludeFramework: true,` : ""}
|
||||
${hasStyles ? `stylesIncludeUi: true,` : ""}
|
||||
${inlineStyles ? `inlineStyles: ${JSON.stringify(inlineStyles)},` : ""}
|
||||
assetVersion: ${JSON.stringify(assetVersion)},
|
||||
clientRuntimes: ${JSON.stringify(emittedPluginAssets.runtimes)},
|
||||
|
||||
@@ -75,7 +75,7 @@ function writePluginArtifacts(root: string, contributions?: PluginContributions)
|
||||
});
|
||||
writeFileSync(
|
||||
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",
|
||||
);
|
||||
const docsDir = join(root, ".wrnexus", "documentation");
|
||||
@@ -182,7 +182,6 @@ export function generateApplicationTypes(
|
||||
.map((name) => join(root, name))
|
||||
.find(existsSync);
|
||||
const code = `// AUTO-GENERATED by \`wrnexus generate types\` - do not edit.
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
declare namespace WRNexusGenerated {
|
||||
type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer 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);
|
||||
});
|
||||
@@ -91,6 +91,8 @@ export interface ProdOptions {
|
||||
inlineStyles?: string;
|
||||
/** `stylesPath` contains theme + UI + app CSS in cascade order. */
|
||||
stylesIncludeFramework?: boolean;
|
||||
/** `stylesPath` already contains the shared Wire UI stylesheet. */
|
||||
stylesIncludeUi?: boolean;
|
||||
/** Absolute path to the pre-built reactive runtime. */
|
||||
reactivePath?: string;
|
||||
/** Absolute path to the on-demand component controller runtime. */
|
||||
@@ -501,6 +503,7 @@ export function createProductionHandlers(
|
||||
i18n: opts.i18n,
|
||||
inlineStyles: opts.inlineStyles,
|
||||
stylesIncludeFramework: opts.stylesIncludeFramework,
|
||||
stylesIncludeUi: opts.stylesIncludeUi,
|
||||
assetVersion: opts.assetVersion,
|
||||
clientRuntimes: opts.clientRuntimes,
|
||||
head: opts.head,
|
||||
|
||||
@@ -160,6 +160,7 @@ export interface RuntimeDeps {
|
||||
hasFrameworkStyles?: boolean;
|
||||
/** App stylesheet already contains theme + UI CSS and is the only CSS request needed. */
|
||||
stylesIncludeFramework?: boolean;
|
||||
stylesIncludeUi?: boolean;
|
||||
/** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */
|
||||
theme?: ResolvedTheme;
|
||||
/** 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
|
||||
// own CSS (loaded last) can override both the tokens and the UI classes.
|
||||
const headParts: string[] = [];
|
||||
if (deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
|
||||
if (!deps.theme && deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
|
||||
headParts.push(
|
||||
`<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(
|
||||
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/ui.css", deps.assetVersion)}" />`,
|
||||
);
|
||||
@@ -1847,7 +1848,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
if (accentName) {
|
||||
attrs.push(`data-accent="${accentName}"`);
|
||||
}
|
||||
if (!deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
|
||||
if (!deps.stylesIncludeFramework) {
|
||||
const themeHref = versionAssetUrl(
|
||||
activeThemeCssHref(themeName, accentName),
|
||||
deps.assetVersion,
|
||||
|
||||
@@ -38,4 +38,39 @@ describe("syntax contract and security diagnostics", () => {
|
||||
expect(codes).toContain("WRN-SEC-STRING-TIMER");
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user