Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cli-meta-vite-page-routes.md
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/templates/pageRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
`;
}
70 changes: 67 additions & 3 deletions packages/cli/src/tests/createPage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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" });
Expand All @@ -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'");
});
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/tests/integration/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand All @@ -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");
});

Expand Down
Loading