@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.
- package/LICENSE +21 -0
- package/README.md +63 -2
- package/executors/build/executor.d.ts +12 -0
- package/executors/build/executor.js +50 -0
- package/executors/build/schema.d.ts +18 -0
- package/executors/build/schema.json +18 -0
- package/executors/check/executor.d.ts +11 -0
- package/executors/check/executor.js +31 -0
- package/executors/check/schema.d.ts +12 -0
- package/executors/check/schema.json +18 -0
- package/executors/dev/executor.d.ts +14 -0
- package/executors/dev/executor.js +32 -0
- package/executors/dev/schema.d.ts +16 -0
- package/executors/dev/schema.json +17 -0
- package/executors/package/executor.d.ts +20 -0
- package/executors/package/executor.js +65 -0
- package/executors/package/schema.d.ts +10 -0
- package/executors/package/schema.json +18 -0
- package/executors/prerender/executor.d.ts +8 -0
- package/executors/prerender/executor.js +13 -0
- package/executors/prerender/schema.d.ts +6 -0
- package/executors/prerender/schema.json +12 -0
- package/executors/preview/executor.d.ts +12 -0
- package/executors/preview/executor.js +52 -0
- package/executors/preview/schema.d.ts +10 -0
- package/executors/preview/schema.json +14 -0
- package/executors/run.d.ts +33 -0
- package/executors/run.js +88 -0
- package/executors/test/executor.d.ts +8 -0
- package/executors/test/executor.js +27 -0
- package/executors/test/schema.d.ts +24 -0
- package/executors/test/schema.json +21 -0
- package/executors/typecheck/executor.d.ts +9 -0
- package/executors/typecheck/executor.js +14 -0
- package/executors/typecheck/schema.d.ts +6 -0
- package/executors/typecheck/schema.json +12 -0
- package/executors.json +49 -0
- package/generators/api-route/files/__fileName__.ts.template +11 -0
- package/generators/api-route/generator.d.ts +5 -0
- package/generators/api-route/generator.js +30 -0
- package/generators/api-route/schema.d.ts +9 -0
- package/generators/api-route/schema.json +31 -0
- package/generators/application/files/tests/minimal/src/routes/index.test.tsx.template +9 -0
- package/generators/application/files/tests/server/src/routes/index.server.test.tsx.template +8 -0
- package/generators/application/files/tests/static/src/components/Counter.test.tsx.template +10 -0
- package/generators/application/files/tests/ui/src/components/LikeButton.server.test.tsx.template +7 -0
- package/generators/application/files/vitest/vitest.config.ts.template +16 -0
- package/generators/application/generator.d.ts +20 -0
- package/generators/application/generator.js +161 -0
- package/generators/application/schema.d.ts +24 -0
- package/generators/application/schema.json +57 -0
- package/generators/component/files/basic/__fileName__.tsx.template +24 -0
- package/generators/component/files/overlay/__fileName__.tsx.template +32 -0
- package/generators/component/files/story/__storyName__.tsx.template +24 -0
- package/generators/component/files/test-basic/__fileName__.test.tsx.template +10 -0
- package/generators/component/files/test-overlay/__fileName__.test.tsx.template +10 -0
- package/generators/component/generator.d.ts +5 -0
- package/generators/component/generator.js +45 -0
- package/generators/component/schema.d.ts +17 -0
- package/generators/component/schema.json +35 -0
- package/generators/i18n-setup/files/i18n.config.ts.template +10 -0
- package/generators/i18n-setup/generator.d.ts +7 -0
- package/generators/i18n-setup/generator.js +65 -0
- package/generators/i18n-setup/schema.d.ts +13 -0
- package/generators/i18n-setup/schema.json +24 -0
- package/generators/init/generator.d.ts +8 -0
- package/generators/init/generator.js +48 -0
- package/generators/init/schema.d.ts +13 -0
- package/generators/init/schema.json +19 -0
- package/generators/layout/files/_layout.tsx.template +12 -0
- package/generators/layout/generator.d.ts +5 -0
- package/generators/layout/generator.js +23 -0
- package/generators/layout/schema.d.ts +9 -0
- package/generators/layout/schema.json +26 -0
- package/generators/library/files/lib/README.md.template +18 -0
- package/generators/library/files/lib/package.json.template +12 -0
- package/generators/library/files/lib/src/__className__.tsx.template +16 -0
- package/generators/library/files/lib/src/index.ts.template +1 -0
- package/generators/library/files/lib/tsconfig.json.template +21 -0
- package/generators/library/files/publishable/tsconfig.lib.json.template +13 -0
- package/generators/library/files/subpath/src/__subpath__/__subpathClass__.tsx.template +10 -0
- package/generators/library/files/subpath/src/__subpath__/index.ts.template +1 -0
- package/generators/library/files/vitest/src/__className__.test.tsx.template +13 -0
- package/generators/library/generator.d.ts +13 -0
- package/generators/library/generator.js +116 -0
- package/generators/library/schema.d.ts +19 -0
- package/generators/library/schema.json +32 -0
- package/generators/preset/generator.d.ts +5 -0
- package/generators/preset/generator.js +29 -0
- package/generators/preset/schema.d.ts +16 -0
- package/generators/preset/schema.json +30 -0
- package/generators/route/files/__fileName__.tsx.template +43 -0
- package/generators/route/generator.d.ts +10 -0
- package/generators/route/generator.js +67 -0
- package/generators/route/schema.d.ts +15 -0
- package/generators/route/schema.json +29 -0
- package/generators/ui-setup/generator.d.ts +7 -0
- package/generators/ui-setup/generator.js +55 -0
- package/generators/ui-setup/schema.d.ts +7 -0
- package/generators/ui-setup/schema.json +21 -0
- package/generators.json +66 -0
- package/index.d.ts +22 -0
- package/index.js +24 -0
- package/migrations/0.0.1/example-noop.d.ts +2 -0
- package/migrations/0.0.1/example-noop.js +3 -0
- package/migrations.json +10 -0
- package/package.json +74 -4
- package/plugin.d.ts +12 -0
- package/plugin.js +44 -0
- package/utils/deploy.d.ts +14 -0
- package/utils/deploy.js +86 -0
- package/utils/detect.d.ts +42 -0
- package/utils/detect.js +60 -0
- package/utils/facts.d.ts +8 -0
- package/utils/facts.js +42 -0
- package/utils/project.d.ts +28 -0
- package/utils/project.js +67 -0
- package/utils/starter.d.ts +9 -0
- package/utils/starter.js +34 -0
- package/utils/targets.d.ts +22 -0
- package/utils/targets.js +117 -0
- package/utils/versions.d.ts +13 -0
- package/utils/versions.js +31 -0
- package/utils/vite-config.d.ts +8 -0
- package/utils/vite-config.js +104 -0
- package/utils/workspace.d.ts +13 -0
- package/utils/workspace.js +63 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= importPath %>",
|
|
3
|
+
"version": "0.0.1",<% if (!publishable) { %>
|
|
4
|
+
"private": true,<% } %>
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": <%- exportsJson %>,
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@rivetjs/reactivity": "<%= rivet %>",
|
|
10
|
+
"@rivetjs/runtime": "<%= rivet %>"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Content } from "@rivetjs/runtime/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
export interface <%= className %>Props {
|
|
4
|
+
title: string;
|
|
5
|
+
children?: Content;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** <%= className %>: a titled box. */
|
|
9
|
+
export function <%= className %>(props: <%= className %>Props) {
|
|
10
|
+
return (
|
|
11
|
+
<section class="<%= fileName %>">
|
|
12
|
+
<h2>{props.title}</h2>
|
|
13
|
+
{props.children}
|
|
14
|
+
</section>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { <%= className %>, type <%= className %>Props } from "./<%= className %>.tsx";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2023",
|
|
4
|
+
"lib": ["es2023", "dom", "dom.iterable"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
// TypeScript 6+ defaults this to true (TS2882 on `import "./styles.css"`); Vite handles side-effect imports.
|
|
14
|
+
"noUncheckedSideEffectImports": false,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"jsxImportSource": "@rivetjs/runtime",
|
|
17
|
+
"types": ["node"],
|
|
18
|
+
"plugins": [{ "name": "@rivetjs/typescript-plugin" }]
|
|
19
|
+
},
|
|
20
|
+
"include": ["src", "*.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"rewriteRelativeImportExtensions": true,
|
|
8
|
+
"sourceMap": false,
|
|
9
|
+
"plugins": []
|
|
10
|
+
},
|
|
11
|
+
"include": ["src"],
|
|
12
|
+
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Content } from "@rivetjs/runtime/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
export interface <%= subpathClass %>Props {
|
|
4
|
+
children?: Content;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** <%= subpathClass %> (`<%= importPath %>/<%= subpath %>`: imported alone, nothing else of the library is bundled). */
|
|
8
|
+
export function <%= subpathClass %>(props: <%= subpathClass %>Props) {
|
|
9
|
+
return <div class="<%= subpath %>">{props.children}</div>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { <%= subpathClass %>, type <%= subpathClass %>Props } from "./<%= subpathClass %>.tsx";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { render } from "@rivetjs/runtime/testing";
|
|
3
|
+
import { <%= className %> } from "./<%= className %>.tsx";
|
|
4
|
+
|
|
5
|
+
test("<%= className %> renders its title and children", () => {
|
|
6
|
+
const view = render(() => (
|
|
7
|
+
<<%= className %> title="Hello">
|
|
8
|
+
<p>Body</p>
|
|
9
|
+
</<%= className %>>
|
|
10
|
+
));
|
|
11
|
+
expect(view.find("h2").textContent).toBe("Hello");
|
|
12
|
+
expect(view.find("p").textContent).toBe("Body");
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type GeneratorCallback, type Tree } from "@nx/devkit";
|
|
2
|
+
import type { LibraryGeneratorSchema } from "./schema.js";
|
|
3
|
+
export type { LibraryGeneratorSchema } from "./schema.js";
|
|
4
|
+
type Exports = Record<string, {
|
|
5
|
+
types: string;
|
|
6
|
+
default: string;
|
|
7
|
+
}>;
|
|
8
|
+
/** `.` + one entry per subpath, pointing at the sources (the package build maps them to the emitted files). */
|
|
9
|
+
export declare function libraryExports(subpaths: string[]): Exports;
|
|
10
|
+
/** `@scope/name` with the root package.json's scope (`@proj/source` → `@proj`). */
|
|
11
|
+
export declare function defaultImportPath(tree: Tree, name: string): string;
|
|
12
|
+
export declare function libraryGenerator(tree: Tree, schema: LibraryGeneratorSchema): Promise<GeneratorCallback>;
|
|
13
|
+
export default libraryGenerator;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// `nx g @rivetjs/nx:library libs/ui-kit [--publishable] [--subpaths=button,card]`: a Rivet component library.
|
|
2
|
+
// Consumed from source inside the workspace (package manager workspaces resolve its `exports` to `.ts` / `.tsx`, the
|
|
3
|
+
// app's Rivet compiler compiles them); `--publishable` adds the package build for npm.
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { addDependenciesToPackageJson, formatFiles, generateFiles, getProjects, installPackagesTask, joinPathFragments, names, readJson, readNxJson, writeJson, } from "@nx/devkit";
|
|
6
|
+
import { treeFacts } from "../../utils/facts.js";
|
|
7
|
+
import { rivetTargets } from "../../utils/targets.js";
|
|
8
|
+
import { HAPPY_DOM_VERSION, rivetRange, VITEST_VERSION } from "../../utils/versions.js";
|
|
9
|
+
import { addToWorkspaces, isWorkspacePackage, offsetTo, shouldAddPlugin } from "../../utils/workspace.js";
|
|
10
|
+
import { initGenerator } from "../init/generator.js";
|
|
11
|
+
const FILES = fileURLToPath(new URL("./files", import.meta.url));
|
|
12
|
+
const VITEST = fileURLToPath(new URL("../application/files/vitest", import.meta.url));
|
|
13
|
+
/** `.` + one entry per subpath, pointing at the sources (the package build maps them to the emitted files). */
|
|
14
|
+
export function libraryExports(subpaths) {
|
|
15
|
+
const exports = { ".": { types: "./src/index.ts", default: "./src/index.ts" } };
|
|
16
|
+
for (const subpath of subpaths)
|
|
17
|
+
exports[`./${subpath}`] = { types: `./src/${subpath}/index.ts`, default: `./src/${subpath}/index.ts` };
|
|
18
|
+
return exports;
|
|
19
|
+
}
|
|
20
|
+
/** `@scope/name` with the root package.json's scope (`@proj/source` → `@proj`). */
|
|
21
|
+
export function defaultImportPath(tree, name) {
|
|
22
|
+
const root = tree.exists("package.json") ? readJson(tree, "package.json").name : undefined;
|
|
23
|
+
const scope = root?.startsWith("@") ? root.split("/")[0] : undefined;
|
|
24
|
+
return scope ? `${scope}/${name}` : name;
|
|
25
|
+
}
|
|
26
|
+
export async function libraryGenerator(tree, schema) {
|
|
27
|
+
const directory = schema.directory.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
28
|
+
if (!directory)
|
|
29
|
+
throw new Error("library: directory is required (e.g. libs/ui-kit)");
|
|
30
|
+
const name = schema.name ?? names(directory.split("/").pop()).fileName;
|
|
31
|
+
if (getProjects(tree).has(name))
|
|
32
|
+
throw new Error(`library: a project named "${name}" already exists`);
|
|
33
|
+
if (tree.exists(directory) && tree.children(directory).length)
|
|
34
|
+
throw new Error(`library: ${directory} is not empty`);
|
|
35
|
+
const importPath = schema.importPath ?? defaultImportPath(tree, name);
|
|
36
|
+
const subpaths = (schema.subpaths ?? "")
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((subpath) => names(subpath.trim()).fileName)
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
const publishable = Boolean(schema.publishable);
|
|
41
|
+
const vitest = (schema.unitTestRunner ?? "vitest") === "vitest";
|
|
42
|
+
const rivet = rivetRange(schema.rivetVersion);
|
|
43
|
+
const addPlugin = shouldAddPlugin(tree, schema.addPlugin);
|
|
44
|
+
const outputPath = `dist/${directory}`;
|
|
45
|
+
const { className, fileName } = names(name);
|
|
46
|
+
await initGenerator(tree, {
|
|
47
|
+
addPlugin,
|
|
48
|
+
skipFormat: true,
|
|
49
|
+
skipInstall: true,
|
|
50
|
+
...(schema.rivetVersion ? { rivetVersion: schema.rivetVersion } : {}),
|
|
51
|
+
...(schema.skipPackageJson ? { skipPackageJson: true } : {}),
|
|
52
|
+
});
|
|
53
|
+
const vars = {
|
|
54
|
+
name,
|
|
55
|
+
importPath,
|
|
56
|
+
className,
|
|
57
|
+
fileName,
|
|
58
|
+
publishable,
|
|
59
|
+
rivet,
|
|
60
|
+
outputPath,
|
|
61
|
+
subpaths: subpaths.map((subpath) => names(subpath)),
|
|
62
|
+
exportsJson: JSON.stringify(libraryExports(subpaths), null, 2).replace(/\n/g, "\n "),
|
|
63
|
+
};
|
|
64
|
+
generateFiles(tree, joinPathFragments(FILES, "lib"), directory, vars);
|
|
65
|
+
for (const subpath of subpaths) {
|
|
66
|
+
generateFiles(tree, joinPathFragments(FILES, "subpath"), directory, { ...vars, subpath, subpathClass: names(subpath).className });
|
|
67
|
+
}
|
|
68
|
+
if (publishable)
|
|
69
|
+
generateFiles(tree, joinPathFragments(FILES, "publishable"), directory, vars);
|
|
70
|
+
const pkgPath = joinPathFragments(directory, "package.json");
|
|
71
|
+
const pkg = readJson(tree, pkgPath);
|
|
72
|
+
if (vitest) {
|
|
73
|
+
generateFiles(tree, VITEST, directory, {});
|
|
74
|
+
generateFiles(tree, joinPathFragments(FILES, "vitest"), directory, vars);
|
|
75
|
+
pkg.devDependencies = { "@rivetjs/testing": rivet, "happy-dom": HAPPY_DOM_VERSION, vitest: VITEST_VERSION };
|
|
76
|
+
writeJson(tree, pkgPath, pkg);
|
|
77
|
+
}
|
|
78
|
+
const targets = {};
|
|
79
|
+
const facts = treeFacts(tree, directory);
|
|
80
|
+
// Inference finds libraries through their Rivet vitest config; without one, check / typecheck go to project.json.
|
|
81
|
+
if (!addPlugin || !facts) {
|
|
82
|
+
Object.assign(targets, rivetTargets(facts ?? { kind: "lib", static: false, csr: false, test: false, tsconfig: true, serveScript: false, prerenderScript: false }));
|
|
83
|
+
}
|
|
84
|
+
if (publishable) {
|
|
85
|
+
targets["build"] = {
|
|
86
|
+
executor: "@rivetjs/nx:package",
|
|
87
|
+
cache: true,
|
|
88
|
+
dependsOn: ["^build"],
|
|
89
|
+
inputs: readNxJson(tree)?.namedInputs?.["production"]
|
|
90
|
+
? ["production", "^production"]
|
|
91
|
+
: ["default", "^default", "!{projectRoot}/**/*.test.{ts,tsx}", "!{projectRoot}/vitest.config.{ts,mts,js,mjs}"],
|
|
92
|
+
outputs: ["{options.outputPath}"],
|
|
93
|
+
options: { tsConfig: "tsconfig.lib.json", outputPath },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
writeJson(tree, joinPathFragments(directory, "project.json"), {
|
|
97
|
+
name,
|
|
98
|
+
$schema: `${offsetTo(directory)}/node_modules/nx/schemas/project-schema.json`,
|
|
99
|
+
projectType: "library",
|
|
100
|
+
sourceRoot: `${directory}/src`,
|
|
101
|
+
tags: (schema.tags ?? "")
|
|
102
|
+
.split(",")
|
|
103
|
+
.map((tag) => tag.trim())
|
|
104
|
+
.filter(Boolean),
|
|
105
|
+
targets,
|
|
106
|
+
});
|
|
107
|
+
addToWorkspaces(tree, directory);
|
|
108
|
+
const workspacePackage = isWorkspacePackage(tree, directory);
|
|
109
|
+
if (!schema.skipPackageJson) {
|
|
110
|
+
addDependenciesToPackageJson(tree, { "@rivetjs/reactivity": rivet, "@rivetjs/runtime": rivet }, workspacePackage ? {} : (pkg.devDependencies ?? {}));
|
|
111
|
+
}
|
|
112
|
+
if (!schema.skipFormat)
|
|
113
|
+
await formatFiles(tree);
|
|
114
|
+
return schema.skipInstall ? () => { } : () => installPackagesTask(tree, workspacePackage);
|
|
115
|
+
}
|
|
116
|
+
export default libraryGenerator;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface LibraryGeneratorSchema {
|
|
2
|
+
/** Project folder, workspace-relative (`libs/ui-kit`). */
|
|
3
|
+
directory: string;
|
|
4
|
+
/** Project name (default: the folder name). */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** npm name (default `@<workspace scope>/<name>`, or `<name>` without a scope). */
|
|
7
|
+
importPath?: string;
|
|
8
|
+
/** Publishable: a `build` target (`@rivetjs/nx:package`) emitting `.js` / `.jsx` + `.d.ts` into `dist/<directory>`. */
|
|
9
|
+
publishable?: boolean;
|
|
10
|
+
/** Extra subpath exports (comma-separated: `button,card` → `<importPath>/button`, …), each tree-shakeable on its own. */
|
|
11
|
+
subpaths?: string;
|
|
12
|
+
unitTestRunner?: "vitest" | "none";
|
|
13
|
+
tags?: string;
|
|
14
|
+
rivetVersion?: string;
|
|
15
|
+
addPlugin?: boolean;
|
|
16
|
+
skipPackageJson?: boolean;
|
|
17
|
+
skipInstall?: boolean;
|
|
18
|
+
skipFormat?: boolean;
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "RivetLibrary",
|
|
4
|
+
"title": "Rivet library",
|
|
5
|
+
"description": "Creates a Rivet component library: source exports per subpath (tree-shakeable, sideEffects false), Vitest with rivetTest(), and a package build when publishable.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"directory": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Project folder, relative to the workspace root (e.g. libs/ui-kit).",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 },
|
|
12
|
+
"x-prompt": "Which directory do you want to create the library in?"
|
|
13
|
+
},
|
|
14
|
+
"name": { "type": "string", "description": "Project name (default: the folder name).", "pattern": "^[a-zA-Z][^:]*$" },
|
|
15
|
+
"importPath": { "type": "string", "description": "npm name (default @<workspace scope>/<name>)." },
|
|
16
|
+
"publishable": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"default": false,
|
|
19
|
+
"description": "Add a build target (@rivetjs/nx:package) that emits .js / .jsx + .d.ts into dist/<directory>."
|
|
20
|
+
},
|
|
21
|
+
"subpaths": { "type": "string", "description": "Extra subpath exports, comma-separated (button,card → <importPath>/button, …)." },
|
|
22
|
+
"unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "default": "vitest", "description": "Test runner." },
|
|
23
|
+
"tags": { "type": "string", "description": "Comma-separated project tags." },
|
|
24
|
+
"rivetVersion": { "type": "string", "description": "@rivetjs/* dependency spec (default: this plugin's version)." },
|
|
25
|
+
"addPlugin": { "type": "boolean", "x-priority": "internal", "description": "Use inferred targets (default: true unless NX_ADD_PLUGINS=false)." },
|
|
26
|
+
"skipPackageJson": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Do not add dependencies to the root package.json." },
|
|
27
|
+
"skipInstall": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Do not run the package manager install." },
|
|
28
|
+
"skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
|
|
29
|
+
},
|
|
30
|
+
"required": ["directory"],
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type GeneratorCallback, type Tree } from "@nx/devkit";
|
|
2
|
+
import type { PresetGeneratorSchema } from "./schema.js";
|
|
3
|
+
export type { PresetGeneratorSchema } from "./schema.js";
|
|
4
|
+
export declare function presetGenerator(tree: Tree, options: PresetGeneratorSchema): Promise<GeneratorCallback>;
|
|
5
|
+
export default presetGenerator;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// `npx create-nx-workspace <name> --preset=@rivetjs/nx [--template=…]`: init + the first app, package-based
|
|
2
|
+
// layout (npm / yarn / bun `workspaces: ["apps/*", "libs/*"]` unless the workspace already has a list).
|
|
3
|
+
import { formatFiles, readJson, updateJson } from "@nx/devkit";
|
|
4
|
+
import { applicationGenerator } from "../application/generator.js";
|
|
5
|
+
import { initGenerator } from "../init/generator.js";
|
|
6
|
+
export async function presetGenerator(tree, options) {
|
|
7
|
+
const appName = options.appName ?? options.name;
|
|
8
|
+
if (tree.exists("package.json") && !tree.exists("pnpm-workspace.yaml") && !readJson(tree, "package.json").workspaces) {
|
|
9
|
+
updateJson(tree, "package.json", (pkg) => ({ ...pkg, workspaces: ["apps/*", "libs/*"] }));
|
|
10
|
+
}
|
|
11
|
+
const rivet = options.rivetVersion ? { rivetVersion: options.rivetVersion } : {};
|
|
12
|
+
await initGenerator(tree, { skipFormat: true, skipInstall: true, ...rivet });
|
|
13
|
+
const install = await applicationGenerator(tree, {
|
|
14
|
+
directory: options.directory ?? `apps/${appName}`,
|
|
15
|
+
name: appName,
|
|
16
|
+
...(options.template ? { template: options.template } : {}),
|
|
17
|
+
...(options.adapter ? { adapter: options.adapter } : {}),
|
|
18
|
+
...(options.deploy ? { deploy: options.deploy } : {}),
|
|
19
|
+
...(options.tailwind !== undefined ? { tailwind: options.tailwind } : {}),
|
|
20
|
+
...(options.unitTestRunner ? { unitTestRunner: options.unitTestRunner } : {}),
|
|
21
|
+
...rivet,
|
|
22
|
+
skipFormat: true,
|
|
23
|
+
...(options.skipInstall ? { skipInstall: true } : {}),
|
|
24
|
+
});
|
|
25
|
+
if (!options.skipFormat)
|
|
26
|
+
await formatFiles(tree);
|
|
27
|
+
return install;
|
|
28
|
+
}
|
|
29
|
+
export default presetGenerator;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PresetGeneratorSchema {
|
|
2
|
+
/** Workspace name (create-nx-workspace passes it); the default app name. */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Name of the first app (default: the workspace name). */
|
|
5
|
+
appName?: string;
|
|
6
|
+
/** Folder of the first app (default `apps/<appName>`). */
|
|
7
|
+
directory?: string;
|
|
8
|
+
template?: "static" | "server" | "ui" | "minimal";
|
|
9
|
+
adapter?: "node" | "static";
|
|
10
|
+
deploy?: "vercel" | "netlify" | "cloudflare" | "bun" | "deno";
|
|
11
|
+
tailwind?: boolean;
|
|
12
|
+
unitTestRunner?: "vitest" | "none";
|
|
13
|
+
rivetVersion?: string;
|
|
14
|
+
skipInstall?: boolean;
|
|
15
|
+
skipFormat?: boolean;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "RivetPreset",
|
|
4
|
+
"title": "Rivet workspace preset",
|
|
5
|
+
"description": "npx create-nx-workspace --preset=@rivetjs/nx: an Nx workspace with the Rivet plugin and a first Rivet app.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"name": { "type": "string", "description": "Workspace name; also the default app name.", "$default": { "$source": "argv", "index": 0 } },
|
|
9
|
+
"appName": { "type": "string", "description": "Name of the first app (default: the workspace name)." },
|
|
10
|
+
"directory": { "type": "string", "description": "Folder of the first app (default apps/<appName>)." },
|
|
11
|
+
"template": { "type": "string", "enum": ["static", "server", "ui", "minimal"], "default": "static", "description": "Starter of the first app." },
|
|
12
|
+
"adapter": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"enum": ["node", "static"],
|
|
15
|
+
"description": "node: pages render per request; static: prerendered HTML. Default: from the template."
|
|
16
|
+
},
|
|
17
|
+
"deploy": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": ["vercel", "netlify", "cloudflare", "bun", "deno"],
|
|
20
|
+
"description": "Platform target of the first app (create-rivet --deploy). bun and deno need --template=server."
|
|
21
|
+
},
|
|
22
|
+
"tailwind": { "type": "boolean", "description": "Tailwind v4. Default: on, except the minimal starter." },
|
|
23
|
+
"unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "default": "vitest", "description": "Test runner of the first app." },
|
|
24
|
+
"rivetVersion": { "type": "string", "description": "@rivetjs/* dependency spec (default: this plugin's version)." },
|
|
25
|
+
"skipInstall": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Do not run the package manager install." },
|
|
26
|
+
"skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
|
|
27
|
+
},
|
|
28
|
+
"required": ["name"],
|
|
29
|
+
"additionalProperties": true
|
|
30
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<% if (routerImports.length) { %>import { <%- routerImports.join(", ") %> } from "@rivetjs/router";
|
|
2
|
+
<% } %><% if (guard) { %>
|
|
3
|
+
/** Runs on the server before the loader. Return `redirect()`, `deny()` or `notFound()` to stop the request. */
|
|
4
|
+
export const serverGuard = ({ url }: RequestEvent) => {
|
|
5
|
+
if (url.searchParams.has("deny")) return deny();
|
|
6
|
+
};
|
|
7
|
+
<% } %><% if (entries) { %>
|
|
8
|
+
/** Parameter values to prerender (static mode): one page per entry. */
|
|
9
|
+
export const serverEntries = () => [<%- entries %>];
|
|
10
|
+
<% } %><% if (loader) { %>
|
|
11
|
+
/** Data for the page, loaded on the server; removed from the browser build with everything only it uses. */
|
|
12
|
+
export const serverLoad = (<% if (params.length) { %>{ params }<% } else { %>_event<% } %>: RequestEvent) => ({
|
|
13
|
+
title: <%- loaderTitle %>,
|
|
14
|
+
});
|
|
15
|
+
<% } %><% if (action) { %>
|
|
16
|
+
export const serverActions = {
|
|
17
|
+
/** Handles `<form method="post" action="?/save">`. Works without JavaScript. */
|
|
18
|
+
save: async ({ request, url }: RequestEvent) => {
|
|
19
|
+
const text = String((await request.formData()).get("text") ?? "").trim();
|
|
20
|
+
console.log("saved", text.slice(0, 200));
|
|
21
|
+
// Post/redirect/get: reloading the page does not submit the form again.
|
|
22
|
+
return redirect(url.pathname, 303);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
<% } %>
|
|
26
|
+
export const head = { title: "<%= title %>" };
|
|
27
|
+
|
|
28
|
+
/** <%= title %> (<%= url %>). */
|
|
29
|
+
export default function <%= componentName %>(<%- props %>) {
|
|
30
|
+
return (
|
|
31
|
+
<main>
|
|
32
|
+
<h1><% if (loader) { %>{data.title}<% } else { %><%= title %><% } %></h1><% for (const param of params) { %>
|
|
33
|
+
<p>
|
|
34
|
+
<%= param.name %>: {params.<%= param.name %>}
|
|
35
|
+
</p><% } %><% if (action) { %>
|
|
36
|
+
<form method="post" action="?/save">
|
|
37
|
+
<label for="text">Text</label>
|
|
38
|
+
<input id="text" name="text" required />
|
|
39
|
+
<button>Save</button>
|
|
40
|
+
</form><% } %>
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Tree } from "@nx/devkit";
|
|
2
|
+
import { routeParams } from "../../utils/project.js";
|
|
3
|
+
import type { RouteGeneratorSchema } from "./schema.js";
|
|
4
|
+
export type { RouteGeneratorSchema } from "./schema.js";
|
|
5
|
+
/** Sample `serverEntries` value of a route's params (`{ slug: "example" }`), or undefined without required params. */
|
|
6
|
+
export declare function sampleEntry(params: ReturnType<typeof routeParams>): string | undefined;
|
|
7
|
+
/** The URL pattern of route segments (`/blog/:slug`). */
|
|
8
|
+
export declare function routeUrl(segments: string[]): string;
|
|
9
|
+
export declare function routeGenerator(tree: Tree, options: RouteGeneratorSchema): Promise<void>;
|
|
10
|
+
export default routeGenerator;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// `nx g @rivetjs/nx:route blog/[slug] --project=web [--loader] [--action] [--guard] [--layout]`.
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { formatFiles, generateFiles, joinPathFragments } from "@nx/devkit";
|
|
4
|
+
import { routeComponentName, routedApp, routeParams, routeSegments } from "../../utils/project.js";
|
|
5
|
+
import { layoutGenerator } from "../layout/generator.js";
|
|
6
|
+
const FILES = fileURLToPath(new URL("./files", import.meta.url));
|
|
7
|
+
/** Sample `serverEntries` value of a route's params (`{ slug: "example" }`), or undefined without required params. */
|
|
8
|
+
export function sampleEntry(params) {
|
|
9
|
+
const required = params.filter((param) => param.kind !== "optional");
|
|
10
|
+
if (!required.length)
|
|
11
|
+
return undefined;
|
|
12
|
+
return `{ ${required.map((param) => `${param.name}: "${param.kind === "rest" ? "example/page" : "example"}"`).join(", ")} }`;
|
|
13
|
+
}
|
|
14
|
+
/** The URL pattern of route segments (`/blog/:slug`). */
|
|
15
|
+
export function routeUrl(segments) {
|
|
16
|
+
const parts = segments
|
|
17
|
+
.filter((segment) => !/^\(.*\)$/.test(segment))
|
|
18
|
+
.map((segment) => segment
|
|
19
|
+
.replace(/^\[\[(.+)\]\]$/, ":$1?")
|
|
20
|
+
.replace(/^\[\.\.\.(.+)\]$/, "*$1")
|
|
21
|
+
.replace(/^\[(.+)\]$/, ":$1"));
|
|
22
|
+
return "/" + parts.join("/");
|
|
23
|
+
}
|
|
24
|
+
export async function routeGenerator(tree, options) {
|
|
25
|
+
const project = routedApp(tree, options.project, "route");
|
|
26
|
+
const segments = routeSegments(options.path);
|
|
27
|
+
const params = routeParams(segments);
|
|
28
|
+
if (options.action && project.facts?.static)
|
|
29
|
+
throw new Error('route: serverActions need a server; this app is prerendered (rivetRouter({ mode: "static" }))');
|
|
30
|
+
const index = !segments.length || /^\(.*\)$/.test(segments.at(-1)) || /(^|\/)index(\.\w+)?\/?$/.test(options.path) || options.path.endsWith("/");
|
|
31
|
+
const folder = joinPathFragments(project.routes, ...(index ? segments : segments.slice(0, -1)));
|
|
32
|
+
const fileName = index ? "index" : segments.at(-1);
|
|
33
|
+
for (const ext of [".tsx", ".jsx", ".ts"]) {
|
|
34
|
+
if (tree.exists(joinPathFragments(folder, fileName + ext)))
|
|
35
|
+
throw new Error(`route: ${joinPathFragments(folder, fileName + ext)} already exists`);
|
|
36
|
+
}
|
|
37
|
+
const name = routeComponentName(segments.filter((segment) => !/^\(.*\)$/.test(segment)));
|
|
38
|
+
const title = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
39
|
+
const first = params[0];
|
|
40
|
+
const routerImports = [
|
|
41
|
+
...(options.guard ? ["deny"] : []),
|
|
42
|
+
...(options.action ? ["redirect"] : []),
|
|
43
|
+
...(options.loader || params.length ? ["type PageProps"] : []),
|
|
44
|
+
...(options.loader || options.action || options.guard ? ["type RequestEvent"] : []),
|
|
45
|
+
];
|
|
46
|
+
const props = [...(options.loader ? ["data"] : []), ...(params.length ? ["params"] : [])];
|
|
47
|
+
generateFiles(tree, FILES, folder, {
|
|
48
|
+
fileName,
|
|
49
|
+
componentName: name + "Page",
|
|
50
|
+
title,
|
|
51
|
+
url: routeUrl(segments),
|
|
52
|
+
routerImports,
|
|
53
|
+
guard: Boolean(options.guard),
|
|
54
|
+
loader: Boolean(options.loader),
|
|
55
|
+
action: Boolean(options.action),
|
|
56
|
+
params,
|
|
57
|
+
entries: project.facts?.static ? (sampleEntry(params) ?? "") : "",
|
|
58
|
+
loaderTitle: first ? `\`${title}: \${params.${first.name} ?? ""}\`` : JSON.stringify(title),
|
|
59
|
+
props: props.length ? `{ ${props.join(", ")} }: PageProps${options.loader ? "<typeof serverLoad>" : ""}` : "",
|
|
60
|
+
});
|
|
61
|
+
if (options.layout && !tree.exists(joinPathFragments(folder, "_layout.tsx"))) {
|
|
62
|
+
await layoutGenerator(tree, { project: options.project, directory: folder.slice(project.routes.length + 1), skipFormat: true });
|
|
63
|
+
}
|
|
64
|
+
if (!options.skipFormat)
|
|
65
|
+
await formatFiles(tree);
|
|
66
|
+
}
|
|
67
|
+
export default routeGenerator;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RouteGeneratorSchema {
|
|
2
|
+
/** Route path under `src/routes` (`about`, `blog/[slug]`, `docs/[[lang]]/[...path]`, `(marketing)/pricing`, `blog/index`). */
|
|
3
|
+
path: string;
|
|
4
|
+
/** The Rivet app. */
|
|
5
|
+
project: string;
|
|
6
|
+
/** `serverLoad` + typed `data` prop. */
|
|
7
|
+
loader?: boolean;
|
|
8
|
+
/** `serverActions` + a progressive-enhancement form (node adapter only). */
|
|
9
|
+
action?: boolean;
|
|
10
|
+
/** `serverGuard`. */
|
|
11
|
+
guard?: boolean;
|
|
12
|
+
/** Also create a `_layout.tsx` in the route's folder (when it has none). */
|
|
13
|
+
layout?: boolean;
|
|
14
|
+
skipFormat?: boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "RivetRoute",
|
|
4
|
+
"title": "Rivet route",
|
|
5
|
+
"description": "Adds a file route (page) to a Rivet app: dynamic segments ([param], [[optional]], [...rest]), groups ((name)), optional serverLoad / serverActions / serverGuard and a folder layout.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"path": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Route path under src/routes: about, blog/[slug], docs/[[lang]]/[...path], (marketing)/pricing, blog/index.",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 },
|
|
12
|
+
"x-prompt": "Route path (under src/routes, e.g. blog/[slug])?"
|
|
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
|
+
"loader": { "type": "boolean", "default": false, "description": "Add serverLoad and a typed data prop." },
|
|
22
|
+
"action": { "type": "boolean", "default": false, "description": "Add serverActions and a form that posts to it (node adapter only)." },
|
|
23
|
+
"guard": { "type": "boolean", "default": false, "description": "Add serverGuard." },
|
|
24
|
+
"layout": { "type": "boolean", "default": false, "description": "Also create _layout.tsx in the route's folder (when it has none)." },
|
|
25
|
+
"skipFormat": { "type": "boolean", "default": false, "x-priority": "internal", "description": "Skip formatting files." }
|
|
26
|
+
},
|
|
27
|
+
"required": ["path", "project"],
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type GeneratorCallback, type Tree } from "@nx/devkit";
|
|
2
|
+
import type { UiSetupGeneratorSchema } from "./schema.js";
|
|
3
|
+
export type { UiSetupGeneratorSchema } from "./schema.js";
|
|
4
|
+
/** `src/styles.css` with Tailwind first, then the ui styles (a `@import` must precede other rules). */
|
|
5
|
+
export declare function uiStyles(css: string): string;
|
|
6
|
+
export declare function uiSetupGenerator(tree: Tree, options: UiSetupGeneratorSchema): Promise<GeneratorCallback>;
|
|
7
|
+
export default uiSetupGenerator;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// `nx g @rivetjs/nx:ui-setup web`: @rivetjs/ui on an existing app, the way the `ui` starter
|
|
2
|
+
// sets it up (Tailwind v4 plugin, `@import "tailwindcss"; @import "@rivetjs/ui/styles";`, ui + kit + lucide). Idempotent.
|
|
3
|
+
import { addDependenciesToPackageJson, formatFiles, installPackagesTask, joinPathFragments, readJson, writeJson, } from "@nx/devkit";
|
|
4
|
+
import { viteConfigPath } from "../../utils/facts.js";
|
|
5
|
+
import { routedApp } from "../../utils/project.js";
|
|
6
|
+
import { rivetRange, starterVersions, toolVersion } from "../../utils/versions.js";
|
|
7
|
+
import { addImport, addPlugin, setRouterOption } from "../../utils/vite-config.js";
|
|
8
|
+
import { isWorkspacePackage } from "../../utils/workspace.js";
|
|
9
|
+
/** `src/styles.css` with Tailwind first, then the ui styles (a `@import` must precede other rules). */
|
|
10
|
+
export function uiStyles(css) {
|
|
11
|
+
const ui = '@import "@rivetjs/ui/styles";';
|
|
12
|
+
if (css.includes("@rivetjs/ui/styles"))
|
|
13
|
+
return css;
|
|
14
|
+
const lines = css.split("\n");
|
|
15
|
+
const tailwind = lines.findIndex((line) => /@import\s+["']tailwindcss["']/.test(line));
|
|
16
|
+
if (tailwind >= 0) {
|
|
17
|
+
lines.splice(tailwind + 1, 0, ui);
|
|
18
|
+
return lines.join("\n");
|
|
19
|
+
}
|
|
20
|
+
return `@import "tailwindcss";\n${ui}\n/* Scan the app's source for class names (explicit, so it also works in ignored folders). */\n@source "./**/*.{ts,tsx}";\n\n${css}`;
|
|
21
|
+
}
|
|
22
|
+
export async function uiSetupGenerator(tree, options) {
|
|
23
|
+
const project = routedApp(tree, options.project, "ui-setup");
|
|
24
|
+
const vite = viteConfigPath(tree, project.root);
|
|
25
|
+
let config = tree.read(vite, "utf8");
|
|
26
|
+
config = addImport(config, 'import tailwindcss from "@tailwindcss/vite";');
|
|
27
|
+
config = addPlugin(config, "tailwindcss()", "last");
|
|
28
|
+
if (!/\bstyles\s*:/.test(config))
|
|
29
|
+
config = setRouterOption(config, "styles", '["src/styles.css"]');
|
|
30
|
+
tree.write(vite, config);
|
|
31
|
+
const styles = joinPathFragments(project.src, "styles.css");
|
|
32
|
+
tree.write(styles, uiStyles(tree.exists(styles) ? tree.read(styles, "utf8") : ""));
|
|
33
|
+
let install = () => { };
|
|
34
|
+
if (!options.skipPackageJson) {
|
|
35
|
+
const rivet = rivetRange(options.rivetVersion);
|
|
36
|
+
const ui = starterVersions("starter-ui");
|
|
37
|
+
const deps = { "@rivetjs/ui": rivet, "@rivetjs/kit": rivet, lucide: ui["lucide"] ?? toolVersion("lucide") };
|
|
38
|
+
const devDeps = { tailwindcss: toolVersion("tailwindcss"), "@tailwindcss/vite": toolVersion("@tailwindcss/vite") };
|
|
39
|
+
const pkgPath = joinPathFragments(project.root, "package.json");
|
|
40
|
+
if (tree.exists(pkgPath)) {
|
|
41
|
+
const pkg = readJson(tree, pkgPath);
|
|
42
|
+
pkg.dependencies = { ...pkg.dependencies, ...deps };
|
|
43
|
+
pkg.devDependencies = { ...pkg.devDependencies, ...devDeps };
|
|
44
|
+
writeJson(tree, pkgPath, pkg);
|
|
45
|
+
}
|
|
46
|
+
const workspacePackage = tree.exists(pkgPath) && isWorkspacePackage(tree, project.root);
|
|
47
|
+
if (!workspacePackage)
|
|
48
|
+
addDependenciesToPackageJson(tree, deps, devDeps);
|
|
49
|
+
install = () => installPackagesTask(tree, workspacePackage);
|
|
50
|
+
}
|
|
51
|
+
if (!options.skipFormat)
|
|
52
|
+
await formatFiles(tree);
|
|
53
|
+
return install;
|
|
54
|
+
}
|
|
55
|
+
export default uiSetupGenerator;
|