@pylonsync/create-pylon 0.3.16 → 0.3.18
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/bin/create-pylon.js +343 -92
- package/package.json +1 -1
package/bin/create-pylon.js
CHANGED
|
@@ -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
|
|
9
|
-
* - api
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
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/ui — shared 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.
|
|
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: ["
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
162
|
-
- Add
|
|
163
|
-
-
|
|
164
|
-
|
|
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
|
|
184
|
+
// apps/api — Pylon backend
|
|
174
185
|
// ---------------------------------------------------------------------------
|
|
175
186
|
|
|
176
|
-
writeJson("api/package.json", {
|
|
177
|
-
name:
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
\
|
|
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
|
|
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
|
-
//
|
|
330
|
+
// packages/ui — shared shadcn-style primitives
|
|
281
331
|
// ---------------------------------------------------------------------------
|
|
282
332
|
|
|
283
|
-
writeJson("
|
|
284
|
-
name:
|
|
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
|
|
341
|
-
*
|
|
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", {
|
|
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
|
|
453
|
-
\t\t\t\t\
|
|
454
|
-
\t\t\t\t\t<code className="font-mono text-xs">
|
|
455
|
-
\t\t\t\t\
|
|
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
|
|
482
|
-
*
|
|
483
|
-
*
|
|
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<
|
|
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<
|
|
770
|
+
\t\t\t\t<Button
|
|
524
771
|
\t\t\t\t\ttype="submit"
|
|
525
|
-
\t\t\t\t\
|
|
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</
|
|
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
|
|
611
|
-
-
|
|
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.
|
|
3
|
+
"version": "0.3.18",
|
|
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"
|