create-runlot 0.1.0-rc.95 → 0.1.0-rc.97
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/dist/index.js +456 -12
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -18,19 +18,21 @@ var POSTS_MIGRATION = [
|
|
|
18
18
|
"where not exists (select 1 from posts);",
|
|
19
19
|
""
|
|
20
20
|
].join("\n");
|
|
21
|
-
var TEMPLATE_NAMES = ["next", "hono", "static"];
|
|
21
|
+
var TEMPLATE_NAMES = ["next", "hono", "static", "studio"];
|
|
22
22
|
function isTemplateName(v) {
|
|
23
23
|
return TEMPLATE_NAMES.includes(v);
|
|
24
24
|
}
|
|
25
25
|
var TEMPLATE_LIST = [
|
|
26
26
|
{ name: "next", desc: "Next.js App Router \u2014 uses env.db and env.auth from server components" },
|
|
27
27
|
{ name: "hono", desc: "Hono worker \u2014 REST API example (/posts, /upload)" },
|
|
28
|
-
{ name: "static", desc: "A single static HTML page \u2014 assets only, no worker" }
|
|
28
|
+
{ name: "static", desc: "A single static HTML page \u2014 assets only, no worker" },
|
|
29
|
+
// Last, so the numbers the other three answer to do not move.
|
|
30
|
+
{ name: "studio", desc: "Vite + React + Tailwind and a Hono API \u2014 what runlot Studio builds" }
|
|
29
31
|
];
|
|
30
32
|
var USAGE = [
|
|
31
|
-
"create-runlot <dir> [--org <slug>] [--template next|hono|static]",
|
|
33
|
+
"create-runlot <dir> [--org <slug>] [--template next|hono|static|studio]",
|
|
32
34
|
"",
|
|
33
|
-
" Creates a project from a Next.js, Hono,
|
|
35
|
+
" Creates a project from a Next.js, Hono, static-site, or studio template.",
|
|
34
36
|
" Without --template, in a terminal, it asks which template to use.",
|
|
35
37
|
" Then run: cd <dir> && runlot deploy"
|
|
36
38
|
].join("\n");
|
|
@@ -170,8 +172,8 @@ function nextTemplate(ctx) {
|
|
|
170
172
|
"react-dom": "^19.2.8",
|
|
171
173
|
// The npm job in release.yml rewrites the version range with the same
|
|
172
174
|
// number as the tag — the value written here is the last published version.
|
|
173
|
-
"@runlot/next": "^0.1.0-rc.
|
|
174
|
-
"@runlot/pg": "^0.1.0-rc.
|
|
175
|
+
"@runlot/next": "^0.1.0-rc.97",
|
|
176
|
+
"@runlot/pg": "^0.1.0-rc.97"
|
|
175
177
|
},
|
|
176
178
|
devDependencies: {
|
|
177
179
|
typescript: "^5.7.2",
|
|
@@ -290,8 +292,8 @@ function honoTemplate(ctx) {
|
|
|
290
292
|
},
|
|
291
293
|
dependencies: {
|
|
292
294
|
hono: "^4.10.7",
|
|
293
|
-
"@runlot/pg": "^0.1.0-rc.
|
|
294
|
-
"@runlot/auth": "^0.1.0-rc.
|
|
295
|
+
"@runlot/pg": "^0.1.0-rc.97",
|
|
296
|
+
"@runlot/auth": "^0.1.0-rc.97"
|
|
295
297
|
},
|
|
296
298
|
devDependencies: {
|
|
297
299
|
typescript: "^5.7.2",
|
|
@@ -378,11 +380,444 @@ function staticTemplate(ctx) {
|
|
|
378
380
|
"README.md": staticReadme(name)
|
|
379
381
|
};
|
|
380
382
|
}
|
|
383
|
+
var STUDIO_MIGRATION = [
|
|
384
|
+
"-- Applied by `runlot deploy` (and by `runlot pg migrate`).",
|
|
385
|
+
"--",
|
|
386
|
+
"-- Forward only. An applied migration is compared by content hash on the next run",
|
|
387
|
+
"-- (`Migration changed after it was applied`), so change the schema by adding",
|
|
388
|
+
"-- 0002_*.sql rather than by editing this file.",
|
|
389
|
+
"create table if not exists notes (",
|
|
390
|
+
" id serial primary key,",
|
|
391
|
+
" owner text not null,",
|
|
392
|
+
" title text not null,",
|
|
393
|
+
" created_at timestamptz not null default now()",
|
|
394
|
+
");",
|
|
395
|
+
"",
|
|
396
|
+
"-- The list is per signed-in user, newest first.",
|
|
397
|
+
"create index if not exists notes_owner_id on notes (owner, id desc);",
|
|
398
|
+
""
|
|
399
|
+
].join("\n");
|
|
400
|
+
var STUDIO_SEED = [
|
|
401
|
+
"-- Development data only. It is applied by `runlot dev`, never by `runlot deploy`.",
|
|
402
|
+
"insert into notes (owner, title)",
|
|
403
|
+
"select 'dev', '\uCCAB \uBA54\uBAA8\uC785\uB2C8\uB2E4'",
|
|
404
|
+
"where not exists (select 1 from notes);",
|
|
405
|
+
""
|
|
406
|
+
].join("\n");
|
|
407
|
+
function studioIndexHtml(name) {
|
|
408
|
+
return [
|
|
409
|
+
"<!doctype html>",
|
|
410
|
+
'<html lang="ko">',
|
|
411
|
+
" <head>",
|
|
412
|
+
' <meta charset="utf-8" />',
|
|
413
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
414
|
+
` <title>${name}</title>`,
|
|
415
|
+
" </head>",
|
|
416
|
+
" <body>",
|
|
417
|
+
' <div id="root"></div>',
|
|
418
|
+
' <script type="module" src="/src/main.tsx"></script>',
|
|
419
|
+
" </body>",
|
|
420
|
+
"</html>",
|
|
421
|
+
""
|
|
422
|
+
].join("\n");
|
|
423
|
+
}
|
|
424
|
+
var STUDIO_VITE_CONFIG = [
|
|
425
|
+
'import tailwindcss from "@tailwindcss/vite";',
|
|
426
|
+
'import react from "@vitejs/plugin-react";',
|
|
427
|
+
'import { defineConfig } from "vite";',
|
|
428
|
+
"",
|
|
429
|
+
"export default defineConfig({",
|
|
430
|
+
" plugins: [react(), tailwindcss()],",
|
|
431
|
+
' // `runlot.json` points "assets" at this directory, and `runlot deploy` ships it as',
|
|
432
|
+
" // it stands on disk \u2014 it does not build it. That is why `npm run deploy` is two",
|
|
433
|
+
" // commands.",
|
|
434
|
+
' build: { outDir: "assets", emptyOutDir: true },',
|
|
435
|
+
" server: {",
|
|
436
|
+
" // /api belongs to the worker. `npm run dev:api` runs it under workerd on 8787,",
|
|
437
|
+
" // with env.db, env.auth and env.storage wired up.",
|
|
438
|
+
' proxy: { "/api": "http://127.0.0.1:8787" },',
|
|
439
|
+
" },",
|
|
440
|
+
"});",
|
|
441
|
+
""
|
|
442
|
+
].join("\n");
|
|
443
|
+
var STUDIO_MAIN_TSX = [
|
|
444
|
+
'import { createRoot } from "react-dom/client";',
|
|
445
|
+
'import { App } from "./App";',
|
|
446
|
+
'import "./styles.css";',
|
|
447
|
+
"",
|
|
448
|
+
"// This module exports no component, so editing it is a full reload rather than a",
|
|
449
|
+
"// Fast Refresh. That is Vite's rule, and it is why App.tsx holds the component.",
|
|
450
|
+
'const root = document.getElementById("root");',
|
|
451
|
+
'if (!root) throw new Error("#root is missing in index.html");',
|
|
452
|
+
"createRoot(root).render(<App />);",
|
|
453
|
+
""
|
|
454
|
+
].join("\n");
|
|
455
|
+
var STUDIO_STYLES_CSS = [
|
|
456
|
+
'@import "tailwindcss";',
|
|
457
|
+
""
|
|
458
|
+
].join("\n");
|
|
459
|
+
var STUDIO_API_TS = [
|
|
460
|
+
"// The worker's answers, typed, and the two calls the page makes.",
|
|
461
|
+
"//",
|
|
462
|
+
"// The paths are relative. The same bundle is served on the dev port, on a Studio",
|
|
463
|
+
"// preview, on a share link and on the project's own hostname, so a hard-coded",
|
|
464
|
+
"// origin would be wrong on three of the four.",
|
|
465
|
+
"export interface Note {",
|
|
466
|
+
" id: number;",
|
|
467
|
+
" title: string;",
|
|
468
|
+
"}",
|
|
469
|
+
"",
|
|
470
|
+
"export interface NotesView {",
|
|
471
|
+
" signedIn: boolean;",
|
|
472
|
+
" notes: Note[];",
|
|
473
|
+
"}",
|
|
474
|
+
"",
|
|
475
|
+
"export async function listNotes(): Promise<NotesView> {",
|
|
476
|
+
' const res = await fetch("/api/notes");',
|
|
477
|
+
" // env.auth.user() answers null until someone signs in, which is every first",
|
|
478
|
+
" // visit \u2014 not an error.",
|
|
479
|
+
" if (res.status === 401) return { signedIn: false, notes: [] };",
|
|
480
|
+
" if (!res.ok) throw new Error(`GET /api/notes: ${res.status}`);",
|
|
481
|
+
" return { signedIn: true, notes: (await res.json()) as Note[] };",
|
|
482
|
+
"}",
|
|
483
|
+
"",
|
|
484
|
+
"export async function addNote(title: string): Promise<void> {",
|
|
485
|
+
' const res = await fetch("/api/notes", {',
|
|
486
|
+
' method: "POST",',
|
|
487
|
+
' headers: { "Content-Type": "application/json" },',
|
|
488
|
+
" body: JSON.stringify({ title }),",
|
|
489
|
+
" });",
|
|
490
|
+
" if (!res.ok) throw new Error(`POST /api/notes: ${res.status}`);",
|
|
491
|
+
"}",
|
|
492
|
+
""
|
|
493
|
+
].join("\n");
|
|
494
|
+
var STUDIO_APP_TSX = [
|
|
495
|
+
'import { useEffect, useState } from "react";',
|
|
496
|
+
'import { addNote, listNotes, type Note } from "./api";',
|
|
497
|
+
"",
|
|
498
|
+
"export function App() {",
|
|
499
|
+
" const [notes, setNotes] = useState<Note[]>([]);",
|
|
500
|
+
" const [signedIn, setSignedIn] = useState(true);",
|
|
501
|
+
' const [title, setTitle] = useState("");',
|
|
502
|
+
" const [error, setError] = useState<string | null>(null);",
|
|
503
|
+
"",
|
|
504
|
+
" function reload(): void {",
|
|
505
|
+
" listNotes()",
|
|
506
|
+
" .then((v) => {",
|
|
507
|
+
" setSignedIn(v.signedIn);",
|
|
508
|
+
" setNotes(v.notes);",
|
|
509
|
+
" setError(null);",
|
|
510
|
+
" })",
|
|
511
|
+
" .catch((e: unknown) => setError(String(e)));",
|
|
512
|
+
" }",
|
|
513
|
+
"",
|
|
514
|
+
" useEffect(reload, []);",
|
|
515
|
+
"",
|
|
516
|
+
" return (",
|
|
517
|
+
' <main className="mx-auto max-w-md p-8">',
|
|
518
|
+
' <h1 className="text-2xl font-semibold">\uBA54\uBAA8</h1>',
|
|
519
|
+
"",
|
|
520
|
+
" {!signedIn && (",
|
|
521
|
+
' <p className="mt-4 text-sm">',
|
|
522
|
+
' <a className="underline" href="/__runlot/auth/sign-in">',
|
|
523
|
+
" \uB85C\uADF8\uC778\uD569\uB2C8\uB2E4",
|
|
524
|
+
" </a>",
|
|
525
|
+
" </p>",
|
|
526
|
+
" )}",
|
|
527
|
+
"",
|
|
528
|
+
' {error && <p className="mt-4 text-sm text-red-600">{error}</p>}',
|
|
529
|
+
"",
|
|
530
|
+
" <form",
|
|
531
|
+
' className="mt-4 flex gap-2"',
|
|
532
|
+
" onSubmit={(e) => {",
|
|
533
|
+
" e.preventDefault();",
|
|
534
|
+
" if (!title.trim()) return;",
|
|
535
|
+
" addNote(title)",
|
|
536
|
+
" .then(() => {",
|
|
537
|
+
' setTitle("");',
|
|
538
|
+
" reload();",
|
|
539
|
+
" })",
|
|
540
|
+
" .catch((err: unknown) => setError(String(err)));",
|
|
541
|
+
" }}",
|
|
542
|
+
" >",
|
|
543
|
+
" <input",
|
|
544
|
+
' className="flex-1 rounded border px-2 py-1"',
|
|
545
|
+
' placeholder="\uBB34\uC5C7\uC744 \uC801\uC744\uAE4C\uC694?"',
|
|
546
|
+
" value={title}",
|
|
547
|
+
" onChange={(e) => setTitle(e.target.value)}",
|
|
548
|
+
" />",
|
|
549
|
+
' <button className="rounded bg-black px-3 py-1 text-white" type="submit">',
|
|
550
|
+
" \uCD94\uAC00",
|
|
551
|
+
" </button>",
|
|
552
|
+
" </form>",
|
|
553
|
+
"",
|
|
554
|
+
' <ul className="mt-6 space-y-1">',
|
|
555
|
+
" {notes.map((n) => (",
|
|
556
|
+
" <li key={n.id}>{n.title}</li>",
|
|
557
|
+
" ))}",
|
|
558
|
+
" </ul>",
|
|
559
|
+
"",
|
|
560
|
+
' {signedIn && notes.length === 0 && <p className="mt-6 text-sm">\uC544\uC9C1 \uBA54\uBAA8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>}',
|
|
561
|
+
" </main>",
|
|
562
|
+
" );",
|
|
563
|
+
"}",
|
|
564
|
+
""
|
|
565
|
+
].join("\n");
|
|
566
|
+
var STUDIO_WORKER_TS = [
|
|
567
|
+
'import type { AuthBinding } from "@runlot/auth";',
|
|
568
|
+
'import { Pool } from "@runlot/pg";',
|
|
569
|
+
'import type { Db, Storage } from "@runlot/pg";',
|
|
570
|
+
'import { Hono } from "hono";',
|
|
571
|
+
"",
|
|
572
|
+
"// Declared in runlot.json; `runlot deploy` creates each one and binds it here.",
|
|
573
|
+
"interface Env {",
|
|
574
|
+
" db: Db;",
|
|
575
|
+
" auth: AuthBinding;",
|
|
576
|
+
" storage: Storage;",
|
|
577
|
+
" // The Vite build output. It ships in the same bundle as this worker.",
|
|
578
|
+
" assets: { fetch(request: Request): Promise<Response> };",
|
|
579
|
+
"}",
|
|
580
|
+
"",
|
|
581
|
+
"const app = new Hono<{ Bindings: Env }>();",
|
|
582
|
+
"",
|
|
583
|
+
'app.get("/api/notes", async (c) => {',
|
|
584
|
+
" const user = await c.env.auth.user(c.req.raw);",
|
|
585
|
+
" // null until someone signs in, which is every first visit.",
|
|
586
|
+
' if (!user) return c.json({ error: "sign in at /__runlot/auth/sign-in" }, 401);',
|
|
587
|
+
"",
|
|
588
|
+
" const db = new Pool({ db: c.env.db });",
|
|
589
|
+
" const { rows } = await db.query<{ id: number; title: string }>(",
|
|
590
|
+
' "select id, title from notes where owner = $1 order by id desc limit 50",',
|
|
591
|
+
" [user.id],",
|
|
592
|
+
" );",
|
|
593
|
+
" return c.json(rows);",
|
|
594
|
+
"});",
|
|
595
|
+
"",
|
|
596
|
+
'app.post("/api/notes", async (c) => {',
|
|
597
|
+
" const user = await c.env.auth.user(c.req.raw);",
|
|
598
|
+
' if (!user) return c.json({ error: "sign in at /__runlot/auth/sign-in" }, 401);',
|
|
599
|
+
"",
|
|
600
|
+
" const body = (await c.req.json()) as { title?: string };",
|
|
601
|
+
" const title = body.title?.trim();",
|
|
602
|
+
' if (!title) return c.json({ error: "title is required" }, 400);',
|
|
603
|
+
"",
|
|
604
|
+
" const db = new Pool({ db: c.env.db });",
|
|
605
|
+
" // One statement per request. The node's ceiling is 10 s and a tenant cannot",
|
|
606
|
+
" // raise it, so work that takes longer belongs in a cron, not in a request.",
|
|
607
|
+
' await db.query("insert into notes (owner, title) values ($1, $2)", [user.id, title]);',
|
|
608
|
+
" return c.json({ ok: true }, 201);",
|
|
609
|
+
"});",
|
|
610
|
+
"",
|
|
611
|
+
'app.get("/api/me", async (c) => {',
|
|
612
|
+
" const user = await c.env.auth.user(c.req.raw);",
|
|
613
|
+
" // The origin is read out of the request, never written as a constant: this same",
|
|
614
|
+
" // bundle answers on the dev port, on a Studio preview, on a share link and on the",
|
|
615
|
+
" // project's own hostname (docs/studio.md \xA76.8).",
|
|
616
|
+
' const signIn = new URL("/__runlot/auth/sign-in", c.req.url).toString();',
|
|
617
|
+
" return c.json({ id: user?.id ?? null, signIn });",
|
|
618
|
+
"});",
|
|
619
|
+
"",
|
|
620
|
+
"// With `main` set the worker is the entry point and static files are not served",
|
|
621
|
+
"// ahead of it, so whatever the routes above did not answer is handed to the assets",
|
|
622
|
+
"// binding here. Without this line every file in assets/ is a 404",
|
|
623
|
+
"// (docs/field-notes.md #71).",
|
|
624
|
+
'app.all("*", (c) => c.env.assets.fetch(c.req.raw));',
|
|
625
|
+
"",
|
|
626
|
+
"export default app;",
|
|
627
|
+
""
|
|
628
|
+
].join("\n");
|
|
629
|
+
function studioAgents(name) {
|
|
630
|
+
return [
|
|
631
|
+
`# ${name}`,
|
|
632
|
+
"",
|
|
633
|
+
"This project runs on Runlot. Read this before changing code: the platform refuses a",
|
|
634
|
+
"few things Node and Next allow, and the refusals are not visible in the type system.",
|
|
635
|
+
"",
|
|
636
|
+
"## What is bound",
|
|
637
|
+
"",
|
|
638
|
+
"| Binding | Declared in `runlot.json` | Used as |",
|
|
639
|
+
"|---|---|---|",
|
|
640
|
+
'| `env.db` | `"database": true` | `new Pool({ db: c.env.db })` \u2014 Postgres |',
|
|
641
|
+
'| `env.auth` | `"auth": true` | `c.env.auth.user(c.req.raw)` \u2014 **null** until someone signs in |',
|
|
642
|
+
'| `env.storage` | `"storage": true` | `c.env.storage.put(key, file)` |',
|
|
643
|
+
'| `env.assets` | `"assets": "assets"` | the Vite build output, served by the catch-all route |',
|
|
644
|
+
"",
|
|
645
|
+
'`env.ai` is deliberately **not** declared. Add `"ai": true` in the same change that',
|
|
646
|
+
"first calls it, and say so in that turn: it is metered.",
|
|
647
|
+
"",
|
|
648
|
+
"## Layout",
|
|
649
|
+
"",
|
|
650
|
+
"- `src/` \u2014 Vite + React + Tailwind. `npm run build` writes it into `assets/`.",
|
|
651
|
+
"- `worker/index.ts` \u2014 one Hono worker. Every API path is under `/api`.",
|
|
652
|
+
"- `migrations/NNNN_*.sql` \u2014 the schema, forward only. `seed.sql` is development data.",
|
|
653
|
+
"",
|
|
654
|
+
"## Refusals",
|
|
655
|
+
"",
|
|
656
|
+
"- **No `node:fs`, and `nodejs_compat` is not a single switch.** Importing a node",
|
|
657
|
+
" built-in requires the flag, which the deploy turns on when it sees the import \u2014 but",
|
|
658
|
+
" *which* built-ins that flag hands out is decided by `compatibilityDate`, not by the",
|
|
659
|
+
" build. Prefer Web APIs (`fetch`, `URL`, `crypto`, `TextEncoder`); there is no file",
|
|
660
|
+
" system at any date.",
|
|
661
|
+
"- **No long-running process.** A request ends when its response does. Recurring work",
|
|
662
|
+
' is a cron (`"triggers": { "crons": [...] }`) or a queue \u2014 never a `setInterval`.',
|
|
663
|
+
"- **10 s per statement.** The node enforces the ceiling and a tenant cannot raise it;",
|
|
664
|
+
" a slower statement ends in `57014`. Split the query or move it to a cron.",
|
|
665
|
+
"- **No install scripts.** A dependency's `postinstall` does not run \u2014 Studio installs",
|
|
666
|
+
" packages itself, and nothing at deploy time runs one either. A package that needs",
|
|
667
|
+
" one to place a native binary does not work here.",
|
|
668
|
+
'- **Build absolute URLs out of the request.** `new URL("/path", request.url)`, never a',
|
|
669
|
+
" hard-coded origin. The same bundle answers on the dev port, on a Studio preview, on",
|
|
670
|
+
" a share link and on the project's own hostname, and a constant is wrong on three of",
|
|
671
|
+
" those four.",
|
|
672
|
+
"- **`runWorkerFirst` is on.** The worker sees every request, so the catch-all",
|
|
673
|
+
' `app.all("*", \u2026)` at the bottom of `worker/index.ts` is what serves `assets/`.',
|
|
674
|
+
" Delete it and every static file is a 404.",
|
|
675
|
+
"",
|
|
676
|
+
"## Hot reload",
|
|
677
|
+
"",
|
|
678
|
+
"The dev loop is Vite's, so the idiom is Vite's: `import.meta.hot`. What survives an",
|
|
679
|
+
"edit depends on what the module exports.",
|
|
680
|
+
"",
|
|
681
|
+
"- **A module that exports only components hot-swaps and keeps state.** `src/App.tsx`",
|
|
682
|
+
" is one. Keep it that way \u2014 a half-filled form surviving an edit is the whole point.",
|
|
683
|
+
"- Anything that is not a component (types, constants, helpers) goes in its own file,",
|
|
684
|
+
" like `src/api.ts`. Exporting one alongside a component downgrades that module to a",
|
|
685
|
+
" reload.",
|
|
686
|
+
"- `worker/` rebuilds by itself: the page stays and the next `/api` call reaches the",
|
|
687
|
+
" new code. A change to `migrations/` or `package.json` is a full reload.",
|
|
688
|
+
"",
|
|
689
|
+
"## Commands",
|
|
690
|
+
"",
|
|
691
|
+
"```",
|
|
692
|
+
"npm install",
|
|
693
|
+
"npm run dev # the UI on :5173, with /api proxied to the worker",
|
|
694
|
+
"npm run dev:api # the worker under workerd with the bindings (second terminal)",
|
|
695
|
+
"npm run deploy # vite build, then runlot deploy",
|
|
696
|
+
"```",
|
|
697
|
+
"",
|
|
698
|
+
"`runlot deploy` ships `assets/` as it stands on disk and does not build it. That is",
|
|
699
|
+
"why `deploy` is two commands, and why `assets/` is in `.gitignore`.",
|
|
700
|
+
""
|
|
701
|
+
].join("\n");
|
|
702
|
+
}
|
|
703
|
+
function studioReadme(name) {
|
|
704
|
+
return [
|
|
705
|
+
`# ${name}`,
|
|
706
|
+
"",
|
|
707
|
+
"Vite + React + Tailwind in `src/`, one Hono worker in `worker/index.ts`, and",
|
|
708
|
+
"Postgres, login and object storage declared in `runlot.json`.",
|
|
709
|
+
"",
|
|
710
|
+
"```",
|
|
711
|
+
"npm install",
|
|
712
|
+
"npm run dev:api # the worker under workerd, with env.db, env.auth, env.storage",
|
|
713
|
+
"npm run dev # the UI, with /api proxied to it",
|
|
714
|
+
"npm run deploy # vite build, then runlot deploy",
|
|
715
|
+
"```",
|
|
716
|
+
"",
|
|
717
|
+
"`npm run deploy` is two commands because `runlot deploy` ships `assets/` as it",
|
|
718
|
+
"stands on disk: the Vite build has to run first.",
|
|
719
|
+
"",
|
|
720
|
+
"`AGENTS.md` states the bindings and what the platform refuses. `CLAUDE.md` is the",
|
|
721
|
+
"same file under the name Claude Code reads.",
|
|
722
|
+
""
|
|
723
|
+
].join("\n");
|
|
724
|
+
}
|
|
725
|
+
function studioTemplate(ctx) {
|
|
726
|
+
const { name, org } = ctx;
|
|
727
|
+
const runlotConfig = { name };
|
|
728
|
+
if (org) runlotConfig["org"] = org;
|
|
729
|
+
runlotConfig["main"] = "worker/index.ts";
|
|
730
|
+
runlotConfig["assets"] = "assets";
|
|
731
|
+
runlotConfig["notFound"] = "spa";
|
|
732
|
+
runlotConfig["runWorkerFirst"] = true;
|
|
733
|
+
runlotConfig["database"] = true;
|
|
734
|
+
runlotConfig["auth"] = true;
|
|
735
|
+
runlotConfig["storage"] = true;
|
|
736
|
+
const packageJson = {
|
|
737
|
+
name,
|
|
738
|
+
version: "0.0.0",
|
|
739
|
+
private: true,
|
|
740
|
+
type: "module",
|
|
741
|
+
scripts: {
|
|
742
|
+
dev: "vite",
|
|
743
|
+
"dev:api": "runlot dev",
|
|
744
|
+
build: "vite build",
|
|
745
|
+
// `runlot deploy` does not build. Without the build in front of it, the deploy
|
|
746
|
+
// ships whatever `assets/` last held — or fails because it is not there.
|
|
747
|
+
deploy: "npm run build && runlot deploy",
|
|
748
|
+
typecheck: "tsc --noEmit"
|
|
749
|
+
},
|
|
750
|
+
dependencies: {
|
|
751
|
+
hono: "^4.10.7",
|
|
752
|
+
react: "^19.2.8",
|
|
753
|
+
"react-dom": "^19.2.8",
|
|
754
|
+
// The npm job in release.yml rewrites these ranges with the same number as the
|
|
755
|
+
// tag — what is written here is the last published version.
|
|
756
|
+
"@runlot/pg": "^0.1.0-rc.97",
|
|
757
|
+
"@runlot/auth": "^0.1.0-rc.97"
|
|
758
|
+
},
|
|
759
|
+
devDependencies: {
|
|
760
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
761
|
+
"@types/react": "^19.2.18",
|
|
762
|
+
"@types/react-dom": "^19.2.5",
|
|
763
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
764
|
+
tailwindcss: "^4.3.3",
|
|
765
|
+
typescript: "^5.7.2",
|
|
766
|
+
vite: "^8.2.2"
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const tsconfig = pkgJson({
|
|
770
|
+
compilerOptions: {
|
|
771
|
+
target: "ES2022",
|
|
772
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
773
|
+
module: "ESNext",
|
|
774
|
+
moduleResolution: "bundler",
|
|
775
|
+
jsx: "react-jsx",
|
|
776
|
+
strict: true,
|
|
777
|
+
skipLibCheck: true,
|
|
778
|
+
noEmit: true,
|
|
779
|
+
// `import.meta.hot` and `import.meta.env` are typed by this and nothing else.
|
|
780
|
+
types: ["vite/client"]
|
|
781
|
+
},
|
|
782
|
+
include: ["src", "worker", "vite.config.ts"]
|
|
783
|
+
});
|
|
784
|
+
const agents = studioAgents(name);
|
|
785
|
+
return {
|
|
786
|
+
"package.json": pkgJson(packageJson),
|
|
787
|
+
"runlot.json": pkgJson(runlotConfig),
|
|
788
|
+
"tsconfig.json": tsconfig,
|
|
789
|
+
"vite.config.ts": STUDIO_VITE_CONFIG,
|
|
790
|
+
"index.html": studioIndexHtml(name),
|
|
791
|
+
"src/main.tsx": STUDIO_MAIN_TSX,
|
|
792
|
+
"src/App.tsx": STUDIO_APP_TSX,
|
|
793
|
+
"src/api.ts": STUDIO_API_TS,
|
|
794
|
+
"src/styles.css": STUDIO_STYLES_CSS,
|
|
795
|
+
"worker/index.ts": STUDIO_WORKER_TS,
|
|
796
|
+
"migrations/0001_init.sql": STUDIO_MIGRATION,
|
|
797
|
+
"seed.sql": STUDIO_SEED,
|
|
798
|
+
// Both names, one text. Claude Code reads CLAUDE.md and other agents read
|
|
799
|
+
// AGENTS.md; generating them together is what keeps them saying the same thing.
|
|
800
|
+
"AGENTS.md": agents,
|
|
801
|
+
"CLAUDE.md": `<!-- The same text as AGENTS.md. Change both, or the two agents disagree. -->
|
|
802
|
+
${agents}`,
|
|
803
|
+
// `assets/` is build output. `.runlot` holds the unpacked bundle and the local
|
|
804
|
+
// object store, and `.env.runlot` holds secrets.
|
|
805
|
+
".gitignore": "node_modules\nassets\n.runlot\n.env.runlot\n",
|
|
806
|
+
"README.md": studioReadme(name)
|
|
807
|
+
};
|
|
808
|
+
}
|
|
381
809
|
var TEMPLATES = {
|
|
382
810
|
next: nextTemplate,
|
|
383
811
|
hono: honoTemplate,
|
|
384
|
-
static: staticTemplate
|
|
812
|
+
static: staticTemplate,
|
|
813
|
+
studio: studioTemplate
|
|
385
814
|
};
|
|
815
|
+
function templateFiles(template, ctx = {}) {
|
|
816
|
+
if (!isTemplateName(template)) {
|
|
817
|
+
throw new Error(`"${template}" is not a template. Use one of: ${TEMPLATE_NAMES.join(", ")}.`);
|
|
818
|
+
}
|
|
819
|
+
return TEMPLATES[template]({ name: ctx.name ?? "app", org: ctx.org });
|
|
820
|
+
}
|
|
386
821
|
function scaffold(opts) {
|
|
387
822
|
const { target, org, template } = opts;
|
|
388
823
|
const dir = resolve(target);
|
|
@@ -400,8 +835,15 @@ function scaffold(opts) {
|
|
|
400
835
|
}
|
|
401
836
|
console.log(`Created ${dir}.`);
|
|
402
837
|
console.log(` cd ${target}`);
|
|
403
|
-
if (template
|
|
404
|
-
|
|
838
|
+
if (template === "studio") {
|
|
839
|
+
console.log(" npm install");
|
|
840
|
+
console.log(" npm run dev:api # a second terminal: the worker with the bindings");
|
|
841
|
+
console.log(" npm run dev");
|
|
842
|
+
console.log(org ? " npm run deploy" : " npm run deploy -- --org <slug>");
|
|
843
|
+
} else {
|
|
844
|
+
if (template !== "next") console.log(" runlot dev");
|
|
845
|
+
console.log(org ? " runlot deploy" : " runlot deploy --org <slug>");
|
|
846
|
+
}
|
|
405
847
|
return { dir, files: relPaths };
|
|
406
848
|
}
|
|
407
849
|
async function defaultAsk(question) {
|
|
@@ -489,7 +931,9 @@ if (runDirectly(process.argv[1])) {
|
|
|
489
931
|
main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
490
932
|
}
|
|
491
933
|
export {
|
|
934
|
+
TEMPLATE_NAMES,
|
|
492
935
|
main,
|
|
493
936
|
runDirectly,
|
|
494
|
-
scaffold
|
|
937
|
+
scaffold,
|
|
938
|
+
templateFiles
|
|
495
939
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-runlot",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.97",
|
|
4
4
|
"description": "npm create runlot — Next.js, Hono, and static-site templates",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"directory": "web/apps/create-runlot"
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.js"
|
|
16
|
+
},
|
|
14
17
|
"bin": {
|
|
15
18
|
"create-runlot": "./dist/index.js"
|
|
16
19
|
},
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
"esbuild": "^0.24.2",
|
|
22
25
|
"typescript": "^5.7.2",
|
|
23
26
|
"@types/node": "^22.10.2",
|
|
24
|
-
"@runlot/config": "0.1.0-rc.
|
|
27
|
+
"@runlot/config": "0.1.0-rc.97"
|
|
25
28
|
},
|
|
26
29
|
"engines": {
|
|
27
30
|
"node": ">=20"
|