@pylonsync/create-pylon 0.3.16 → 0.3.17

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 (2) hide show
  1. package/bin/create-pylon.js +343 -92
  2. package/package.json +1 -1
@@ -5,10 +5,10 @@
5
5
  * Run via `npm create @pylonsync/pylon@latest [name]` (or yarn/pnpm/bun
6
6
  * create @pylonsync/pylon).
7
7
  *
8
- * Generates a workspace with two packages:
9
- * - api/ — Pylon backend (schema + functions; runs `pylon dev` from
10
- * the @pylonsync/cli npm package, no global binary required)
11
- * - web/ — Next.js 16 + React 19 frontend wired to @pylonsync/react
8
+ * Generates a workspace with three packages under apps/* + packages/*:
9
+ * - apps/api — Pylon backend (schema + functions/* handlers).
10
+ * - apps/web — Next.js 16 + React 19 + Tailwind v4 frontend.
11
+ * - packages/uishared shadcn-style UI primitives consumed by web.
12
12
  *
13
13
  * Node-runnable (no Bun required) so `npm create` works for every
14
14
  * package manager. Uses only Node-builtin APIs — no runtime deps.
@@ -25,7 +25,7 @@ import { stdin, stdout, exit, argv, cwd } from "node:process";
25
25
  // of the pylon stack).
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
- const PYLON_VERSION = "0.3.16";
28
+ const PYLON_VERSION = "0.3.17";
29
29
 
30
30
  // ---------------------------------------------------------------------------
31
31
  // CLI args + interactive prompt
@@ -86,11 +86,11 @@ writeJson("package.json", {
86
86
  name: projectName,
87
87
  private: true,
88
88
  type: "module",
89
- workspaces: ["api", "web"],
89
+ workspaces: ["apps/*", "packages/*"],
90
90
  scripts: {
91
91
  dev: "npm-run-all --parallel dev:api dev:web",
92
- "dev:api": "npm --workspace api run dev",
93
- "dev:web": "npm --workspace web run dev",
92
+ "dev:api": "npm --workspace apps/api run dev",
93
+ "dev:web": "npm --workspace apps/web run dev",
94
94
  build: "npm --workspaces run build --if-present",
95
95
  },
96
96
  devDependencies: {
@@ -107,8 +107,8 @@ out/
107
107
  .env.local
108
108
  *.db
109
109
  *.db-journal
110
- api/pylon.manifest.json
111
- api/pylon.client.ts
110
+ apps/api/pylon.manifest.json
111
+ apps/api/pylon.client.ts
112
112
  `);
113
113
 
114
114
  write(".env.example", `# Backend port the Pylon control plane listens on.
@@ -126,7 +126,33 @@ write(
126
126
  "README.md",
127
127
  `# ${projectName}
128
128
 
129
- Realtime backend + Next.js dashboard, scaffolded by [create-pylon](https://npmjs.com/create-pylon).
129
+ Realtime backend + Next.js dashboard, scaffolded by [@pylonsync/create-pylon](https://npmjs.com/@pylonsync/create-pylon).
130
+
131
+ ## Layout
132
+
133
+ \`\`\`
134
+ apps/
135
+ api/ Pylon backend — schema, policies, function handlers
136
+ schema.ts
137
+ functions/
138
+ listTodos.ts live query handler
139
+ addTodo.ts mutation handler
140
+
141
+ web/ Next.js 16 + React 19 + Tailwind v4 frontend
142
+ src/
143
+ app/
144
+ layout.tsx
145
+ page.tsx server component → fetches initial todos
146
+ components/
147
+ TodoList.tsx client component → optimistic add
148
+ lib/
149
+ pylon.ts cookie-attached fetch helper
150
+
151
+ packages/
152
+ ui/ Shared shadcn-style primitives (Button, Input, etc.)
153
+ src/
154
+ button.tsx, input.tsx, card.tsx, ...
155
+ \`\`\`
130
156
 
131
157
  ## Getting started
132
158
 
@@ -137,31 +163,16 @@ ${flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`}
137
163
 
138
164
  That spins up two processes:
139
165
 
140
- - **api** on http://localhost:4321 — Pylon control plane (schema, queries,
141
- mutations, live sync, auth)
142
- - **web** on http://localhost:3000 — Next.js 16 frontend wired to the API
143
- via [\`@pylonsync/react\`](https://npmjs.com/package/@pylonsync/react)
144
-
145
- ## Project layout
146
-
147
- \`\`\`
148
- api/
149
- schema.ts entities + policies + manifest
150
- functions/ TS query / mutation / action handlers
151
- pylon.manifest.json (codegen — gitignored)
152
- pylon.client.ts (typed client codegen — gitignored)
153
-
154
- web/
155
- src/app/ Next.js app-router pages
156
- src/lib/pylon.ts Pylon server helper (cookie-attached fetches)
157
- \`\`\`
166
+ - **api** on http://localhost:4321 — Pylon control plane
167
+ - **web** on http://localhost:3000 — Next.js frontend wired via
168
+ [\`@pylonsync/next\`](https://npmjs.com/@pylonsync/next)
158
169
 
159
170
  ## What to do next
160
171
 
161
- - Edit \`api/schema.ts\` to add your entities + policies.
162
- - Add TS handlers to \`api/functions/\` — they're auto-discovered.
163
- - Edit \`web/src/app/page.tsx\` it uses the typed client codegen
164
- produced from your manifest.
172
+ - Edit \`apps/api/schema.ts\` to add entities + policies.
173
+ - Add handlers under \`apps/api/functions/\` — auto-discovered by name.
174
+ - Drop new UI primitives into \`packages/ui/src/\`; import them from
175
+ any app via \`import { Button } from "@${projectName}/ui";\`.
165
176
 
166
177
  ## Docs
167
178
 
@@ -170,11 +181,11 @@ web/
170
181
  );
171
182
 
172
183
  // ---------------------------------------------------------------------------
173
- // api/the Pylon control plane
184
+ // apps/api — Pylon backend
174
185
  // ---------------------------------------------------------------------------
175
186
 
176
- writeJson("api/package.json", {
177
- name: `${projectName}-api`,
187
+ writeJson("apps/api/package.json", {
188
+ name: `@${projectName}/api`,
178
189
  version: "0.0.1",
179
190
  private: true,
180
191
  type: "module",
@@ -194,7 +205,7 @@ writeJson("api/package.json", {
194
205
  },
195
206
  });
196
207
 
197
- writeJson("api/tsconfig.json", {
208
+ writeJson("apps/api/tsconfig.json", {
198
209
  compilerOptions: {
199
210
  target: "ES2022",
200
211
  module: "ESNext",
@@ -208,9 +219,18 @@ writeJson("api/tsconfig.json", {
208
219
  include: ["schema.ts", "functions/**/*.ts"],
