@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,24 @@
1
+ export interface ApplicationGeneratorSchema {
2
+ /** Project folder, workspace-relative (`apps/web`). */
3
+ directory: string;
4
+ /** Project name (default: the folder name). */
5
+ name?: string;
6
+ /** Starter: `static` (prerendered pages, Tailwind), `server` (loader + action, Node), `ui` (@rivetjs/ui), `minimal`. */
7
+ template?: "static" | "server" | "ui" | "minimal";
8
+ /** `node`: pages render per request (`@rivetjs/router/node`); `static`: prerendered HTML files. Default: from the template. */
9
+ adapter?: "node" | "static";
10
+ /** Platform target (create-rivet `--deploy`): adapter + platform files; `deploy` / `preview` Nx targets. bun / deno: server template. */
11
+ deploy?: "vercel" | "netlify" | "cloudflare" | "bun" | "deno";
12
+ /** Tailwind v4. Default: on, except the minimal starter. */
13
+ tailwind?: boolean;
14
+ /** `vitest`: vitest.config.ts with rivetTest() + a first test. */
15
+ unitTestRunner?: "vitest" | "none";
16
+ /** Comma-separated project tags. */
17
+ tags?: string;
18
+ /** `@rivetjs/*` dependency spec (default: this plugin's version). */
19
+ rivetVersion?: string;
20
+ addPlugin?: boolean;
21
+ skipPackageJson?: boolean;
22
+ skipInstall?: boolean;
23
+ skipFormat?: boolean;
24
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "RivetApplication",
4
+ "title": "Rivet application",
5
+ "description": "Creates a Rivet app from one of the create-rivet starters, with Nx targets (inferred or in project.json) and an optional Vitest setup.",
6
+ "type": "object",
7
+ "properties": {
8
+ "directory": {
9
+ "type": "string",
10
+ "description": "Project folder, relative to the workspace root (e.g. apps/web).",
11
+ "$default": { "$source": "argv", "index": 0 },
12
+ "x-prompt": "Which directory do you want to create the application in?"
13
+ },
14
+ "name": { "type": "string", "description": "Project name (default: the folder name).", "pattern": "^[a-zA-Z][^:]*$" },
15
+ "template": {
16
+ "type": "string",
17
+ "enum": ["static", "server", "ui", "minimal"],
18
+ "default": "static",
19
+ "description": "Starter: static (prerendered pages, Tailwind), server (loader + form action per request, Node), ui (@rivetjs/ui + Tailwind v4), minimal (one page, no Tailwind).",
20
+ "x-prompt": {
21
+ "message": "Which starter?",
22
+ "type": "list",
23
+ "items": [
24
+ { "value": "static", "label": "Static site (prerendered pages, Tailwind)" },
25
+ { "value": "server", "label": "Node server (loader + form action per request)" },
26
+ { "value": "ui", "label": "UI components (@rivetjs/ui + Tailwind v4)" },
27
+ { "value": "minimal", "label": "Minimal (one page, one signal)" }
28
+ ]
29
+ }
30
+ },
31
+ "adapter": {
32
+ "type": "string",
33
+ "enum": ["node", "static"],
34
+ "description": "node: pages render per request; static: prerendered HTML files. Default: from the template (server → node, others → static)."
35
+ },
36
+ "deploy": {
37
+ "type": "string",
38
+ "enum": ["vercel", "netlify", "cloudflare", "bun", "deno"],
39
+ "description": "Platform target, the same files as create-rivet --deploy: rivetRouter({ adapter }) + platform config; Nx targets instead of npm scripts (vercel / cloudflare: deploy; cloudflare / bun / deno: preview). bun and deno need --template=server."
40
+ },
41
+ "tailwind": { "type": "boolean", "description": "Tailwind v4. Default: on, except the minimal starter." },
42
+ "unitTestRunner": {
43
+ "type": "string",
44
+ "enum": ["vitest", "none"],
45
+ "default": "vitest",
46
+ "description": "Test runner: vitest.config.ts with rivetTest() and a first test."
47
+ },
48
+ "tags": { "type": "string", "description": "Comma-separated project tags." },
49
+ "rivetVersion": { "type": "string", "description": "@rivetjs/* dependency spec (default: this plugin's version)." },
50
+ "addPlugin": { "type": "boolean", "x-priority": "internal", "description": "Use inferred targets (default: true unless NX_ADD_PLUGINS=false)." },
51
+ "skipPackageJson": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Do not add dependencies to the root package.json." },
52
+ "skipInstall": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Do not run the package manager install." },
53
+ "skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
54
+ },
55
+ "required": ["directory"],
56
+ "additionalProperties": false
57
+ }
@@ -0,0 +1,24 @@
1
+ import { signal } from "@rivetjs/reactivity";
2
+ import type { Content } from "@rivetjs/runtime/jsx-runtime";
3
+
4
+ export interface <%= className %>Props {
5
+ /** Button text. */
6
+ label?: string;
7
+ children?: Content;
8
+ }
9
+
10
+ /**
11
+ * <%= className %>. The click handler is an inline arrow: the compiler captures it, so on a resumed page the button
12
+ * works without running the component in the browser (its code loads on first interaction).
13
+ */
14
+ export function <%= className %>(props: <%= className %>Props) {
15
+ const count = signal(0);
16
+ return (
17
+ <div class="<%= cssName %>">
18
+ {props.children}
19
+ <button type="button" onClick={() => count.update((n) => n + 1)}>
20
+ {props.label ?? "Clicked"} {count()}
21
+ </button>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,32 @@
1
+ import { signal } from "@rivetjs/reactivity";
2
+ import type { Slot } from "@rivetjs/runtime/jsx-runtime";
3
+
4
+ export interface <%= className %>Props {
5
+ /** Trigger text (also the panel's accessible name). */
6
+ label: string;
7
+ /**
8
+ * Render-function children (`{() => <p>…</p>}`): the content of the closed panel is created when it opens, also on a
9
+ * resumed page. Plain JSX children would be created right away and kept for later, which a resumed page cannot save.
10
+ */
11
+ children: Slot;
12
+ }
13
+
14
+ /** <%= className %>: a trigger and a panel that opens below it. Handlers are inline arrows (resumable). */
15
+ export function <%= className %>(props: <%= className %>Props) {
16
+ const open = signal(false);
17
+ return (
18
+ <div class="<%= cssName %>">
19
+ <button type="button" aria-expanded={open() ? "true" : "false"} onClick={() => open.update((value) => !value)}>
20
+ {props.label}
21
+ </button>
22
+ {open() && (
23
+ <div role="dialog" aria-label={props.label}>
24
+ {props.children()}
25
+ <button type="button" onClick={() => open.set(false)}>
26
+ Close
27
+ </button>
28
+ </div>
29
+ )}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,24 @@
1
+ import { <%= className %> } from "<%= importPath %>";
2
+
3
+ export const head = { title: "<%= className %> story" };
4
+
5
+ /** Story page of <%= className %> (<%= url %>): its states side by side. */
6
+ export default function <%= className %>Story() {
7
+ return (
8
+ <main>
9
+ <h1><%= className %></h1>
10
+ <section>
11
+ <h2>Default</h2>
12
+ <% if (overlay) { %> <<%= className %> label="Open">{() => <p>Panel content</p>}</<%= className %>>
13
+ <% } else { %> <<%= className %> />
14
+ <% } %> </section>
15
+ <section>
16
+ <h2>With content</h2>
17
+ <% if (overlay) { %> <<%= className %> label="More">{() => <ul><li>One</li><li>Two</li></ul>}</<%= className %>>
18
+ <% } else { %> <<%= className %> label="Likes">
19
+ <p>Children render above the button.</p>
20
+ </<%= className %>>
21
+ <% } %> </section>
22
+ </main>
23
+ );
24
+ }
@@ -0,0 +1,10 @@
1
+ import { expect, test } from "vitest";
2
+ import { fire, render } from "@rivetjs/runtime/testing";
3
+ import { <%= className %> } from "./<%= fileName %>.tsx";
4
+
5
+ test("<%= className %> counts clicks", () => {
6
+ const view = render(() => <<%= className %> label="Count" />);
7
+ const button = view.find("button");
8
+ fire(button, "click");
9
+ expect(button.textContent).toBe("Count 1");
10
+ });
@@ -0,0 +1,10 @@
1
+ import { expect, test } from "vitest";
2
+ import { fire, render } from "@rivetjs/runtime/testing";
3
+ import { <%= className %> } from "./<%= fileName %>.tsx";
4
+
5
+ test("<%= className %> creates its content when it opens", () => {
6
+ const view = render(() => <<%= className %> label="Details">{() => <p>Inside</p>}</<%= className %>>);
7
+ expect(view.container.querySelector("p")).toBeNull();
8
+ fire(view.find("button"), "click");
9
+ expect(view.find("[role=dialog] p").textContent).toBe("Inside");
10
+ });
@@ -0,0 +1,5 @@
1
+ import { type Tree } from "@nx/devkit";
2
+ import type { ComponentGeneratorSchema } from "./schema.js";
3
+ export type { ComponentGeneratorSchema } from "./schema.js";
4
+ export declare function componentGenerator(tree: Tree, options: ComponentGeneratorSchema): Promise<void>;
5
+ export default componentGenerator;
@@ -0,0 +1,45 @@
1
+ // `nx g @rivetjs/nx:component UserCard --project=web [--kind=overlay] [--story] [--no-test]`.
2
+ import { fileURLToPath } from "node:url";
3
+ import { formatFiles, generateFiles, joinPathFragments, names } from "@nx/devkit";
4
+ import { relativeImport, rivetProject } from "../../utils/project.js";
5
+ const FILES = fileURLToPath(new URL("./files", import.meta.url));
6
+ export async function componentGenerator(tree, options) {
7
+ const project = rivetProject(tree, options.project);
8
+ const { className } = names(options.name);
9
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(className))
10
+ throw new Error(`component: "${options.name}" is not a valid component name`);
11
+ const kind = options.kind ?? "basic";
12
+ const directory = (options.directory ?? (project.library ? "" : "components")).replace(/^\/+|\/+$/g, "");
13
+ const folder = joinPathFragments(project.src, directory);
14
+ const file = joinPathFragments(folder, `${className}.tsx`);
15
+ if (tree.exists(file))
16
+ throw new Error(`component: ${file} already exists`);
17
+ // The file keeps the component name (`UserCard.tsx`); the markup class is kebab-case (`user-card`).
18
+ const vars = { className, fileName: className, cssName: names(className).fileName, overlay: kind === "overlay" };
19
+ generateFiles(tree, joinPathFragments(FILES, kind), folder, vars);
20
+ if (options.test ?? Boolean(project.facts?.test))
21
+ generateFiles(tree, joinPathFragments(FILES, `test-${kind}`), folder, vars);
22
+ if (options.story) {
23
+ if (project.facts?.kind !== "app")
24
+ throw new Error("component: --story needs a Rivet app with file routes");
25
+ const stories = joinPathFragments(project.routes, "stories");
26
+ const storyName = names(className).fileName;
27
+ generateFiles(tree, joinPathFragments(FILES, "story"), stories, {
28
+ ...vars,
29
+ storyName,
30
+ url: `/stories/${storyName}`,
31
+ importPath: relativeImport(stories, file),
32
+ });
33
+ }
34
+ if (options.export ?? project.library) {
35
+ const index = joinPathFragments(project.src, "index.ts");
36
+ const from = relativeImport(project.src, file);
37
+ const line = `export { ${className}, type ${className}Props } from "${from}";\n`;
38
+ const text = tree.exists(index) ? tree.read(index, "utf8") : "";
39
+ if (!text.includes(line))
40
+ tree.write(index, (text && !text.endsWith("\n") ? text + "\n" : text) + line);
41
+ }
42
+ if (!options.skipFormat)
43
+ await formatFiles(tree);
44
+ }
45
+ export default componentGenerator;
@@ -0,0 +1,17 @@
1
+ export interface ComponentGeneratorSchema {
2
+ /** Component name (PascalCase or any case: `user-card` → `UserCard`). */
3
+ name: string;
4
+ /** The app or library. */
5
+ project: string;
6
+ /** Folder under the project's `src` (default `components` for apps, `` for libraries). */
7
+ directory?: string;
8
+ /** `basic` (signal + inline handler) or `overlay` (trigger + panel with render-function children). */
9
+ kind?: "basic" | "overlay";
10
+ /** Write `<Name>.test.tsx`. Default: when the project has a vitest config. */
11
+ test?: boolean;
12
+ /** Story page route `src/routes/stories/<name>.tsx` (apps only). */
13
+ story?: boolean;
14
+ /** Re-export from the library's `src/index.ts`. Default: true for libraries. */
15
+ export?: boolean;
16
+ skipFormat?: boolean;
17
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "RivetComponent",
4
+ "title": "Rivet component",
5
+ "description": "Adds a resumability-correct component (.tsx) to a Rivet app or library: inline-arrow handlers, render-function children for overlays, plus a test and an optional story route.",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "Component name (user-card becomes UserCard).",
11
+ "$default": { "$source": "argv", "index": 0 },
12
+ "x-prompt": "Component name?"
13
+ },
14
+ "project": {
15
+ "type": "string",
16
+ "description": "The app or library.",
17
+ "$default": { "$source": "projectName" },
18
+ "x-dropdown": "projects",
19
+ "x-prompt": "Which project?"
20
+ },
21
+ "directory": { "type": "string", "description": "Folder under the project's src (default components for apps, the src root for libraries)." },
22
+ "kind": {
23
+ "type": "string",
24
+ "enum": ["basic", "overlay"],
25
+ "default": "basic",
26
+ "description": "basic: signal + inline handler; overlay: trigger + panel with render-function children."
27
+ },
28
+ "test": { "type": "boolean", "description": "Write <Name>.test.tsx. Default: when the project has a vitest config." },
29
+ "story": { "type": "boolean", "default": false, "description": "Story page route src/routes/stories/<name>.tsx (apps only)." },
30
+ "export": { "type": "boolean", "description": "Re-export from the library's src/index.ts. Default: true for libraries." },
31
+ "skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
32
+ },
33
+ "required": ["name", "project"],
34
+ "additionalProperties": false
35
+ }
@@ -0,0 +1,10 @@
1
+ import type { I18nRouteConfig } from "@rivetjs/i18n/router";
2
+
3
+ // Locales (the default first), ICU catalogs per locale, and how the locale appears in URLs. Used by the i18n() Vite
4
+ // plugin (vite.config.ts) and by `i18nGuard(config)` in a `[[lang]]` layout (see the Locale URLs guide).
5
+ export const config = {
6
+ locales: <%- localesJson %>,
7
+ defaultLocale: "<%= defaultLocale %>",
8
+ catalogs: "locales/{locale}.json",
9
+ routing: { prefix: "<%= prefix %>" },
10
+ } satisfies I18nRouteConfig & { catalogs: string };
@@ -0,0 +1,7 @@
1
+ import { type GeneratorCallback, type Tree } from "@nx/devkit";
2
+ import type { I18nSetupGeneratorSchema } from "./schema.js";
3
+ export type { I18nSetupGeneratorSchema } from "./schema.js";
4
+ /** A first catalog: one plain message and one with a plural (ICU MessageFormat). */
5
+ export declare function catalog(locale: string): Record<string, unknown>;
6
+ export declare function i18nSetupGenerator(tree: Tree, options: I18nSetupGeneratorSchema): Promise<GeneratorCallback>;
7
+ export default i18nSetupGenerator;
@@ -0,0 +1,65 @@
1
+ // `nx g @rivetjs/nx:i18n-setup web --locales=en,de`: compile-time i18n (`t()` calls are replaced per locale at build
2
+ // time). The inferred build target reads `locales` from i18n.config.ts: one client build per locale (`RIVET_LOCALE`).
3
+ import { fileURLToPath } from "node:url";
4
+ import { addDependenciesToPackageJson, formatFiles, generateFiles, installPackagesTask, joinPathFragments, readJson, writeJson, } from "@nx/devkit";
5
+ import { viteConfigPath } from "../../utils/facts.js";
6
+ import { routedApp } from "../../utils/project.js";
7
+ import { rivetRange } from "../../utils/versions.js";
8
+ import { addImport, addPlugin } from "../../utils/vite-config.js";
9
+ import { ensureLines, isWorkspacePackage } from "../../utils/workspace.js";
10
+ const FILES = fileURLToPath(new URL("./files", import.meta.url));
11
+ /** A first catalog: one plain message and one with a plural (ICU MessageFormat). */
12
+ export function catalog(locale) {
13
+ const german = locale.startsWith("de");
14
+ return german
15
+ ? { home: { title: "Willkommen", items: "{count, plural, one {# Eintrag} other {# Einträge}}" } }
16
+ : { home: { title: "Welcome", items: "{count, plural, one {# item} other {# items}}" } };
17
+ }
18
+ export async function i18nSetupGenerator(tree, options) {
19
+ const project = routedApp(tree, options.project, "i18n-setup");
20
+ const configFile = joinPathFragments(project.root, "i18n.config.ts");
21
+ if (tree.exists(configFile))
22
+ throw new Error(`i18n-setup: ${configFile} already exists`);
23
+ const locales = (options.locales ?? "en")
24
+ .split(",")
25
+ .map((locale) => locale.trim())
26
+ .filter(Boolean);
27
+ if (!locales.length)
28
+ throw new Error("i18n-setup: no locales");
29
+ const defaultLocale = options.defaultLocale ?? locales[0];
30
+ if (!locales.includes(defaultLocale))
31
+ throw new Error(`i18n-setup: defaultLocale "${defaultLocale}" is not in locales`);
32
+ generateFiles(tree, FILES, project.root, { localesJson: JSON.stringify(locales), defaultLocale, prefix: options.prefix ?? "as-needed" });
33
+ for (const locale of locales) {
34
+ const path = joinPathFragments(project.root, "locales", `${locale}.json`);
35
+ if (!tree.exists(path))
36
+ writeJson(tree, path, catalog(locale));
37
+ }
38
+ const vite = viteConfigPath(tree, project.root);
39
+ let config = tree.read(vite, "utf8");
40
+ config = addImport(config, 'import { i18n } from "@rivetjs/i18n/vite";');
41
+ config = addImport(config, 'import { config as i18nConfig } from "./i18n.config.ts";');
42
+ // i18n() runs before the router / compiler (`enforce: "pre"`); first in the list keeps that obvious.
43
+ config = addPlugin(config, "i18n(i18nConfig)", "first");
44
+ tree.write(vite, config);
45
+ // Generated message types (`dts` default) are build output.
46
+ ensureLines(tree, joinPathFragments(project.root, ".gitignore"), ["src/rivet-i18n.d.ts"]);
47
+ let install = () => { };
48
+ if (!options.skipPackageJson) {
49
+ const deps = { "@rivetjs/i18n": rivetRange(options.rivetVersion) };
50
+ const pkgPath = joinPathFragments(project.root, "package.json");
51
+ if (tree.exists(pkgPath)) {
52
+ const pkg = readJson(tree, pkgPath);
53
+ pkg.dependencies = { ...pkg.dependencies, ...deps };
54
+ writeJson(tree, pkgPath, pkg);
55
+ }
56
+ const workspacePackage = tree.exists(pkgPath) && isWorkspacePackage(tree, project.root);
57
+ if (!workspacePackage)
58
+ addDependenciesToPackageJson(tree, deps, {});
59
+ install = () => installPackagesTask(tree, workspacePackage);
60
+ }
61
+ if (!options.skipFormat)
62
+ await formatFiles(tree);
63
+ return install;
64
+ }
65
+ export default i18nSetupGenerator;
@@ -0,0 +1,13 @@
1
+ export interface I18nSetupGeneratorSchema {
2
+ /** The Rivet app. */
3
+ project: string;
4
+ /** Comma-separated locales, the default first (`en,de`). */
5
+ locales?: string;
6
+ /** Default locale (default: the first of `locales`). */
7
+ defaultLocale?: string;
8
+ /** Locale prefix in URLs: `as-needed` (default), `always`, `never`. */
9
+ prefix?: "as-needed" | "always" | "never";
10
+ rivetVersion?: string;
11
+ skipPackageJson?: boolean;
12
+ skipFormat?: boolean;
13
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "RivetI18nSetup",
4
+ "title": "Set up @rivetjs/i18n",
5
+ "description": "Adds compile-time i18n to a Rivet app: i18n.config.ts, one ICU catalog per locale (locales/<locale>.json), the i18n() Vite plugin before the router, and the dependency. The build target then runs one client build per locale.",
6
+ "type": "object",
7
+ "properties": {
8
+ "project": {
9
+ "type": "string",
10
+ "description": "The Rivet app.",
11
+ "$default": { "$source": "argv", "index": 0 },
12
+ "x-dropdown": "projects",
13
+ "x-prompt": "Which project?"
14
+ },
15
+ "locales": { "type": "string", "default": "en", "description": "Comma-separated locales, the default first (en,de)." },
16
+ "defaultLocale": { "type": "string", "description": "Default locale (default: the first locale)." },
17
+ "prefix": { "type": "string", "enum": ["as-needed", "always", "never"], "default": "as-needed", "description": "Locale prefix in URLs." },
18
+ "rivetVersion": { "type": "string", "description": "@rivetjs/* dependency spec (default: this plugin's version)." },
19
+ "skipPackageJson": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Do not change package.json files." },
20
+ "skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
21
+ },
22
+ "required": ["project"],
23
+ "additionalProperties": false
24
+ }
@@ -0,0 +1,8 @@
1
+ import { type GeneratorCallback, type Tree } from "@nx/devkit";
2
+ import type { InitGeneratorSchema } from "./schema.js";
3
+ export type { InitGeneratorSchema } from "./schema.js";
4
+ export declare const PLUGIN = "@rivetjs/nx/plugin";
5
+ /** Adds `@rivetjs/nx/plugin` to nx.json `plugins` (unless it is already there, as a string or `{ plugin }`). */
6
+ export declare function registerPlugin(tree: Tree): void;
7
+ export declare function initGenerator(tree: Tree, options?: InitGeneratorSchema): Promise<GeneratorCallback>;
8
+ export default initGenerator;
@@ -0,0 +1,48 @@
1
+ // `nx g @rivetjs/nx:init`: workspace setup shared by every other generator (idempotent).
2
+ import { addDependenciesToPackageJson, formatFiles, readNxJson, updateNxJson } from "@nx/devkit";
3
+ import { rivetRange, toolVersion } from "../../utils/versions.js";
4
+ import { ensureLines, shouldAddPlugin } from "../../utils/workspace.js";
5
+ export const PLUGIN = "@rivetjs/nx/plugin";
6
+ /** Adds `@rivetjs/nx/plugin` to nx.json `plugins` (unless it is already there, as a string or `{ plugin }`). */
7
+ export function registerPlugin(tree) {
8
+ const nxJson = readNxJson(tree) ?? {};
9
+ const plugins = nxJson.plugins ?? [];
10
+ if (plugins.some((entry) => (typeof entry === "string" ? entry : entry.plugin) === PLUGIN))
11
+ return;
12
+ plugins.push({
13
+ plugin: PLUGIN,
14
+ options: {
15
+ buildTargetName: "build",
16
+ devTargetName: "dev",
17
+ previewTargetName: "preview",
18
+ testTargetName: "test",
19
+ checkTargetName: "check",
20
+ typecheckTargetName: "typecheck",
21
+ },
22
+ });
23
+ nxJson.plugins = plugins;
24
+ updateNxJson(tree, nxJson);
25
+ }
26
+ export async function initGenerator(tree, options = {}) {
27
+ if (shouldAddPlugin(tree, options.addPlugin))
28
+ registerPlugin(tree);
29
+ ensureLines(tree, ".gitignore", [".rivet", "dist"]);
30
+ let install = () => { };
31
+ if (!options.skipPackageJson) {
32
+ const rivet = rivetRange(options.rivetVersion);
33
+ install = addDependenciesToPackageJson(tree, {}, {
34
+ "@rivetjs/nx": rivet,
35
+ "@rivetjs/compiler": rivet,
36
+ "@rivetjs/language-core": rivet,
37
+ "@rivetjs/typescript-plugin": rivet,
38
+ vite: toolVersion("vite"),
39
+ typescript: toolVersion("typescript"),
40
+ "@types/node": toolVersion("@types/node"),
41
+ }, undefined, options.keepExistingVersions);
42
+ }
43
+ if (!options.skipFormat)
44
+ await formatFiles(tree);
45
+ // The callback installs the added packages (`installPackagesTask`), after the files are on disk.
46
+ return options.skipInstall ? () => { } : install;
47
+ }
48
+ export default initGenerator;
@@ -0,0 +1,13 @@
1
+ export interface InitGeneratorSchema {
2
+ /** Register `@rivetjs/nx/plugin` in nx.json (inferred targets). Default: true unless `NX_ADD_PLUGINS=false` / `useInferencePlugins: false`. */
3
+ addPlugin?: boolean;
4
+ /** Do not add dependencies to the root package.json. */
5
+ skipPackageJson?: boolean;
6
+ /** Keep versions already in package.json. */
7
+ keepExistingVersions?: boolean;
8
+ /** `@rivetjs/*` dependency spec (default: this plugin's version). */
9
+ rivetVersion?: string;
10
+ /** Do not run the package manager install after generating. */
11
+ skipInstall?: boolean;
12
+ skipFormat?: boolean;
13
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "RivetInit",
4
+ "title": "Initialize @rivetjs/nx",
5
+ "description": "Adds @rivetjs/nx and the shared Rivet tooling to the workspace and registers the inference plugin in nx.json.",
6
+ "type": "object",
7
+ "properties": {
8
+ "addPlugin": {
9
+ "type": "boolean",
10
+ "description": "Register @rivetjs/nx/plugin in nx.json (inferred targets). Default: true unless NX_ADD_PLUGINS=false or nx.json useInferencePlugins is false."
11
+ },
12
+ "skipPackageJson": { "type": "boolean", "default": false, "description": "Do not add dependencies to the root package.json." },
13
+ "keepExistingVersions": { "type": "boolean", "default": false, "description": "Keep versions already in package.json." },
14
+ "rivetVersion": { "type": "string", "description": "@rivetjs/* dependency spec (default: this plugin's version)." },
15
+ "skipInstall": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Do not run the package manager install." },
16
+ "skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
17
+ },
18
+ "additionalProperties": false
19
+ }
@@ -0,0 +1,12 @@
1
+ <% if (guard) { %>import { redirect, type LayoutProps, type RequestEvent } from "@rivetjs/router";
2
+
3
+ /** Runs for every page and endpoint in this folder, before their loaders. */
4
+ export const serverGuard = ({ cookies, url }: RequestEvent) => {
5
+ if (!cookies.get("session")) return redirect("/login?next=" + encodeURIComponent(url.pathname));
6
+ };
7
+ <% } else { %>import type { LayoutProps } from "@rivetjs/router";
8
+ <% } %>
9
+ /** Wraps every page in <%= where %>. Stays mounted across client navigation between them. */
10
+ export default function <%= componentName %>({ children }: LayoutProps) {
11
+ return <div class="<%= className %>">{children}</div>;
12
+ }
@@ -0,0 +1,5 @@
1
+ import { type Tree } from "@nx/devkit";
2
+ import type { LayoutGeneratorSchema } from "./schema.js";
3
+ export type { LayoutGeneratorSchema } from "./schema.js";
4
+ export declare function layoutGenerator(tree: Tree, options: LayoutGeneratorSchema): Promise<void>;
5
+ export default layoutGenerator;
@@ -0,0 +1,23 @@
1
+ // `nx g @rivetjs/nx:layout blog --project=web [--guard]`: `src/routes/<directory>/_layout.tsx`.
2
+ import { fileURLToPath } from "node:url";
3
+ import { formatFiles, generateFiles, joinPathFragments, names } from "@nx/devkit";
4
+ import { routeComponentName, routedApp, routeSegments } from "../../utils/project.js";
5
+ const FILES = fileURLToPath(new URL("./files", import.meta.url));
6
+ export async function layoutGenerator(tree, options) {
7
+ const project = routedApp(tree, options.project, "layout");
8
+ const segments = routeSegments(options.directory ?? "");
9
+ const folder = joinPathFragments(project.routes, ...segments);
10
+ const file = joinPathFragments(folder, "_layout.tsx");
11
+ if (tree.exists(file))
12
+ throw new Error(`layout: ${file} already exists`);
13
+ const base = segments.length ? routeComponentName(segments) : "Root";
14
+ generateFiles(tree, FILES, folder, {
15
+ componentName: base + "Layout",
16
+ className: names(base).fileName + "-layout",
17
+ where: segments.length ? `src/routes/${segments.join("/")}` : "the app",
18
+ guard: Boolean(options.guard),
19
+ });
20
+ if (!options.skipFormat)
21
+ await formatFiles(tree);
22
+ }
23
+ export default layoutGenerator;
@@ -0,0 +1,9 @@
1
+ export interface LayoutGeneratorSchema {
2
+ /** Folder under `src/routes` the layout wraps (`blog`, `(marketing)`); empty = the root layout. */
3
+ directory?: string;
4
+ /** The Rivet app. */
5
+ project: string;
6
+ /** `serverGuard` for the whole folder (redirects to /login without a `session` cookie). */
7
+ guard?: boolean;
8
+ skipFormat?: boolean;
9
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "RivetLayout",
4
+ "title": "Rivet layout",
5
+ "description": "Adds a _layout.tsx to a folder of a Rivet app's routes (the root layout without a directory), optionally with a serverGuard for everything below it.",
6
+ "type": "object",
7
+ "properties": {
8
+ "directory": {
9
+ "type": "string",
10
+ "default": "",
11
+ "description": "Folder under src/routes the layout wraps (blog, (marketing)); empty for the root layout.",
12
+ "$default": { "$source": "argv", "index": 0 }
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
+ "guard": { "type": "boolean", "default": false, "description": "Add a serverGuard for the whole folder." },
22
+ "skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
23
+ },
24
+ "required": ["project"],
25
+ "additionalProperties": false
26
+ }
@@ -0,0 +1,18 @@
1
+ # <%= importPath %>
2
+
3
+ Rivet library. Components are `.tsx` modules compiled by the app's Rivet compiler (`rivetRouter()` / `rivet()` in the
4
+ app's Vite config), so they resume like the app's own components.
5
+
6
+ ```tsx
7
+ import { <%= className %> } from "<%= importPath %>";
8
+ ```
9
+ <% if (subpaths.length) { %>
10
+ Subpaths (each one tree-shakes on its own):
11
+ <% for (const subpath of subpaths) { %>
12
+ - `<%= importPath %>/<%= subpath.fileName %>`<% } %>
13
+ <% } %>
14
+ ## Nx
15
+
16
+ - `nx test <%= name %>`: Vitest (DOM + server targets)
17
+ - `nx check <%= name %>` / `nx typecheck <%= name %>`<% if (publishable) { %>
18
+ - `nx build <%= name %>`: publishable package in `<%= outputPath %>` (`.js` / `.jsx` + `.d.ts`)<% } %>