diff --git a/.changeset/cli-meta-vite-page-routes.md b/.changeset/cli-meta-vite-page-routes.md new file mode 100644 index 000000000..7440177dc --- /dev/null +++ b/.changeset/cli-meta-vite-page-routes.md @@ -0,0 +1,5 @@ +--- +"@croco/cli": patch +--- + +Default page generation now emits the current meta-vite `defineRoute` route shape while keeping SPA route config generation explicit to `--mode spa`. diff --git a/packages/cli/README.md b/packages/cli/README.md index 05c6ba8b4..e9563bdf7 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -99,7 +99,7 @@ apps/console-web/pages/{name}/ route.ts # Route definition ``` -The `--mode` flag switches the route template between SSR (default) and SPA rendering. SSR routes are supported for console web apps that declare `@croco/meta-vite`, and SPA routes are supported for console web apps that declare `@croco/frontend-vite`. If a scaffold manifest only supports the other mode, the CLI reports that before writing files. +The `--mode` flag switches the route template between SSR (default) and SPA rendering. SSR routes are supported for console web apps that declare `@croco/meta-vite` and generate a `defineRoute(route)` export typed with `PageRouteDefinition`, matching the current app templates. SPA routes are the explicit legacy `@croco/frontend-vite` path and generate `routeConfig`. If a scaffold manifest only supports the other mode, the CLI reports that before writing files. ## Troubleshooting diff --git a/packages/cli/src/templates/pageRoute.ts b/packages/cli/src/templates/pageRoute.ts index 2b7b8c3c2..aa1e96502 100644 --- a/packages/cli/src/templates/pageRoute.ts +++ b/packages/cli/src/templates/pageRoute.ts @@ -14,13 +14,15 @@ export const routeConfig = { `; } - return `import { defineRoute } from '@croco/meta-vite'; + return `import { defineRoute, type PageRouteDefinition } from '@croco/meta-vite'; import Page from './Page'; -export default defineRoute({ +const route = { path: '${options.path}', mode: 'ssr', component: Page, -}); +} satisfies PageRouteDefinition; + +export default defineRoute(route); `; } diff --git a/packages/cli/src/tests/createPage.spec.ts b/packages/cli/src/tests/createPage.spec.ts index 8ff5d9de7..0ad75abda 100644 --- a/packages/cli/src/tests/createPage.spec.ts +++ b/packages/cli/src/tests/createPage.spec.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import { Project } from "ts-morph"; +import { Project, ts } from "ts-morph"; import { describe, expect, it } from "vitest"; import { runCreatePage } from "../commands/createPage.js"; @@ -30,13 +30,74 @@ describe("runCreatePage", () => { expect(result?.files.map((file) => file.status)).toEqual(["created", "created"]); expect(pageContent).toContain("export default function DashboardPage"); expect(pageContent).not.toContain("@croco/frontend-react"); - expect(routeContent).toContain("import { defineRoute } from '@croco/meta-vite';"); + expect(routeContent).toContain( + "import { defineRoute, type PageRouteDefinition } from '@croco/meta-vite';", + ); + expect(routeContent).toContain("satisfies PageRouteDefinition"); + expect(routeContent).toContain("export default defineRoute(route)"); expect(routeContent).toContain("mode: 'ssr'"); expect(pageContent).not.toContain("CrocoDataFn"); + expect(routeContent).not.toContain("routeConfig"); + expect(routeContent).not.toContain("Component: Page"); expect(routeContent).toContain("path: '/dashboard'"); }); - it("should create a SPA page file set", async () => { + it("should typecheck the generated SSR route against the meta-vite contract", async () => { + const cwd = await createWorkspace(); + + await runCreatePage("Dashboard", { cwd, mode: "ssr" }); + const pageDir = path.join(cwd, "apps", "console-web", "pages", "dashboard"); + const pagePath = path.join(pageDir, "Page.tsx"); + const routePath = path.join(pageDir, "route.ts"); + + const project = new Project({ + useInMemoryFileSystem: true, + compilerOptions: { + jsx: ts.JsxEmit.Preserve, + strict: true, + target: ts.ScriptTarget.ES2022, + }, + }); + project.createSourceFile( + "/types/jsx.d.ts", + `declare namespace JSX { + type Element = unknown; + + interface IntrinsicElements { + main: unknown; + h1: unknown; + p: unknown; + } +} +`, + ); + project.createSourceFile( + "/types/meta-vite.d.ts", + `declare module '@croco/meta-vite' { + export type RenderRouteComponentProps = { + readonly request: Request; + readonly context?: unknown; + }; + + export type PageRouteDefinition = { + readonly path: string; + readonly component: (props: RenderRouteComponentProps) => JSX.Element; + readonly mode?: 'ssr' | 'ssg' | 'isr' | 'rsc'; + }; + + export function defineRoute(route: PageRouteDefinition): PageRouteDefinition; +} +`, + ); + project.createSourceFile(pagePath, await fs.readFile(pagePath, "utf-8")); + project.createSourceFile(routePath, await fs.readFile(routePath, "utf-8")); + + const diagnostics = project.getPreEmitDiagnostics(); + + expect(project.formatDiagnosticsWithColorAndContext(diagnostics)).toBe(""); + }); + + it("should create an explicit SPA legacy frontend-vite page file set", async () => { const cwd = await createWorkspace(); await runCreatePage("SettingsPanel", { cwd, mode: "spa" }); @@ -47,6 +108,9 @@ describe("runCreatePage", () => { expect(pageContent).toContain("export default function SettingsPanelPage"); expect(pageContent).not.toContain("@croco/frontend-react"); expect(routeContent).toContain("Component: Page"); + expect(routeContent).toContain("export const routeConfig"); + expect(routeContent).not.toContain("@croco/meta-vite"); + expect(routeContent).not.toContain("defineRoute"); expect(routeContent).not.toContain("react-router"); expect(routeContent).toContain("path: '/settings-panel'"); }); diff --git a/packages/cli/src/tests/integration/e2e.spec.ts b/packages/cli/src/tests/integration/e2e.spec.ts index 857a7dc10..81f04a146 100644 --- a/packages/cli/src/tests/integration/e2e.spec.ts +++ b/packages/cli/src/tests/integration/e2e.spec.ts @@ -56,10 +56,11 @@ describe("container-fullstack generator e2e", () => { expect(result?.files.map((file) => file.status)).toEqual(["created", "created"]); expect(pageContent).toContain("export default function DashboardPage"); expect(routeContent).toContain("defineRoute"); + expect(routeContent).toContain("satisfies PageRouteDefinition"); expect(routeContent).toContain("path: '/dashboard'"); }); - it("should create a SPA page", async () => { + it("should create an explicit SPA legacy frontend-vite page", async () => { const cwd = await createWorkspace(); const result = await runCreatePage("SettingsPanel", { cwd, mode: "spa" }); @@ -70,6 +71,8 @@ describe("container-fullstack generator e2e", () => { expect(result?.files.map((file) => file.status)).toEqual(["created", "created"]); expect(pageContent).toContain("export default function SettingsPanelPage"); expect(routeContent).toContain("Component: Page"); + expect(routeContent).toContain("routeConfig"); + expect(routeContent).not.toContain("defineRoute"); expect(routeContent).not.toContain("react-router"); });