209
220
  });
210
221
 
222
+ // Schema declares NAMES only — the SDK's query/action/mutation are
223
+ // pure manifest declarations. Handler code lives under functions/*.
211
224
  write(
212
- "api/schema.ts",
213
- `import { entity, field, defineRoute, query, action, policy, buildManifest } from "@pylonsync/sdk";
225
+ "apps/api/schema.ts",
226
+ `import {
227
+ \tentity,
228
+ \tfield,
229
+ \tquery,
230
+ \taction,
231
+ \tpolicy,
232
+ \tbuildManifest,
233
+ } from "@pylonsync/sdk";
214
234
 
215
235
  // ---------------------------------------------------------------------------
216
236
  // Schema
@@ -223,32 +243,18 @@ const Todo = entity("Todo", {
223
243
  });
224
244
 
225
245
  // ---------------------------------------------------------------------------
226
- // Queries / mutations
246
+ // Function declarations — names only. Implementations live under
247
+ // functions/<name>.ts and are auto-discovered by the runtime.
227
248
  // ---------------------------------------------------------------------------
228
249
 
229
- const listTodos = query("listTodos", {
230
- \thandler: \`
231
- \t\tasync (ctx) => {
232
- \t\t\treturn await ctx.db.query("Todo", { $order: { createdAt: "desc" } });
233
- \t\t}
234
- \t\`,
235
- });
250
+ const listTodos = query("listTodos");
236
251
 
237
252
  const addTodo = action("addTodo", {
238
- \targs: { title: { type: "string" } },
239
- \thandler: \`
240
- \t\tasync (ctx, args) => {
241
- \t\t\treturn await ctx.db.insert("Todo", {
242
- \t\t\t\ttitle: args.title,
243
- \t\t\t\tdone: false,
244
- \t\t\t\tcreatedAt: new Date().toISOString(),
245
- \t\t\t});
246
- \t\t}
247
- \t\`,
253
+ \tinput: [{ name: "title", type: "string" }],
248
254
  });
249
255
 
250
256
  // ---------------------------------------------------------------------------
251
- // Policies — wide-open by default. Tighten before production.
257
+ // Policies — wide-open by default. Tighten for production.
252
258
  // ---------------------------------------------------------------------------
253
259
 
254
260
  const todoPolicy = policy({
@@ -261,7 +267,7 @@ const todoPolicy = policy({
261
267
  });
262
268
 
263
269
  // ---------------------------------------------------------------------------
264
- // Manifest — codegen reads this and emits pylon.manifest.json
270
+ // Manifest — pylon codegen reads this and emits pylon.manifest.json
265
271
  // ---------------------------------------------------------------------------
266
272
 
267
273
  export default buildManifest({
@@ -276,12 +282,240 @@ export default buildManifest({
276
282
  `,
277
283
  );
278
284
 
285
+ write(
286
+ "apps/api/functions/listTodos.ts",
287
+ `import { query } from "@pylonsync/functions";
288
+
289
+ /**
290
+ * Live query — every Todo, newest first. The Pylon runtime
291
+ * subscribes the calling client to row-change events so any
292
+ * \`useQuery("Todo")\` consumer auto-refreshes when this list
293
+ * changes.
294
+ */
295
+ export default query({
296
+ \targs: {},
297
+ \tasync handler(ctx) {
298
+ \t\treturn await ctx.db.query("Todo", { $order: { createdAt: "desc" } });
299
+ \t},
300
+ });
301
+ `,
302
+ );
303
+
304
+ write(
305
+ "apps/api/functions/addTodo.ts",
306
+ `import { action, v } from "@pylonsync/functions";
307
+
308
+ /**
309
+ * Insert a new Todo. Runs as an action (not a mutation) so the
310
+ * client can call it via POST /api/fn/addTodo and get the
311
+ * inserted row back synchronously. The change-event broadcast
312
+ * the runtime emits for the insert is what wakes up
313
+ * \`useQuery("Todo")\` consumers without an explicit refetch.
314
+ */
315
+ export default action({
316
+ \targs: { title: v.string() },
317
+ \tasync handler(ctx, args: { title: string }) {
318
+ \t\tconst id = await ctx.db.insert("Todo", {
319
+ \t\t\ttitle: args.title,
320
+ \t\t\tdone: false,
321
+ \t\t\tcreatedAt: new Date().toISOString(),
322
+ \t\t});
323
+ \t\treturn await ctx.db.get("Todo", id);
324
+ \t},
325
+ });
326
+ `,
327
+ );
328
+
279
329
  // ---------------------------------------------------------------------------
280
- // web/ — Next.js 16 + React 19 + Tailwind v4 + @pylonsync/react
330
+ // packages/uishared shadcn-style primitives
281
331
  // ---------------------------------------------------------------------------
282
332
 
283
- writeJson("web/package.json", {
284
- name: `${projectName}-web`,
333
+ writeJson("packages/ui/package.json", {
334
+ name: `@${projectName}/ui`,
335
+ version: "0.0.1",
336
+ private: true,
337
+ type: "module",
338
+ main: "src/index.ts",
339
+ types: "src/index.ts",
340
+ exports: {
341
+ ".": "./src/index.ts",
342
+ "./button": "./src/button.tsx",
343
+ "./input": "./src/input.tsx",
344
+ "./card": "./src/card.tsx",
345
+ "./cn": "./src/cn.ts",
346
+ },
347
+ dependencies: {
348
+ clsx: "^2.1.0",
349
+ "tailwind-merge": "^2.5.0",
350
+ },
351
+ peerDependencies: {
352
+ react: "^19.0.0",
353
+ },
354
+ devDependencies: {
355
+ "@types/react": "^19.0.0",
356
+ typescript: "^5.5.0",
357
+ },
358
+ });
359
+
360
+ writeJson("packages/ui/tsconfig.json", {
361
+ compilerOptions: {
362
+ target: "ES2022",
363
+ lib: ["dom", "esnext"],
364
+ jsx: "preserve",
365
+ module: "ESNext",
366
+ moduleResolution: "Bundler",
367
+ strict: true,
368
+ skipLibCheck: true,
369
+ noEmit: true,
370
+ esModuleInterop: true,
371
+ allowSyntheticDefaultImports: true,
372
+ },
373
+ include: ["src/**/*.ts", "src/**/*.tsx"],
374
+ });
375
+
376
+ write(
377
+ "packages/ui/src/cn.ts",
378
+ `import { clsx, type ClassValue } from "clsx";
379
+ import { twMerge } from "tailwind-merge";
380
+
381
+ /**
382
+ * Tailwind-aware class merger. Last-class-wins semantics so a
383
+ * caller's \`className\` reliably overrides a default in a UI
384
+ * primitive (e.g. <Button className="bg-red-500"> beats the
385
+ * primitive's bg-neutral-900 base).
386
+ */
387
+ export function cn(...inputs: ClassValue[]): string {
388
+ \treturn twMerge(clsx(inputs));
389
+ }
390
+ `,
391
+ );
392
+
393
+ write(
394
+ "packages/ui/src/button.tsx",
395
+ `import * as React from "react";
396
+ import { cn } from "./cn";
397
+
398
+ type Variant = "default" | "primary" | "ghost";
399
+ type Size = "sm" | "md";
400
+
401
+ const variants: Record<Variant, string> = {
402
+ \tdefault:
403
+ \t\t"bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:text-neutral-100",
404
+ \tprimary:
405
+ \t\t"bg-neutral-900 hover:bg-neutral-800 text-white dark:bg-white dark:hover:bg-neutral-200 dark:text-neutral-900",
406
+ \tghost:
407
+ \t\t"bg-transparent hover:bg-neutral-100 text-neutral-700 dark:hover:bg-neutral-800 dark:text-neutral-300",
408
+ };
409
+
410
+ const sizes: Record<Size, string> = {
411
+ \tsm: "h-8 px-3 text-[13px]",
412
+ \tmd: "h-9 px-4 text-sm",
413
+ };
414
+
415
+ export interface ButtonProps
416
+ \textends React.ButtonHTMLAttributes<HTMLButtonElement> {
417
+ \tvariant?: Variant;
418
+ \tsize?: Size;
419
+ }
420
+
421
+ export function Button({
422
+ \tclassName,
423
+ \tvariant = "default",
424
+ \tsize = "md",
425
+ \t...props
426
+ }: ButtonProps) {
427
+ \treturn (
428
+ \t\t<button
429
+ \t\t\tclassName={cn(
430
+ \t\t\t\t"inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
431
+ \t\t\t\tvariants[variant],
432
+ \t\t\t\tsizes[size],
433
+ \t\t\t\tclassName,
434
+ \t\t\t)}
435
+ \t\t\t{...props}
436
+ \t\t/>
437
+ \t);
438
+ }
439
+ `,
440
+ );
441
+
442
+ write(
443
+ "packages/ui/src/input.tsx",
444
+ `import * as React from "react";
445
+ import { cn } from "./cn";
446
+
447
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
448
+
449
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
450
+ \tfunction Input({ className, ...props }, ref) {
451
+ \t\treturn (
452
+ \t\t\t<input
453
+ \t\t\t\tref={ref}
454
+ \t\t\t\tclassName={cn(
455
+ \t\t\t\t\t"flex h-9 w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-50",
456
+ \t\t\t\t\tclassName,
457
+ \t\t\t\t)}
458
+ \t\t\t\t{...props}
459
+ \t\t\t/>
460
+ \t\t);
461
+ \t},
462
+ );
463
+ `,
464
+ );
465
+
466
+ write(
467
+ "packages/ui/src/card.tsx",
468
+ `import * as React from "react";
469
+ import { cn } from "./cn";
470
+
471
+ export function Card({
472
+ \tclassName,
473
+ \t...props
474
+ }: React.HTMLAttributes<HTMLDivElement>) {
475
+ \treturn (
476
+ \t\t<div
477
+ \t\t\tclassName={cn(
478
+ \t\t\t\t"rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900",
479
+ \t\t\t\tclassName,
480
+ \t\t\t)}
481
+ \t\t\t{...props}
482
+ \t\t/>
483
+ \t);
484
+ }
485
+
486
+ export function CardHeader({
487
+ \tclassName,
488
+ \t...props
489
+ }: React.HTMLAttributes<HTMLDivElement>) {
490
+ \treturn (
491
+ \t\t<div className={cn("p-5 border-b border-neutral-200 dark:border-neutral-800", className)} {...props} />
492
+ \t);
493
+ }
494
+
495
+ export function CardContent({
496
+ \tclassName,
497
+ \t...props
498
+ }: React.HTMLAttributes<HTMLDivElement>) {
499
+ \treturn <div className={cn("p-5", className)} {...props} />;
500
+ }
501
+ `,
502
+ );
503
+
504
+ write(
505
+ "packages/ui/src/index.ts",
506
+ `export { cn } from "./cn";
507
+ export { Button, type ButtonProps } from "./button";
508
+ export { Input, type InputProps } from "./input";
509
+ export { Card, CardHeader, CardContent } from "./card";
510
+ `,
511
+ );
512
+
513
+ // ---------------------------------------------------------------------------
514
+ // apps/web — Next.js 16 + React 19 + Tailwind v4
515
+ // ---------------------------------------------------------------------------
516
+
517
+ writeJson("apps/web/package.json", {
518
+ name: `@${projectName}/web`,
285
519
  version: "0.0.1",
286
520
  private: true,
287
521
  type: "module",
@@ -292,6 +526,7 @@ writeJson("web/package.json", {
292
526
  lint: "next lint",
293
527
  },
294
528
  dependencies: {
529
+ [`@${projectName}/ui`]: "workspace:*",
295
530
  "@pylonsync/sdk": `^${PYLON_VERSION}`,
296
531
  "@pylonsync/react": `^${PYLON_VERSION}`,
297
532
  "@pylonsync/next": `^${PYLON_VERSION}`,
@@ -300,16 +535,16 @@ writeJson("web/package.json", {
300
535
  "react-dom": "^19.0.0",
301
536
  },
302
537
  devDependencies: {
538
+ "@types/node": "^20.0.0",
303
539
  "@types/react": "^19.0.0",
304
540
  "@types/react-dom": "^19.0.0",
305
- "@types/node": "^20.0.0",
306
541
  "@tailwindcss/postcss": "^4.0.0",
307
542
  tailwindcss: "^4.0.0",
308
543
  typescript: "^5.5.0",
309
544
  },
310
545
  });
311
546
 
312
- writeJson("web/tsconfig.json", {
547
+ writeJson("apps/web/tsconfig.json", {
313
548
  compilerOptions: {
314
549
  target: "ES2022",
315
550
  lib: ["dom", "dom.iterable", "esnext"],
@@ -332,16 +567,17 @@ writeJson("web/tsconfig.json", {
332
567
  });
333
568
 
334
569
  write(
335
- "web/next.config.ts",
570
+ "apps/web/next.config.ts",
336
571
  `import type { NextConfig } from "next";
337
572
 
338
573
  /**
339
574
  * Pylon's typed client + functions packages re-export across the
340
- * server/client boundary; \`transpilePackages\` makes Next bundle them
341
- * cleanly from the workspace.
575
+ * server/client boundary AND the workspace UI package ships TSX.
576
+ * \`transpilePackages\` makes Next bundle them cleanly.
342
577
  */
343
578
  const config: NextConfig = {
344
579
  \ttranspilePackages: [
580
+ \t\t"@${projectName}/ui",
345
581
  \t\t"@pylonsync/sdk",
346
582
  \t\t"@pylonsync/react",
347
583
  \t\t"@pylonsync/next",
@@ -355,7 +591,7 @@ export default config;
355
591
  );
356
592
 
357
593
  write(
358
- "web/postcss.config.mjs",
594
+ "apps/web/postcss.config.mjs",
359
595
  `/** Tailwind v4 PostCSS pipeline. */
360
596
  export default {
361
597
  \tplugins: { "@tailwindcss/postcss": {} },
@@ -364,8 +600,9 @@ export default {
364
600
  );
365
601
 
366
602
  write(
367
- "web/src/app/globals.css",
603
+ "apps/web/src/app/globals.css",
368
604
  `@import "tailwindcss";
605
+ @source "../../../../packages/ui/src/**/*.{ts,tsx}";
369
606
 
370
607
  :root {
371
608
  \tcolor-scheme: light dark;
@@ -377,7 +614,7 @@ body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
377
614
  );
378
615
 
379
616
  write(
380
- "web/src/app/layout.tsx",
617
+ "apps/web/src/app/layout.tsx",
381
618
  `import type { Metadata } from "next";
382
619
  import "./globals.css";
383
620
 
@@ -403,7 +640,7 @@ export default function RootLayout({
403
640
  );
404
641
 
405
642
  write(
406
- "web/src/lib/pylon.ts",
643
+ "apps/web/src/lib/pylon.ts",
407
644
  `import { createPylonServer } from "@pylonsync/next/server";
408
645
 
409
646
  /**
@@ -422,9 +659,9 @@ export const pylon = createPylonServer({
422
659
  );
423
660
 
424
661
  write(
425
- "web/src/app/page.tsx",
662
+ "apps/web/src/app/page.tsx",
426
663
  `import { pylon } from "@/lib/pylon";
427
- import { TodoList } from "./TodoList";
664
+ import { TodoList } from "./components/TodoList";
428
665
 
429
666
  // Force dynamic — every render reads the live todo list from Pylon.
430
667
  // Without this Next would try to statically generate the page and
@@ -440,7 +677,11 @@ type Todo = {
440
677
 
441
678
  export default async function HomePage() {
442
679
  \tconst todos = await pylon
443
- \t\t.json<Todo[]>("/api/fn/listTodos", { method: "POST", body: "{}", headers: { "Content-Type": "application/json" } })
680
+ \t\t.json<Todo[]>("/api/fn/listTodos", {
681
+ \t\t\tmethod: "POST",
682
+ \t\t\tbody: "{}",
683
+ \t\t\theaders: { "Content-Type": "application/json" },
684
+ \t\t})
444
685
  \t\t.catch(() => [] as Todo[]);
445
686
 
446
687
  \treturn (
@@ -449,10 +690,14 @@ export default async function HomePage() {
449
690
  \t\t\t\t<h1 className="text-3xl font-semibold tracking-tight">${projectName}</h1>
450
691
  \t\t\t\t<p className="text-sm text-neutral-500 dark:text-neutral-400">
451
692
  \t\t\t\t\tA Pylon-powered realtime app. Edit{" "}
452
- \t\t\t\t\t<code className="font-mono text-xs">api/schema.ts</code> to change the
453
- \t\t\t\t\tdata model or{" "}
454
- \t\t\t\t\t<code className="font-mono text-xs">web/src/app/page.tsx</code> for
455
- \t\t\t\t\tthe UI.
693
+ \t\t\t\t\t<code className="font-mono text-xs">apps/api/schema.ts</code> to change
694
+ \t\t\t\t\tthe data model,{" "}
695
+ \t\t\t\t\t<code className="font-mono text-xs">apps/api/functions/</code> to add
696
+ \t\t\t\t\thandlers, or{" "}
697
+ \t\t\t\t\t<code className="font-mono text-xs">
698
+ \t\t\t\t\t\tapps/web/src/app/components/TodoList.tsx
699
+ \t\t\t\t\t</code>{" "}
700
+ \t\t\t\t\tfor the UI.
456
701
  \t\t\t\t</p>
457
702
  \t\t\t</header>
458
703
 
@@ -464,10 +709,12 @@ export default async function HomePage() {
464
709
  );
465
710
 
466
711
  write(
467
- "web/src/app/TodoList.tsx",
712
+ "apps/web/src/app/components/TodoList.tsx",
468
713
  `"use client";
469
714
 
470
715
  import { useState, useTransition } from "react";
716
+ import { Button } from "@${projectName}/ui";
717
+ import { Input } from "@${projectName}/ui";
471
718
 
472
719
  type Todo = {
473
720
  \tid: string;
@@ -478,9 +725,9 @@ type Todo = {
478
725
 
479
726
  /**
480
727
  * Optimistic todo list — local state mirrors the server-fetched
481
- * initial list and refreshes on every successful add. For full
482
- * real-time updates wire \`@pylonsync/react\`'s \`useQuery\` hook
483
- * (see https://pylonsync.com/docs/clients/react).
728
+ * initial list and prepends new rows on successful add. Wire
729
+ * \`@pylonsync/react\`'s \`useQuery\` hook for full realtime updates
730
+ * that re-render on every change-event push.
484
731
  */
485
732
  export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
486
733
  \tconst [todos, setTodos] = useState(initialTodos);
@@ -513,20 +760,20 @@ export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
513
760
  \t\t\t\t}}
514
761
  \t\t\t\tclassName="flex gap-2"
515
762
  \t\t\t>
516
- \t\t\t\t<input
763
+ \t\t\t\t<Input
517
764
  \t\t\t\t\tvalue={title}
518
765
  \t\t\t\t\tonChange={(e) => setTitle(e.target.value)}
519
766
  \t\t\t\t\tplaceholder="What needs doing?"
520
- \t\t\t\t\tclassName="flex-1 rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
521
767
  \t\t\t\t\tdisabled={pending}
768
+ \t\t\t\t\tclassName="flex-1"
522
769
  \t\t\t\t/>
523
- \t\t\t\t<button
770
+ \t\t\t\t<Button
524
771
  \t\t\t\t\ttype="submit"
525
- \t\t\t\t\tclassName="rounded-md bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 px-4 py-2 text-sm font-medium disabled:opacity-50"
772
+ \t\t\t\t\tvariant="primary"
526
773
  \t\t\t\t\tdisabled={pending || !title.trim()}
527
774
  \t\t\t\t>
528
775
  \t\t\t\t\tAdd
529
- \t\t\t\t</button>
776
+ \t\t\t\t</Button>
530
777
  \t\t\t</form>
531
778
 
532
779
  \t\t\t{todos.length === 0 ? (
@@ -554,7 +801,7 @@ export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
554
801
  );
555
802
 
556
803
  write(
557
- "web/next-env.d.ts",
804
+ "apps/web/next-env.d.ts",
558
805
  `/// <reference types="next" />
559
806
  /// <reference types="next/image-types/global" />
560
807
  `,
@@ -605,11 +852,15 @@ console.log(`
605
852
  → api http://localhost:4321 (Pylon control plane)
606
853
  → web http://localhost:3000 (Next.js dashboard)
607
854
 
855
+ Layout:
856
+ apps/api schema + functions/ handlers
857
+ apps/web Next.js 16 + React 19 + Tailwind v4
858
+ packages/ui shared shadcn-style primitives
859
+
608
860
  Next:
609
- - Edit api/schema.ts to add entities + policies.
610
- - Drop TypeScript handlers into api/functions/ — auto-discovered.
611
- - The Next page at web/src/app/page.tsx talks to the API via the
612
- cookie-attached helper in web/src/lib/pylon.ts.
861
+ - Edit apps/api/schema.ts to add entities + policies.
862
+ - Drop handlers into apps/api/functions/ — auto-discovered by name.
863
+ - Components go in apps/web/src/app/components/.
613
864
 
614
865
  Docs: https://pylonsync.com/docs
615
866
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + Next.js frontend in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"