@pylonsync/create-pylon 0.3.16
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 +615 -0
- package/package.json +33 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @pylonsync/create-pylon — scaffold a new Pylon app.
|
|
4
|
+
*
|
|
5
|
+
* Run via `npm create @pylonsync/pylon@latest [name]` (or yarn/pnpm/bun
|
|
6
|
+
* create @pylonsync/pylon).
|
|
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
|
|
12
|
+
*
|
|
13
|
+
* Node-runnable (no Bun required) so `npm create` works for every
|
|
14
|
+
* package manager. Uses only Node-builtin APIs — no runtime deps.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join, resolve } from "node:path";
|
|
19
|
+
import { createInterface } from "node:readline/promises";
|
|
20
|
+
import { stdin, stdout, exit, argv, cwd } from "node:process";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Version pin — every generated dep references this version of @pylonsync/*.
|
|
24
|
+
// Bumped via the workspace's release-please flow (same version as the rest
|
|
25
|
+
// of the pylon stack).
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const PYLON_VERSION = "0.3.16";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// CLI args + interactive prompt
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const args = argv.slice(2);
|
|
35
|
+
let projectName = args.find((a) => !a.startsWith("--"));
|
|
36
|
+
|
|
37
|
+
const flags = {
|
|
38
|
+
pm:
|
|
39
|
+
args.find((a) => a === "--bun" || a === "--pnpm" || a === "--yarn" || a === "--npm")?.slice(2) ??
|
|
40
|
+
detectPackageManager(),
|
|
41
|
+
skipInstall: args.includes("--skip-install"),
|
|
42
|
+
help: args.includes("--help") || args.includes("-h"),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (flags.help) {
|
|
46
|
+
process.stdout.write(`\nUsage: npm create @pylonsync/pylon [name] [--bun|--pnpm|--yarn|--npm] [--skip-install]\n\n`);
|
|
47
|
+
exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!projectName) {
|
|
51
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
52
|
+
projectName = (await rl.question("Project name: ")).trim() || "my-pylon-app";
|
|
53
|
+
rl.close();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const root = resolve(cwd(), projectName);
|
|
57
|
+
|
|
58
|
+
if (existsSync(root) && readdirSync(root).length > 0) {
|
|
59
|
+
console.error(`\nError: ${root} already exists and is not empty.\n`);
|
|
60
|
+
exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`\nCreating ${projectName} in ${root}\n`);
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// File-tree generator — every `write(path, content)` call creates parent
|
|
67
|
+
// dirs as needed and writes UTF-8 text. Keeping the scaffold inline (no
|
|
68
|
+
// template files) means create-pylon stays a single zero-dep file.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function write(path, content) {
|
|
72
|
+
const full = join(root, path);
|
|
73
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
74
|
+
writeFileSync(full, content);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeJson(path, value) {
|
|
78
|
+
write(path, JSON.stringify(value, null, 2) + "\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Root workspace
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
writeJson("package.json", {
|
|
86
|
+
name: projectName,
|
|
87
|
+
private: true,
|
|
88
|
+
type: "module",
|
|
89
|
+
workspaces: ["api", "web"],
|
|
90
|
+
scripts: {
|
|
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",
|
|
94
|
+
build: "npm --workspaces run build --if-present",
|
|
95
|
+
},
|
|
96
|
+
devDependencies: {
|
|
97
|
+
"npm-run-all": "^4.1.5",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
write(".gitignore", `node_modules/
|
|
102
|
+
.next/
|
|
103
|
+
.turbo/
|
|
104
|
+
dist/
|
|
105
|
+
out/
|
|
106
|
+
.env
|
|
107
|
+
.env.local
|
|
108
|
+
*.db
|
|
109
|
+
*.db-journal
|
|
110
|
+
api/pylon.manifest.json
|
|
111
|
+
api/pylon.client.ts
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
write(".env.example", `# Backend port the Pylon control plane listens on.
|
|
115
|
+
PYLON_PORT=4321
|
|
116
|
+
|
|
117
|
+
# Where the Next.js dev server can reach the control plane.
|
|
118
|
+
PYLON_TARGET=http://localhost:4321
|
|
119
|
+
|
|
120
|
+
# Cookie name the auth helpers look for.
|
|
121
|
+
# Pattern: \`\${app_name}_session\` from the Pylon manifest.
|
|
122
|
+
PYLON_COOKIE_NAME=${projectName}_session
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
write(
|
|
126
|
+
"README.md",
|
|
127
|
+
`# ${projectName}
|
|
128
|
+
|
|
129
|
+
Realtime backend + Next.js dashboard, scaffolded by [create-pylon](https://npmjs.com/create-pylon).
|
|
130
|
+
|
|
131
|
+
## Getting started
|
|
132
|
+
|
|
133
|
+
\`\`\`sh
|
|
134
|
+
${flags.pm === "npm" ? "npm install" : `${flags.pm} install`}
|
|
135
|
+
${flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`}
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
That spins up two processes:
|
|
139
|
+
|
|
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
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
## What to do next
|
|
160
|
+
|
|
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.
|
|
165
|
+
|
|
166
|
+
## Docs
|
|
167
|
+
|
|
168
|
+
[pylonsync.com/docs](https://pylonsync.com/docs)
|
|
169
|
+
`,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// api/ — the Pylon control plane
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
writeJson("api/package.json", {
|
|
177
|
+
name: `${projectName}-api`,
|
|
178
|
+
version: "0.0.1",
|
|
179
|
+
private: true,
|
|
180
|
+
type: "module",
|
|
181
|
+
scripts: {
|
|
182
|
+
dev: "pylon dev schema.ts --port 4321",
|
|
183
|
+
build: "pylon codegen schema.ts --out pylon.manifest.json && pylon codegen client pylon.manifest.json --out pylon.client.ts",
|
|
184
|
+
"schema:push": "pylon schema push pylon.manifest.json --sqlite dev.db",
|
|
185
|
+
"schema:inspect": "pylon schema inspect --sqlite dev.db",
|
|
186
|
+
},
|
|
187
|
+
dependencies: {
|
|
188
|
+
"@pylonsync/sdk": `^${PYLON_VERSION}`,
|
|
189
|
+
"@pylonsync/functions": `^${PYLON_VERSION}`,
|
|
190
|
+
},
|
|
191
|
+
devDependencies: {
|
|
192
|
+
"@pylonsync/cli": `^${PYLON_VERSION}`,
|
|
193
|
+
typescript: "^5.5.0",
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
writeJson("api/tsconfig.json", {
|
|
198
|
+
compilerOptions: {
|
|
199
|
+
target: "ES2022",
|
|
200
|
+
module: "ESNext",
|
|
201
|
+
moduleResolution: "Bundler",
|
|
202
|
+
strict: true,
|
|
203
|
+
skipLibCheck: true,
|
|
204
|
+
noEmit: true,
|
|
205
|
+
esModuleInterop: true,
|
|
206
|
+
allowSyntheticDefaultImports: true,
|
|
207
|
+
},
|
|
208
|
+
include: ["schema.ts", "functions/**/*.ts"],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
write(
|
|
212
|
+
"api/schema.ts",
|
|
213
|
+
`import { entity, field, defineRoute, query, action, policy, buildManifest } from "@pylonsync/sdk";
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Schema
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
const Todo = entity("Todo", {
|
|
220
|
+
\ttitle: field.string(),
|
|
221
|
+
\tdone: field.bool().default(false),
|
|
222
|
+
\tcreatedAt: field.datetime().default("now"),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Queries / mutations
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
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
|
+
});
|
|
236
|
+
|
|
237
|
+
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\`,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Policies — wide-open by default. Tighten before production.
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
const todoPolicy = policy({
|
|
255
|
+
\tname: "todo_open",
|
|
256
|
+
\tentity: "Todo",
|
|
257
|
+
\tallowRead: "true",
|
|
258
|
+
\tallowInsert: "true",
|
|
259
|
+
\tallowUpdate: "true",
|
|
260
|
+
\tallowDelete: "true",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Manifest — codegen reads this and emits pylon.manifest.json
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
export default buildManifest({
|
|
268
|
+
\tname: "${projectName}",
|
|
269
|
+
\tversion: "0.0.1",
|
|
270
|
+
\tentities: [Todo],
|
|
271
|
+
\tqueries: [listTodos],
|
|
272
|
+
\tactions: [addTodo],
|
|
273
|
+
\tpolicies: [todoPolicy],
|
|
274
|
+
\troutes: [],
|
|
275
|
+
});
|
|
276
|
+
`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// web/ — Next.js 16 + React 19 + Tailwind v4 + @pylonsync/react
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
writeJson("web/package.json", {
|
|
284
|
+
name: `${projectName}-web`,
|
|
285
|
+
version: "0.0.1",
|
|
286
|
+
private: true,
|
|
287
|
+
type: "module",
|
|
288
|
+
scripts: {
|
|
289
|
+
dev: "next dev --port 3000",
|
|
290
|
+
build: "next build",
|
|
291
|
+
start: "next start",
|
|
292
|
+
lint: "next lint",
|
|
293
|
+
},
|
|
294
|
+
dependencies: {
|
|
295
|
+
"@pylonsync/sdk": `^${PYLON_VERSION}`,
|
|
296
|
+
"@pylonsync/react": `^${PYLON_VERSION}`,
|
|
297
|
+
"@pylonsync/next": `^${PYLON_VERSION}`,
|
|
298
|
+
next: "^16.0.0",
|
|
299
|
+
react: "^19.0.0",
|
|
300
|
+
"react-dom": "^19.0.0",
|
|
301
|
+
},
|
|
302
|
+
devDependencies: {
|
|
303
|
+
"@types/react": "^19.0.0",
|
|
304
|
+
"@types/react-dom": "^19.0.0",
|
|
305
|
+
"@types/node": "^20.0.0",
|
|
306
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
307
|
+
tailwindcss: "^4.0.0",
|
|
308
|
+
typescript: "^5.5.0",
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
writeJson("web/tsconfig.json", {
|
|
313
|
+
compilerOptions: {
|
|
314
|
+
target: "ES2022",
|
|
315
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
316
|
+
allowJs: true,
|
|
317
|
+
skipLibCheck: true,
|
|
318
|
+
strict: true,
|
|
319
|
+
noEmit: true,
|
|
320
|
+
esModuleInterop: true,
|
|
321
|
+
module: "esnext",
|
|
322
|
+
moduleResolution: "bundler",
|
|
323
|
+
resolveJsonModule: true,
|
|
324
|
+
isolatedModules: true,
|
|
325
|
+
jsx: "preserve",
|
|
326
|
+
incremental: true,
|
|
327
|
+
plugins: [{ name: "next" }],
|
|
328
|
+
paths: { "@/*": ["./src/*"] },
|
|
329
|
+
},
|
|
330
|
+
include: ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts"],
|
|
331
|
+
exclude: ["node_modules"],
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
write(
|
|
335
|
+
"web/next.config.ts",
|
|
336
|
+
`import type { NextConfig } from "next";
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Pylon's typed client + functions packages re-export across the
|
|
340
|
+
* server/client boundary; \`transpilePackages\` makes Next bundle them
|
|
341
|
+
* cleanly from the workspace.
|
|
342
|
+
*/
|
|
343
|
+
const config: NextConfig = {
|
|
344
|
+
\ttranspilePackages: [
|
|
345
|
+
\t\t"@pylonsync/sdk",
|
|
346
|
+
\t\t"@pylonsync/react",
|
|
347
|
+
\t\t"@pylonsync/next",
|
|
348
|
+
\t\t"@pylonsync/functions",
|
|
349
|
+
\t\t"@pylonsync/sync",
|
|
350
|
+
\t],
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export default config;
|
|
354
|
+
`,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
write(
|
|
358
|
+
"web/postcss.config.mjs",
|
|
359
|
+
`/** Tailwind v4 PostCSS pipeline. */
|
|
360
|
+
export default {
|
|
361
|
+
\tplugins: { "@tailwindcss/postcss": {} },
|
|
362
|
+
};
|
|
363
|
+
`,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
write(
|
|
367
|
+
"web/src/app/globals.css",
|
|
368
|
+
`@import "tailwindcss";
|
|
369
|
+
|
|
370
|
+
:root {
|
|
371
|
+
\tcolor-scheme: light dark;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
html, body { height: 100%; }
|
|
375
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
|
376
|
+
`,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
write(
|
|
380
|
+
"web/src/app/layout.tsx",
|
|
381
|
+
`import type { Metadata } from "next";
|
|
382
|
+
import "./globals.css";
|
|
383
|
+
|
|
384
|
+
export const metadata: Metadata = {
|
|
385
|
+
\ttitle: "${projectName}",
|
|
386
|
+
\tdescription: "Realtime app powered by Pylon",
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export default function RootLayout({
|
|
390
|
+
\tchildren,
|
|
391
|
+
}: {
|
|
392
|
+
\tchildren: React.ReactNode;
|
|
393
|
+
}) {
|
|
394
|
+
\treturn (
|
|
395
|
+
\t\t<html lang="en">
|
|
396
|
+
\t\t\t<body className="antialiased min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
|
|
397
|
+
\t\t\t\t{children}
|
|
398
|
+
\t\t\t</body>
|
|
399
|
+
\t\t</html>
|
|
400
|
+
\t);
|
|
401
|
+
}
|
|
402
|
+
`,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
write(
|
|
406
|
+
"web/src/lib/pylon.ts",
|
|
407
|
+
`import { createPylonServer } from "@pylonsync/next/server";
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Single server-helper instance. Imported by every Server Component
|
|
411
|
+
* and Server Action that needs to talk to the Pylon control plane.
|
|
412
|
+
*
|
|
413
|
+
* \`cookieName\` MUST match the backend's emitted cookie. Pylon uses
|
|
414
|
+
* \`\${app_name}_session\` from the manifest — for this app that's
|
|
415
|
+
* \`${projectName}_session\`. Pin it in code (NOT env) so a bad
|
|
416
|
+
* deployment env can't silently break auth.
|
|
417
|
+
*/
|
|
418
|
+
export const pylon = createPylonServer({
|
|
419
|
+
\tcookieName: "${projectName}_session",
|
|
420
|
+
});
|
|
421
|
+
`,
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
write(
|
|
425
|
+
"web/src/app/page.tsx",
|
|
426
|
+
`import { pylon } from "@/lib/pylon";
|
|
427
|
+
import { TodoList } from "./TodoList";
|
|
428
|
+
|
|
429
|
+
// Force dynamic — every render reads the live todo list from Pylon.
|
|
430
|
+
// Without this Next would try to statically generate the page and
|
|
431
|
+
// the cookie-attached fetch in pylon.json would error at build time.
|
|
432
|
+
export const dynamic = "force-dynamic";
|
|
433
|
+
|
|
434
|
+
type Todo = {
|
|
435
|
+
\tid: string;
|
|
436
|
+
\ttitle: string;
|
|
437
|
+
\tdone: boolean;
|
|
438
|
+
\tcreatedAt: string;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
export default async function HomePage() {
|
|
442
|
+
\tconst todos = await pylon
|
|
443
|
+
\t\t.json<Todo[]>("/api/fn/listTodos", { method: "POST", body: "{}", headers: { "Content-Type": "application/json" } })
|
|
444
|
+
\t\t.catch(() => [] as Todo[]);
|
|
445
|
+
|
|
446
|
+
\treturn (
|
|
447
|
+
\t\t<main className="mx-auto max-w-2xl px-6 py-12 space-y-8">
|
|
448
|
+
\t\t\t<header className="space-y-2">
|
|
449
|
+
\t\t\t\t<h1 className="text-3xl font-semibold tracking-tight">${projectName}</h1>
|
|
450
|
+
\t\t\t\t<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
451
|
+
\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.
|
|
456
|
+
\t\t\t\t</p>
|
|
457
|
+
\t\t\t</header>
|
|
458
|
+
|
|
459
|
+
\t\t\t<TodoList initialTodos={todos} />
|
|
460
|
+
\t\t</main>
|
|
461
|
+
\t);
|
|
462
|
+
}
|
|
463
|
+
`,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
write(
|
|
467
|
+
"web/src/app/TodoList.tsx",
|
|
468
|
+
`"use client";
|
|
469
|
+
|
|
470
|
+
import { useState, useTransition } from "react";
|
|
471
|
+
|
|
472
|
+
type Todo = {
|
|
473
|
+
\tid: string;
|
|
474
|
+
\ttitle: string;
|
|
475
|
+
\tdone: boolean;
|
|
476
|
+
\tcreatedAt: string;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 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).
|
|
484
|
+
*/
|
|
485
|
+
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
|
|
486
|
+
\tconst [todos, setTodos] = useState(initialTodos);
|
|
487
|
+
\tconst [title, setTitle] = useState("");
|
|
488
|
+
\tconst [pending, startTransition] = useTransition();
|
|
489
|
+
|
|
490
|
+
\tasync function add() {
|
|
491
|
+
\t\tif (!title.trim()) return;
|
|
492
|
+
\t\tconst newTitle = title;
|
|
493
|
+
\t\tsetTitle("");
|
|
494
|
+
\t\tstartTransition(async () => {
|
|
495
|
+
\t\t\tconst res = await fetch("/api/fn/addTodo", {
|
|
496
|
+
\t\t\t\tmethod: "POST",
|
|
497
|
+
\t\t\t\theaders: { "Content-Type": "application/json" },
|
|
498
|
+
\t\t\t\tbody: JSON.stringify({ title: newTitle }),
|
|
499
|
+
\t\t\t});
|
|
500
|
+
\t\t\tif (res.ok) {
|
|
501
|
+
\t\t\t\tconst todo = (await res.json()) as Todo;
|
|
502
|
+
\t\t\t\tsetTodos([todo, ...todos]);
|
|
503
|
+
\t\t\t}
|
|
504
|
+
\t\t});
|
|
505
|
+
\t}
|
|
506
|
+
|
|
507
|
+
\treturn (
|
|
508
|
+
\t\t<div className="space-y-4">
|
|
509
|
+
\t\t\t<form
|
|
510
|
+
\t\t\t\tonSubmit={(e) => {
|
|
511
|
+
\t\t\t\t\te.preventDefault();
|
|
512
|
+
\t\t\t\t\tadd();
|
|
513
|
+
\t\t\t\t}}
|
|
514
|
+
\t\t\t\tclassName="flex gap-2"
|
|
515
|
+
\t\t\t>
|
|
516
|
+
\t\t\t\t<input
|
|
517
|
+
\t\t\t\t\tvalue={title}
|
|
518
|
+
\t\t\t\t\tonChange={(e) => setTitle(e.target.value)}
|
|
519
|
+
\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
|
+
\t\t\t\t\tdisabled={pending}
|
|
522
|
+
\t\t\t\t/>
|
|
523
|
+
\t\t\t\t<button
|
|
524
|
+
\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"
|
|
526
|
+
\t\t\t\t\tdisabled={pending || !title.trim()}
|
|
527
|
+
\t\t\t\t>
|
|
528
|
+
\t\t\t\t\tAdd
|
|
529
|
+
\t\t\t\t</button>
|
|
530
|
+
\t\t\t</form>
|
|
531
|
+
|
|
532
|
+
\t\t\t{todos.length === 0 ? (
|
|
533
|
+
\t\t\t\t<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-8">
|
|
534
|
+
\t\t\t\t\tNo todos yet. Add one above.
|
|
535
|
+
\t\t\t\t</p>
|
|
536
|
+
\t\t\t) : (
|
|
537
|
+
\t\t\t\t<ul className="divide-y divide-neutral-200 dark:divide-neutral-800 rounded-md border border-neutral-200 dark:border-neutral-800">
|
|
538
|
+
\t\t\t\t\t{todos.map((t) => (
|
|
539
|
+
\t\t\t\t\t\t<li
|
|
540
|
+
\t\t\t\t\t\t\tkey={t.id}
|
|
541
|
+
\t\t\t\t\t\t\tclassName="flex items-center gap-3 px-4 py-3 text-sm"
|
|
542
|
+
\t\t\t\t\t\t>
|
|
543
|
+
\t\t\t\t\t\t\t<span className={t.done ? "line-through text-neutral-400" : ""}>
|
|
544
|
+
\t\t\t\t\t\t\t\t{t.title}
|
|
545
|
+
\t\t\t\t\t\t\t</span>
|
|
546
|
+
\t\t\t\t\t\t</li>
|
|
547
|
+
\t\t\t\t\t))}
|
|
548
|
+
\t\t\t\t</ul>
|
|
549
|
+
\t\t\t)}
|
|
550
|
+
\t\t</div>
|
|
551
|
+
\t);
|
|
552
|
+
}
|
|
553
|
+
`,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
write(
|
|
557
|
+
"web/next-env.d.ts",
|
|
558
|
+
`/// <reference types="next" />
|
|
559
|
+
/// <reference types="next/image-types/global" />
|
|
560
|
+
`,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
// Detect package manager — read npm_config_user_agent set by the runner.
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
function detectPackageManager() {
|
|
568
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
569
|
+
if (ua.startsWith("bun")) return "bun";
|
|
570
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
571
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
572
|
+
return "npm";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
// Optional: install dependencies
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
if (!flags.skipInstall) {
|
|
580
|
+
console.log(`Installing dependencies with ${flags.pm}...`);
|
|
581
|
+
const { spawnSync } = await import("node:child_process");
|
|
582
|
+
const result = spawnSync(flags.pm, ["install"], {
|
|
583
|
+
cwd: root,
|
|
584
|
+
stdio: "inherit",
|
|
585
|
+
});
|
|
586
|
+
if (result.status !== 0) {
|
|
587
|
+
console.warn(
|
|
588
|
+
`\n${flags.pm} install exited with code ${result.status}. Re-run from ${projectName}/.\n`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
// Final instructions
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
const runDev = flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`;
|
|
598
|
+
|
|
599
|
+
console.log(`
|
|
600
|
+
✓ Created ${projectName}
|
|
601
|
+
|
|
602
|
+
cd ${projectName}
|
|
603
|
+
${runDev}
|
|
604
|
+
|
|
605
|
+
→ api http://localhost:4321 (Pylon control plane)
|
|
606
|
+
→ web http://localhost:3000 (Next.js dashboard)
|
|
607
|
+
|
|
608
|
+
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.
|
|
613
|
+
|
|
614
|
+
Docs: https://pylonsync.com/docs
|
|
615
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pylonsync/create-pylon",
|
|
3
|
+
"version": "0.3.16",
|
|
4
|
+
"description": "Scaffold a new Pylon app — realtime backend + Next.js frontend in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"create-pylon": "./bin/create-pylon.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"pylon",
|
|
20
|
+
"scaffold",
|
|
21
|
+
"create",
|
|
22
|
+
"nextjs",
|
|
23
|
+
"realtime",
|
|
24
|
+
"backend"
|
|
25
|
+
],
|
|
26
|
+
"homepage": "https://pylonsync.com",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/pylonsync/pylon",
|
|
30
|
+
"directory": "packages/create-pylon"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|