toiljs 0.0.115 → 0.0.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
package/src/cli/create.ts DELETED
@@ -1,924 +0,0 @@
1
- /**
2
- * `toiljs create`, an interactive project scaffolder (Clack-powered) that wires a new
3
- * app to the enforced toiljs presets (tsconfig / eslint / prettier) and file-based routing.
4
- * Supports a non-interactive path via flags (`--yes`, `--template`, …) for scripting/CI.
5
- */
6
- import fs from 'node:fs/promises';
7
- import path from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- import {
11
- cancel,
12
- confirm,
13
- intro,
14
- isCancel,
15
- multiselect,
16
- note,
17
- outro,
18
- select,
19
- spinner,
20
- text,
21
- } from '@clack/prompts';
22
- import { AI_HELPER_IDS, AI_HELPERS, aiHelperFiles, TOIL_DOCS, TOIL_ENV_DTS } from 'toiljs/compiler';
23
- import pc from 'picocolors';
24
-
25
- import {
26
- PKG_VERSION,
27
- type Preprocessor,
28
- PREPROCESSORS,
29
- requiredPackages,
30
- setStyleImports,
31
- styleEntry,
32
- type StyleFeatures,
33
- styleImportLines,
34
- TAILWIND_CSS,
35
- TAILWIND_ENTRY,
36
- } from './features.js';
37
- import { run } from './proc.js';
38
- import { accent, dim, version } from './ui.js';
39
- import { isPackageManager, isValidName, resolveProjectDir } from './validate.js';
40
-
41
- export type Template = 'app' | 'minimal' | 'agent';
42
-
43
- /**
44
- * The `server/migrations/` README, scaffolded by `create` and ensured by `doctor`/`update`. ToilDB
45
- * migrations are enforced by convention: a `@migrate` function MUST live in a `*.migration.ts` file
46
- * under a `migrations/` folder (the toilscript compiler raises a compile error otherwise), and the
47
- * build auto-discovers every such file. Keeping the convention documented next to the folder means a
48
- * fresh project ships with a place to evolve `@data` value types from day one.
49
- */
50
- export const MIGRATIONS_README =
51
- '# migrations/\n\n' +
52
- 'ToilDB schema migrations. One file per evolving `@data` value type, named `<Type>.migration.ts`\n' +
53
- '(e.g. `User.migration.ts`).\n\n' +
54
- 'Each file keeps the OLD `@data` shapes (e.g. `UserV1`) alongside the `@migrate` transform(s) that\n' +
55
- 'carry old records forward, and imports the CURRENT value type from the app. The build\n' +
56
- 'auto-discovers every `*.migration.ts` under the project.\n\n' +
57
- 'Do NOT put `@migrate` anywhere else: a `@migrate` outside a `*.migration.ts` file in a\n' +
58
- '`migrations/` folder is a compile error.\n\n' +
59
- '## Example\n\n' +
60
- '`User.migration.ts`:\n\n' +
61
- '```ts\n' +
62
- "import { User } from '../models/User';\n\n" +
63
- '// The previous on-disk shape of a User record.\n' +
64
- '@data\n' +
65
- 'export class UserV1 {\n' +
66
- " name: string = '';\n" +
67
- '}\n\n' +
68
- '// Carry a UserV1 forward into the current User. Runs once per record on read.\n' +
69
- '@migrate\n' +
70
- 'export function up(old: UserV1, into: User): void {\n' +
71
- ' into.name = old.name;\n' +
72
- '}\n' +
73
- '```\n';
74
-
75
- /** Human label for each preprocessor in the styling picker. */
76
- const PREPROCESSOR_LABEL: Record<Preprocessor, string> = {
77
- css: 'Plain CSS',
78
- sass: 'Sass (SCSS)',
79
- less: 'Less',
80
- stylus: 'Stylus',
81
- };
82
-
83
- /** Default global stylesheet contents (palette base styles), shared by every preprocessor. */
84
- const DEFAULT_STYLE_CONTENT =
85
- ':root {\n color-scheme: dark;\n}\n\n' +
86
- 'body {\n margin: 0;\n background: #080d11;\n color: #f5f6fa;\n' +
87
- ' font-family: system-ui, -apple-system, sans-serif;\n line-height: 1.6;\n}\n\n' +
88
- 'a {\n color: #2563ff;\n text-decoration: none;\n}\n\n' +
89
- 'a:hover {\n color: #22e3ab;\n}\n\n' +
90
- 'code {\n background: #11161f;\n color: #22e3ab;\n padding: 0.1rem 0.4rem;\n' +
91
- ' border-radius: 4px;\n font-size: 0.9em;\n}\n\n' +
92
- 'h1 {\n background: linear-gradient(90deg, #2563ff, #7c3aed, #22e3ab);\n' +
93
- ' -webkit-background-clip: text;\n background-clip: text;\n color: transparent;\n}\n';
94
-
95
- /** A selectable template in the `create` wizard. */
96
- interface TemplateOption {
97
- readonly value: Template;
98
- readonly label: string;
99
- readonly hint: string;
100
- }
101
-
102
- export interface CreateOptions {
103
- readonly name?: string;
104
- readonly template?: Template;
105
- readonly preprocessor?: Preprocessor;
106
- readonly tailwind?: boolean;
107
- /** AI assistant files to scaffold: `true` = all, `false` = none, omitted = ask. */
108
- readonly ai?: boolean;
109
- /** Enable build-time image optimization. Default `true`; omitted = ask. */
110
- readonly images?: boolean;
111
- readonly install?: boolean;
112
- readonly git?: boolean;
113
- readonly pm?: string;
114
- readonly yes?: boolean;
115
- readonly cwd: string;
116
- }
117
-
118
- /** Aborts the wizard cleanly on Ctrl-C / cancel, narrowing the prompt result to its value type. */
119
- function bail<T>(value: T | symbol): asserts value is T {
120
- if (isCancel(value)) {
121
- cancel('Scaffolding cancelled.');
122
- process.exit(0);
123
- }
124
- }
125
-
126
- async function isEmptyDir(dir: string): Promise<boolean> {
127
- try {
128
- const entries = await fs.readdir(dir);
129
- return entries.length === 0;
130
- } catch {
131
- return true;
132
- }
133
- }
134
-
135
- /** Builds the full file map (relative path → contents) for a scaffolded project. */
136
- function scaffold(
137
- name: string,
138
- template: Template,
139
- features: StyleFeatures,
140
- aiTools: readonly string[],
141
- images: boolean,
142
- ): Record<string, string> {
143
- const toilVersion = version();
144
- const devDependencies: Record<string, string> = {
145
- '@types/react': '^19.2.15',
146
- '@types/react-dom': '^19.2.3',
147
- eslint: '^10.2.0',
148
- prettier: '^3.8.1',
149
- toilscript: '^0.1.55',
150
- typescript: '^6.0.3',
151
- };
152
- for (const dep of requiredPackages(features).sort()) {
153
- devDependencies[dep] = PKG_VERSION[dep] ?? 'latest';
154
- }
155
- const pkg = {
156
- name: path.basename(name),
157
- private: true,
158
- type: 'module',
159
- scripts: {
160
- start: 'toiljs start',
161
- dev: 'toiljs dev',
162
- build: 'toiljs build',
163
- 'build:server': 'toiljs build --server',
164
- lint: 'eslint client',
165
- typecheck: 'tsc --noEmit',
166
- format: 'prettier --write "client/**/*.{ts,tsx,css,scss,less}" "client/public/**/*.html" "server/**/*.ts"',
167
- },
168
- dependencies: {
169
- toiljs: `^${toilVersion}`,
170
- react: '^19.2.6',
171
- 'react-dom': '^19.2.6',
172
- },
173
- devDependencies,
174
- };
175
-
176
- const files: Record<string, string> = {
177
- 'package.json': JSON.stringify(pkg, null, 4) + '\n',
178
- 'toil.config.ts':
179
- "import { defineConfig } from 'toiljs/compiler';\n\n" +
180
- 'export default defineConfig({\n' +
181
- ' client: {\n' +
182
- ' // Optimize images at build time (resize/compress imported images).\n' +
183
- ` images: ${String(images)},\n` +
184
- ' },\n' +
185
- '});\n',
186
- 'tsconfig.json':
187
- '{\n' +
188
- ' "extends": "toiljs/tsconfig",\n' +
189
- ' "compilerOptions": {\n' +
190
- ' "paths": { "shared/*": ["./shared/*"] }\n' +
191
- ' },\n' +
192
- ' "include": ["client", "shared", "emails", "toil.config.ts", "toil-env.d.ts", "toil-routes.d.ts"]\n' +
193
- '}\n',
194
- 'eslint.config.js': "import toiljs from 'toiljs/eslint';\n\nexport default toiljs;\n",
195
- '.prettierrc': '"toiljs/prettier"\n',
196
- // Generated files don't need formatting. (toilscript server decorators like @main /
197
- // @remote-on-functions are handled by the toiljs/prettier-plugin, so server/ is not ignored.)
198
- '.prettierignore':
199
- 'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nserver/_emails.ts\nserver/_ssr/\nserver/toil-server-env.d.ts\n',
200
- '.gitignore':
201
- 'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nserver/_ssr/\nhosts/*/_tmpl/\n# Local dev env vars/secrets (never commit)\n.env\n.env.secrets\n',
202
- // Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled
203
- // version, and prompt to switch, so the editor loads the toilscript LS plugin wired
204
- // in server/tsconfig.json (which clears the @database / @data editor false positives).
205
- '.vscode/settings.json':
206
- JSON.stringify(
207
- {
208
- 'typescript.tsdk': 'node_modules/typescript/lib',
209
- 'typescript.enablePromptUseWorkspaceTsdk': true,
210
- },
211
- null,
212
- 4,
213
- ) + '\n',
214
- 'toil-env.d.ts': TOIL_ENV_DTS,
215
- // Stub typed-routes augmentation (RoutePath = string until the first dev/build regenerates it).
216
- 'toil-routes.d.ts': '// AUTO-GENERATED by toil, do not edit.\nexport {};\n',
217
- 'toilconfig.json':
218
- JSON.stringify(
219
- {
220
- // `toiljs build` compiles every decorated server file (recursively) so
221
- // dropped-in @data/@rest files are picked up; main.ts imports the surface
222
- // modules so a direct `toilscript` run builds the same server.
223
- entries: ['server/main.ts'],
224
- targets: {
225
- release: {
226
- outFile: 'build/server/release.wasm',
227
- textFile: 'build/server/release.wat',
228
- },
229
- },
230
- options: {
231
- sourceMap: false,
232
- optimizeLevel: 3,
233
- shrinkLevel: 1,
234
- converge: true,
235
- noAssert: false,
236
- enable: [
237
- 'sign-extension',
238
- 'mutable-globals',
239
- 'nontrapping-f2i',
240
- 'bulk-memory',
241
- 'simd',
242
- 'reference-types',
243
- 'multi-value',
244
- ],
245
- runtime: 'stub',
246
- // toiljs server globals (exports become ambient, used
247
- // with no import -- e.g. `AuthService` for the
248
- // post-quantum auth primitive), exactly like `crypto`.
249
- lib: ['node_modules/toiljs/server/globals'],
250
- // Reserve [0, 64 KiB) for the request envelope the
251
- // edge writes at offset 0. Static data starts ABOVE
252
- // it, so a large request can never overwrite guest
253
- // state; the edge rejects envelopes past this window.
254
- // Raise to accept larger request bodies (costs
255
- // initial memory).
256
- memoryBase: 65536,
257
- initialMemory: 4,
258
- debug: false,
259
- trapMode: 'allow',
260
- },
261
- },
262
- null,
263
- 4,
264
- ) + '\n',
265
- // The toilscript LS plugin teaches the editor about compiler-injected members
266
- // (`@database` static collections like `Db.users`, the `@data` codec, `@user`), so
267
- // stock TypeScript stops false-flagging them as TS2339. Editor-only; ignored by tsc.
268
- 'server/tsconfig.json':
269
- JSON.stringify(
270
- {
271
- extends: 'toilscript/std/assembly.json',
272
- compilerOptions: {
273
- plugins: [{ name: 'toilscript/std/ts-plugin.cjs' }],
274
- },
275
- include: ['./**/*.ts'],
276
- },
277
- null,
278
- 4,
279
- ) + '\n',
280
- 'README.md': [
281
- '# ' + path.basename(name),
282
- '',
283
- 'A [toiljs](https://toil.org) app.',
284
- '',
285
- '## Develop',
286
- '',
287
- ' npm install',
288
- ' npm run dev',
289
- '',
290
- '## Build',
291
- '',
292
- ' npm run build',
293
- '',
294
- ].join('\n'),
295
- // ToilDB migrations live under server/migrations/ (folder + `*.migration.ts` is enforced by
296
- // the compiler; the build auto-discovers them). Ship the folder, documented, with no live
297
- // *.migration.ts: a @migrate referencing types a fresh project lacks would break the build.
298
- 'server/migrations/README.md': MIGRATIONS_README,
299
- };
300
-
301
- // The `app` template's client UI + server are copied from examples/basic at runtime; the
302
- // `minimal` and `agent` templates ship an inline client + working server here. `agent` adds
303
- // one correctly-structured @rest endpoint (a @data model + controller) and a home page that
304
- // calls it, the smallest complete client-to-backend loop for an agent to build on.
305
- if (template === 'minimal') {
306
- Object.assign(files, minimalClient(name, features));
307
- Object.assign(files, minimalServer());
308
- } else if (template === 'agent') {
309
- Object.assign(files, agentClient(name, features));
310
- Object.assign(files, agentServer());
311
- }
312
-
313
- // Selected AI-assistant pointer files at the root (committed). The real docs are always seeded
314
- // under .toil/docs (gitignored; regenerated by dev/build) since the framework manages them.
315
- Object.assign(files, aiHelperFiles(aiTools));
316
- for (const [docName, content] of Object.entries(TOIL_DOCS)) {
317
- files[`.toil/docs/${docName}`] = content;
318
- }
319
-
320
- return files;
321
- }
322
-
323
- /**
324
- * Editor-only ambient declarations for the toiljs cookie globals (`Cookie` /
325
- * `Cookies` / `SecureCookies` / `SameSite` / ...). They are `@global` in the
326
- * runtime, so handlers use them without an import (like `crypto`); this gives the
327
- * editor their shapes. Auto-included via the server tsconfig and ignored by the
328
- * compiler. Keep in sync with `toiljs/server/runtime/http/*`.
329
- */
330
- // Editor-only ambient declarations for the server-runtime globals, scaffolded
331
- // into the server dir. Kept BYTE-IDENTICAL to `TOIL_SERVER_ENV_DTS` in
332
- // src/compiler/generate.ts (which `toiljs build`/`dev` regenerate, and `doctor
333
- // --fix` rewrites), so create / build / doctor never disagree and flip-flop.
334
- export const TOIL_SERVER_ENV_DTS = `// AUTO-GENERATED by toil, do not edit. Editor-only ambient declarations for
335
- // the toiljs server-runtime globals (Cookie, Cookies, SecureCookies, the
336
- // cookie enums): @global in the runtime, used with no import. These alias the
337
- // real runtime types so a global-built Cookie is exactly what Response.setCookie
338
- // / SecureCookies.seal expect. The toilscript compiler registers them itself.
339
- declare const SameSite: typeof import('toiljs/server/runtime/http/cookie').SameSite;
340
- type SameSite = import('toiljs/server/runtime/http/cookie').SameSite;
341
- declare const CookieEncoding: typeof import('toiljs/server/runtime/http/cookie').CookieEncoding;
342
- type CookieEncoding = import('toiljs/server/runtime/http/cookie').CookieEncoding;
343
- declare const CookiePrefix: typeof import('toiljs/server/runtime/http/cookie').CookiePrefix;
344
- type CookiePrefix = import('toiljs/server/runtime/http/cookie').CookiePrefix;
345
- declare const CookieValidation: typeof import('toiljs/server/runtime/http/cookie').CookieValidation;
346
- type CookieValidation = import('toiljs/server/runtime/http/cookie').CookieValidation;
347
- declare const Cookie: typeof import('toiljs/server/runtime/http/cookie').Cookie;
348
- type Cookie = import('toiljs/server/runtime/http/cookie').Cookie;
349
- declare const CookieMap: typeof import('toiljs/server/runtime/http/cookies').CookieMap;
350
- type CookieMap = import('toiljs/server/runtime/http/cookies').CookieMap;
351
- declare const Cookies: typeof import('toiljs/server/runtime/http/cookies').Cookies;
352
- type Cookies = import('toiljs/server/runtime/http/cookies').Cookies;
353
- declare const SecureCookies: typeof import('toiljs/server/runtime/http/securecookies').SecureCookies;
354
- type SecureCookies = import('toiljs/server/runtime/http/securecookies').SecureCookies;
355
- declare const Time: typeof import('toiljs/server/runtime/time').Time;
356
- // Email, rate-limit, 2FA, and auth globals (server/globals/*), hand-declared
357
- // because their AssemblyScript source can't be type-aliased from tsc.
358
- declare enum EmailStatus { Sent, Disabled, Budget, RecipientCapped, Deduped, TryLater, BadRecipient, ProviderError }
359
- declare namespace EmailService { function send(to: string, subject: string, body: string, purpose?: string, html?: string): EmailStatus; }
360
- declare class RenderedEmail { subject: string; body: string; html: string; constructor(subject: string, body: string, html: string); }
361
- declare class EmailTemplate { constructor(subject: string, body: string, html?: string); render(vars: Map<string, string>): RenderedEmail; send(to: string, vars: Map<string, string>, purpose?: string): EmailStatus; }
362
- declare namespace RateLimitService { function guard(routeId: i32, strategy: i32, limit: i32, window: i32): import('toiljs/server/runtime/response').Response | null; function guardKeyed(routeId: i32, strategy: i32, limit: i32, window: i32, key: string): import('toiljs/server/runtime/response').Response | null; }
363
- declare namespace Environment { function get(key: string): string | null; function getSecure(key: string): string | null; }
364
- declare class TwoFactorIssue { code: string; token: string; constructor(code: string, token: string); }
365
- declare class TwoFactorChallenge { token: string; status: EmailStatus; constructor(token: string, status: EmailStatus); }
366
- declare namespace TwoFactor { const DEFAULT_TTL_SECS: u64; const DEFAULT_DIGITS: i32; function setSecret(secret: Uint8Array): void; function issue(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorIssue; function send(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorChallenge; function verify(token: string, recipient: string, code: string): bool; }
367
- declare namespace AuthService { const SESSION_COOKIE: string; const USER_COOKIE: string; const LOGIN_CONTEXT: string; const PUBLIC_KEY_LEN: i32; const SIGNATURE_LEN: i32; const DEFAULT_SESSION_TTL_SECS: u64; function setSecret(secret: Uint8Array): void; function hasSession(): bool; function getSessionBytes(): Uint8Array | null; function getUser(): __ToilAuthUser | null; function mintSession(userData: Uint8Array, ttlSecs?: u64): Cookie; function clearSession(): Cookie; function userCookie(userData: Uint8Array, ttlSecs?: u64): Cookie; function clearUserCookie(): Cookie; function buildLoginMessage(sub: string, aud: string, cid: Uint8Array, nonce: Uint8Array, iat: u64, exp: u64): Uint8Array; function verifyLogin(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): bool; }
368
- interface __ToilAuthUser {}
369
- `;
370
-
371
- /** The `server/README.md` shipped by the inline templates: the folder-conventions map. */
372
- const SERVER_README =
373
- '# server/\n\n' +
374
- 'Your ToilScript backend, compiled to a single WebAssembly module. One folder per concern:\n\n' +
375
- '| Folder | What lives here |\n' +
376
- '| --- | --- |\n' +
377
- '| `main.ts` | The entry point: wires the handler and imports the surface modules. |\n' +
378
- '| `core/` | The request handler and shared app logic (state, helpers). |\n' +
379
- '| `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |\n' +
380
- '| `migrations/` | ToilDB schema migrations: a `<Type>.migration.ts` per evolving `@data` value type, holding the old shapes + the `@migrate` transform. |\n' +
381
- '| `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |\n' +
382
- '| `services/` | `@service` classes and free `@remote` functions (typed RPC). |\n' +
383
- '| `scheduled/` | Reserved for scheduled tasks (not shipped yet). |\n\n' +
384
- 'New decorated files are picked up automatically by `toiljs build`/`dev`; also add an import\n' +
385
- 'in `main.ts` so a direct `toilscript` run builds the same server.\n';
386
-
387
- /**
388
- * A minimal but working server for the `minimal` template (the `app` template copies
389
- * examples/basic/server). Same folder conventions as the full starter, just fewer files:
390
- * the entry in main.ts, the handler under core/, and a README mapping where new
391
- * routes/services/models go.
392
- */
393
- function minimalServer(): Record<string, string> {
394
- return {
395
- 'server/toil-server-env.d.ts': TOIL_SERVER_ENV_DTS,
396
- 'server/core/AppHandler.ts':
397
- "import { ToilHandler, Request, Response, Method } from 'toiljs/server/runtime';\n\n" +
398
- '/** Every request enters here. Add `@rest` controllers under routes/ as you grow. */\n' +
399
- 'export class AppHandler extends ToilHandler {\n' +
400
- ' public handle(req: Request): Response {\n' +
401
- ' if (req.method != Method.GET && req.method != Method.HEAD) {\n' +
402
- " return Response.empty(405).setHeader('allow', 'GET, HEAD');\n" +
403
- ' }\n' +
404
- " if (req.path == '/api/hello') {\n" +
405
- " return Response.text('hello from toiljs\\n');\n" +
406
- ' }\n' +
407
- " if (req.path == '/api/hash') {\n" +
408
- ' // `crypto` is a global (no import), synchronous Web Crypto.\n' +
409
- " return Response.text(crypto.toHex(crypto.sha256Text(req.path)) + '\\n');\n" +
410
- ' }\n' +
411
- ' // Yield page routes and assets to the client: under `toiljs dev`\n' +
412
- ' // this falls through to Vite so the app renders at /.\n' +
413
- ' return Response.unhandled();\n' +
414
- ' }\n' +
415
- '}\n',
416
- 'server/main.ts':
417
- "import { Server } from 'toiljs/server/runtime';\n" +
418
- "import { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\n" +
419
- "import { AppHandler } from './core/AppHandler';\n\n" +
420
- '// As you add surface modules (@rest routes, @service/@remote RPC), import them here\n' +
421
- '// so a direct `toilscript` run builds the same server `toiljs build` does, e.g.:\n' +
422
- "// import './routes/Players';\n\n" +
423
- '// Wire your handler here.\n' +
424
- 'Server.handler = () => new AppHandler();\n\n' +
425
- '// Required: re-export the WASM entry points and the abort hook.\n' +
426
- "export * from 'toiljs/server/runtime/exports';\n" +
427
- 'export function abort(message: string, fileName: string, line: u32, column: u32): void {\n' +
428
- ' revertOnError(message, fileName, line, column);\n' +
429
- '}\n',
430
- 'server/README.md': SERVER_README,
431
- };
432
- }
433
-
434
- /**
435
- * A working server for the `agent` template: one correctly-structured `@rest` endpoint. A `@data`
436
- * model under models/, a controller under routes/, an AppHandler that tries the REST controllers
437
- * via `Rest.dispatch` (then yields to the client), and main.ts importing the route. The server
438
- * build turns this into the typed `Server.REST.greeting.hello()` fetch the home page calls.
439
- */
440
- function agentServer(): Record<string, string> {
441
- return {
442
- 'server/toil-server-env.d.ts': TOIL_SERVER_ENV_DTS,
443
- 'server/models/Greeting.ts':
444
- '/** The response body for `GET /greeting`. `@data` classes cross the wire as JSON and\n' +
445
- ' * land on the client as this exact typed class. One `@data` type per file. */\n' +
446
- '@data\n' +
447
- 'export class Greeting {\n' +
448
- " message: string = '';\n" +
449
- '}\n',
450
- 'server/routes/Greet.ts':
451
- "import { Greeting } from '../models/Greeting';\n\n" +
452
- '/**\n' +
453
- ' * The greeting endpoint, mounted at `/greeting`. `@rest` / `@get` are ambient compiler\n' +
454
- ' * decorators (no import). On the client this is one typed fetch:\n' +
455
- ' * await Server.REST.greeting.hello()\n' +
456
- ' */\n' +
457
- "@rest('greeting')\n" +
458
- 'class Greet {\n' +
459
- ' /** `GET /greeting` - returns a `Greeting`, serialized to JSON for the client. */\n' +
460
- " @get('/')\n" +
461
- ' public hello(): Greeting {\n' +
462
- ' const g = new Greeting();\n' +
463
- " g.message = 'Hello from the toiljs backend';\n" +
464
- ' return g;\n' +
465
- ' }\n' +
466
- '}\n',
467
- 'server/core/AppHandler.ts':
468
- "import { Request, Response, Rest, ToilHandler } from 'toiljs/server/runtime';\n\n" +
469
- '/**\n' +
470
- ' * Every request enters here. The `@rest` controllers under routes/ are tried first via\n' +
471
- ' * `Rest.dispatch`; anything they do not claim yields to the client (page routes, assets).\n' +
472
- ' */\n' +
473
- 'export class AppHandler extends ToilHandler {\n' +
474
- ' public handle(req: Request): Response {\n' +
475
- ' // Rest.dispatch returns the first matching route\'s Response, or null when nothing\n' +
476
- ' // matched. REST composes; it never takes over handle().\n' +
477
- ' const hit = Rest.dispatch(req);\n' +
478
- ' if (hit != null) {\n' +
479
- ' return hit;\n' +
480
- ' }\n' +
481
- ' // Nothing matched: yield to the client. Under `toiljs dev` this falls through to\n' +
482
- ' // Vite so the app renders at /.\n' +
483
- ' return Response.unhandled();\n' +
484
- ' }\n' +
485
- '}\n',
486
- 'server/main.ts':
487
- "import { Server } from 'toiljs/server/runtime';\n" +
488
- "import { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\n" +
489
- "import { AppHandler } from './core/AppHandler';\n\n" +
490
- '// Surface modules: import every @rest route (and @service/@remote RPC) here so a direct\n' +
491
- '// `toilscript` run builds the same server `toiljs build` does.\n' +
492
- "import './routes/Greet';\n\n" +
493
- '// Wire your handler here.\n' +
494
- 'Server.handler = () => new AppHandler();\n\n' +
495
- '// Required: re-export the WASM entry points and the abort hook.\n' +
496
- "export * from 'toiljs/server/runtime/exports';\n" +
497
- 'export function abort(message: string, fileName: string, line: u32, column: u32): void {\n' +
498
- ' revertOnError(message, fileName, line, column);\n' +
499
- '}\n',
500
- 'server/README.md': SERVER_README,
501
- };
502
- }
503
-
504
- /**
505
- * The shared client shell for the inline templates (`minimal`, `agent`): the public assets,
506
- * `toil.tsx` mount, global stylesheet, and the (optional) Tailwind entry. Each template layers
507
- * its own `layout.tsx` + `routes/` on top; the `app` template copies examples/basic/client instead.
508
- */
509
- function clientShell(name: string, features: StyleFeatures): Record<string, string> {
510
- const files: Record<string, string> = {
511
- 'client/public/index.html':
512
- '<!doctype html>\n<html lang="en">\n <head>\n' +
513
- ' <meta charset="utf-8" />\n' +
514
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />\n' +
515
- ' <meta name="theme-color" content="#080D11" />\n' +
516
- ' <meta name="description" content="" />\n' +
517
- ' <link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n' +
518
- ' <link rel="manifest" href="/manifest.webmanifest" />\n' +
519
- ` <title>${path.basename(name)}</title>\n` +
520
- ' </head>\n <body>\n <div id="root"></div>\n </body>\n</html>\n',
521
- 'client/public/favicon.svg':
522
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">\n' +
523
- ' <defs>\n' +
524
- ' <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">\n' +
525
- ' <stop offset="0" stop-color="#2563FF" />\n' +
526
- ' <stop offset="0.5" stop-color="#7C3AED" />\n' +
527
- ' <stop offset="1" stop-color="#22E3AB" />\n' +
528
- ' </linearGradient>\n' +
529
- ' </defs>\n' +
530
- ' <rect width="32" height="32" rx="7" fill="#080D11" />\n' +
531
- ' <path d="M9 10h14v3.2h-5.4V24h-3.2V13.2H9z" fill="url(#g)" />\n' +
532
- '</svg>\n',
533
- 'client/public/robots.txt': 'User-agent: *\nAllow: /\n',
534
- 'client/public/manifest.webmanifest':
535
- JSON.stringify(
536
- {
537
- name: path.basename(name),
538
- short_name: path.basename(name),
539
- start_url: '/',
540
- display: 'standalone',
541
- background_color: '#080D11',
542
- theme_color: '#080D11',
543
- icons: [{ src: '/favicon.svg', type: 'image/svg+xml', sizes: 'any' }],
544
- },
545
- null,
546
- 4,
547
- ) + '\n',
548
- 'client/public/images/.gitkeep':
549
- '# Place images and other static assets here; served at /images/*.\n',
550
- 'client/toil.tsx':
551
- "import { routes, layout, notFound, globalError, slots } from 'toiljs/routes';\n\n" +
552
- styleImportLines(features).join('\n') +
553
- '\n\n' +
554
- 'Toil.mount(routes, layout, notFound, globalError, slots);\n',
555
- [`client/${styleEntry(features.preprocessor)}`]: DEFAULT_STYLE_CONTENT,
556
- 'client/components/.gitkeep': '# Place shared React components here.\n',
557
- };
558
- if (features.tailwind) files[`client/${TAILWIND_ENTRY}`] = TAILWIND_CSS;
559
- return files;
560
- }
561
-
562
- /** The root `layout.tsx`: the app frame + a nav bar of `Toil.Link`s (one per `nav` entry). */
563
- function layoutTsx(name: string, nav: readonly { href: string; label: string }[]): string {
564
- const links = nav
565
- .map((l) => ` <Toil.Link href="${l.href}">${l.label}</Toil.Link>`)
566
- .join('\n');
567
- return `import { type ReactNode } from 'react';
568
-
569
- export default function Layout({ children }: { children?: ReactNode }) {
570
- return (
571
- <div style={{ maxWidth: 680, margin: '0 auto', padding: '3rem 1.5rem' }}>
572
- <header
573
- style={{
574
- display: 'flex',
575
- gap: '1rem',
576
- alignItems: 'baseline',
577
- borderBottom: '1px solid #1b2330',
578
- paddingBottom: '0.75rem',
579
- marginBottom: '1.5rem',
580
- }}>
581
- <strong style={{ color: '#2563FF', fontSize: '1.1rem' }}>${path.basename(name)}</strong>
582
- <nav style={{ display: 'flex', gap: '1rem' }}>
583
- ${links}
584
- </nav>
585
- </header>
586
- {children}
587
- </div>
588
- );
589
- }
590
- `;
591
- }
592
-
593
- /** The inline client UI for the `minimal` template (the `app` template copies examples/basic/client). */
594
- function minimalClient(name: string, features: StyleFeatures): Record<string, string> {
595
- return {
596
- ...clientShell(name, features),
597
- 'client/layout.tsx': layoutTsx(name, [{ href: '/', label: 'home' }]),
598
- 'client/routes/index.tsx':
599
- 'export default function Home() {\n' +
600
- ' return (\n <main>\n' +
601
- ' <h1>Welcome to toiljs</h1>\n' +
602
- ' <p>File-based routing, bundled by Vite, zero config.</p>\n' +
603
- ' </main>\n );\n}\n',
604
- };
605
- }
606
-
607
- /**
608
- * The inline client UI for the `agent` template: two file-routed pages (basic routing) and a home
609
- * page that calls the backend's one @rest endpoint through the generated, typed `Server.REST`
610
- * fetch client. `Server` and `parseError` are client globals (no import), like `Toil`.
611
- */
612
- function agentClient(name: string, features: StyleFeatures): Record<string, string> {
613
- return {
614
- ...clientShell(name, features),
615
- 'client/layout.tsx': layoutTsx(name, [
616
- { href: '/', label: 'home' },
617
- { href: '/about', label: 'about' },
618
- ]),
619
- 'client/routes/index.tsx':
620
- "import { useEffect, useState } from 'react';\n\n" +
621
- '// The home page calls the backend. `Server.REST.greeting.hello()` is a real, typed\n' +
622
- '// fetch generated from the @rest controller in server/routes/Greet.ts (see the\n' +
623
- '// gitignored shared/server.ts, emitted by the server build). It needs the server running.\n' +
624
- 'export default function Home() {\n' +
625
- " const [message, setMessage] = useState('loading…');\n\n" +
626
- ' useEffect(() => {\n' +
627
- ' Server.REST.greeting\n' +
628
- ' .hello()\n' +
629
- ' .then((g) => setMessage(g.message))\n' +
630
- ' .catch((err) => setMessage(parseError(err)));\n' +
631
- ' }, []);\n\n' +
632
- ' return (\n <main>\n' +
633
- ' <h1>Welcome to toiljs</h1>\n' +
634
- ' <p>File-based routing on the client, a ToilScript backend behind one typed fetch.</p>\n' +
635
- ' <p>\n' +
636
- ' <code>GET /greeting</code> says: <strong>{message}</strong>\n' +
637
- ' </p>\n' +
638
- ' </main>\n );\n}\n',
639
- 'client/routes/about.tsx':
640
- 'export default function About() {\n' +
641
- ' return (\n <main>\n' +
642
- ' <h1>About</h1>\n' +
643
- ' <p>\n' +
644
- ' A second page, routed by its file name. Add more under <code>client/routes/</code>.\n' +
645
- ' </p>\n' +
646
- ' </main>\n );\n}\n',
647
- };
648
- }
649
-
650
- /**
651
- * Absolute path to the `app` starter client UI. There is a single source: `examples/basic/client`
652
- * (shipped in the package), the runnable example IS the create template, so there's nothing to
653
- * keep in sync.
654
- */
655
- function appClientDir(): string {
656
- return path.resolve(
657
- path.dirname(fileURLToPath(import.meta.url)),
658
- '..',
659
- '..',
660
- 'examples',
661
- 'basic',
662
- 'client',
663
- );
664
- }
665
-
666
- /** Absolute path to the `app` starter server (`examples/basic/server`), shipped in the package. */
667
- function appServerDir(): string {
668
- return path.resolve(
669
- path.dirname(fileURLToPath(import.meta.url)),
670
- '..',
671
- '..',
672
- 'examples',
673
- 'basic',
674
- 'server',
675
- );
676
- }
677
-
678
- /**
679
- * Applies the chosen styling to a copied template's client dir: renames the stylesheet to the
680
- * preprocessor's extension, adds the Tailwind entry, and rewrites `toil.tsx`'s style imports.
681
- */
682
- async function applyStyling(clientDir: string, features: StyleFeatures): Promise<void> {
683
- // Plain CSS without Tailwind is exactly what the template ships, leave it byte-for-byte.
684
- if (features.preprocessor === 'css' && !features.tailwind) return;
685
- const entry = styleEntry(features.preprocessor);
686
- if (entry !== 'styles/main.css') {
687
- await fs.rename(path.join(clientDir, 'styles', 'main.css'), path.join(clientDir, entry));
688
- }
689
- if (features.tailwind) {
690
- await fs.writeFile(path.join(clientDir, TAILWIND_ENTRY), TAILWIND_CSS, 'utf8');
691
- }
692
- const toilPath = path.join(clientDir, 'toil.tsx');
693
- await fs.writeFile(
694
- toilPath,
695
- setStyleImports(await fs.readFile(toilPath, 'utf8'), features),
696
- 'utf8',
697
- );
698
- }
699
-
700
- async function writeFiles(dir: string, files: Record<string, string>): Promise<void> {
701
- for (const [rel, contents] of Object.entries(files)) {
702
- const full = path.join(dir, rel);
703
- await fs.mkdir(path.dirname(full), { recursive: true });
704
- await fs.writeFile(full, contents, 'utf8');
705
- }
706
- }
707
-
708
- /** Runs the create flow (interactive unless `--yes`). */
709
- export async function runCreate(opts: CreateOptions): Promise<void> {
710
- intro(accent(' toiljs create '));
711
-
712
- let name = opts.name;
713
- if (!name) {
714
- if (opts.yes) {
715
- name = 'my-toil-app';
716
- } else {
717
- const answer = await text({
718
- message: 'Project name',
719
- placeholder: 'my-toil-app',
720
- defaultValue: 'my-toil-app',
721
- validate: (v) => {
722
- const result = isValidName(v || 'my-toil-app');
723
- return result === true ? undefined : result;
724
- },
725
- });
726
- bail(answer);
727
- name = answer.trim() || 'my-toil-app';
728
- }
729
- }
730
- const valid = isValidName(name);
731
- if (valid !== true) {
732
- cancel(valid);
733
- process.exit(1);
734
- }
735
-
736
- const targetDir = resolveProjectDir(opts.cwd, name);
737
- if (targetDir === null) {
738
- cancel('Project name must stay inside the current directory (no "..", no absolute paths).');
739
- process.exit(1);
740
- }
741
- const rel = path.relative(opts.cwd, targetDir) || '.';
742
-
743
- if (!(await isEmptyDir(targetDir))) {
744
- if (opts.yes) {
745
- cancel(`Directory ${pc.cyan(rel)} is not empty.`);
746
- process.exit(1);
747
- }
748
- const proceed = await confirm({
749
- message: `Directory ${pc.cyan(rel)} is not empty. Scaffold into it anyway?`,
750
- initialValue: false,
751
- });
752
- bail(proceed);
753
- if (!proceed) {
754
- cancel('Scaffolding cancelled.');
755
- process.exit(0);
756
- }
757
- }
758
-
759
- let template: Template = opts.template ?? 'app';
760
- if (!opts.template && !opts.yes) {
761
- const templateOptions: TemplateOption[] = [
762
- {
763
- value: 'app',
764
- label: 'App',
765
- hint: 'the full ToilJS starter, landing page, layout, styles, demo routes',
766
- },
767
- { value: 'minimal', label: 'Minimal', hint: 'just a layout and a home route' },
768
- {
769
- value: 'agent',
770
- label: 'Agent',
771
- hint: 'minimal, plus one @rest endpoint the home page calls, a base to build on',
772
- },
773
- ];
774
- const choice = await select({
775
- message: 'Which template?',
776
- options: templateOptions,
777
- initialValue: 'app',
778
- });
779
- bail(choice);
780
- template = choice === 'minimal' || choice === 'agent' ? choice : 'app';
781
- }
782
-
783
- let preprocessor: Preprocessor = opts.preprocessor ?? 'css';
784
- let tailwind = opts.tailwind ?? false;
785
- if (!opts.yes) {
786
- if (opts.preprocessor === undefined) {
787
- const choice = await select<Preprocessor>({
788
- message: 'Styling',
789
- options: PREPROCESSORS.map((value) => ({
790
- value,
791
- label: PREPROCESSOR_LABEL[value],
792
- })),
793
- initialValue: 'css',
794
- });
795
- bail(choice);
796
- preprocessor = choice;
797
- }
798
- if (opts.tailwind === undefined) {
799
- const tw = await confirm({ message: 'Add Tailwind CSS?', initialValue: false });
800
- bail(tw);
801
- tailwind = tw;
802
- }
803
- }
804
- const features: StyleFeatures = { preprocessor, tailwind };
805
-
806
- // AI assistant files: --ai = all, --no-ai = none, otherwise ask (default: all selected).
807
- let aiTools: string[] = opts.ai === false ? [] : [...AI_HELPER_IDS];
808
- if (opts.ai === undefined && !opts.yes) {
809
- const picked = await multiselect<string>({
810
- message: 'AI assistant files (read by Claude, Cursor, Codex, Copilot)',
811
- options: [
812
- ...AI_HELPERS.map((h) => ({ value: h.id, label: h.label })),
813
- { value: 'none', label: 'None' },
814
- ],
815
- initialValues: [...AI_HELPER_IDS],
816
- required: false,
817
- });
818
- bail(picked);
819
- // Selecting "None" (or deselecting everything) scaffolds no AI helper files.
820
- aiTools = picked.includes('none') ? [] : picked;
821
- }
822
-
823
- // Build-time image optimization: on by default (just press enter to keep it).
824
- let images = opts.images ?? true;
825
- if (opts.images === undefined && !opts.yes) {
826
- const im = await confirm({ message: 'Optimize images at build time?', initialValue: true });
827
- bail(im);
828
- images = im;
829
- }
830
-
831
- let initGit = opts.git ?? false;
832
- let install = opts.install ?? true;
833
- const pm = opts.pm ?? 'npm';
834
- if (!isPackageManager(pm)) {
835
- cancel(`Unsupported package manager: ${pm} (use npm, pnpm, yarn, or bun).`);
836
- process.exit(1);
837
- }
838
- if (!opts.yes) {
839
- if (opts.git === undefined) {
840
- const g = await confirm({
841
- message: 'Initialize a git repository?',
842
- initialValue: true,
843
- });
844
- bail(g);
845
- initGit = g;
846
- }
847
- if (opts.install === undefined) {
848
- const i = await confirm({ message: 'Install dependencies now?', initialValue: true });
849
- bail(i);
850
- install = i;
851
- }
852
- }
853
-
854
- const s = spinner();
855
- s.start('Scaffolding project');
856
- await writeFiles(targetDir, scaffold(name, template, features, aiTools, images));
857
- if (template === 'app') {
858
- // Copy the example client + server (the single starter source), set the <title>, then style.
859
- await fs.cp(appClientDir(), path.join(targetDir, 'client'), { recursive: true });
860
- // Only the canonical starter layout ships; anything else sitting in the example's
861
- // server/ (local experiments, scratch entries) stays out of scaffolded apps.
862
- const serverAllow = new Set([
863
- 'main.ts',
864
- 'README.md',
865
- 'tsconfig.json',
866
- 'toil-server-env.d.ts',
867
- 'core',
868
- 'models',
869
- 'routes',
870
- 'services',
871
- 'scheduled',
872
- ]);
873
- const serverSrc = appServerDir();
874
- await fs.cp(serverSrc, path.join(targetDir, 'server'), {
875
- recursive: true,
876
- filter: (src) => {
877
- const rel = path.relative(serverSrc, src);
878
- if (rel === '') return true;
879
- return serverAllow.has(rel.split(path.sep)[0]);
880
- },
881
- });
882
- const indexHtml = path.join(targetDir, 'client', 'public', 'index.html');
883
- const html = await fs.readFile(indexHtml, 'utf8');
884
- await fs.writeFile(
885
- indexHtml,
886
- html.replace(/<title>[^<]*<\/title>/, `<title>${path.basename(name)}</title>`),
887
- );
888
- await applyStyling(path.join(targetDir, 'client'), features);
889
- }
890
- s.stop(`Scaffolded ${pc.cyan(rel)}`);
891
-
892
- if (initGit) {
893
- const g = spinner();
894
- g.start('Initializing git repository');
895
- try {
896
- await run('git', ['init', '-q'], targetDir);
897
- await run('git', ['add', '-A'], targetDir);
898
- g.stop('Initialized git repository');
899
- } catch {
900
- g.stop(pc.yellow('Skipped git init (git not available)'));
901
- }
902
- }
903
-
904
- if (install) {
905
- const i = spinner();
906
- i.start(`Installing dependencies with ${pm}`);
907
- try {
908
- await run(pm, ['install'], targetDir);
909
- i.stop('Installed dependencies');
910
- } catch {
911
- i.stop(pc.yellow(`Could not install with ${pm}, run it yourself later`));
912
- install = false;
913
- }
914
- }
915
-
916
- const steps: string[] = [];
917
- if (rel !== '.') steps.push(`cd ${rel}`);
918
- if (!install) steps.push('npm install');
919
- steps.push(`${accent('npm run dev')} ${dim('start the dev server')}`);
920
- steps.push(`${accent('npm run build')} ${dim('build for production')}`);
921
- note(steps.map((l) => dim(' ') + l).join('\n'), 'Next steps');
922
-
923
- outro(`Created ${accent(path.basename(name))}, happy building! ${dim('(v' + version() + ')')}`);
924
- }