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,463 @@
1
+ // Feature extras: Stripe, Resend/React Email, PostHog, i18n, PWA, deploy configs,
2
+ // Recharts, GSAP, Tiptap, Leaflet.
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Stripe (payments)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export function stripeLib() {
9
+ return `import { loadStripe } from "@stripe/stripe-js";
10
+
11
+ // Stripe.js client — safe for the browser (publishable key only).
12
+ // Server-side operations (checkout sessions, webhooks) need STRIPE_SECRET_KEY
13
+ // and must run on your backend. See the README's Stripe section.
14
+ export const stripePromise = loadStripe(
15
+ import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY,
16
+ );
17
+ `;
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Resend + React Email
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export function welcomeEmail(c) {
25
+ return `import {
26
+ Body,
27
+ Button,
28
+ Container,
29
+ Head,
30
+ Heading,
31
+ Html,
32
+ Preview,
33
+ Text,
34
+ } from "@react-email/components";
35
+
36
+ // Edit this template, then send it server-side with Resend:
37
+ //
38
+ // import { Resend } from "resend";
39
+ // const resend = new Resend(process.env.RESEND_API_KEY);
40
+ // await resend.emails.send({
41
+ // from: "you@yourdomain.com",
42
+ // to: user.email,
43
+ // subject: "Welcome!",
44
+ // react: <WelcomeEmail name={user.name} />,
45
+ // });
46
+ export function WelcomeEmail({ name = "there" }: { name?: string }) {
47
+ return (
48
+ <Html>
49
+ <Head />
50
+ <Preview>Welcome to ${c.name}</Preview>
51
+ <Body style={{ backgroundColor: "#f4f4f5", fontFamily: "sans-serif" }}>
52
+ <Container
53
+ style={{
54
+ backgroundColor: "#ffffff",
55
+ borderRadius: "8px",
56
+ margin: "40px auto",
57
+ maxWidth: "480px",
58
+ padding: "32px",
59
+ }}
60
+ >
61
+ <Heading style={{ fontSize: "22px" }}>Welcome, {name}!</Heading>
62
+ <Text style={{ color: "#52525b" }}>
63
+ Your ${c.name} account is ready. Jump back in to get started.
64
+ </Text>
65
+ <Button
66
+ href="http://localhost:5173"
67
+ style={{
68
+ backgroundColor: "#18181b",
69
+ borderRadius: "6px",
70
+ color: "#ffffff",
71
+ display: "inline-block",
72
+ padding: "12px 20px",
73
+ }}
74
+ >
75
+ Open the app
76
+ </Button>
77
+ </Container>
78
+ </Body>
79
+ </Html>
80
+ );
81
+ }
82
+ `;
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // PostHog (product analytics)
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export function posthogInit() {
90
+ return `import posthog from "posthog-js";
91
+
92
+ const key = import.meta.env.VITE_POSTHOG_KEY;
93
+
94
+ // Only initialize when a key is configured (so local dev without PostHog works).
95
+ if (key) {
96
+ posthog.init(key, {
97
+ api_host: import.meta.env.VITE_POSTHOG_HOST || "https://us.i.posthog.com",
98
+ defaults: "2025-05-24",
99
+ });
100
+ }
101
+
102
+ export { posthog };
103
+ `;
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // i18n (react-i18next)
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export function i18nSetup() {
111
+ return `import i18n from "i18next";
112
+ import { initReactI18next } from "react-i18next";
113
+
114
+ // Add languages and translations here. Usage in components:
115
+ // const { t, i18n } = useTranslation();
116
+ // <h1>{t("home.title")}</h1>
117
+ // i18n.changeLanguage("es")
118
+ const resources = {
119
+ en: {
120
+ translation: {
121
+ home: {
122
+ title: "Welcome",
123
+ subtitle: "Your stack is wired up and ready to build.",
124
+ },
125
+ },
126
+ },
127
+ es: {
128
+ translation: {
129
+ home: {
130
+ title: "Bienvenido",
131
+ subtitle: "Tu stack está configurado y listo para construir.",
132
+ },
133
+ },
134
+ },
135
+ };
136
+
137
+ void i18n.use(initReactI18next).init({
138
+ resources,
139
+ lng: "en",
140
+ fallbackLng: "en",
141
+ interpolation: {
142
+ escapeValue: false, // React already escapes
143
+ },
144
+ });
145
+
146
+ export default i18n;
147
+ `;
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Deployment configs
152
+ // ---------------------------------------------------------------------------
153
+
154
+ export function dockerfile(c) {
155
+ const build = {
156
+ bun: `FROM oven/bun:1 AS build
157
+ WORKDIR /app
158
+ COPY package.json bun.lock* ./
159
+ RUN bun install
160
+ COPY . .
161
+ RUN bun run build`,
162
+ pnpm: `FROM node:22-alpine AS build
163
+ WORKDIR /app
164
+ RUN corepack enable
165
+ COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
166
+ RUN pnpm install
167
+ COPY . .
168
+ RUN pnpm run build`,
169
+ npm: `FROM node:22-alpine AS build
170
+ WORKDIR /app
171
+ COPY package.json package-lock.json* .npmrc* ./
172
+ RUN npm install
173
+ COPY . .
174
+ RUN npm run build`,
175
+ }[c.pm];
176
+
177
+ return `# Multi-stage build: compile the app, then serve the static files with nginx.
178
+ # docker build -t ${c.name} .
179
+ # docker run -p 8080:80 ${c.name}
180
+ ${build}
181
+
182
+ FROM nginx:alpine
183
+ COPY --from=build /app/dist /usr/share/nginx/html
184
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
185
+ EXPOSE 80
186
+ CMD ["nginx", "-g", "daemon off;"]
187
+ `;
188
+ }
189
+
190
+ export function nginxConf() {
191
+ return `server {
192
+ listen 80;
193
+ root /usr/share/nginx/html;
194
+ index index.html;
195
+
196
+ # Single-page app: route everything that isn't a file to index.html
197
+ location / {
198
+ try_files $uri $uri/ /index.html;
199
+ }
200
+
201
+ # Cache hashed assets aggressively
202
+ location /assets/ {
203
+ expires 1y;
204
+ add_header Cache-Control "public, immutable";
205
+ }
206
+ }
207
+ `;
208
+ }
209
+
210
+ export function dockerignore() {
211
+ return `node_modules
212
+ dist
213
+ .git
214
+ .env
215
+ .env.*
216
+ *.log
217
+ e2e
218
+ test-results
219
+ playwright-report
220
+ `;
221
+ }
222
+
223
+ export function vercelJson() {
224
+ return JSON.stringify(
225
+ {
226
+ $schema: "https://openapi.vercel.sh/vercel.json",
227
+ rewrites: [{ source: "/(.*)", destination: "/index.html" }],
228
+ },
229
+ null,
230
+ 2,
231
+ );
232
+ }
233
+
234
+ export function netlifyToml() {
235
+ return `[build]
236
+ publish = "dist"
237
+
238
+ # Single-page app: route everything to index.html
239
+ [[redirects]]
240
+ from = "/*"
241
+ to = "/index.html"
242
+ status = 200
243
+ `;
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Recharts (charts demo)
248
+ // ---------------------------------------------------------------------------
249
+
250
+ export function chartDemo() {
251
+ return `import {
252
+ Area,
253
+ AreaChart,
254
+ CartesianGrid,
255
+ ResponsiveContainer,
256
+ Tooltip,
257
+ XAxis,
258
+ YAxis,
259
+ } from "recharts";
260
+ import {
261
+ Card,
262
+ CardContent,
263
+ CardDescription,
264
+ CardHeader,
265
+ CardTitle,
266
+ } from "@/components/ui/card";
267
+
268
+ const data = [
269
+ { month: "Jan", users: 120 },
270
+ { month: "Feb", users: 240 },
271
+ { month: "Mar", users: 380 },
272
+ { month: "Apr", users: 470 },
273
+ { month: "May", users: 690 },
274
+ { month: "Jun", users: 940 },
275
+ ];
276
+
277
+ export function ChartDemo() {
278
+ return (
279
+ <Card>
280
+ <CardHeader>
281
+ <CardTitle>Recharts</CardTitle>
282
+ <CardDescription>
283
+ Edit <code>src/components/chart-demo.tsx</code> to chart your own data.
284
+ </CardDescription>
285
+ </CardHeader>
286
+ <CardContent>
287
+ <div className="h-64 w-full">
288
+ <ResponsiveContainer width="100%" height="100%">
289
+ <AreaChart data={data} margin={{ top: 4, right: 4, bottom: 0, left: -20 }}>
290
+ <CartesianGrid strokeDasharray="3 3" className="stroke-border" />
291
+ <XAxis dataKey="month" fontSize={12} tickLine={false} axisLine={false} />
292
+ <YAxis fontSize={12} tickLine={false} axisLine={false} />
293
+ <Tooltip />
294
+ <Area
295
+ type="monotone"
296
+ dataKey="users"
297
+ stroke="var(--color-primary)"
298
+ fill="var(--color-primary)"
299
+ fillOpacity={0.15}
300
+ />
301
+ </AreaChart>
302
+ </ResponsiveContainer>
303
+ </div>
304
+ </CardContent>
305
+ </Card>
306
+ );
307
+ }
308
+ `;
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // GSAP (animation demo)
313
+ // ---------------------------------------------------------------------------
314
+
315
+ export function gsapDemo() {
316
+ return `import { useRef } from "react";
317
+ import gsap from "gsap";
318
+ import { useGSAP } from "@gsap/react";
319
+ import { Button } from "@/components/ui/button";
320
+ import {
321
+ Card,
322
+ CardContent,
323
+ CardDescription,
324
+ CardHeader,
325
+ CardTitle,
326
+ } from "@/components/ui/card";
327
+
328
+ gsap.registerPlugin(useGSAP);
329
+
330
+ export function GsapDemo() {
331
+ const container = useRef<HTMLDivElement>(null);
332
+
333
+ const { contextSafe } = useGSAP({ scope: container });
334
+
335
+ const replay = contextSafe(() => {
336
+ gsap.fromTo(
337
+ ".gsap-box",
338
+ { y: 24, opacity: 0, scale: 0.8 },
339
+ { y: 0, opacity: 1, scale: 1, stagger: 0.08, ease: "back.out(1.7)" },
340
+ );
341
+ });
342
+
343
+ useGSAP(
344
+ () => {
345
+ replay();
346
+ },
347
+ { scope: container },
348
+ );
349
+
350
+ return (
351
+ <Card>
352
+ <CardHeader>
353
+ <CardTitle>GSAP</CardTitle>
354
+ <CardDescription>
355
+ Staggered entrance animation — see <code>src/components/gsap-demo.tsx</code>.
356
+ </CardDescription>
357
+ </CardHeader>
358
+ <CardContent ref={container} className="flex items-center gap-4">
359
+ <div className="flex gap-2">
360
+ {[0, 1, 2, 3, 4].map((i) => (
361
+ <div key={i} className="gsap-box bg-primary size-8 rounded-md" />
362
+ ))}
363
+ </div>
364
+ <Button variant="outline" size="sm" onClick={replay}>
365
+ Replay
366
+ </Button>
367
+ </CardContent>
368
+ </Card>
369
+ );
370
+ }
371
+ `;
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Tiptap (rich text editor demo)
376
+ // ---------------------------------------------------------------------------
377
+
378
+ export function editorDemo() {
379
+ return `import { EditorContent, useEditor } from "@tiptap/react";
380
+ import StarterKit from "@tiptap/starter-kit";
381
+ import {
382
+ Card,
383
+ CardContent,
384
+ CardDescription,
385
+ CardHeader,
386
+ CardTitle,
387
+ } from "@/components/ui/card";
388
+
389
+ export function EditorDemo() {
390
+ const editor = useEditor({
391
+ extensions: [StarterKit],
392
+ content: "<p>A rich text editor powered by <strong>Tiptap</strong>. Try <em>bold</em>, lists, headings…</p>",
393
+ editorProps: {
394
+ attributes: {
395
+ class:
396
+ "min-h-28 rounded-md border px-3 py-2 text-sm focus:outline-none [&_p]:my-2 [&_h1]:text-xl [&_h1]:font-bold [&_h2]:text-lg [&_h2]:font-semibold [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_blockquote]:border-l-2 [&_blockquote]:pl-3",
397
+ },
398
+ },
399
+ });
400
+
401
+ return (
402
+ <Card>
403
+ <CardHeader>
404
+ <CardTitle>Tiptap editor</CardTitle>
405
+ <CardDescription>
406
+ Headless rich text editing — see <code>src/components/editor-demo.tsx</code>.
407
+ </CardDescription>
408
+ </CardHeader>
409
+ <CardContent>
410
+ <EditorContent editor={editor} />
411
+ </CardContent>
412
+ </Card>
413
+ );
414
+ }
415
+ `;
416
+ }
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // Leaflet (maps demo)
420
+ // ---------------------------------------------------------------------------
421
+
422
+ export function mapDemo() {
423
+ return `import "leaflet/dist/leaflet.css";
424
+ import { CircleMarker, MapContainer, Popup, TileLayer } from "react-leaflet";
425
+ import {
426
+ Card,
427
+ CardContent,
428
+ CardDescription,
429
+ CardHeader,
430
+ CardTitle,
431
+ } from "@/components/ui/card";
432
+
433
+ export function MapDemo() {
434
+ return (
435
+ <Card>
436
+ <CardHeader>
437
+ <CardTitle>Leaflet map</CardTitle>
438
+ <CardDescription>
439
+ OpenStreetMap tiles, no API key needed — see{" "}
440
+ <code>src/components/map-demo.tsx</code>.
441
+ </CardDescription>
442
+ </CardHeader>
443
+ <CardContent>
444
+ <MapContainer
445
+ center={[51.505, -0.09]}
446
+ zoom={13}
447
+ scrollWheelZoom={false}
448
+ className="h-64 w-full rounded-md"
449
+ >
450
+ <TileLayer
451
+ attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
452
+ url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
453
+ />
454
+ <CircleMarker center={[51.505, -0.09]} radius={10}>
455
+ <Popup>Hello from Leaflet!</Popup>
456
+ </CircleMarker>
457
+ </MapContainer>
458
+ </CardContent>
459
+ </Card>
460
+ );
461
+ }
462
+ `;
463
+ }
@@ -0,0 +1,159 @@
1
+ // Quality tooling: Playwright E2E, MSW API mocking, Fallow codebase intelligence.
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Fallow (Rust-native codebase intelligence: dead code, duplication,
5
+ // complexity, health score, PR audit — supersedes Knip)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /** .fallowrc.json — tuned so a fresh starter passes its own quality gate. */
9
+ export function fallowConfig(c) {
10
+ // Generated files: never analyze
11
+ const ignorePatterns = [];
12
+ if (c.router === "tanstack") ignorePatterns.push("src/routeTree.gen.ts");
13
+ if (c.backend === "convex") ignorePatterns.push("convex/_generated/**");
14
+ if (c.orm === "prisma") ignorePatterns.push("src/generated/**");
15
+ // shadcn/ui is a vendored component library — its exports are intentional surface
16
+ ignorePatterns.push("src/components/ui/**");
17
+
18
+ // Scaffolding the user imports later + files loaded by tools (not import graphs)
19
+ const entry = [];
20
+ if (c.orm === "drizzle") entry.push("db/index.ts");
21
+ if (c.extras.includes("redis")) entry.push("db/redis.ts", "db/ratelimit.ts");
22
+ if (c.extras.includes("testing")) entry.push("src/test/setup.ts");
23
+ if (c.extras.includes("resend")) entry.push("src/emails/*.tsx");
24
+ if (c.extras.includes("motion")) entry.push("src/components/fade-in.tsx");
25
+
26
+ // Install-only extras: deps the user will import once they build features
27
+ const ignoreDependencies = [];
28
+ if (c.extras.includes("forms")) ignoreDependencies.push("react-hook-form", "@hookform/resolvers");
29
+ if (c.extras.includes("dates")) ignoreDependencies.push("date-fns");
30
+ if (c.extras.includes("stripe")) ignoreDependencies.push("@stripe/stripe-js");
31
+ if (c.extras.includes("resend")) ignoreDependencies.push("resend");
32
+
33
+ const jsonList = (arr) => arr.map((p) => JSON.stringify(p)).join(", ");
34
+
35
+ return `{
36
+ "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
37
+ "entry": [${jsonList(entry)}],
38
+ "ignorePatterns": [${jsonList(ignorePatterns)}],
39
+ "ignoreDependencies": [${jsonList(ignoreDependencies)}],
40
+ "rules": {
41
+ "unused-files": "error",
42
+ "unused-exports": "warn",
43
+ "unused-dependencies": "warn"
44
+ }
45
+ }
46
+ `;
47
+ }
48
+
49
+ /** Extra job appended to the CI workflow when Fallow + CI are both selected. */
50
+ export function fallowCiJob() {
51
+ return `
52
+
53
+ quality:
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ contents: read
57
+ pull-requests: write
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ with:
61
+ fetch-depth: 0 # full history for changed-code attribution
62
+
63
+ # Fallow: dead code, duplication, complexity and PR risk audit.
64
+ # Posts a sticky summary comment + inline review comments on PRs.
65
+ - uses: fallow-rs/fallow@v2
66
+ with:
67
+ command: audit
68
+ comment: true
69
+ review-comments: true`;
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Playwright (E2E)
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export function playwrightConfig(c) {
77
+ // For Convex/Hono backends the dev script starts multiple servers;
78
+ // E2E only needs the frontend.
79
+ const devScript =
80
+ c.backend === "convex" ? "dev:frontend" : c.backend === "hono" ? "dev:web" : "dev";
81
+ return `import { defineConfig, devices } from "@playwright/test";
82
+
83
+ export default defineConfig({
84
+ testDir: "./e2e",
85
+ fullyParallel: true,
86
+ forbidOnly: !!process.env.CI,
87
+ retries: process.env.CI ? 2 : 0,
88
+ reporter: "html",
89
+ use: {
90
+ baseURL: "http://localhost:5173",
91
+ trace: "on-first-retry",
92
+ },
93
+ projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
94
+ webServer: {
95
+ command: "${c.pmRunLabel(devScript)}",
96
+ url: "http://localhost:5173",
97
+ reuseExistingServer: !process.env.CI,
98
+ },
99
+ });
100
+ `;
101
+ }
102
+
103
+ export function playwrightExampleTest(c) {
104
+ return `import { expect, test } from "@playwright/test";
105
+
106
+ test("home page renders the app", async ({ page }) => {
107
+ await page.goto("/");
108
+ await expect(page.getByRole("heading", { level: 1 })).toContainText("${c.name}");
109
+ });
110
+
111
+ test("theme toggle switches dark mode", async ({ page }) => {
112
+ await page.goto("/");
113
+ await page.getByRole("button", { name: "Toggle theme" }).click();
114
+ await expect(page.locator("html")).toHaveClass(/dark/);
115
+ });
116
+ `;
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // MSW (Mock Service Worker)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export function mswHandlers() {
124
+ return `import { HttpResponse, http } from "msw";
125
+
126
+ // Add request handlers here — they intercept fetch/XHR in tests.
127
+ // Docs: https://mswjs.io/docs/basics/mocking-responses
128
+ export const handlers = [
129
+ http.get("/api/example", () => {
130
+ return HttpResponse.json({ message: "Hello from MSW!" });
131
+ }),
132
+ ];
133
+ `;
134
+ }
135
+
136
+ export function mswServer() {
137
+ return `import { setupServer } from "msw/node";
138
+ import { handlers } from "./handlers";
139
+
140
+ export const server = setupServer(...handlers);
141
+ `;
142
+ }
143
+
144
+ /** src/test/setup.ts — composed based on whether MSW is enabled. */
145
+ export function testSetup(c) {
146
+ if (!c.extras.includes("msw")) {
147
+ return `import "@testing-library/jest-dom/vitest";
148
+ `;
149
+ }
150
+ return `import "@testing-library/jest-dom/vitest";
151
+ import { afterAll, afterEach, beforeAll } from "vitest";
152
+ import { server } from "./mocks/server";
153
+
154
+ // Start MSW so tests can mock network requests (see src/test/mocks/handlers.ts)
155
+ beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
156
+ afterEach(() => server.resetHandlers());
157
+ afterAll(() => server.close());
158
+ `;
159
+ }