toiljs 0.0.33 → 0.0.36

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 (130) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +1 -0
  3. package/as-pect.config.js +8 -2
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +124 -7
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/auth.d.ts +42 -0
  8. package/build/client/auth.js +179 -0
  9. package/build/client/index.d.ts +5 -1
  10. package/build/client/index.js +3 -1
  11. package/build/client/routing/loader.d.ts +1 -0
  12. package/build/client/routing/loader.js +37 -0
  13. package/build/client/routing/mount.js +32 -1
  14. package/build/client/ssr/markers.d.ts +34 -0
  15. package/build/client/ssr/markers.js +49 -0
  16. package/build/compiler/.tsbuildinfo +1 -1
  17. package/build/compiler/docs.js +88 -1
  18. package/build/compiler/generate.d.ts +2 -0
  19. package/build/compiler/generate.js +2 -2
  20. package/build/compiler/index.js +2 -0
  21. package/build/compiler/ssr-codegen.d.ts +2 -0
  22. package/build/compiler/ssr-codegen.js +36 -0
  23. package/build/compiler/template-build.d.ts +29 -0
  24. package/build/compiler/template-build.js +150 -0
  25. package/build/compiler/template.d.ts +22 -0
  26. package/build/compiler/template.js +169 -0
  27. package/build/devserver/.tsbuildinfo +1 -1
  28. package/build/devserver/cache.d.ts +8 -0
  29. package/build/devserver/cache.js +0 -0
  30. package/build/devserver/crypto.js +15 -0
  31. package/build/devserver/host.js +1 -0
  32. package/build/devserver/index.js +10 -1
  33. package/build/devserver/module.d.ts +1 -0
  34. package/build/devserver/module.js +23 -1
  35. package/docs/README.md +56 -0
  36. package/docs/auth.md +261 -0
  37. package/docs/caching.md +115 -0
  38. package/docs/cookies.md +457 -0
  39. package/docs/crypto.md +130 -0
  40. package/docs/data.md +131 -0
  41. package/docs/getting-started.md +128 -0
  42. package/docs/routing.md +259 -0
  43. package/docs/rpc.md +149 -0
  44. package/docs/ssr.md +184 -0
  45. package/docs/time.md +43 -0
  46. package/examples/basic/client/routes/auth.tsx +198 -0
  47. package/examples/basic/client/routes/cookies.tsx +199 -0
  48. package/examples/basic/client/routes/features/index.tsx +34 -10
  49. package/examples/basic/client/routes/hello.tsx +43 -0
  50. package/examples/basic/client/routes/pq.tsx +135 -0
  51. package/examples/basic/server/AuthTestHandler.ts +15 -0
  52. package/examples/basic/server/AuthVerifyHandler.ts +23 -0
  53. package/examples/basic/server/CacheHandler.ts +25 -0
  54. package/examples/basic/server/DecoCache.ts +18 -0
  55. package/examples/basic/server/FastTrapHandler.ts +8 -0
  56. package/examples/basic/server/README.md +19 -0
  57. package/examples/basic/server/SpinHandler.ts +18 -0
  58. package/examples/basic/server/SsrGreetingRender.ts +27 -0
  59. package/examples/basic/server/authexample-main.ts +8 -0
  60. package/examples/basic/server/authtest-main.ts +8 -0
  61. package/examples/basic/server/authverify-main.ts +8 -0
  62. package/examples/basic/server/cache-main.ts +8 -0
  63. package/examples/basic/server/core/AppHandler.ts +290 -0
  64. package/examples/basic/server/core/store.ts +31 -0
  65. package/examples/basic/server/deco-main.ts +18 -0
  66. package/examples/basic/server/main.ts +13 -2
  67. package/examples/basic/server/models/NewPlayer.ts +5 -0
  68. package/examples/basic/server/models/Player.ts +8 -0
  69. package/examples/basic/server/models/ScoreDelta.ts +5 -0
  70. package/examples/basic/server/models/Standings.ts +7 -0
  71. package/examples/basic/server/routes/Auth.ts +184 -0
  72. package/examples/basic/server/routes/Leaderboard.ts +20 -0
  73. package/examples/basic/server/routes/Players.ts +53 -0
  74. package/examples/basic/server/routes/PqDemo.ts +109 -0
  75. package/examples/basic/server/routes/Session.ts +73 -0
  76. package/examples/basic/server/scheduled/README.md +7 -0
  77. package/examples/basic/server/services/Stats.ts +11 -0
  78. package/examples/basic/server/services/remotes.ts +7 -0
  79. package/examples/basic/server/spin-main.ts +13 -0
  80. package/examples/basic/server/ssr/greeting.slots.ts +19 -0
  81. package/examples/basic/server/ssr-main.ts +18 -0
  82. package/examples/basic/server/toil-server-env.d.ts +94 -0
  83. package/examples/basic/server/trap-main.ts +8 -0
  84. package/package.json +5 -3
  85. package/server/globals/auth.ts +281 -0
  86. package/server/runtime/README.md +61 -0
  87. package/server/runtime/env/Server.ts +12 -0
  88. package/server/runtime/exports/index.ts +17 -0
  89. package/server/runtime/exports/render.ts +51 -0
  90. package/server/runtime/http/base64.ts +104 -0
  91. package/server/runtime/http/cookie.ts +416 -0
  92. package/server/runtime/http/cookies.ts +197 -0
  93. package/server/runtime/http/date.ts +72 -0
  94. package/server/runtime/http/percent.ts +76 -0
  95. package/server/runtime/http/securecookies.ts +224 -0
  96. package/server/runtime/index.ts +17 -0
  97. package/server/runtime/request.ts +24 -0
  98. package/server/runtime/response.ts +85 -0
  99. package/server/runtime/ssr/Ssr.ts +43 -0
  100. package/server/runtime/ssr/encode.ts +110 -0
  101. package/server/runtime/ssr/escape.ts +83 -0
  102. package/server/runtime/ssr/slots.ts +144 -0
  103. package/server/runtime/time.ts +29 -0
  104. package/src/cli/create.ts +159 -14
  105. package/src/client/auth.ts +322 -0
  106. package/src/client/index.ts +5 -1
  107. package/src/client/routing/loader.ts +56 -0
  108. package/src/client/routing/mount.tsx +37 -1
  109. package/src/client/ssr/markers.tsx +140 -0
  110. package/src/compiler/docs.ts +88 -1
  111. package/src/compiler/generate.ts +2 -2
  112. package/src/compiler/index.ts +5 -0
  113. package/src/compiler/ssr-codegen.ts +85 -0
  114. package/src/compiler/template-build.ts +275 -0
  115. package/src/compiler/template.ts +265 -0
  116. package/src/devserver/cache.ts +0 -0
  117. package/src/devserver/crypto.ts +23 -0
  118. package/src/devserver/host.ts +4 -0
  119. package/src/devserver/index.ts +21 -1
  120. package/src/devserver/module.ts +39 -1
  121. package/test/assembly/cookie.spec.ts +302 -0
  122. package/test/assembly/example.spec.ts +5 -1
  123. package/test/assembly/ssr.spec.ts +94 -0
  124. package/test/devserver.test.ts +48 -4
  125. package/test/fixtures/bignum-wire/spec.ts +27 -0
  126. package/test/rpc-bignum-wire.test.ts +164 -0
  127. package/test/ssr-render.test.ts +128 -0
  128. package/test/ssr-template.test.tsx +348 -0
  129. package/examples/basic/server/HelloHandler.ts +0 -42
  130. package/examples/basic/server/api.ts +0 -137
