@pylonsync/create-pylon 0.3.229 → 0.3.231
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 +147 -100
- package/package.json +1 -1
- package/templates/_root/gitignore +30 -0
- package/templates/ssr/README.md +42 -0
- package/templates/ssr/app/counter/page.tsx +52 -0
- package/templates/ssr/app/globals.css +3 -0
- package/templates/ssr/app/layout.tsx +66 -0
- package/templates/ssr/app/page.tsx +57 -0
- package/templates/ssr/app.ts +32 -0
- package/templates/ssr/functions/_keep.ts +13 -0
- package/templates/ssr/gitignore +10 -0
- package/templates/ssr/package.json +26 -0
- package/templates/ssr/tsconfig.json +14 -0
package/bin/create-pylon.js
CHANGED
|
@@ -71,6 +71,16 @@ const PYLON_VERSION = JSON.parse(
|
|
|
71
71
|
const PLATFORMS_AVAILABLE = ["web", "vite", "ios", "mac", "expo"];
|
|
72
72
|
|
|
73
73
|
const TEMPLATE_REGISTRY = {
|
|
74
|
+
ssr: {
|
|
75
|
+
blurb:
|
|
76
|
+
"Full-stack SSR — server-rendered React + Link/Image/Tailwind, one server, no Next.js.",
|
|
77
|
+
// `unified` templates are a single Pylon app (app.ts + app/ routes +
|
|
78
|
+
// functions/), NOT a monorepo of apps/api + apps/web. `pylon dev`
|
|
79
|
+
// serves the SSR frontend and the API from one port. They take no
|
|
80
|
+
// `--platforms` — the app IS the whole stack.
|
|
81
|
+
platforms: [],
|
|
82
|
+
unified: true,
|
|
83
|
+
},
|
|
74
84
|
barebones: {
|
|
75
85
|
blurb: "Single entity, list + create. The smallest working app.",
|
|
76
86
|
platforms: ["web", "ios", "mac", "expo"],
|
|
@@ -128,7 +138,8 @@ function pickValue(arr, ...candidates) {
|
|
|
128
138
|
|
|
129
139
|
if (flags.help) {
|
|
130
140
|
const tmplLines = Object.entries(TEMPLATE_REGISTRY).map(
|
|
131
|
-
([k, v]) =>
|
|
141
|
+
([k, v]) =>
|
|
142
|
+
` ${k.padEnd(10)} ${v.blurb} (${v.unified ? "single app" : v.platforms.join(", ")})`,
|
|
132
143
|
);
|
|
133
144
|
process.stdout.write(`
|
|
134
145
|
Usage: npm create @pylonsync/pylon [name] [options]
|
|
@@ -137,10 +148,12 @@ Usage: npm create @pylonsync/pylon [name] [options]
|
|
|
137
148
|
${tmplLines.join("\n")}
|
|
138
149
|
|
|
139
150
|
--platforms <list> comma list: ${PLATFORMS_AVAILABLE.join(",")} (default: web)
|
|
151
|
+
ignored for ssr — it's a single full-stack app, no platforms
|
|
140
152
|
--bun|--pnpm|--yarn|--npm
|
|
141
153
|
--skip-install scaffold only, don't run install
|
|
142
154
|
|
|
143
155
|
Examples:
|
|
156
|
+
npm create @pylonsync/pylon my-app --template ssr # full-stack SSR, no Next.js
|
|
144
157
|
npm create @pylonsync/pylon my-app
|
|
145
158
|
npm create @pylonsync/pylon my-app --template todo --platforms web,ios
|
|
146
159
|
npm create @pylonsync/pylon my-app --template b2b --platforms web,mac
|
|
@@ -167,7 +180,10 @@ if (!flags.template) {
|
|
|
167
180
|
.toLowerCase();
|
|
168
181
|
flags.template = TEMPLATES_AVAILABLE.includes(ans) ? ans : "todo";
|
|
169
182
|
}
|
|
170
|
-
|
|
183
|
+
// `unified` templates (ssr) are a single app, not a monorepo — they take
|
|
184
|
+
// no platforms. Skip the platform prompt + validation for them entirely.
|
|
185
|
+
const isUnified = TEMPLATE_REGISTRY[flags.template]?.unified === true;
|
|
186
|
+
if (!isUnified && !flags.platforms) {
|
|
171
187
|
const supported = TEMPLATE_REGISTRY[flags.template].platforms.join(", ");
|
|
172
188
|
const ans = (
|
|
173
189
|
await rl.question(
|
|
@@ -188,29 +204,35 @@ if (!flags.pm) {
|
|
|
188
204
|
}
|
|
189
205
|
rl.close();
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
exit(1);
|
|
203
|
-
}
|
|
204
|
-
if (platforms.length === 0) {
|
|
205
|
-
console.error(`\nError: at least one platform required.\n`);
|
|
206
|
-
exit(1);
|
|
207
|
-
}
|
|
208
|
-
if (platforms.includes("web") && platforms.includes("vite")) {
|
|
209
|
-
console.error(
|
|
210
|
-
`\nError: --platforms web and vite are mutually exclusive (both render into apps/web).\n` +
|
|
211
|
-
` Pick one: web for Next.js 16, vite for plain Vite + React.\n`,
|
|
207
|
+
// Unified templates own no platforms — they ARE the whole app. For
|
|
208
|
+
// everything else, parse + validate the platform list.
|
|
209
|
+
const platforms = isUnified
|
|
210
|
+
? []
|
|
211
|
+
: (flags.platforms ?? "web")
|
|
212
|
+
.split(",")
|
|
213
|
+
.map((p) => p.trim().toLowerCase())
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
if (!isUnified) {
|
|
216
|
+
const unknownPlatforms = platforms.filter(
|
|
217
|
+
(p) => !PLATFORMS_AVAILABLE.includes(p),
|
|
212
218
|
);
|
|
213
|
-
|
|
219
|
+
if (unknownPlatforms.length > 0) {
|
|
220
|
+
console.error(
|
|
221
|
+
`\nError: unknown platform(s): ${unknownPlatforms.join(", ")}. Valid: ${PLATFORMS_AVAILABLE.join(", ")}\n`,
|
|
222
|
+
);
|
|
223
|
+
exit(1);
|
|
224
|
+
}
|
|
225
|
+
if (platforms.length === 0) {
|
|
226
|
+
console.error(`\nError: at least one platform required.\n`);
|
|
227
|
+
exit(1);
|
|
228
|
+
}
|
|
229
|
+
if (platforms.includes("web") && platforms.includes("vite")) {
|
|
230
|
+
console.error(
|
|
231
|
+
`\nError: --platforms web and vite are mutually exclusive (both render into apps/web).\n` +
|
|
232
|
+
` Pick one: web for Next.js 16, vite for plain Vite + React.\n`,
|
|
233
|
+
);
|
|
234
|
+
exit(1);
|
|
235
|
+
}
|
|
214
236
|
}
|
|
215
237
|
if (!TEMPLATES_AVAILABLE.includes(flags.template)) {
|
|
216
238
|
console.error(
|
|
@@ -251,7 +273,7 @@ if (existsSync(root) && readdirSync(root).length > 0) {
|
|
|
251
273
|
mkdirSync(root, { recursive: true });
|
|
252
274
|
|
|
253
275
|
console.log(
|
|
254
|
-
`\nCreating ${projectName} (${flags.template}
|
|
276
|
+
`\nCreating ${projectName} (${flags.template}${isUnified ? "" : `, ${platforms.join(" + ")}`}) in ${root}\n`,
|
|
255
277
|
);
|
|
256
278
|
|
|
257
279
|
// ---------------------------------------------------------------------------
|
|
@@ -274,6 +296,7 @@ const SUBS = {
|
|
|
274
296
|
__APP_NAME_PASCAL__: APP_NAME_PASCAL,
|
|
275
297
|
__PYLON_VERSION__: PYLON_VERSION,
|
|
276
298
|
__WORKSPACE_DEP__: workspaceDepSpec,
|
|
299
|
+
__RUN_DEV__: flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`,
|
|
277
300
|
};
|
|
278
301
|
|
|
279
302
|
// Filenames that contain placeholders get renamed AFTER copy. Keeps
|
|
@@ -302,7 +325,12 @@ function substituteFile(absPath) {
|
|
|
302
325
|
function walkAndSubstitute(dir) {
|
|
303
326
|
for (const entry of readdirSync(dir)) {
|
|
304
327
|
const abs = join(dir, entry);
|
|
305
|
-
|
|
328
|
+
let renamed = substituteString(entry);
|
|
329
|
+
// npm strips a literal `.gitignore` from published tarballs, so
|
|
330
|
+
// templates ship it as `gitignore` and we restore the dot at
|
|
331
|
+
// scaffold time — otherwise the new project has no ignore file
|
|
332
|
+
// and node_modules / .pylon / *.db get committed.
|
|
333
|
+
if (renamed === "gitignore") renamed = ".gitignore";
|
|
306
334
|
let target = abs;
|
|
307
335
|
if (renamed !== entry) {
|
|
308
336
|
target = join(dir, renamed);
|
|
@@ -332,85 +360,94 @@ function copyTemplate(srcSubpath, destSubpath = "") {
|
|
|
332
360
|
// 5. Root package.json — generated, not templated
|
|
333
361
|
// ---------------------------------------------------------------------------
|
|
334
362
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
copyTemplate(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
363
|
+
if (isUnified) {
|
|
364
|
+
// Single unified app: app.ts + app/ routes + functions/, served by
|
|
365
|
+
// `pylon dev` (frontend + API, one port). The template ships its own
|
|
366
|
+
// package.json — no monorepo root, no turbo, no workspaces.
|
|
367
|
+
copyTemplate(flags.template);
|
|
368
|
+
} else {
|
|
369
|
+
copyTemplate("_root");
|
|
370
|
+
copyTemplate(`backend/${flags.template}`);
|
|
371
|
+
|
|
372
|
+
// `web` (Next.js) and `vite` are alternative web-frontend toolchains;
|
|
373
|
+
// the mutex check above guarantees at most one of them is set. Either
|
|
374
|
+
// way we also pull in packages/ui so the shared primitives are present.
|
|
375
|
+
if (platforms.includes("web")) {
|
|
376
|
+
copyTemplate("ui");
|
|
377
|
+
copyTemplate(`web/${flags.template}`);
|
|
378
|
+
}
|
|
379
|
+
if (platforms.includes("vite")) {
|
|
380
|
+
copyTemplate("ui");
|
|
381
|
+
copyTemplate(`vite/${flags.template}`);
|
|
382
|
+
}
|
|
383
|
+
for (const p of ["ios", "mac", "expo"]) {
|
|
384
|
+
if (platforms.includes(p)) copyTemplate(`${p}/${flags.template}`);
|
|
385
|
+
}
|
|
351
386
|
}
|
|
352
387
|
|
|
353
388
|
walkAndSubstitute(root);
|
|
354
389
|
|
|
355
390
|
// ---------------------------------------------------------------------------
|
|
356
|
-
// Root package.json — generated based on selected platforms
|
|
357
|
-
//
|
|
358
|
-
//
|
|
391
|
+
// Root package.json — generated based on selected platforms (monorepo
|
|
392
|
+
// templates only). Unified templates ship their own single-app
|
|
393
|
+
// package.json, so skip this entirely.
|
|
359
394
|
// ---------------------------------------------------------------------------
|
|
360
395
|
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
"
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// Turbo 2.x refuses to run without packageManager set. Pick a recent-
|
|
378
|
-
// stable for whichever PM the user picked. npm doesn't enforce this
|
|
379
|
-
// field but turbo still expects it to be present.
|
|
380
|
-
const PACKAGE_MANAGERS = {
|
|
381
|
-
bun: "bun@1.2.19",
|
|
382
|
-
pnpm: "pnpm@9.12.0",
|
|
383
|
-
yarn: "yarn@4.5.0",
|
|
384
|
-
npm: "npm@10.9.0",
|
|
385
|
-
};
|
|
396
|
+
if (!isUnified) {
|
|
397
|
+
// Turborepo orchestrates the workspace. `turbo dev` runs the `dev`
|
|
398
|
+
// task in every package that defines one (apps/api always; apps/web,
|
|
399
|
+
// apps/expo when scaffolded). Native targets (ios, mac) aren't
|
|
400
|
+
// `turbo dev`-shaped — Xcode / `swift run` block — so they get
|
|
401
|
+
// dedicated escape-hatch scripts instead. turbo.json ships in
|
|
402
|
+
// _root/, so it's already in the project.
|
|
403
|
+
const helperScripts = {};
|
|
404
|
+
if (platforms.includes("ios")) {
|
|
405
|
+
helperScripts["dev:ios"] =
|
|
406
|
+
"echo 'cd apps/ios && xcodegen generate && open *.xcodeproj (or: swift run for a quick macOS preview)'";
|
|
407
|
+
}
|
|
408
|
+
if (platforms.includes("mac")) {
|
|
409
|
+
helperScripts["dev:mac"] =
|
|
410
|
+
"echo 'cd apps/mac && swift run (or: xcodegen generate && open *.xcodeproj)'";
|
|
411
|
+
}
|
|
386
412
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
// Turbo 2.x refuses to run without packageManager set. Pick a recent-
|
|
414
|
+
// stable for whichever PM the user picked. npm doesn't enforce this
|
|
415
|
+
// field but turbo still expects it to be present.
|
|
416
|
+
const PACKAGE_MANAGERS = {
|
|
417
|
+
bun: "bun@1.2.19",
|
|
418
|
+
pnpm: "pnpm@9.12.0",
|
|
419
|
+
yarn: "yarn@4.5.0",
|
|
420
|
+
npm: "npm@10.9.0",
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const rootPkg = {
|
|
424
|
+
name: APP_NAME_KEBAB,
|
|
425
|
+
private: true,
|
|
426
|
+
type: "module",
|
|
427
|
+
packageManager: PACKAGE_MANAGERS[flags.pm],
|
|
428
|
+
workspaces: ["apps/*", "packages/*"].filter((p) => {
|
|
429
|
+
// Only declare packages/* as a workspace if we actually scaffolded
|
|
430
|
+
// packages/ui — otherwise the empty match warns on bun install.
|
|
431
|
+
if (p === "packages/*")
|
|
432
|
+
return platforms.includes("web") || platforms.includes("vite");
|
|
433
|
+
return true;
|
|
434
|
+
}),
|
|
435
|
+
scripts: {
|
|
436
|
+
dev: "turbo dev",
|
|
437
|
+
build: "turbo build",
|
|
438
|
+
check: "turbo check",
|
|
439
|
+
lint: "turbo lint",
|
|
440
|
+
...helperScripts,
|
|
441
|
+
},
|
|
442
|
+
devDependencies: {
|
|
443
|
+
turbo: "^2.3.0",
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
writeFileSync(
|
|
447
|
+
join(root, "package.json"),
|
|
448
|
+
JSON.stringify(rootPkg, null, 2) + "\n",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
414
451
|
|
|
415
452
|
// ---------------------------------------------------------------------------
|
|
416
453
|
// Optional: install dependencies
|
|
@@ -437,7 +474,10 @@ if (!flags.skipInstall) {
|
|
|
437
474
|
const runDev = flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`;
|
|
438
475
|
|
|
439
476
|
const platformLines = [];
|
|
440
|
-
|
|
477
|
+
if (isUnified)
|
|
478
|
+
platformLines.push(" → app http://localhost:4321 (SSR frontend + API, one port)");
|
|
479
|
+
else
|
|
480
|
+
platformLines.push(" → api http://localhost:4321 (Pylon control plane)");
|
|
441
481
|
if (platforms.includes("web"))
|
|
442
482
|
platformLines.push(" → web http://localhost:3000 (Next.js)");
|
|
443
483
|
if (platforms.includes("vite"))
|
|
@@ -449,8 +489,15 @@ if (platforms.includes("ios"))
|
|
|
449
489
|
if (platforms.includes("mac"))
|
|
450
490
|
platformLines.push(` → mac cd apps/mac && swift run (or xcodegen for .app)`);
|
|
451
491
|
|
|
452
|
-
const layoutLines =
|
|
453
|
-
|
|
492
|
+
const layoutLines = isUnified
|
|
493
|
+
? [
|
|
494
|
+
" app.ts data model + manifest (entities, functions, routes)",
|
|
495
|
+
" app/ file-based SSR routes (app/page.tsx → /)",
|
|
496
|
+
" app/layout.tsx root layout (receives url + auth)",
|
|
497
|
+
" functions/ server functions (query/action)",
|
|
498
|
+
]
|
|
499
|
+
: [" apps/api schema + functions/ handlers"];
|
|
500
|
+
if (!isUnified && platforms.includes("web")) {
|
|
454
501
|
layoutLines.push(" apps/web Next.js 16 + React 19 + Tailwind v4");
|
|
455
502
|
layoutLines.push(" packages/ui shared UI primitives");
|
|
456
503
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.231",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.next/
|
|
3
|
+
.turbo/
|
|
4
|
+
dist/
|
|
5
|
+
out/
|
|
6
|
+
.env
|
|
7
|
+
.env.local
|
|
8
|
+
*.db
|
|
9
|
+
*.db-journal
|
|
10
|
+
# Local pylon dev state: sqlite db, jobs queue, sessions, uploads dir.
|
|
11
|
+
# Created by `pylon dev` on first run; safe to delete to reset.
|
|
12
|
+
.pylon/
|
|
13
|
+
apps/api/pylon.manifest.json
|
|
14
|
+
apps/api/pylon.client.ts
|
|
15
|
+
|
|
16
|
+
# Swift / Xcode
|
|
17
|
+
.build/
|
|
18
|
+
DerivedData/
|
|
19
|
+
*.xcworkspace
|
|
20
|
+
*.xcodeproj
|
|
21
|
+
Package.resolved
|
|
22
|
+
|
|
23
|
+
# Expo / RN
|
|
24
|
+
.expo/
|
|
25
|
+
.expo-shared/
|
|
26
|
+
ios/Pods/
|
|
27
|
+
ios/build/
|
|
28
|
+
android/.gradle/
|
|
29
|
+
android/build/
|
|
30
|
+
android/app/build/
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A full-stack [Pylon](https://pylonsync.com) app — server-rendered React,
|
|
4
|
+
file-based routes, a synced database, and a typed client, served from one
|
|
5
|
+
binary on one port. No Next.js, no separate API server.
|
|
6
|
+
|
|
7
|
+
## Develop
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
__RUN_DEV__
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open http://localhost:4321. Edit any file under `app/` and save — the page
|
|
14
|
+
reloads instantly.
|
|
15
|
+
|
|
16
|
+
## Layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
app.ts your data model + manifest (entities, functions, policies, routes)
|
|
20
|
+
app/ file-based SSR routes — app/page.tsx is "/", app/counter/page.tsx is "/counter"
|
|
21
|
+
app/layout.tsx the root layout wrapping every page (receives url + auth)
|
|
22
|
+
app/globals.css Tailwind entrypoint (compiled by Pylon)
|
|
23
|
+
functions/ server functions (query/action) — typed RPC, auto-exposed
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Add a route
|
|
27
|
+
|
|
28
|
+
Drop a file at `app/about/page.tsx` and visit `/about`. Pages receive
|
|
29
|
+
`{ url, auth, searchParams }` from the SSR runtime.
|
|
30
|
+
|
|
31
|
+
## Add data
|
|
32
|
+
|
|
33
|
+
Edit `app.ts`. Every `entity()` becomes a synced table with a REST +
|
|
34
|
+
realtime API and a typed client — no migrations, no resolvers.
|
|
35
|
+
|
|
36
|
+
## Deploy
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pylon deploy
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Docs: https://docs.pylonsync.com
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface PageProps {
|
|
4
|
+
url: string;
|
|
5
|
+
searchParams: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// `app/counter/page.tsx` → `/counter`. This page is server-rendered AND
|
|
9
|
+
// interactive: the HTML arrives with the initial count already in it (try
|
|
10
|
+
// /counter?start=10), then the per-route chunk hydrates and useState takes
|
|
11
|
+
// over. No client/server split to manage — it's one component.
|
|
12
|
+
export default function CounterPage({ searchParams }: PageProps) {
|
|
13
|
+
const start = Number(searchParams.start ?? "0") || 0;
|
|
14
|
+
const [count, setCount] = React.useState(start);
|
|
15
|
+
return (
|
|
16
|
+
<div className="space-y-6">
|
|
17
|
+
<h1 className="text-2xl font-semibold tracking-tight">Counter</h1>
|
|
18
|
+
<p className="text-zinc-600">
|
|
19
|
+
Rendered on the server, hydrated in the browser. The buttons work
|
|
20
|
+
because the page's JS chunk hydrated this exact markup.
|
|
21
|
+
</p>
|
|
22
|
+
<div className="flex items-center gap-4">
|
|
23
|
+
<button
|
|
24
|
+
onClick={() => setCount((c) => c - 1)}
|
|
25
|
+
className="rounded-lg border border-zinc-300 px-4 py-2 text-lg hover:bg-zinc-100"
|
|
26
|
+
>
|
|
27
|
+
−
|
|
28
|
+
</button>
|
|
29
|
+
<span className="min-w-12 text-center text-2xl font-semibold tabular-nums">
|
|
30
|
+
{count}
|
|
31
|
+
</span>
|
|
32
|
+
<button
|
|
33
|
+
onClick={() => setCount((c) => c + 1)}
|
|
34
|
+
className="rounded-lg border border-zinc-300 px-4 py-2 text-lg hover:bg-zinc-100"
|
|
35
|
+
>
|
|
36
|
+
+
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
<p className="text-xs text-zinc-500">
|
|
40
|
+
Initial value comes from <code>?start=</code> — search params flow
|
|
41
|
+
through SSR. Try{" "}
|
|
42
|
+
<a
|
|
43
|
+
href="/counter?start=10"
|
|
44
|
+
className="text-blue-600 underline-offset-4 hover:underline"
|
|
45
|
+
>
|
|
46
|
+
/counter?start=10
|
|
47
|
+
</a>
|
|
48
|
+
.
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "@pylonsync/react";
|
|
3
|
+
|
|
4
|
+
// Auth shape injected by the SSR runtime. `auth.user_id` is null for
|
|
5
|
+
// anonymous visitors. Wire a sign-in flow with @pylonsync/client when
|
|
6
|
+
// you're ready — for now this just shows the session state.
|
|
7
|
+
interface AuthShape {
|
|
8
|
+
user_id: string | null;
|
|
9
|
+
is_admin: boolean;
|
|
10
|
+
tenant_id: string | null;
|
|
11
|
+
roles: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface LayoutProps {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
url: string;
|
|
17
|
+
auth: AuthShape;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The root layout wraps every page. It receives `url` and `auth` from the
|
|
21
|
+
// SSR runtime on every render — server-side, before the HTML is sent.
|
|
22
|
+
export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
23
|
+
const signedIn = Boolean(auth?.user_id);
|
|
24
|
+
return (
|
|
25
|
+
<html lang="en" className="bg-zinc-50">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charSet="utf-8" />
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
29
|
+
<title>__APP_NAME__</title>
|
|
30
|
+
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
31
|
+
stylesheet link is injected here automatically — nothing to
|
|
32
|
+
wire up. */}
|
|
33
|
+
</head>
|
|
34
|
+
<body className="min-h-screen text-zinc-900 antialiased">
|
|
35
|
+
<header className="sticky top-0 z-10 border-b border-zinc-200 bg-white/80 backdrop-blur">
|
|
36
|
+
<div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
|
|
37
|
+
<Link
|
|
38
|
+
href="/"
|
|
39
|
+
className="text-sm font-semibold tracking-tight hover:text-zinc-600"
|
|
40
|
+
>
|
|
41
|
+
__APP_NAME__
|
|
42
|
+
</Link>
|
|
43
|
+
<nav className="flex items-center gap-4 text-sm text-zinc-600">
|
|
44
|
+
<Link href="/" className="hover:text-zinc-900">
|
|
45
|
+
Home
|
|
46
|
+
</Link>
|
|
47
|
+
<Link href="/counter" className="hover:text-zinc-900">
|
|
48
|
+
Counter
|
|
49
|
+
</Link>
|
|
50
|
+
<span
|
|
51
|
+
className={signedIn ? "text-emerald-600" : "text-zinc-400"}
|
|
52
|
+
title={url}
|
|
53
|
+
>
|
|
54
|
+
{signedIn ? `· ${auth.user_id}` : "· anon"}
|
|
55
|
+
</span>
|
|
56
|
+
</nav>
|
|
57
|
+
</div>
|
|
58
|
+
</header>
|
|
59
|
+
<main className="mx-auto max-w-3xl px-4 py-10">{children}</main>
|
|
60
|
+
<footer className="border-t border-zinc-200 py-6 text-center text-xs text-zinc-500">
|
|
61
|
+
Rendered by Pylon · one server, one port
|
|
62
|
+
</footer>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "@pylonsync/react";
|
|
3
|
+
|
|
4
|
+
interface PageProps {
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// `app/page.tsx` → `/`. Pages receive `{ url, auth, searchParams }` from
|
|
9
|
+
// the SSR runtime. This renders to HTML on the server; the per-route
|
|
10
|
+
// chunk hydrates it in the browser so interactive pages (see /counter)
|
|
11
|
+
// just work.
|
|
12
|
+
export default function IndexPage({ url }: PageProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-8">
|
|
15
|
+
<section>
|
|
16
|
+
<h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
|
|
17
|
+
<p className="mt-2 text-zinc-600">
|
|
18
|
+
A full-stack Pylon app. Server-rendered React, file-based routes,
|
|
19
|
+
a synced database, and a typed client — served from one binary on
|
|
20
|
+
one port. No Next.js, no separate API server.
|
|
21
|
+
</p>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section className="rounded-2xl border border-zinc-200 bg-white p-6">
|
|
25
|
+
<h2 className="text-lg font-semibold">Next steps</h2>
|
|
26
|
+
<ul className="mt-3 space-y-2 text-sm text-zinc-700">
|
|
27
|
+
<li>
|
|
28
|
+
Add a route: drop a file at{" "}
|
|
29
|
+
<code className="rounded bg-zinc-100 px-1">app/about/page.tsx</code>{" "}
|
|
30
|
+
and visit <code className="rounded bg-zinc-100 px-1">/about</code>.
|
|
31
|
+
</li>
|
|
32
|
+
<li>
|
|
33
|
+
Add data: edit{" "}
|
|
34
|
+
<code className="rounded bg-zinc-100 px-1">app.ts</code> — every{" "}
|
|
35
|
+
<code className="rounded bg-zinc-100 px-1">entity()</code> gets a
|
|
36
|
+
REST + realtime API and a typed client automatically.
|
|
37
|
+
</li>
|
|
38
|
+
<li>
|
|
39
|
+
See hydration in action on{" "}
|
|
40
|
+
<Link
|
|
41
|
+
href="/counter"
|
|
42
|
+
className="text-blue-600 underline-offset-4 hover:underline"
|
|
43
|
+
>
|
|
44
|
+
/counter
|
|
45
|
+
</Link>
|
|
46
|
+
.
|
|
47
|
+
</li>
|
|
48
|
+
</ul>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<p className="text-xs text-zinc-500">
|
|
52
|
+
You're at <code>{url}</code>. Edit{" "}
|
|
53
|
+
<code>app/page.tsx</code> and save — the page reloads instantly.
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { buildManifest, discoverAppRoutes, entity, field } from "@pylonsync/sdk";
|
|
2
|
+
|
|
3
|
+
// Your data model. Every `entity()` becomes a synced table with a REST +
|
|
4
|
+
// realtime API and a typed client — no migrations to write, no resolvers
|
|
5
|
+
// to wire. Add fields here and they show up everywhere.
|
|
6
|
+
const Note = entity("Note", {
|
|
7
|
+
body: field.string(),
|
|
8
|
+
done: field.boolean().default(false),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// The manifest is your whole app in one object: data, server functions,
|
|
12
|
+
// access policies, and the file-based routes under `app/`. `pylon dev`
|
|
13
|
+
// reads this, serves the SSR frontend and the API from one port, and
|
|
14
|
+
// regenerates a typed client on every change.
|
|
15
|
+
//
|
|
16
|
+
// File-based routing: `discoverAppRoutes()` walks `app/**/page.tsx` and
|
|
17
|
+
// emits one route per page. Drop `app/about/page.tsx` to add `/about` —
|
|
18
|
+
// no route table to maintain.
|
|
19
|
+
const manifest = buildManifest({
|
|
20
|
+
name: "__APP_NAME__",
|
|
21
|
+
version: "0.1.0",
|
|
22
|
+
entities: [Note],
|
|
23
|
+
queries: [],
|
|
24
|
+
actions: [],
|
|
25
|
+
policies: [],
|
|
26
|
+
routes: await discoverAppRoutes(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Emit canonical manifest JSON to stdout for `pylon codegen`.
|
|
30
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
31
|
+
|
|
32
|
+
export default manifest;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Server functions go here. Each file in this directory that exports a
|
|
2
|
+
// query() or action() becomes a typed RPC endpoint, callable from your
|
|
3
|
+
// pages and client with full type inference. Delete this placeholder when
|
|
4
|
+
// you add your first one.
|
|
5
|
+
//
|
|
6
|
+
// Example (functions/notes.ts):
|
|
7
|
+
//
|
|
8
|
+
// import { query } from "@pylonsync/functions";
|
|
9
|
+
//
|
|
10
|
+
// export const listNotes = query(async (ctx) => {
|
|
11
|
+
// return ctx.db.list("Note");
|
|
12
|
+
// });
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME_KEBAB__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"deploy": "pylon deploy",
|
|
9
|
+
"check": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
13
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
15
|
+
"react": "^19.0.0",
|
|
16
|
+
"react-dom": "^19.0.0",
|
|
17
|
+
"tailwindcss": "^4.3.0",
|
|
18
|
+
"@tailwindcss/cli": "^4.3.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
22
|
+
"@types/react": "^19.0.0",
|
|
23
|
+
"@types/react-dom": "^19.0.0",
|
|
24
|
+
"typescript": "^5.6.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"lib": ["ES2022", "DOM"],
|
|
11
|
+
"types": ["react", "react-dom"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["app.ts", "app/**/*"]
|
|
14
|
+
}
|