@rivetjs/nx 0.0.0-stage → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -2
  3. package/executors/build/executor.d.ts +12 -0
  4. package/executors/build/executor.js +50 -0
  5. package/executors/build/schema.d.ts +18 -0
  6. package/executors/build/schema.json +18 -0
  7. package/executors/check/executor.d.ts +11 -0
  8. package/executors/check/executor.js +31 -0
  9. package/executors/check/schema.d.ts +12 -0
  10. package/executors/check/schema.json +18 -0
  11. package/executors/dev/executor.d.ts +14 -0
  12. package/executors/dev/executor.js +32 -0
  13. package/executors/dev/schema.d.ts +16 -0
  14. package/executors/dev/schema.json +17 -0
  15. package/executors/package/executor.d.ts +20 -0
  16. package/executors/package/executor.js +65 -0
  17. package/executors/package/schema.d.ts +10 -0
  18. package/executors/package/schema.json +18 -0
  19. package/executors/prerender/executor.d.ts +8 -0
  20. package/executors/prerender/executor.js +13 -0
  21. package/executors/prerender/schema.d.ts +6 -0
  22. package/executors/prerender/schema.json +12 -0
  23. package/executors/preview/executor.d.ts +12 -0
  24. package/executors/preview/executor.js +52 -0
  25. package/executors/preview/schema.d.ts +10 -0
  26. package/executors/preview/schema.json +14 -0
  27. package/executors/run.d.ts +33 -0
  28. package/executors/run.js +88 -0
  29. package/executors/test/executor.d.ts +8 -0
  30. package/executors/test/executor.js +27 -0
  31. package/executors/test/schema.d.ts +24 -0
  32. package/executors/test/schema.json +21 -0
  33. package/executors/typecheck/executor.d.ts +9 -0
  34. package/executors/typecheck/executor.js +14 -0
  35. package/executors/typecheck/schema.d.ts +6 -0
  36. package/executors/typecheck/schema.json +12 -0
  37. package/executors.json +49 -0
  38. package/generators/api-route/files/__fileName__.ts.template +11 -0
  39. package/generators/api-route/generator.d.ts +5 -0
  40. package/generators/api-route/generator.js +30 -0
  41. package/generators/api-route/schema.d.ts +9 -0
  42. package/generators/api-route/schema.json +31 -0
  43. package/generators/application/files/tests/minimal/src/routes/index.test.tsx.template +9 -0
  44. package/generators/application/files/tests/server/src/routes/index.server.test.tsx.template +8 -0
  45. package/generators/application/files/tests/static/src/components/Counter.test.tsx.template +10 -0
  46. package/generators/application/files/tests/ui/src/components/LikeButton.server.test.tsx.template +7 -0
  47. package/generators/application/files/vitest/vitest.config.ts.template +16 -0
  48. package/generators/application/generator.d.ts +20 -0
  49. package/generators/application/generator.js +161 -0
  50. package/generators/application/schema.d.ts +24 -0
  51. package/generators/application/schema.json +57 -0
  52. package/generators/component/files/basic/__fileName__.tsx.template +24 -0
  53. package/generators/component/files/overlay/__fileName__.tsx.template +32 -0
  54. package/generators/component/files/story/__storyName__.tsx.template +24 -0
  55. package/generators/component/files/test-basic/__fileName__.test.tsx.template +10 -0
  56. package/generators/component/files/test-overlay/__fileName__.test.tsx.template +10 -0
  57. package/generators/component/generator.d.ts +5 -0
  58. package/generators/component/generator.js +45 -0
  59. package/generators/component/schema.d.ts +17 -0
  60. package/generators/component/schema.json +35 -0
  61. package/generators/i18n-setup/files/i18n.config.ts.template +10 -0
  62. package/generators/i18n-setup/generator.d.ts +7 -0
  63. package/generators/i18n-setup/generator.js +65 -0
  64. package/generators/i18n-setup/schema.d.ts +13 -0
  65. package/generators/i18n-setup/schema.json +24 -0
  66. package/generators/init/generator.d.ts +8 -0
  67. package/generators/init/generator.js +48 -0
  68. package/generators/init/schema.d.ts +13 -0
  69. package/generators/init/schema.json +19 -0
  70. package/generators/layout/files/_layout.tsx.template +12 -0
  71. package/generators/layout/generator.d.ts +5 -0
  72. package/generators/layout/generator.js +23 -0
  73. package/generators/layout/schema.d.ts +9 -0
  74. package/generators/layout/schema.json +26 -0
  75. package/generators/library/files/lib/README.md.template +18 -0
  76. package/generators/library/files/lib/package.json.template +12 -0
  77. package/generators/library/files/lib/src/__className__.tsx.template +16 -0
  78. package/generators/library/files/lib/src/index.ts.template +1 -0
  79. package/generators/library/files/lib/tsconfig.json.template +21 -0
  80. package/generators/library/files/publishable/tsconfig.lib.json.template +13 -0
  81. package/generators/library/files/subpath/src/__subpath__/__subpathClass__.tsx.template +10 -0
  82. package/generators/library/files/subpath/src/__subpath__/index.ts.template +1 -0
  83. package/generators/library/files/vitest/src/__className__.test.tsx.template +13 -0
  84. package/generators/library/generator.d.ts +13 -0
  85. package/generators/library/generator.js +116 -0
  86. package/generators/library/schema.d.ts +19 -0
  87. package/generators/library/schema.json +32 -0
  88. package/generators/preset/generator.d.ts +5 -0
  89. package/generators/preset/generator.js +29 -0
  90. package/generators/preset/schema.d.ts +16 -0
  91. package/generators/preset/schema.json +30 -0
  92. package/generators/route/files/__fileName__.tsx.template +43 -0
  93. package/generators/route/generator.d.ts +10 -0
  94. package/generators/route/generator.js +67 -0
  95. package/generators/route/schema.d.ts +15 -0
  96. package/generators/route/schema.json +29 -0
  97. package/generators/ui-setup/generator.d.ts +7 -0
  98. package/generators/ui-setup/generator.js +55 -0
  99. package/generators/ui-setup/schema.d.ts +7 -0
  100. package/generators/ui-setup/schema.json +21 -0
  101. package/generators.json +66 -0
  102. package/index.d.ts +22 -0
  103. package/index.js +24 -0
  104. package/migrations/0.0.1/example-noop.d.ts +2 -0
  105. package/migrations/0.0.1/example-noop.js +3 -0
  106. package/migrations.json +10 -0
  107. package/package.json +74 -4
  108. package/plugin.d.ts +12 -0
  109. package/plugin.js +44 -0
  110. package/utils/deploy.d.ts +14 -0
  111. package/utils/deploy.js +86 -0
  112. package/utils/detect.d.ts +42 -0
  113. package/utils/detect.js +60 -0
  114. package/utils/facts.d.ts +8 -0
  115. package/utils/facts.js +42 -0
  116. package/utils/project.d.ts +28 -0
  117. package/utils/project.js +67 -0
  118. package/utils/starter.d.ts +9 -0
  119. package/utils/starter.js +34 -0
  120. package/utils/targets.d.ts +22 -0
  121. package/utils/targets.js +117 -0
  122. package/utils/versions.d.ts +13 -0
  123. package/utils/versions.js +31 -0
  124. package/utils/vite-config.d.ts +8 -0
  125. package/utils/vite-config.js +104 -0
  126. package/utils/workspace.d.ts +13 -0
  127. package/utils/workspace.js +63 -0