@@ -0,0 +1,83 @@
1
+ /**
2
+ * HTML escaping for edge-SSR hole values, byte-for-byte identical to
3
+ * react-dom/server's `escapeTextForBrowser` (the regex `/["'&<>]/`). React
4
+ * runs the SAME escaper for text children and attribute values, so a single
5
+ * function serves both kinds. Matching React exactly is load-bearing: the
6
+ * browser's `hydrateRoot` compares the server markup to its own first render,
7
+ * and any escaping difference triggers a hydration mismatch.
8
+ *
9
+ * Note the entities: `'` becomes `&#x27;` (React's choice, NOT `&#39;` or
10
+ * `&apos;`), and `"` becomes `&quot;`. Do not "simplify" these.
11
+ */
12
+
13
+ @inline function needsEscape(c: i32): bool {
14
+ // 0x22 " | 0x26 & | 0x27 ' | 0x3C < | 0x3E >
15
+ return c == 0x22 || c == 0x26 || c == 0x27 || c == 0x3c || c == 0x3e;
16
+ }
17
+
18
+ /** React-exact HTML text/attribute escape. */
19
+ export function escapeHtml(s: string): string {
20
+ let hit = false;
21
+ for (let i = 0; i < s.length; i++) {
22
+ if (needsEscape(s.charCodeAt(i))) {
23
+ hit = true;
24
+ break;
25
+ }
26
+ }
27
+ if (!hit) return s;
28
+
29
+ let out = '';
30
+ let last = 0;
31
+ for (let i = 0; i < s.length; i++) {
32
+ const c = s.charCodeAt(i);
33
+ let rep: string = '';
34
+ if (c == 0x22) rep = '&quot;';
35
+ else if (c == 0x26) rep = '&amp;';
36
+ else if (c == 0x27) rep = '&#x27;';
37
+ else if (c == 0x3c) rep = '&lt;';
38
+ else if (c == 0x3e) rep = '&gt;';
39
+ else continue;
40
+ out += s.substring(last, i);
41
+ out += rep;
42
+ last = i + 1;
43
+ }
44
+ out += s.substring(last, s.length);
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * Escape a JSON string for safe embedding inside a `<script>` element (the
50
+ * `__toil_state` hydration blob). Neutralises `<`, `>`, `&` and the JS line
51
+ * terminators U+2028/U+2029 to their `\uXXXX` forms so the JSON can never
52
+ * close the script element or break the parse. Mirrors the standard
53
+ * "serialize-javascript"/Next.js approach; the bytes stay valid JSON.
54
+ */
55
+ export function escapeJsonForScript(json: string): string {
56
+ let hit = false;
57
+ for (let i = 0; i < json.length; i++) {
58
+ const c = json.charCodeAt(i);
59
+ if (c == 0x3c || c == 0x3e || c == 0x26 || c == 0x2028 || c == 0x2029) {
60
+ hit = true;
61
+ break;
62
+ }
63
+ }
64
+ if (!hit) return json;
65
+
66
+ let out = '';
67
+ let last = 0;
68
+ for (let i = 0; i < json.length; i++) {
69
+ const c = json.charCodeAt(i);
70
+ let rep: string = '';
71
+ if (c == 0x3c) rep = '\\u003c';
72
+ else if (c == 0x3e) rep = '\\u003e';
73
+ else if (c == 0x26) rep = '\\u0026';
74
+ else if (c == 0x2028) rep = '\\u2028';
75
+ else if (c == 0x2029) rep = '\\u2029';
76
+ else continue;
77
+ out += json.substring(last, i);
78
+ out += rep;
79
+ last = i + 1;
80
+ }
81
+ out += json.substring(last, json.length);
82
+ return out;
83
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The values the guest `render` returns for one request: the hole values the
3
+ * edge splices into the precompiled template. The guest never emits the static
4
+ * template bytes (the host has those, mmap'd); it only fills the holes.
5
+ *
6
+ * Authoring is normally generated by the toiljs compiler from the route's JSX,
7
+ * but the typed API here is hand-writable too (it is the escape hatch). A
8
+ * generated render looks like:
9
+ *
10
+ * ```ts
11
+ * export function renderProfile(req: Request): SlotValues {
12
+ * const v = new SlotValues(HASH);
13
+ * v.setText(Slot.username, params(req, 'name'));
14
+ * v.setRaw(Slot.bio, bioHtml);
15
+ * const rows = new HtmlBuilder();
16
+ * for (let i = 0; i < posts.length; i++) rows.raw('<li>').text(posts[i]).raw('</li>');
17
+ * v.setRepeat(Slot.posts, rows);
18
+ * return v;
19
+ * }
20
+ * ```
21
+ */
22
+
23
+ import { Header } from '../request';
24
+ import { escapeHtml } from './escape';
25
+
26
+ /** Slot kind discriminants. Mirror `toil-backend` `SlotKind` and the on-disk
27
+ * `.slots` manifest; do not reorder. */
28
+ export namespace SlotKind {
29
+ // @ts-ignore: decorator
30
+ @inline export const TEXT: u8 = 0;
31
+ // @ts-ignore: decorator
32
+ @inline export const RAW: u8 = 1;
33
+ // @ts-ignore: decorator
34
+ @inline export const ATTR: u8 = 2;
35
+ // @ts-ignore: decorator
36
+ @inline export const REPEAT: u8 = 3;
37
+ }
38
+
39
+ /** Number of bytes in the coherence hash; must match the host's `HASH_LEN`. */
40
+ // @ts-ignore: decorator
41
+ @inline export const HASH_LEN: i32 = 32;
42
+
43
+ /** One filled hole: its stable id, kind, and the already-escaped bytes. The id
44
+ * is held as `i32` so a generated `Slot` enum member (AS enums are `i32`) is
45
+ * passed without a cast; it is narrowed to `u16` only at encode time. */
46
+ export class SlotValue {
47
+ constructor(
48
+ public slotId: i32,
49
+ public kind: u8,
50
+ public bytes: Uint8Array,
51
+ ) {}
52
+ }
53
+
54
+ @inline function utf8(s: string): Uint8Array {
55
+ return Uint8Array.wrap(String.UTF8.encode(s));
56
+ }
57
+
58
+ /**
59
+ * Append-only HTML byte builder for repeat regions (and any place the
60
+ * generated code assembles a run of static chunks + escaped values). `raw`
61
+ * appends verbatim template bytes; `text`/`attr` append React-escaped values.
62
+ * Chaining keeps generated loops terse.
63
+ */
64
+ export class HtmlBuilder {
65
+ private parts: Array<string> = new Array<string>();
66
+
67
+ raw(s: string): HtmlBuilder {
68
+ this.parts.push(s);
69
+ return this;
70
+ }
71
+
72
+ text(s: string): HtmlBuilder {
73
+ this.parts.push(escapeHtml(s));
74
+ return this;
75
+ }
76
+
77
+ /** Attribute escaping is identical to text escaping in React. */
78
+ attr(s: string): HtmlBuilder {
79
+ this.parts.push(escapeHtml(s));
80
+ return this;
81
+ }
82
+
83
+ toBytes(): Uint8Array {
84
+ return utf8(this.parts.join(''));
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The render result: status, coherence hash, response headers, and the ordered
90
+ * filled holes. Serialised by `encodeValues` into the values envelope the host
91
+ * splices against the template manifest.
92
+ */
93
+ export class SlotValues {
94
+ status: u16 = 200;
95
+ headers: Array<Header> = new Array<Header>();
96
+ slots: Array<SlotValue> = new Array<SlotValue>();
97
+
98
+ /** `hash` is the route's compiled-in coherence hash (32 bytes); the host
99
+ * rejects the response if it does not match the deployed template. */
100
+ constructor(public templateHash: StaticArray<u8>) {}
101
+
102
+ /** A text hole: React-escaped. */
103
+ setText(slotId: i32, value: string): SlotValues {
104
+ this.slots.push(new SlotValue(slotId, SlotKind.TEXT, utf8(escapeHtml(value))));
105
+ return this;
106
+ }
107
+
108
+ /** A raw-HTML hole: inserted verbatim. The author owns sanitisation, same
109
+ * as React `dangerouslySetInnerHTML`. */
110
+ setRaw(slotId: i32, html: string): SlotValues {
111
+ this.slots.push(new SlotValue(slotId, SlotKind.RAW, utf8(html)));
112
+ return this;
113
+ }
114
+
115
+ /** An attribute-value hole. */
116
+ setAttr(slotId: i32, value: string): SlotValues {
117
+ this.slots.push(new SlotValue(slotId, SlotKind.ATTR, utf8(escapeHtml(value))));
118
+ return this;
119
+ }
120
+
121
+ /** A repeat region: the guest pre-stamped + concatenated rows (built via
122
+ * [`HtmlBuilder`]); the host inserts them verbatim at the region offset. */
123
+ setRepeat(slotId: i32, rows: HtmlBuilder): SlotValues {
124
+ this.slots.push(new SlotValue(slotId, SlotKind.REPEAT, rows.toBytes()));
125
+ return this;
126
+ }
127
+
128
+ /** Add a response header (e.g. a Set-Cookie or Cache-Control). */
129
+ setHeader(name: string, value: string): SlotValues {
130
+ this.headers.push(new Header(name, value));
131
+ return this;
132
+ }
133
+
134
+ setStatus(status: u16): SlotValues {
135
+ this.status = status;
136
+ return this;
137
+ }
138
+ }
139
+
140
+ /** A zeroed 32-byte hash. Used for the fail-safe empty result, which the host
141
+ * rejects as a hash mismatch (-> 500) rather than serving a broken page. */
142
+ export function zeroHash(): StaticArray<u8> {
143
+ return new StaticArray<u8>(HASH_LEN);
144
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `Time` — the guest's wall-clock, backed by the host's `Date.now()` binding
3
+ * (`env.Date.now`). Both the edge (`toil-backend` `date_now_import.rs`) and the
4
+ * dev server (`toiljs/src/devserver/host.ts`) provide it, so time behaves the
5
+ * same in `toiljs dev` and in production.
6
+ *
7
+ * This is the toiljs-blessed way to read the clock; prefer it over a raw
8
+ * `Date.now()` so the host boundary (and its single millisecond unit) is
9
+ * explicit and easy to find. Like browser `Date.now()` it is WALL-CLOCK, not
10
+ * monotonic: it can step backward across an NTP correction, so never use it to
11
+ * measure elapsed time, only to stamp/compare absolute instants (session
12
+ * `iat`/`exp`, challenge expiry, cache ages).
13
+ *
14
+ * Exposed as an ambient global (`@global`, usable with no import in a handler)
15
+ * and re-exported from `toiljs/server/runtime`.
16
+ */
17
+ @global
18
+ export class Time {
19
+ /** Milliseconds since the Unix epoch (the host `Date.now()` value). */
20
+ static nowMillis(): i64 {
21
+ return <i64>Date.now();
22
+ }
23
+
24
+ /** Whole seconds since the Unix epoch (`nowMillis() / 1000`), the unit used
25
+ * for session and challenge timestamps. */
26
+ static nowSeconds(): u64 {
27
+ return <u64>(Date.now() / 1000);
28
+ }
29
+ }
package/src/cli/create.ts CHANGED
@@ -175,12 +175,10 @@ function scaffold(
175
175
  'toilconfig.json':
176
176
  JSON.stringify(
177
177
  {
178
- // `toiljs build` compiles every server/*.ts so dropped-in @data/@rest files are
179
- // picked up; these entries are the fallback when running `toilscript` directly.
180
- entries:
181
- template === 'minimal'
182
- ? ['server/main.ts']
183
- : ['server/main.ts', 'server/api.ts'],
178
+ // `toiljs build` compiles every decorated server file (recursively) so
179
+ // dropped-in @data/@rest files are picked up; main.ts imports the surface
180
+ // modules so a direct `toilscript` run builds the same server.
181
+ entries: ['server/main.ts'],
184
182
  targets: {
185
183
  release: {
186
184
  outFile: 'build/server/release.wasm',
@@ -203,6 +201,10 @@ function scaffold(
203
201
  'multi-value',
204
202
  ],
205
203
  runtime: 'stub',
204
+ // toiljs server globals (exports become ambient, used
205
+ // with no import -- e.g. `AuthService` for the
206
+ // post-quantum auth primitive), exactly like `crypto`.
207
+ lib: ['node_modules/toiljs/server/globals'],
206
208
  // Reserve [0, 64 KiB) for the request envelope the
207
209
  // edge writes at offset 0. Static data starts ABOVE
208
210
  // it, so a large request can never overwrite guest
@@ -261,12 +263,118 @@ function scaffold(
261
263
  return files;
262
264
  }
263
265
 
264
- /** A minimal but working server for the `minimal` template (the `app` template copies examples/basic/server). */
266
+ /**
267
+ * Editor-only ambient declarations for the toiljs cookie globals (`Cookie` /
268
+ * `Cookies` / `SecureCookies` / `SameSite` / ...). They are `@global` in the
269
+ * runtime, so handlers use them without an import (like `crypto`); this gives the
270
+ * editor their shapes. Auto-included via the server tsconfig and ignored by the
271
+ * compiler. Keep in sync with `toiljs/server/runtime/http/*`.
272
+ */
273
+ const TOIL_SERVER_ENV_DTS = `/**
274
+ * Editor-only ambient declarations for the toiljs cookie globals.
275
+ *
276
+ * \`Cookie\`, \`Cookies\`, \`SecureCookies\`, and the \`SameSite\` / \`CookieEncoding\` /
277
+ * \`CookiePrefix\` enums are \`@global\` in the toiljs server runtime, so a handler
278
+ * uses them with no import (exactly like \`crypto\`). The toilscript compiler
279
+ * registers them from the runtime; this file just gives the editor their shapes.
280
+ * It is auto-included by the server tsconfig and ignored by the compiler.
281
+ */
282
+
283
+ declare enum SameSite {
284
+ Default = 0,
285
+ None = 1,
286
+ Lax = 2,
287
+ Strict = 3,
288
+ }
289
+
290
+ declare enum CookieEncoding {
291
+ Percent = 0,
292
+ Raw = 1,
293
+ Base64Url = 2,
294
+ }
295
+
296
+ declare enum CookiePrefix {
297
+ None = 0,
298
+ Secure = 1,
299
+ Host = 2,
300
+ }
301
+
302
+ declare class CookieValidation {
303
+ valid: bool;
304
+ errors: Array<string>;
305
+ fail(msg: string): void;
306
+ }
307
+
308
+ declare class Cookie {
309
+ name: string;
310
+ value: string;
311
+ encoding: CookieEncoding;
312
+ constructor(name: string, value: string);
313
+ static create(name: string, value: string): Cookie;
314
+ domain(v: string): Cookie;
315
+ path(v: string): Cookie;
316
+ maxAge(seconds: i64): Cookie;
317
+ expires(epochSeconds: i64): Cookie;
318
+ expiresRaw(date: string): Cookie;
319
+ secure(on?: bool): Cookie;
320
+ httpOnly(on?: bool): Cookie;
321
+ sameSite(s: SameSite): Cookie;
322
+ partitioned(on?: bool): Cookie;
323
+ priority(p: string): Cookie;
324
+ extension(av: string): Cookie;
325
+ withEncoding(e: CookieEncoding): Cookie;
326
+ asSecurePrefixed(): Cookie;
327
+ asHostPrefixed(): Cookie;
328
+ detectedPrefix(): CookiePrefix;
329
+ encodedValue(): string;
330
+ validate(): CookieValidation;
331
+ serialize(strict?: bool): string;
332
+ toString(): string;
333
+ }
334
+
335
+ declare class CookieMap {
336
+ set(name: string, value: string): void;
337
+ get(name: string): string | null;
338
+ has(name: string): bool;
339
+ names(): Array<string>;
340
+ readonly size: i32;
341
+ }
342
+
343
+ declare class Cookies {
344
+ static parse(cookieHeader: string): CookieMap;
345
+ static get(cookieHeader: string, name: string): string | null;
346
+ static serialize(name: string, value: string): string;
347
+ static parseSetCookie(setCookie: string): Cookie;
348
+ static encodeValue(raw: string): string;
349
+ static decodeValue(enc: string): string;
350
+ }
351
+
352
+ declare class SecureCookies {
353
+ static signed(key: Uint8Array): SecureCookies;
354
+ static encrypted(key: Uint8Array): SecureCookies;
355
+ addKey(key: Uint8Array): SecureCookies;
356
+ sign(name: string, value: string): string;
357
+ unsign(name: string, sealed: string): string | null;
358
+ encrypt(name: string, value: string): string;
359
+ decrypt(name: string, sealed: string): string | null;
360
+ seal(cookie: Cookie): Cookie;
361
+ open(jar: CookieMap, name: string): string | null;
362
+ }
363
+ `;
364
+
365
+ /**
366
+ * A minimal but working server for the `minimal` template (the `app` template copies
367
+ * examples/basic/server). Same folder conventions as the full starter, just fewer files:
368
+ * the entry in main.ts, the handler under core/, and a README mapping where new
369
+ * routes/services/models go.
370
+ */
265
371
  function minimalServer(): Record<string, string> {
266
372
  return {
267
- 'server/HelloHandler.ts':
373
+ 'server/toil-server-env.d.ts': TOIL_SERVER_ENV_DTS,
374
+ 'server/core/AppHandler.ts':
268
375
  "import { ToilHandler, Request, Response, Method } from 'toiljs/server/runtime';\n\n" +
269
- 'export class HelloHandler extends ToilHandler {\n' +
376
+ '/** Every request enters here. Add `@rest` controllers under routes/ as you grow. */\n' +
377
+ 'export class AppHandler extends ToilHandler {\n' +
270
378
  ' public handle(req: Request): Response {\n' +
271
379
  ' if (req.method != Method.GET && req.method != Method.HEAD) {\n' +
272
380
  " return Response.empty(405).setHeader('allow', 'GET, HEAD');\n" +
@@ -285,15 +393,31 @@ function minimalServer(): Record<string, string> {
285
393
  '}\n',
286
394
  'server/main.ts':
287
395
  "import { Server } from 'toiljs/server/runtime';\n" +
288
- "import { revertOnError } from 'toiljs/server/runtime/abort/abort';\n" +
289
- "import { HelloHandler } from './HelloHandler';\n\n" +
396
+ "import { revertOnError } from 'toiljs/server/runtime/abort/abort';\n\n" +
397
+ "import { AppHandler } from './core/AppHandler';\n\n" +
398
+ '// As you add surface modules (@rest routes, @service/@remote RPC), import them here\n' +
399
+ '// so a direct `toilscript` run builds the same server `toiljs build` does, e.g.:\n' +
400
+ "// import './routes/Players';\n\n" +
290
401
  '// Wire your handler here.\n' +
291
- 'Server.handler = () => new HelloHandler();\n\n' +
402
+ 'Server.handler = () => new AppHandler();\n\n' +
292
403
  '// Required: re-export the WASM entry points and the abort hook.\n' +
293
404
  "export * from 'toiljs/server/runtime/exports';\n" +
294
405
  'export function abort(message: string, fileName: string, line: u32, column: u32): void {\n' +
295
406
  ' revertOnError(message, fileName, line, column);\n' +
296
407
  '}\n',
408
+ 'server/README.md':
409
+ '# server/\n\n' +
410
+ 'Your ToilScript backend, compiled to a single WebAssembly module. One folder per concern:\n\n' +
411
+ '| Folder | What lives here |\n' +
412
+ '| --- | --- |\n' +
413
+ '| `main.ts` | The entry point: wires the handler and imports the surface modules. |\n' +
414
+ '| `core/` | The request handler and shared app logic (state, helpers). |\n' +
415
+ '| `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |\n' +
416
+ '| `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |\n' +
417
+ '| `services/` | `@service` classes and free `@remote` functions (typed RPC). |\n' +
418
+ '| `scheduled/` | Reserved for scheduled tasks (not shipped yet). |\n\n' +
419
+ 'New decorated files are picked up automatically by `toiljs build`/`dev`; also add an import\n' +
420
+ 'in `main.ts` so a direct `toilscript` run builds the same server.\n',
297
421
  };
298
422
  }
299
423
 
@@ -586,7 +710,28 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
586
710
  if (template === 'app') {
587
711
  // Copy the example client + server (the single starter source), set the <title>, then style.
588
712
  await fs.cp(appClientDir(), path.join(targetDir, 'client'), { recursive: true });
589
- await fs.cp(appServerDir(), path.join(targetDir, 'server'), { recursive: true });
713
+ // Only the canonical starter layout ships; anything else sitting in the example's
714
+ // server/ (local experiments, scratch entries) stays out of scaffolded apps.
715
+ const serverAllow = new Set([
716
+ 'main.ts',
717
+ 'README.md',
718
+ 'tsconfig.json',
719
+ 'toil-server-env.d.ts',
720
+ 'core',
721
+ 'models',
722
+ 'routes',
723
+ 'services',
724
+ 'scheduled',
725
+ ]);
726
+ const serverSrc = appServerDir();
727
+ await fs.cp(serverSrc, path.join(targetDir, 'server'), {
728
+ recursive: true,
729
+ filter: (src) => {
730
+ const rel = path.relative(serverSrc, src);
731
+ if (rel === '') return true;
732
+ return serverAllow.has(rel.split(path.sep)[0]);
733
+ },
734
+ });
590
735
  const indexHtml = path.join(targetDir, 'client', 'public', 'index.html');
591
736
  const html = await fs.readFile(indexHtml, 'utf8');
592
737
  await fs.writeFile(
@@ -628,5 +773,5 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
628
773
  steps.push(`${accent('npm run build')} ${dim('build for production')}`);
629
774
  note(steps.map((l) => dim(' ') + l).join('\n'), 'Next steps');
630
775
 
631
- outro(`Created ${accent(path.basename(name))}, happy building! ${dim('· v' + version())}`);
776
+ outro(`Created ${accent(path.basename(name))}, happy building! ${dim('(v' + version() + ')')}`);
632
777
  }