create-reactor 0.1.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 +123 -0
- package/create-app.mjs +712 -0
- package/lib/build.mjs +434 -0
- package/lib/pm.mjs +85 -0
- package/lib/presets.mjs +122 -0
- package/lib/templates/ai-docs.mjs +80 -0
- package/lib/templates/app.mjs +961 -0
- package/lib/templates/backend.mjs +715 -0
- package/lib/templates/base.mjs +671 -0
- package/lib/templates/biome.mjs +107 -0
- package/lib/templates/extras.mjs +360 -0
- package/lib/templates/features.mjs +463 -0
- package/lib/templates/quality.mjs +159 -0
- package/lib/templates/readme.mjs +351 -0
- package/lib/templates/security.mjs +70 -0
- package/lib/templates/server.mjs +141 -0
- package/lib/templates/state.mjs +192 -0
- package/package.json +52 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Biome: Rust-based linter + formatter, replaces ESLint + Prettier with one tool.
|
|
2
|
+
|
|
3
|
+
export function biomeJson(c) {
|
|
4
|
+
// Note: output is pre-formatted exactly the way Biome's own formatter wants it
|
|
5
|
+
// (inline short arrays, trailing newline), so `biome check` passes on its own config.
|
|
6
|
+
const ignores = [];
|
|
7
|
+
if (c.backend === "convex") ignores.push("!convex/_generated/**");
|
|
8
|
+
if (c.router === "tanstack") ignores.push("!src/routeTree.gen.ts");
|
|
9
|
+
if (c.orm === "prisma") ignores.push("!src/generated/**");
|
|
10
|
+
const includes = ["**", ...ignores].map((i) => JSON.stringify(i)).join(", ");
|
|
11
|
+
|
|
12
|
+
return `{
|
|
13
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
14
|
+
"vcs": {
|
|
15
|
+
"enabled": true,
|
|
16
|
+
"clientKind": "git",
|
|
17
|
+
"useIgnoreFile": true
|
|
18
|
+
},
|
|
19
|
+
"files": {
|
|
20
|
+
"ignoreUnknown": true,
|
|
21
|
+
"includes": [${includes}]
|
|
22
|
+
},
|
|
23
|
+
"formatter": {
|
|
24
|
+
"enabled": true,
|
|
25
|
+
"indentStyle": "space"
|
|
26
|
+
},
|
|
27
|
+
"linter": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"rules": {
|
|
30
|
+
"recommended": true,
|
|
31
|
+
"style": {
|
|
32
|
+
"noNonNullAssertion": "off"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"javascript": {
|
|
37
|
+
"formatter": {
|
|
38
|
+
"quoteStyle": "double"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"css": {
|
|
42
|
+
"parser": {
|
|
43
|
+
"tailwindDirectives": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"assist": {
|
|
47
|
+
"actions": {
|
|
48
|
+
"source": {
|
|
49
|
+
"organizeImports": "on"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Linter-specific package.json scripts. */
|
|
58
|
+
export function linterScripts(c) {
|
|
59
|
+
if (c.linter === "biome") {
|
|
60
|
+
return {
|
|
61
|
+
lint: "biome check .",
|
|
62
|
+
"lint:fix": "biome check --write .",
|
|
63
|
+
format: "biome format --write .",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// eslint (+ prettier handled separately as an extra)
|
|
67
|
+
const scripts = { lint: "eslint ." };
|
|
68
|
+
if (c.extras.includes("prettier")) {
|
|
69
|
+
scripts.format = "prettier --write .";
|
|
70
|
+
}
|
|
71
|
+
return scripts;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Linter-specific dev dependencies. */
|
|
75
|
+
export function linterDevDeps(c) {
|
|
76
|
+
if (c.linter === "biome") {
|
|
77
|
+
return ["@biomejs/biome"];
|
|
78
|
+
}
|
|
79
|
+
const deps = [
|
|
80
|
+
"eslint",
|
|
81
|
+
"@eslint/js",
|
|
82
|
+
"typescript-eslint",
|
|
83
|
+
"eslint-plugin-react-hooks",
|
|
84
|
+
"eslint-plugin-react-refresh",
|
|
85
|
+
"globals",
|
|
86
|
+
];
|
|
87
|
+
if (c.extras.includes("prettier")) {
|
|
88
|
+
deps.push("prettier", "prettier-plugin-tailwindcss");
|
|
89
|
+
}
|
|
90
|
+
return deps;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** lint-staged config per linter. */
|
|
94
|
+
export function linterLintStaged(c) {
|
|
95
|
+
if (c.linter === "biome") {
|
|
96
|
+
return {
|
|
97
|
+
"*.{ts,tsx,js,jsx,json,css}": ["biome check --write --no-errors-on-unmatched"],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const config = {
|
|
101
|
+
"*.{ts,tsx}": ["eslint --fix --no-warn-ignored"],
|
|
102
|
+
};
|
|
103
|
+
if (c.extras.includes("prettier")) {
|
|
104
|
+
config["*.{ts,tsx,css,md,json}"] = ["prettier --write"];
|
|
105
|
+
}
|
|
106
|
+
return config;
|
|
107
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Extra layers: TanStack Table, Vitest + Testing Library, Motion, Husky, CI, Sentry.
|
|
2
|
+
import { fallowCiJob } from "./quality.mjs";
|
|
3
|
+
|
|
4
|
+
/** src/components/data-table-demo.tsx (TanStack Table + shadcn table) */
|
|
5
|
+
export function dataTableDemo() {
|
|
6
|
+
return `import { useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
type ColumnDef,
|
|
9
|
+
type SortingState,
|
|
10
|
+
flexRender,
|
|
11
|
+
getCoreRowModel,
|
|
12
|
+
getSortedRowModel,
|
|
13
|
+
useReactTable,
|
|
14
|
+
} from "@tanstack/react-table";
|
|
15
|
+
import { ArrowUpDown } from "lucide-react";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import {
|
|
18
|
+
Card,
|
|
19
|
+
CardContent,
|
|
20
|
+
CardDescription,
|
|
21
|
+
CardHeader,
|
|
22
|
+
CardTitle,
|
|
23
|
+
} from "@/components/ui/card";
|
|
24
|
+
import {
|
|
25
|
+
Table,
|
|
26
|
+
TableBody,
|
|
27
|
+
TableCell,
|
|
28
|
+
TableHead,
|
|
29
|
+
TableHeader,
|
|
30
|
+
TableRow,
|
|
31
|
+
} from "@/components/ui/table";
|
|
32
|
+
|
|
33
|
+
interface Person {
|
|
34
|
+
name: string;
|
|
35
|
+
role: string;
|
|
36
|
+
tasks: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data: Person[] = [
|
|
40
|
+
{ name: "Ada Lovelace", role: "Engineer", tasks: 12 },
|
|
41
|
+
{ name: "Grace Hopper", role: "Admiral", tasks: 9 },
|
|
42
|
+
{ name: "Alan Turing", role: "Researcher", tasks: 17 },
|
|
43
|
+
{ name: "Margaret Hamilton", role: "Director", tasks: 23 },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function SortableHeader({
|
|
47
|
+
label,
|
|
48
|
+
onClick,
|
|
49
|
+
}: {
|
|
50
|
+
label: string;
|
|
51
|
+
onClick: () => void;
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<Button variant="ghost" size="sm" className="-ml-3" onClick={onClick}>
|
|
55
|
+
{label}
|
|
56
|
+
<ArrowUpDown className="ml-2 size-3.5" />
|
|
57
|
+
</Button>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const columns: ColumnDef<Person>[] = [
|
|
62
|
+
{
|
|
63
|
+
accessorKey: "name",
|
|
64
|
+
header: ({ column }) => (
|
|
65
|
+
<SortableHeader
|
|
66
|
+
label="Name"
|
|
67
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
accessorKey: "role",
|
|
73
|
+
header: "Role",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
accessorKey: "tasks",
|
|
77
|
+
header: ({ column }) => (
|
|
78
|
+
<SortableHeader
|
|
79
|
+
label="Tasks"
|
|
80
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
81
|
+
/>
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
export function DataTableDemo() {
|
|
87
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
88
|
+
const table = useReactTable({
|
|
89
|
+
data,
|
|
90
|
+
columns,
|
|
91
|
+
state: { sorting },
|
|
92
|
+
onSortingChange: setSorting,
|
|
93
|
+
getCoreRowModel: getCoreRowModel(),
|
|
94
|
+
getSortedRowModel: getSortedRowModel(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Card>
|
|
99
|
+
<CardHeader>
|
|
100
|
+
<CardTitle>TanStack Table</CardTitle>
|
|
101
|
+
<CardDescription>
|
|
102
|
+
Sortable data table — edit{" "}
|
|
103
|
+
<code>src/components/data-table-demo.tsx</code>.
|
|
104
|
+
</CardDescription>
|
|
105
|
+
</CardHeader>
|
|
106
|
+
<CardContent>
|
|
107
|
+
<Table>
|
|
108
|
+
<TableHeader>
|
|
109
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
110
|
+
<TableRow key={headerGroup.id}>
|
|
111
|
+
{headerGroup.headers.map((header) => (
|
|
112
|
+
<TableHead key={header.id}>
|
|
113
|
+
{header.isPlaceholder
|
|
114
|
+
? null
|
|
115
|
+
: flexRender(
|
|
116
|
+
header.column.columnDef.header,
|
|
117
|
+
header.getContext(),
|
|
118
|
+
)}
|
|
119
|
+
</TableHead>
|
|
120
|
+
))}
|
|
121
|
+
</TableRow>
|
|
122
|
+
))}
|
|
123
|
+
</TableHeader>
|
|
124
|
+
<TableBody>
|
|
125
|
+
{table.getRowModel().rows.map((row) => (
|
|
126
|
+
<TableRow key={row.id}>
|
|
127
|
+
{row.getVisibleCells().map((cell) => (
|
|
128
|
+
<TableCell key={cell.id}>
|
|
129
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
130
|
+
</TableCell>
|
|
131
|
+
))}
|
|
132
|
+
</TableRow>
|
|
133
|
+
))}
|
|
134
|
+
</TableBody>
|
|
135
|
+
</Table>
|
|
136
|
+
</CardContent>
|
|
137
|
+
</Card>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Vitest + React Testing Library
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/** src/test/setup.ts */
|
|
148
|
+
export function testSetup() {
|
|
149
|
+
return `import "@testing-library/jest-dom/vitest";
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** src/components/ui/button.test.tsx — sample component test */
|
|
154
|
+
export function sampleTest() {
|
|
155
|
+
return `import { describe, expect, it } from "vitest";
|
|
156
|
+
import { render, screen } from "@testing-library/react";
|
|
157
|
+
import { Button } from "@/components/ui/button";
|
|
158
|
+
|
|
159
|
+
describe("Button", () => {
|
|
160
|
+
it("renders with its label", () => {
|
|
161
|
+
render(<Button>Click me</Button>);
|
|
162
|
+
expect(
|
|
163
|
+
screen.getByRole("button", { name: "Click me" }),
|
|
164
|
+
).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** src/lib/utils.test.ts — sample unit test */
|
|
171
|
+
export function utilsTest() {
|
|
172
|
+
return `import { describe, expect, it } from "vitest";
|
|
173
|
+
import { cn } from "@/lib/utils";
|
|
174
|
+
|
|
175
|
+
describe("cn", () => {
|
|
176
|
+
it("merges class names", () => {
|
|
177
|
+
expect(cn("p-2", "font-bold")).toBe("p-2 font-bold");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("resolves Tailwind conflicts (last one wins)", () => {
|
|
181
|
+
expect(cn("p-2", "p-4")).toBe("p-4");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("ignores falsy values", () => {
|
|
185
|
+
expect(cn("base", undefined, null, "extra")).toBe("base extra");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Motion (Framer Motion)
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/** src/components/fade-in.tsx — reusable animation wrapper */
|
|
196
|
+
export function fadeIn() {
|
|
197
|
+
return `import { motion } from "motion/react";
|
|
198
|
+
import type { ReactNode } from "react";
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Fades content in from below when it mounts.
|
|
202
|
+
*
|
|
203
|
+
* Usage:
|
|
204
|
+
* <FadeIn><Card>...</Card></FadeIn>
|
|
205
|
+
* <FadeIn delay={0.2}>...</FadeIn>
|
|
206
|
+
*/
|
|
207
|
+
export function FadeIn({
|
|
208
|
+
children,
|
|
209
|
+
delay = 0,
|
|
210
|
+
className,
|
|
211
|
+
}: {
|
|
212
|
+
children: ReactNode;
|
|
213
|
+
delay?: number;
|
|
214
|
+
className?: string;
|
|
215
|
+
}) {
|
|
216
|
+
return (
|
|
217
|
+
<motion.div
|
|
218
|
+
className={className}
|
|
219
|
+
initial={{ opacity: 0, y: 8 }}
|
|
220
|
+
animate={{ opacity: 1, y: 0 }}
|
|
221
|
+
transition={{ duration: 0.4, delay, ease: "easeOut" }}
|
|
222
|
+
>
|
|
223
|
+
{children}
|
|
224
|
+
</motion.div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Husky + lint-staged
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/** .husky/pre-commit */
|
|
235
|
+
export function huskyPreCommit(c) {
|
|
236
|
+
return `${c.pmDlxLabel("lint-staged")}
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** lint-staged config object (merged into package.json). */
|
|
241
|
+
export function lintStagedConfig(c) {
|
|
242
|
+
const config = {
|
|
243
|
+
// --no-warn-ignored: lint-staged passes generated files (routeTree.gen.ts,
|
|
244
|
+
// convex/_generated) explicitly; eslint should skip them silently.
|
|
245
|
+
"*.{ts,tsx}": ["eslint --fix --no-warn-ignored"],
|
|
246
|
+
};
|
|
247
|
+
if (c.extras.includes("prettier")) {
|
|
248
|
+
config["*.{ts,tsx,css,md,json}"] = ["prettier --write"];
|
|
249
|
+
}
|
|
250
|
+
return config;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// GitHub Actions CI
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/** .github/workflows/ci.yml */
|
|
258
|
+
export function githubCi(c) {
|
|
259
|
+
const setup = {
|
|
260
|
+
bun: ` - uses: oven-sh/setup-bun@v2
|
|
261
|
+
|
|
262
|
+
- name: Install dependencies
|
|
263
|
+
run: bun install --frozen-lockfile`,
|
|
264
|
+
pnpm: ` - uses: pnpm/action-setup@v4
|
|
265
|
+
|
|
266
|
+
- uses: actions/setup-node@v4
|
|
267
|
+
with:
|
|
268
|
+
node-version: 22
|
|
269
|
+
cache: pnpm
|
|
270
|
+
|
|
271
|
+
- name: Install dependencies
|
|
272
|
+
run: pnpm install --frozen-lockfile`,
|
|
273
|
+
npm: ` - uses: actions/setup-node@v4
|
|
274
|
+
with:
|
|
275
|
+
node-version: 22
|
|
276
|
+
cache: npm
|
|
277
|
+
|
|
278
|
+
- name: Install dependencies
|
|
279
|
+
run: npm ci`,
|
|
280
|
+
}[c.pm];
|
|
281
|
+
|
|
282
|
+
const runCmd = (script) => c.pmRunLabel(script);
|
|
283
|
+
|
|
284
|
+
const steps = [
|
|
285
|
+
` - name: Lint
|
|
286
|
+
run: ${runCmd("lint")}`,
|
|
287
|
+
` - name: Build & typecheck
|
|
288
|
+
run: ${runCmd("build")}`,
|
|
289
|
+
];
|
|
290
|
+
if (c.extras.includes("testing")) {
|
|
291
|
+
steps.push(` - name: Test
|
|
292
|
+
run: ${runCmd("test")}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let e2eJob = "";
|
|
296
|
+
if (c.extras.includes("e2e")) {
|
|
297
|
+
e2eJob = `
|
|
298
|
+
|
|
299
|
+
e2e:
|
|
300
|
+
runs-on: ubuntu-latest
|
|
301
|
+
steps:
|
|
302
|
+
- uses: actions/checkout@v4
|
|
303
|
+
|
|
304
|
+
${setup}
|
|
305
|
+
|
|
306
|
+
- name: Install Playwright browsers
|
|
307
|
+
run: ${c.pmDlxLabel("playwright")} install --with-deps chromium
|
|
308
|
+
|
|
309
|
+
- name: Run E2E tests
|
|
310
|
+
run: ${runCmd("test:e2e")}
|
|
311
|
+
|
|
312
|
+
- uses: actions/upload-artifact@v4
|
|
313
|
+
if: \${{ !cancelled() }}
|
|
314
|
+
with:
|
|
315
|
+
name: playwright-report
|
|
316
|
+
path: playwright-report/
|
|
317
|
+
retention-days: 14`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const fallowJob = c.extras.includes("fallow") ? fallowCiJob() : "";
|
|
321
|
+
|
|
322
|
+
return `name: CI
|
|
323
|
+
|
|
324
|
+
on:
|
|
325
|
+
push:
|
|
326
|
+
branches: [main]
|
|
327
|
+
pull_request:
|
|
328
|
+
|
|
329
|
+
jobs:
|
|
330
|
+
ci:
|
|
331
|
+
runs-on: ubuntu-latest
|
|
332
|
+
steps:
|
|
333
|
+
- uses: actions/checkout@v4
|
|
334
|
+
|
|
335
|
+
${setup}
|
|
336
|
+
|
|
337
|
+
${steps.join("\n\n")}${e2eJob}${fallowJob}
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Sentry
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
/** src/lib/sentry.ts — error monitoring (only activates when a DSN is set) */
|
|
346
|
+
export function sentryInit() {
|
|
347
|
+
return `import * as Sentry from "@sentry/react";
|
|
348
|
+
|
|
349
|
+
const dsn = import.meta.env.VITE_SENTRY_DSN;
|
|
350
|
+
|
|
351
|
+
// Only initialize when a DSN is configured (so local dev without Sentry works).
|
|
352
|
+
if (dsn) {
|
|
353
|
+
Sentry.init({
|
|
354
|
+
dsn,
|
|
355
|
+
sendDefaultPii: true,
|
|
356
|
+
tracesSampleRate: 1.0,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
`;
|
|
360
|
+
}
|