@@ -0,0 +1,33 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ import type { ExecutorContext } from "@nx/devkit";
3
+ /** One child process: `node <args…>` in `cwd`. */
4
+ export interface Step {
5
+ /** Shown before the step runs. */
6
+ label: string;
7
+ /** Arguments after the Node binary (`[bin, …args]` or `["--input-type=module", "-e", code]`). */
8
+ args: string[];
9
+ cwd: string;
10
+ env?: Record<string, string>;
11
+ }
12
+ /** The project folder (absolute) of the running target. */
13
+ export declare function projectRoot(context: ExecutorContext): string;
14
+ /** The package folder of `pkg` as Node would find it from `from` (walks up `node_modules`); real path. */
15
+ export declare function packageDir(pkg: string, from: string): string;
16
+ /** Absolute path of a package's bin (`bin` field; `name` picks one of several). */
17
+ export declare function binPath(pkg: string, from: string, name?: string): string;
18
+ /** Tool lookup the normalisers use; tests pass a fake one. */
19
+ export type BinResolver = (pkg: string, name?: string) => string;
20
+ export declare const resolverFrom: (from: string) => BinResolver;
21
+ /** Command-line flags from an options object (`--port 4510`, `--open`), in a fixed order. */
22
+ export declare function flags(entries: [flag: string, value: string | number | boolean | undefined][]): string[];
23
+ /** Starts a step; `exit` resolves with its status (signals count as failure). */
24
+ export declare function start(step: Step): {
25
+ child: ChildProcess;
26
+ exit: Promise<number>;
27
+ };
28
+ /** Runs steps in order; stops at the first failure. */
29
+ export declare function runSteps(steps: Step[], { verbose }?: {
30
+ verbose?: boolean | undefined;
31
+ }): Promise<{
32
+ success: boolean;
33
+ }>;
@@ -0,0 +1,88 @@
1
+ // Shared executor plumbing: resolve a tool's bin from the project folder (the project's own install wins, then the
2
+ // workspace's, like `npm run`), run steps as child processes (`node <bin> …`, so no shell and no PATH lookup), and
3
+ // stream their output. Executors normalise their options into `Step[]` with pure functions (unit-tested).
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ /** The project folder (absolute) of the running target. */
8
+ export function projectRoot(context) {
9
+ const name = context.projectName;
10
+ const project = name ? context.projectsConfigurations?.projects[name] : undefined;
11
+ if (!project)
12
+ throw new Error(`@rivetjs/nx: no project for ${name ?? "(unknown)"}`);
13
+ return resolve(context.root, project.root);
14
+ }
15
+ /** The package folder of `pkg` as Node would find it from `from` (walks up `node_modules`); real path. */
16
+ export function packageDir(pkg, from) {
17
+ for (let dir = resolve(from);; dir = dirname(dir)) {
18
+ const candidate = join(dir, "node_modules", pkg, "package.json");
19
+ if (existsSync(candidate))
20
+ return realpathSync(dirname(candidate));
21
+ if (dirname(dir) === dir)
22
+ throw new Error(`@rivetjs/nx: cannot find ${pkg} from ${from} (install it in the workspace)`);
23
+ }
24
+ }
25
+ /** Absolute path of a package's bin (`bin` field; `name` picks one of several). */
26
+ export function binPath(pkg, from, name) {
27
+ const dir = packageDir(pkg, from);
28
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
29
+ const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[name ?? pkg.split("/").pop()];
30
+ if (!bin)
31
+ throw new Error(`@rivetjs/nx: ${pkg} has no bin${name ? ` "${name}"` : ""}`);
32
+ return join(dir, bin);
33
+ }
34
+ export const resolverFrom = (from) => (pkg, name) => binPath(pkg, from, name);
35
+ /** Command-line flags from an options object (`--port 4510`, `--open`), in a fixed order. */
36
+ export function flags(entries) {
37
+ const out = [];
38
+ for (const [flag, value] of entries) {
39
+ if (value === undefined || value === false)
40
+ continue;
41
+ out.push(flag);
42
+ if (value !== true)
43
+ out.push(String(value));
44
+ }
45
+ return out;
46
+ }
47
+ /** Children started by this process (killed when it gets SIGINT / SIGTERM, so continuous targets stop cleanly). */
48
+ const children = new Set();
49
+ let hooked = false;
50
+ function hookSignals() {
51
+ if (hooked)
52
+ return;
53
+ hooked = true;
54
+ for (const signal of ["SIGINT", "SIGTERM"]) {
55
+ process.once(signal, () => {
56
+ for (const child of children)
57
+ child.kill(signal);
58
+ process.exit(130);
59
+ });
60
+ }
61
+ }
62
+ /** Starts a step; `exit` resolves with its status (signals count as failure). */
63
+ export function start(step) {
64
+ hookSignals();
65
+ const child = spawn(process.execPath, step.args, { cwd: step.cwd, env: { ...process.env, ...step.env }, stdio: "inherit" });
66
+ children.add(child);
67
+ const exit = new Promise((done) => {
68
+ child.on("error", (error) => {
69
+ console.error(error);
70
+ done(1);
71
+ });
72
+ child.on("exit", (code) => {
73
+ children.delete(child);
74
+ done(code ?? 1);
75
+ });
76
+ });
77
+ return { child, exit };
78
+ }
79
+ /** Runs steps in order; stops at the first failure. */
80
+ export async function runSteps(steps, { verbose = true } = {}) {
81
+ for (const step of steps) {
82
+ if (verbose)
83
+ console.log(`> ${step.label}`);
84
+ if ((await start(step).exit) !== 0)
85
+ return { success: false };
86
+ }
87
+ return { success: true };
88
+ }
@@ -0,0 +1,8 @@
1
+ import type { ExecutorContext } from "@nx/devkit";
2
+ import { type BinResolver, type Step } from "../run.js";
3
+ import type { TestExecutorSchema } from "./schema.js";
4
+ export type { TestExecutorSchema } from "./schema.js";
5
+ export declare function testStep(options: TestExecutorSchema, root: string, bin: BinResolver): Step;
6
+ export default function testExecutor(options: TestExecutorSchema, context: ExecutorContext): Promise<{
7
+ success: boolean;
8
+ }>;
@@ -0,0 +1,27 @@
1
+ import { flags, projectRoot, resolverFrom, runSteps } from "../run.js";
2
+ export function testStep(options, root, bin) {
3
+ return {
4
+ label: options.watch ? "vitest (watch)" : "vitest run",
5
+ args: [
6
+ bin("vitest"),
7
+ ...(options.watch ? [] : ["run"]),
8
+ ...(options.testFiles ?? []),
9
+ ...flags([
10
+ ["--config", options.config],
11
+ ["--coverage", options.coverage],
12
+ ["--maxWorkers", options.maxWorkers],
13
+ ["--passWithNoTests", options.passWithNoTests ?? true],
14
+ ["--update", options.update],
15
+ ["--testNamePattern", options.testNamePattern],
16
+ ]),
17
+ ...(options.project ?? []).flatMap((name) => ["--project", name]),
18
+ ...(options.reporters ?? []).flatMap((name) => ["--reporter", name]),
19
+ ],
20
+ cwd: root,
21
+ env: options.env ?? {},
22
+ };
23
+ }
24
+ export default async function testExecutor(options, context) {
25
+ const root = projectRoot(context);
26
+ return runSteps([testStep(options, root, resolverFrom(root))]);
27
+ }
@@ -0,0 +1,24 @@
1
+ export interface TestExecutorSchema {
2
+ /** Vitest config file, relative to the project (default: Vitest's lookup, `vitest.config.ts`). */
3
+ config?: string;
4
+ /** Watch mode (`vitest` instead of `vitest run`). Default false. */
5
+ watch?: boolean;
6
+ /** Collect coverage (`--coverage`, output `coverage/`). */
7
+ coverage?: boolean;
8
+ /** Worker cap (`--maxWorkers`). */
9
+ maxWorkers?: number | string;
10
+ /** Succeed when no test file matches. Default true. */
11
+ passWithNoTests?: boolean;
12
+ /** Update snapshots (`-u`). */
13
+ update?: boolean;
14
+ /** Vitest projects to run (`--project`, e.g. `dom`, `server`). */
15
+ project?: string[];
16
+ /** Test file filters (positional). */
17
+ testFiles?: string[];
18
+ /** Test name pattern (`-t`). */
19
+ testNamePattern?: string;
20
+ /** Vitest reporter(s). */
21
+ reporters?: string[];
22
+ /** Extra environment variables. */
23
+ env?: Record<string, string>;
24
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "version": 2,
4
+ "title": "Rivet test",
5
+ "description": "Runs Vitest in the project folder (the project's vitest.config.ts with rivetTest() from @rivetjs/testing/vite: DOM target in happy-dom, server target for *.server.test.tsx).",
6
+ "type": "object",
7
+ "properties": {
8
+ "config": { "type": "string", "description": "Vitest config file, relative to the project." },
9
+ "watch": { "type": "boolean", "default": false, "description": "Watch mode." },
10
+ "coverage": { "type": "boolean", "description": "Collect coverage (coverage/)." },
11
+ "maxWorkers": { "oneOf": [{ "type": "number" }, { "type": "string" }], "description": "Worker cap (--maxWorkers)." },
12
+ "passWithNoTests": { "type": "boolean", "default": true, "description": "Succeed when no test file matches." },
13
+ "update": { "type": "boolean", "description": "Update snapshots (-u)." },
14
+ "project": { "type": "array", "items": { "type": "string" }, "description": "Vitest projects to run (--project)." },
15
+ "testFiles": { "type": "array", "items": { "type": "string" }, "description": "Test file filters." },
16
+ "testNamePattern": { "type": "string", "description": "Test name pattern (-t)." },
17
+ "reporters": { "type": "array", "items": { "type": "string" }, "description": "Vitest reporters." },
18
+ "env": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Extra environment variables." }
19
+ },
20
+ "additionalProperties": false
21
+ }
@@ -0,0 +1,9 @@
1
+ import type { ExecutorContext } from "@nx/devkit";
2
+ import type { ProjectFacts } from "../../utils/detect.js";
3
+ import { type BinResolver, type Step } from "../run.js";
4
+ import type { TypecheckExecutorSchema } from "./schema.js";
5
+ export type { TypecheckExecutorSchema } from "./schema.js";
6
+ export declare function typecheckSteps(options: TypecheckExecutorSchema, root: string, facts: ProjectFacts | undefined, bin: BinResolver): Step[];
7
+ export default function typecheckExecutor(options: TypecheckExecutorSchema, context: ExecutorContext): Promise<{
8
+ success: boolean;
9
+ }>;
@@ -0,0 +1,14 @@
1
+ import { projectFacts } from "../../utils/facts.js";
2
+ import { syncStep } from "../check/executor.js";
3
+ import { projectRoot, resolverFrom, runSteps } from "../run.js";
4
+ export function typecheckSteps(options, root, facts, bin) {
5
+ const steps = [];
6
+ if (options.sync ?? facts?.kind === "app")
7
+ steps.push(syncStep(root, bin));
8
+ steps.push({ label: "tsc --noEmit", args: [bin("typescript", "tsc"), "-p", options.tsConfig ?? "tsconfig.json", "--noEmit"], cwd: root });
9
+ return steps;
10
+ }
11
+ export default async function typecheckExecutor(options, context) {
12
+ const root = projectRoot(context);
13
+ return runSteps(typecheckSteps(options, root, projectFacts(root), resolverFrom(root)));
14
+ }
@@ -0,0 +1,6 @@
1
+ export interface TypecheckExecutorSchema {
2
+ /** tsconfig file, relative to the project. Default `tsconfig.json`. */
3
+ tsConfig?: string;
4
+ /** Run `rivet sync` first, so `.rivet/routes.d.ts` is current. Default: routed apps only. */
5
+ sync?: boolean;
6
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "version": 2,
4
+ "title": "Rivet typecheck",
5
+ "description": "tsc --noEmit for a project; routed apps run rivet sync first so the generated route types (.rivet/routes.d.ts) are current.",
6
+ "type": "object",
7
+ "properties": {
8
+ "tsConfig": { "type": "string", "default": "tsconfig.json", "description": "tsconfig file, relative to the project." },
9
+ "sync": { "type": "boolean", "description": "Run rivet sync first. Default: routed apps only." }
10
+ },
11
+ "additionalProperties": false
12
+ }
package/executors.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "executors": {
3
+ "build": {
4
+ "implementation": "./executors/build/executor",
5
+ "schema": "./executors/build/schema.json",
6
+ "description": "Client + server Vite builds (one client build per locale with i18n), then prerender for static apps."
7
+ },
8
+ "dev": {
9
+ "implementation": "./executors/dev/executor",
10
+ "schema": "./executors/dev/schema.json",
11
+ "description": "Vite dev server."
12
+ },
13
+ "serve": {
14
+ "implementation": "./executors/dev/executor",
15
+ "schema": "./executors/dev/schema.json",
16
+ "description": "Vite dev server (alias of dev)."
17
+ },
18
+ "preview": {
19
+ "implementation": "./executors/preview/executor",
20
+ "schema": "./executors/preview/schema.json",
21
+ "description": "Serve the build: the project's serve.js, the router's Node server, or vite preview."
22
+ },
23
+ "prerender": {
24
+ "implementation": "./executors/prerender/executor",
25
+ "schema": "./executors/prerender/schema.json",
26
+ "description": "Render every page of an existing build to HTML files (prerenderBuild)."
27
+ },
28
+ "check": {
29
+ "implementation": "./executors/check/executor",
30
+ "schema": "./executors/check/schema.json",
31
+ "description": "Rivet diagnostics (rivet sync + rivet-check)."
32
+ },
33
+ "test": {
34
+ "implementation": "./executors/test/executor",
35
+ "schema": "./executors/test/schema.json",
36
+ "description": "Vitest with the project's rivetTest() config."
37
+ },
38
+ "typecheck": {
39
+ "implementation": "./executors/typecheck/executor",
40
+ "schema": "./executors/typecheck/schema.json",
41
+ "description": "tsc --noEmit (rivet sync first for routed apps)."
42
+ },
43
+ "package": {
44
+ "implementation": "./executors/package/executor",
45
+ "schema": "./executors/package/schema.json",
46
+ "description": "Publishable library package: .js / .jsx (JSX preserved) + .d.ts and a mapped package.json."
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,11 @@
1
+ import type { RequestEvent } from "@rivetjs/router";
2
+ <% for (const method of methods) { %>
3
+ /** `<%= method.toUpperCase() %> <%= url %>`. Runs only on the server; must return a `Response`. */
4
+ export const on<%= method[0].toUpperCase() + method.slice(1) %> = async (<% if (method === "get" || method === "delete") { %>{ params }<% } else { %>{ request, params }<% } %>: RequestEvent) => {<% if (method === "get") { %>
5
+ return Response.json({ ok: true, params }, { headers: { "cache-control": "no-store" } });<% } else if (method === "delete") { %>
6
+ console.log("delete", params);
7
+ return new Response(null, { status: 204 });<% } else { %>
8
+ const body: unknown = await request.json().catch(() => null);
9
+ return Response.json({ ok: true, params, body }<% if (method === "post") { %>, { status: 201 }<% } %>);<% } %>
10
+ };
11
+ <% } %>
@@ -0,0 +1,5 @@
1
+ import { type Tree } from "@nx/devkit";
2
+ import type { ApiRouteGeneratorSchema } from "./schema.js";
3
+ export type { ApiRouteGeneratorSchema } from "./schema.js";
4
+ export declare function apiRouteGenerator(tree: Tree, options: ApiRouteGeneratorSchema): Promise<void>;
5
+ export default apiRouteGenerator;
@@ -0,0 +1,30 @@
1
+ // `nx g @rivetjs/nx:api-route api/items/[id] --project=web --methods=get,post`: an endpoint route (plain `.ts`
2
+ // in src/routes exporting request handlers). Server-rendered apps only: a prerendered site has no server.
3
+ import { fileURLToPath } from "node:url";
4
+ import { formatFiles, generateFiles, joinPathFragments } from "@nx/devkit";
5
+ import { routedApp, routeSegments } from "../../utils/project.js";
6
+ import { routeUrl } from "../route/generator.js";
7
+ const FILES = fileURLToPath(new URL("./files", import.meta.url));
8
+ const METHODS = ["get", "post", "put", "patch", "delete"];
9
+ export async function apiRouteGenerator(tree, options) {
10
+ const project = routedApp(tree, options.project, "api-route");
11
+ if (project.facts?.static)
12
+ throw new Error('api-route: endpoints need a server; this app is prerendered (rivetRouter({ mode: "static" }))');
13
+ const segments = routeSegments(options.path);
14
+ if (!segments.length)
15
+ throw new Error("api-route: path is required (e.g. api/health)");
16
+ const methods = [...new Set((options.methods ?? ["get"]).flatMap((method) => String(method).split(",")).map((method) => method.trim().toLowerCase()))];
17
+ for (const method of methods)
18
+ if (!METHODS.includes(method))
19
+ throw new Error(`api-route: unknown method "${method}"`);
20
+ const folder = joinPathFragments(project.routes, ...segments.slice(0, -1));
21
+ const fileName = segments.at(-1);
22
+ for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
23
+ if (tree.exists(joinPathFragments(folder, fileName + ext)))
24
+ throw new Error(`api-route: ${joinPathFragments(folder, fileName + ext)} already exists`);
25
+ }
26
+ generateFiles(tree, FILES, folder, { fileName, methods: METHODS.filter((method) => methods.includes(method)), url: routeUrl(segments) });
27
+ if (!options.skipFormat)
28
+ await formatFiles(tree);
29
+ }
30
+ export default apiRouteGenerator;
@@ -0,0 +1,9 @@
1
+ export interface ApiRouteGeneratorSchema {
2
+ /** Endpoint path under `src/routes` (`api/health`, `api/items/[id]`). */
3
+ path: string;
4
+ /** The Rivet app. */
5
+ project: string;
6
+ /** Request handlers to write. Default `["get"]`. */
7
+ methods?: ("get" | "post" | "put" | "patch" | "delete")[];
8
+ skipFormat?: boolean;
9
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "RivetApiRoute",
4
+ "title": "Rivet API route",
5
+ "description": "Adds an endpoint route (a plain .ts file in src/routes with onGet / onPost / … request handlers) to a server-rendered Rivet app.",
6
+ "type": "object",
7
+ "properties": {
8
+ "path": {
9
+ "type": "string",
10
+ "description": "Endpoint path under src/routes (api/health, api/items/[id]).",
11
+ "$default": { "$source": "argv", "index": 0 },
12
+ "x-prompt": "Endpoint path (under src/routes, e.g. api/health)?"
13
+ },
14
+ "project": {
15
+ "type": "string",
16
+ "description": "The Rivet app.",
17
+ "$default": { "$source": "projectName" },
18
+ "x-dropdown": "projects",
19
+ "x-prompt": "Which project?"
20
+ },
21
+ "methods": {
22
+ "type": "array",
23
+ "items": { "type": "string", "enum": ["get", "post", "put", "patch", "delete"] },
24
+ "default": ["get"],
25
+ "description": "Request handlers to write."
26
+ },
27
+ "skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
28
+ },
29
+ "required": ["path", "project"],
30
+ "additionalProperties": false
31
+ }
@@ -0,0 +1,9 @@
1
+ import { expect, test } from "vitest";
2
+ import { fire, render } from "@rivetjs/runtime/testing";
3
+ import Home from "./index.tsx";
4
+
5
+ test("counts clicks", () => {
6
+ const view = render(() => <Home />);
7
+ fire(view.find("button"), "click");
8
+ expect(view.find("button").textContent).toBe("Clicked 1 times");
9
+ });
@@ -0,0 +1,8 @@
1
+ import { expect, test } from "vitest";
2
+ import { renderToString } from "@rivetjs/runtime/testing/server";
3
+ import Home from "./index.tsx";
4
+
5
+ test("renders the loader's notes on the server", async () => {
6
+ const html = await renderToString(() => <Home data={{ notes: ["First note"] }} params={{}} url={new URL("http://localhost/")} />);
7
+ expect(html).toContain("First note");
8
+ });
@@ -0,0 +1,10 @@
1
+ import { expect, test } from "vitest";
2
+ import { fire, render } from "@rivetjs/runtime/testing";
3
+ import { Counter } from "./Counter.tsx";
4
+
5
+ test("counts clicks", () => {
6
+ const view = render(() => <Counter />);
7
+ const button = view.find("button");
8
+ fire(button, "click");
9
+ expect(button.textContent).toBe("Clicked 1 times");
10
+ });
@@ -0,0 +1,7 @@
1
+ import { expect, test } from "vitest";
2
+ import { renderToString } from "@rivetjs/runtime/testing/server";
3
+ import { LikeButton } from "./LikeButton.tsx";
4
+
5
+ test("renders the like count on the server", async () => {
6
+ expect(await renderToString(() => <LikeButton />)).toContain("Liked 0 times");
7
+ });
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { rivetTest } from "@rivetjs/testing/vite";
3
+
4
+ // Two projects: components in happy-dom (DOM target), `*.server.test.tsx` as server HTML in Node.
5
+ export default defineConfig({
6
+ plugins: [rivetTest()],
7
+ test: {
8
+ projects: [
9
+ {
10
+ extends: true,
11
+ test: { name: "dom", include: ["src/**/*.test.tsx"], exclude: ["src/**/*.server.test.tsx"], environment: "happy-dom" },
12
+ },
13
+ { extends: true, test: { name: "server", include: ["src/**/*.server.test.tsx"], environment: "node" } },
14
+ ],
15
+ },
16
+ });
@@ -0,0 +1,20 @@
1
+ import { type GeneratorCallback, type Tree } from "@nx/devkit";
2
+ import { type DeployTarget } from "../../utils/deploy.js";
3
+ import type { ApplicationGeneratorSchema } from "./schema.js";
4
+ export type { ApplicationGeneratorSchema } from "./schema.js";
5
+ export interface NormalizedApplicationOptions {
6
+ directory: string;
7
+ name: string;
8
+ template: "static" | "server" | "ui" | "minimal";
9
+ adapter: "node" | "static";
10
+ /** create-rivet `--deploy` platform target. */
11
+ deploy?: DeployTarget;
12
+ tailwind: boolean;
13
+ unitTestRunner: "vitest" | "none";
14
+ tags: string[];
15
+ rivet: string;
16
+ addPlugin: boolean;
17
+ }
18
+ export declare function normalizeApplicationOptions(tree: Tree, options: ApplicationGeneratorSchema): NormalizedApplicationOptions;
19
+ export declare function applicationGenerator(tree: Tree, schema: ApplicationGeneratorSchema): Promise<GeneratorCallback>;
20
+ export default applicationGenerator;
@@ -0,0 +1,161 @@
1
+ // `nx g @rivetjs/nx:application apps/web --template=static|server|ui|minimal [--adapter=node|static] [--tailwind]`:
2
+ // a create-rivet starter in an Nx workspace. The starter files come from create-rivet (`utils/starter.ts`); this
3
+ // generator only layers Nx concerns over them: project.json, adapter / Tailwind switches, Vitest, dependencies.
4
+ import { fileURLToPath } from "node:url";
5
+ import { addDependenciesToPackageJson, formatFiles, generateFiles, getProjects, joinPathFragments, names, installPackagesTask, readJson, writeJson, } from "@nx/devkit";
6
+ import { applyDeploy, deployProblem, deployScripts, findDeployTarget } from "../../utils/deploy.js";
7
+ import { treeFacts } from "../../utils/facts.js";
8
+ import { starterFile, writeStarter } from "../../utils/starter.js";
9
+ import { rivetTargets } from "../../utils/targets.js";
10
+ import { HAPPY_DOM_VERSION, rivetRange, toolVersion, VITEST_VERSION } from "../../utils/versions.js";
11
+ import { addImport, addPlugin, removePlugin, setRouterOption } from "../../utils/vite-config.js";
12
+ import { addToWorkspaces, isWorkspacePackage, offsetTo, shouldAddPlugin } from "../../utils/workspace.js";
13
+ import { initGenerator } from "../init/generator.js";
14
+ const FILES = fileURLToPath(new URL("./files", import.meta.url));
15
+ export function normalizeApplicationOptions(tree, options) {
16
+ const directory = options.directory.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
17
+ if (!directory)
18
+ throw new Error("application: directory is required (e.g. apps/web)");
19
+ const name = options.name ?? names(directory.split("/").pop()).fileName;
20
+ const template = options.template ?? "static";
21
+ const adapter = options.adapter ?? (template === "server" ? "node" : "static");
22
+ const tailwind = options.tailwind ?? template !== "minimal";
23
+ if (template === "ui" && !tailwind)
24
+ throw new Error("application: the ui starter needs Tailwind (@rivetjs/ui styles are Tailwind v4)");
25
+ if (template === "server" && adapter === "static")
26
+ throw new Error("application: the server starter's form action needs a server; use --template=static for a prerendered site");
27
+ const deploy = options.deploy;
28
+ if (deploy !== undefined) {
29
+ if (!findDeployTarget(deploy))
30
+ throw new Error(`application: unknown deploy target "${deploy}"`);
31
+ const problem = deployProblem(deploy, template);
32
+ if (problem)
33
+ throw new Error(`application: ${problem}`);
34
+ }
35
+ if (getProjects(tree).has(name))
36
+ throw new Error(`application: a project named "${name}" already exists`);
37
+ if (tree.exists(directory) && tree.children(directory).length)
38
+ throw new Error(`application: ${directory} is not empty`);
39
+ return {
40
+ directory,
41
+ name,
42
+ template,
43
+ adapter,
44
+ ...(deploy ? { deploy } : {}),
45
+ tailwind,
46
+ unitTestRunner: options.unitTestRunner ?? "vitest",
47
+ tags: (options.tags ?? "")
48
+ .split(",")
49
+ .map((tag) => tag.trim())
50
+ .filter(Boolean),
51
+ rivet: rivetRange(options.rivetVersion),
52
+ addPlugin: shouldAddPlugin(tree, options.addPlugin),
53
+ };
54
+ }
55
+ /** Static ↔ node: router `mode`, the preview / prerender scripts of the matching starter. */
56
+ function applyAdapter(tree, o) {
57
+ const vite = joinPathFragments(o.directory, "vite.config.ts");
58
+ const isStatic = o.template !== "server";
59
+ if ((o.adapter === "static") === isStatic)
60
+ return;
61
+ // A static starter served by Node: no prerender step, the server starter's serve.js.
62
+ tree.write(vite, setRouterOption(tree.read(vite, "utf8"), "mode", undefined).replace(/\/\/ Pages in src\/routes are rendered to HTML files[^\n]*\n/, ""));
63
+ tree.delete(joinPathFragments(o.directory, "prerender.js"));
64
+ tree.write(joinPathFragments(o.directory, "serve.js"), starterFile("server", "serve.js"));
65
+ }
66
+ const TAILWIND_IMPORT = 'import tailwindcss from "@tailwindcss/vite";';
67
+ /** Tailwind off for a Tailwind starter, or on for the minimal one. */
68
+ function applyTailwind(tree, o, pkg) {
69
+ const vite = joinPathFragments(o.directory, "vite.config.ts");
70
+ const styles = joinPathFragments(o.directory, "src/styles.css");
71
+ const has = tree.read(vite, "utf8").includes("@tailwindcss/vite");
72
+ if (o.tailwind === has)
73
+ return;
74
+ const dev = (pkg.devDependencies ??= {});
75
+ if (!o.tailwind) {
76
+ tree.write(vite, removePlugin(tree.read(vite, "utf8"), "tailwindcss()", /@tailwindcss\/vite/));
77
+ delete dev["tailwindcss"];
78
+ delete dev["@tailwindcss/vite"];
79
+ tree.write(styles, "body {\n margin: 0;\n font-family: system-ui, sans-serif;\n}\n");
80
+ return;
81
+ }
82
+ let text = addImport(tree.read(vite, "utf8"), TAILWIND_IMPORT);
83
+ text = addPlugin(text, "tailwindcss()", "last");
84
+ text = setRouterOption(text, "styles", '["src/styles.css"]');
85
+ tree.write(vite, text);
86
+ tree.write(styles, starterFile("static", "src/styles.css"));
87
+ dev["tailwindcss"] = toolVersion("tailwindcss");
88
+ dev["@tailwindcss/vite"] = toolVersion("@tailwindcss/vite");
89
+ }
90
+ /** The README section that maps the starter's npm scripts to Nx targets. */
91
+ function nxReadme(o) {
92
+ const lines = [
93
+ "",
94
+ "## Nx",
95
+ "",
96
+ "In this workspace the package.json scripts are Nx targets:",
97
+ "",
98
+ `- \`nx dev ${o.name}\`: dev server`,
99
+ `- \`nx build ${o.name}\`: client + server build${o.adapter === "static" ? ", then every page prerendered to `dist/client`" : ""} (cached)`,
100
+ `- \`nx preview ${o.name}\`: serve the build`,
101
+ `- \`nx check ${o.name}\` / \`nx typecheck ${o.name}\`: Rivet diagnostics / TypeScript`,
102
+ ];
103
+ if (o.unitTestRunner === "vitest")
104
+ lines.push(`- \`nx test ${o.name}\`: Vitest`);
105
+ if (o.deploy && deployScripts(o.deploy)["deploy"])
106
+ lines.push(`- \`nx deploy ${o.name}\`: build, then deploy (\`${deployScripts(o.deploy)["deploy"]}\`)`);
107
+ lines.push(`- \`nx g @rivetjs/nx:route <path> --project=${o.name}\`, \`nx g @rivetjs/nx:component <Name> --project=${o.name}\`: new route / component`, "");
108
+ return lines.join("\n");
109
+ }
110
+ export async function applicationGenerator(tree, schema) {
111
+ const o = normalizeApplicationOptions(tree, schema);
112
+ await initGenerator(tree, {
113
+ addPlugin: o.addPlugin,
114
+ skipFormat: true,
115
+ skipInstall: true,
116
+ ...(schema.rivetVersion ? { rivetVersion: schema.rivetVersion } : {}),
117
+ ...(schema.skipPackageJson ? { skipPackageJson: true } : {}),
118
+ });
119
+ writeStarter(tree, o.template, o.directory, o.name, o.rivet);
120
+ const pkgPath = joinPathFragments(o.directory, "package.json");
121
+ let pkg = readJson(tree, pkgPath);
122
+ // Nx targets replace the npm scripts (inferred from vite.config.ts, or written to project.json below).
123
+ delete pkg["scripts"];
124
+ applyAdapter(tree, o);
125
+ applyTailwind(tree, o, pkg);
126
+ // After the adapter / Tailwind edits: the overlay edits the final vite.config.ts (as create-rivet does after copying).
127
+ if (o.deploy)
128
+ pkg = applyDeploy(tree, o.directory, o.template, o.deploy, o.name, pkg);
129
+ if (o.unitTestRunner === "vitest") {
130
+ generateFiles(tree, joinPathFragments(FILES, "vitest"), o.directory, {});
131
+ generateFiles(tree, joinPathFragments(FILES, "tests", o.template), o.directory, {});
132
+ const dev = (pkg.devDependencies ??= {});
133
+ dev["@rivetjs/testing"] = o.rivet;
134
+ dev["vitest"] = VITEST_VERSION;
135
+ dev["happy-dom"] = HAPPY_DOM_VERSION;
136
+ }
137
+ writeJson(tree, pkgPath, pkg);
138
+ const readme = joinPathFragments(o.directory, "README.md");
139
+ if (tree.exists(readme))
140
+ tree.write(readme, tree.read(readme, "utf8").replace(/\n*$/, "\n") + nxReadme(o));
141
+ const facts = treeFacts(tree, o.directory);
142
+ writeJson(tree, joinPathFragments(o.directory, "project.json"), {
143
+ name: o.name,
144
+ $schema: `${offsetTo(o.directory)}/node_modules/nx/schemas/project-schema.json`,
145
+ projectType: "application",
146
+ sourceRoot: `${o.directory}/src`,
147
+ tags: o.tags,
148
+ // Inferred by @rivetjs/nx/plugin; written out when inference is off.
149
+ targets: o.addPlugin || !facts ? {} : rivetTargets(facts),
150
+ });
151
+ // Package-based workspaces install the app's own package.json; others get its dependencies at the root.
152
+ addToWorkspaces(tree, o.directory);
153
+ const workspacePackage = isWorkspacePackage(tree, o.directory);
154
+ if (!schema.skipPackageJson && !workspacePackage)
155
+ addDependenciesToPackageJson(tree, pkg.dependencies ?? {}, pkg.devDependencies ?? {});
156
+ if (!schema.skipFormat)
157
+ await formatFiles(tree);
158
+ // One install after the files are written (a workspace package changes no root manifest: always run then).
159
+ return schema.skipInstall ? () => { } : () => installPackagesTask(tree, workspacePackage);
160
+ }
161
+ export default applicationGenerator;