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.
@@ -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
+ }