toiljs 0.0.21 → 0.0.23
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/CHANGELOG.md +19 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +44 -11
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +59 -1
- package/examples/basic/client/routes/rest.tsx +13 -8
- package/examples/basic/client/routes/rpc.tsx +6 -5
- package/examples/basic/server/HelloHandler.ts +27 -0
- package/examples/basic/server/api.ts +137 -0
- package/examples/basic/server/main.ts +19 -0
- package/examples/basic/server/tsconfig.json +7 -0
- package/package.json +2 -2
- package/src/cli/create.ts +55 -11
- package/src/cli/diagnostics.ts +1 -1
- package/src/cli/index.ts +22 -4
- package/src/compiler/index.ts +81 -8
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// A small REST demo: a players list + a leaderboard.
|
|
2
|
+
//
|
|
3
|
+
// IMPORTANT - the server runs with a FRESH WebAssembly instance per request, so linear memory
|
|
4
|
+
// (and any module-level state, like the `store` below) is reset on every request. It does NOT
|
|
5
|
+
// persist across requests. We seed a few players at module init so the read routes always have
|
|
6
|
+
// data; the write routes (create / addScore) take effect only for the current request's response.
|
|
7
|
+
// For real persistence, call out to a database or KV store from your handler.
|
|
8
|
+
//
|
|
9
|
+
// `@data` types are the wire model (shared by HTTP and RPC). `@rest` controllers expose HTTP
|
|
10
|
+
// routes; `@service`/`@remote` expose typed RPC. Building the server (toilscript --rpcModule)
|
|
11
|
+
// regenerates the typed client into shared/server.ts:
|
|
12
|
+
// @rest -> Server.REST.<controller>.<route>(args) (a working fetch client)
|
|
13
|
+
// @service -> Server.<service>.<method>() (RPC, transport TODO)
|
|
14
|
+
|
|
15
|
+
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
16
|
+
|
|
17
|
+
/** A leaderboard player. */
|
|
18
|
+
@data
|
|
19
|
+
class Player {
|
|
20
|
+
id: u64 = 0;
|
|
21
|
+
name: string = '';
|
|
22
|
+
score: i64 = 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Request body for `POST /players` - the fields a client supplies to create a player. */
|
|
26
|
+
@data
|
|
27
|
+
class NewPlayer {
|
|
28
|
+
name: string = '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Request body for `POST /players/:id/score` - points to add to a player's score. */
|
|
32
|
+
@data
|
|
33
|
+
class ScoreDelta {
|
|
34
|
+
points: i64 = 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A leaderboard page. A `@data` wrapper so the `Player[]` round-trips through the codec. */
|
|
38
|
+
@data
|
|
39
|
+
class Standings {
|
|
40
|
+
players: Player[] = [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Re-seeded on EVERY request - module memory does not persist across requests (see the note at
|
|
44
|
+
// the top of the file). Swap this for a database/KV to keep data between requests.
|
|
45
|
+
const store = new Map<u64, Player>();
|
|
46
|
+
let nextId: u64 = 1;
|
|
47
|
+
|
|
48
|
+
function seed(name: string, score: i64): void {
|
|
49
|
+
const p = new Player();
|
|
50
|
+
p.id = nextId++;
|
|
51
|
+
p.name = name;
|
|
52
|
+
p.score = score;
|
|
53
|
+
store.set(p.id, p);
|
|
54
|
+
}
|
|
55
|
+
seed('Ada', 120);
|
|
56
|
+
seed('Linus', 95);
|
|
57
|
+
seed('Grace', 140);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Players, mounted at `/players`. On the client:
|
|
61
|
+
* await Server.REST.players.get({ params: { id } })
|
|
62
|
+
* await Server.REST.players.create({ body: new NewPlayer('Bob') })
|
|
63
|
+
* await Server.REST.players.addScore({ params: { id }, body: new ScoreDelta(10n) })
|
|
64
|
+
*/
|
|
65
|
+
@rest('players')
|
|
66
|
+
class Players {
|
|
67
|
+
/** `GET /players/:id` - returns a `Response` for full control: a real 404 for a missing id,
|
|
68
|
+
* a custom header, and the `@data` body serialized with `toJSON()`. (The toilscript editor
|
|
69
|
+
* plugin types the compiler-injected `toJSON()`, so this is clean; return the `@data` type
|
|
70
|
+
* directly, like the other routes, when you do not need that control.) */
|
|
71
|
+
@get('/:id')
|
|
72
|
+
public get(ctx: RouteContext): Response {
|
|
73
|
+
const id = u64.parse(ctx.param('id'));
|
|
74
|
+
if (!store.has(id)) return Response.notFound();
|
|
75
|
+
const p = store.get(id);
|
|
76
|
+
|
|
77
|
+
return Response.json(p.toJSON().toString()).setHeader('cache-control', 'no-store');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `POST /players` - build a player from the request body and return it with a fresh id.
|
|
81
|
+
* Note: it is NOT saved (memory resets next request); persist to a real store to keep it. */
|
|
82
|
+
@post('/')
|
|
83
|
+
public create(input: NewPlayer): Player {
|
|
84
|
+
const p = new Player();
|
|
85
|
+
p.id = nextId++;
|
|
86
|
+
p.name = input.name;
|
|
87
|
+
p.score = 0;
|
|
88
|
+
store.set(p.id, p);
|
|
89
|
+
|
|
90
|
+
return p;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `POST /players/:id/score` - add `points` (from the body) to the seeded player named by
|
|
94
|
+
* `:id` and return it. The change applies to this response only (memory resets next request). */
|
|
95
|
+
@post('/:id/score')
|
|
96
|
+
public addScore(input: ScoreDelta, ctx: RouteContext): Player {
|
|
97
|
+
const id = u64.parse(ctx.param('id'));
|
|
98
|
+
if (!store.has(id)) return new Player();
|
|
99
|
+
const p = store.get(id);
|
|
100
|
+
p.score += input.points;
|
|
101
|
+
|
|
102
|
+
return p;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The leaderboard, mounted at `/leaderboard`. On the client:
|
|
108
|
+
* const board = await Server.REST.leaderboard.top(); // typed Standings { players: Player[] }
|
|
109
|
+
*/
|
|
110
|
+
@rest('leaderboard')
|
|
111
|
+
class Leaderboard {
|
|
112
|
+
/** `GET /leaderboard` - the seeded players, highest score first. */
|
|
113
|
+
@get('/')
|
|
114
|
+
public top(): Standings {
|
|
115
|
+
const board = new Standings();
|
|
116
|
+
const all = store.values();
|
|
117
|
+
for (let i = 0; i < all.length; i++) board.players.push(all[i]);
|
|
118
|
+
board.players.sort((a: Player, b: Player): i32 => (a.score < b.score ? 1 : a.score > b.score ? -1 : 0));
|
|
119
|
+
return board;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Typed RPC service (transport still a TODO): reached as `Server.stats.playerCount()` on the client. */
|
|
124
|
+
@service
|
|
125
|
+
class Stats {
|
|
126
|
+
/** Number of seeded players (the RPC transport is a TODO, so this throws on the client for now). */
|
|
127
|
+
@remote
|
|
128
|
+
public playerCount(): i32 {
|
|
129
|
+
return store.size;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A free `@remote` function: `Server.ping(n)` on the client. */
|
|
134
|
+
@remote
|
|
135
|
+
function ping(n: i32): i32 {
|
|
136
|
+
return n + 1;
|
|
137
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Server } from 'toiljs/server/runtime';
|
|
2
|
+
import { revertOnError } from 'toiljs/server/runtime/abort/abort';
|
|
3
|
+
import { HelloHandler } from './HelloHandler';
|
|
4
|
+
|
|
5
|
+
// DO NOT TOUCH THIS.
|
|
6
|
+
Server.handler = () => {
|
|
7
|
+
// ONLY CHANGE THE HANDLER CLASS NAME.
|
|
8
|
+
// DO NOT ADD CUSTOM LOGIC HERE.
|
|
9
|
+
|
|
10
|
+
return new HelloHandler();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// VERY IMPORTANT
|
|
14
|
+
export * from 'toiljs/server/runtime/exports';
|
|
15
|
+
|
|
16
|
+
// VERY IMPORTANT
|
|
17
|
+
export function abort(message: string, fileName: string, line: u32, column: u32): void {
|
|
18
|
+
revertOnError(message, fileName, line, column);
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toiljs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.23",
|
|
5
5
|
"author": "Dacely",
|
|
6
6
|
"description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
|
|
7
7
|
"repository": {
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"eslint-plugin-react-refresh": "^0.5.2",
|
|
122
122
|
"picocolors": "^1.1.1",
|
|
123
123
|
"sharp": "^0.34.5",
|
|
124
|
-
"toilscript": "^0.1.
|
|
124
|
+
"toilscript": "^0.1.15",
|
|
125
125
|
"typescript-eslint": "^8.60.0",
|
|
126
126
|
"vite": "^8.0.14",
|
|
127
127
|
"vite-imagetools": "^10.0.0",
|
package/src/cli/create.ts
CHANGED
|
@@ -114,7 +114,7 @@ function scaffold(
|
|
|
114
114
|
'@types/react-dom': '^19.2.3',
|
|
115
115
|
eslint: '^10.2.0',
|
|
116
116
|
prettier: '^3.8.1',
|
|
117
|
-
toilscript: '^0.1.
|
|
117
|
+
toilscript: '^0.1.15',
|
|
118
118
|
typescript: '^6.0.3',
|
|
119
119
|
};
|
|
120
120
|
for (const dep of requiredPackages(features).sort()) {
|
|
@@ -127,7 +127,7 @@ function scaffold(
|
|
|
127
127
|
scripts: {
|
|
128
128
|
dev: 'toiljs dev',
|
|
129
129
|
build: 'toiljs build',
|
|
130
|
-
'build:server': '
|
|
130
|
+
'build:server': 'toiljs build --server',
|
|
131
131
|
lint: 'eslint client',
|
|
132
132
|
typecheck: 'tsc --noEmit',
|
|
133
133
|
format: 'prettier --write "client/**/*.{ts,tsx,css,scss,less}" "client/public/**/*.html"',
|
|
@@ -175,7 +175,12 @@ function scaffold(
|
|
|
175
175
|
'toilconfig.json':
|
|
176
176
|
JSON.stringify(
|
|
177
177
|
{
|
|
178
|
-
|
|
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'],
|
|
179
184
|
targets: {
|
|
180
185
|
release: {
|
|
181
186
|
outFile: 'build/server/release.wasm',
|
|
@@ -216,10 +221,6 @@ function scaffold(
|
|
|
216
221
|
null,
|
|
217
222
|
4,
|
|
218
223
|
) + '\n',
|
|
219
|
-
'server/index.ts': 'export function add(a: i32, b: i32): i32 {\n return a + b;\n}\n',
|
|
220
|
-
'server/main.ts':
|
|
221
|
-
"import { add } from './index';\n\n" +
|
|
222
|
-
'@main\nfunction run(): i32 {\n return add(40, 2);\n}\n',
|
|
223
224
|
'README.md': [
|
|
224
225
|
'# ' + path.basename(name),
|
|
225
226
|
'',
|
|
@@ -237,9 +238,12 @@ function scaffold(
|
|
|
237
238
|
].join('\n'),
|
|
238
239
|
};
|
|
239
240
|
|
|
240
|
-
// The `app` template's client UI
|
|
241
|
-
// inline client here.
|
|
242
|
-
if (template === 'minimal')
|
|
241
|
+
// The `app` template's client UI + server are copied from examples/basic at runtime; `minimal`
|
|
242
|
+
// ships an inline client + a minimal working server here.
|
|
243
|
+
if (template === 'minimal') {
|
|
244
|
+
Object.assign(files, minimalClient(name, features));
|
|
245
|
+
Object.assign(files, minimalServer());
|
|
246
|
+
}
|
|
243
247
|
|
|
244
248
|
// Selected AI-assistant pointer files at the root (committed). The real docs are always seeded
|
|
245
249
|
// under .toil/docs (gitignored; regenerated by dev/build) since the framework manages them.
|
|
@@ -251,6 +255,33 @@ function scaffold(
|
|
|
251
255
|
return files;
|
|
252
256
|
}
|
|
253
257
|
|
|
258
|
+
/** A minimal but working server for the `minimal` template (the `app` template copies examples/basic/server). */
|
|
259
|
+
function minimalServer(): Record<string, string> {
|
|
260
|
+
return {
|
|
261
|
+
'server/HelloHandler.ts':
|
|
262
|
+
"import { ToilHandler, Request, Response, Method } from 'toiljs/server/runtime';\n\n" +
|
|
263
|
+
'export class HelloHandler extends ToilHandler {\n' +
|
|
264
|
+
' public handle(req: Request): Response {\n' +
|
|
265
|
+
' if (req.method != Method.GET && req.method != Method.HEAD) {\n' +
|
|
266
|
+
" return Response.empty(405).setHeader('allow', 'GET, HEAD');\n" +
|
|
267
|
+
' }\n' +
|
|
268
|
+
" return Response.text('hello from toiljs\\n');\n" +
|
|
269
|
+
' }\n' +
|
|
270
|
+
'}\n',
|
|
271
|
+
'server/main.ts':
|
|
272
|
+
"import { Server } from 'toiljs/server/runtime';\n" +
|
|
273
|
+
"import { revertOnError } from 'toiljs/server/runtime/abort/abort';\n" +
|
|
274
|
+
"import { HelloHandler } from './HelloHandler';\n\n" +
|
|
275
|
+
'// Wire your handler here.\n' +
|
|
276
|
+
'Server.handler = () => new HelloHandler();\n\n' +
|
|
277
|
+
'// Required: re-export the WASM entry points and the abort hook.\n' +
|
|
278
|
+
"export * from 'toiljs/server/runtime/exports';\n" +
|
|
279
|
+
'export function abort(message: string, fileName: string, line: u32, column: u32): void {\n' +
|
|
280
|
+
' revertOnError(message, fileName, line, column);\n' +
|
|
281
|
+
'}\n',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
254
285
|
/** The inline client UI for the `minimal` template (the `app` template copies examples/basic/client). */
|
|
255
286
|
function minimalClient(name: string, features: StyleFeatures): Record<string, string> {
|
|
256
287
|
const files: Record<string, string> = {
|
|
@@ -351,6 +382,18 @@ function appClientDir(): string {
|
|
|
351
382
|
);
|
|
352
383
|
}
|
|
353
384
|
|
|
385
|
+
/** Absolute path to the `app` starter server (`examples/basic/server`), shipped in the package. */
|
|
386
|
+
function appServerDir(): string {
|
|
387
|
+
return path.resolve(
|
|
388
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
389
|
+
'..',
|
|
390
|
+
'..',
|
|
391
|
+
'examples',
|
|
392
|
+
'basic',
|
|
393
|
+
'server',
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
354
397
|
/**
|
|
355
398
|
* Applies the chosen styling to a copied template's client dir: renames the stylesheet to the
|
|
356
399
|
* preprocessor's extension, adds the Tailwind entry, and rewrites `toil.tsx`'s style imports.
|
|
@@ -526,8 +569,9 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
526
569
|
s.start('Scaffolding project');
|
|
527
570
|
await writeFiles(targetDir, scaffold(name, template, features, aiTools, images));
|
|
528
571
|
if (template === 'app') {
|
|
529
|
-
// Copy the example client (the single starter source), set
|
|
572
|
+
// Copy the example client + server (the single starter source), set the <title>, then style.
|
|
530
573
|
await fs.cp(appClientDir(), path.join(targetDir, 'client'), { recursive: true });
|
|
574
|
+
await fs.cp(appServerDir(), path.join(targetDir, 'server'), { recursive: true });
|
|
531
575
|
const indexHtml = path.join(targetDir, 'client', 'public', 'index.html');
|
|
532
576
|
const html = await fs.readFile(indexHtml, 'utf8');
|
|
533
577
|
await fs.writeFile(
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -403,7 +403,7 @@ export function checkWasmBuilt(exists: boolean): Check {
|
|
|
403
403
|
// --- Typed RPC (@data / @remote / @service) -------------------------------------------------------
|
|
404
404
|
|
|
405
405
|
/** Minimum toilscript: @rest/@route HTTP layer + RPC codegen + hardened decoders + @data editor decls. */
|
|
406
|
-
export const RPC_TOILSCRIPT_MIN = '0.1.
|
|
406
|
+
export const RPC_TOILSCRIPT_MIN = '0.1.15';
|
|
407
407
|
|
|
408
408
|
/** Whether each piece of the typed-RPC wiring is in place (computed in `doctor.ts`). */
|
|
409
409
|
export interface RpcFacts {
|
package/src/cli/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ interface Flags {
|
|
|
30
30
|
json?: boolean;
|
|
31
31
|
fix?: boolean;
|
|
32
32
|
target?: string;
|
|
33
|
+
server?: boolean;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function parseArgs(argv: string[]): Flags {
|
|
@@ -106,6 +107,9 @@ function parseArgs(argv: string[]): Flags {
|
|
|
106
107
|
case '--target':
|
|
107
108
|
flags.target = argv[++i];
|
|
108
109
|
break;
|
|
110
|
+
case '--server':
|
|
111
|
+
flags.server = true;
|
|
112
|
+
break;
|
|
109
113
|
default:
|
|
110
114
|
if (!arg.startsWith('-') && flags.name === undefined) flags.name = arg;
|
|
111
115
|
}
|
|
@@ -137,6 +141,7 @@ function printHelp(): void {
|
|
|
137
141
|
cmd('--no-ai', 'create: skip AI assistant files (CLAUDE.md, etc.)'),
|
|
138
142
|
cmd('-y, --yes', 'create: accept defaults (non-interactive)'),
|
|
139
143
|
cmd('--no-install', "create: don't install dependencies"),
|
|
144
|
+
cmd('--server', 'build: build only the server (regenerate shared/server.ts + wasm)'),
|
|
140
145
|
cmd('--json', 'doctor: machine-readable output'),
|
|
141
146
|
cmd('--fix', 'doctor: auto-fix what it can (typed-RPC wiring)'),
|
|
142
147
|
cmd('--target <t>', 'update: latest | minor | patch | newest | greatest'),
|
|
@@ -194,9 +199,17 @@ async function main(): Promise<void> {
|
|
|
194
199
|
|
|
195
200
|
case 'build':
|
|
196
201
|
banner();
|
|
197
|
-
process.stdout.write(
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
process.stdout.write(
|
|
203
|
+
dim(flags.server ? ' building the server…' : ' building for production…') +
|
|
204
|
+
'\n\n',
|
|
205
|
+
);
|
|
206
|
+
await build({ root: flags.root, serverOnly: flags.server });
|
|
207
|
+
process.stdout.write(
|
|
208
|
+
'\n' +
|
|
209
|
+
success(' ✓ ') +
|
|
210
|
+
bold(flags.server ? 'server build complete' : 'build complete') +
|
|
211
|
+
'\n\n',
|
|
212
|
+
);
|
|
200
213
|
break;
|
|
201
214
|
|
|
202
215
|
case 'start': {
|
|
@@ -215,7 +228,12 @@ async function main(): Promise<void> {
|
|
|
215
228
|
case 'doctor':
|
|
216
229
|
// Skip the banner for --json so stdout stays valid JSON.
|
|
217
230
|
if (!flags.json) banner();
|
|
218
|
-
await runDoctor({
|
|
231
|
+
await runDoctor({
|
|
232
|
+
root: flags.root,
|
|
233
|
+
cwd: process.cwd(),
|
|
234
|
+
json: flags.json,
|
|
235
|
+
fix: flags.fix,
|
|
236
|
+
});
|
|
219
237
|
break;
|
|
220
238
|
|
|
221
239
|
case 'update':
|
package/src/compiler/index.ts
CHANGED
|
@@ -11,13 +11,81 @@ import { generate } from './generate.js';
|
|
|
11
11
|
import { prerenderStaticParams } from './ssg.js';
|
|
12
12
|
import { createViteConfig } from './vite.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Every server `.ts` source file (under the directories of the toilconfig `entries`, or `server/`
|
|
16
|
+
* by default). Passed to toilscript as explicit entries so a dropped-in `@data`/`@rest` file is
|
|
17
|
+
* compiled - and its surface picked up into `shared/server.ts` - even if the toilconfig lists only
|
|
18
|
+
* `main.ts`. Paths are returned relative to `root`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* A `@data`/`@rest`/`@service`/`@remote` declaration - a file with one defines client surface.
|
|
22
|
+
* Anchored to line-start (after indentation) so a mention in a comment (e.g. `// the @rest ...`)
|
|
23
|
+
* does not count.
|
|
24
|
+
*/
|
|
25
|
+
const SURFACE_DECORATOR = /^[ \t]*@(data|rest|service|remote)\b/m;
|
|
26
|
+
|
|
27
|
+
function serverEntryFiles(root: string): string[] {
|
|
28
|
+
let entries: string[] = [];
|
|
29
|
+
try {
|
|
30
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
|
|
31
|
+
entries?: unknown;
|
|
32
|
+
};
|
|
33
|
+
if (Array.isArray(cfg.entries)) {
|
|
34
|
+
entries = cfg.entries.filter((e): e is string => typeof e === 'string');
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Start from the toilconfig entries (normalized), then add any server file that declares a
|
|
41
|
+
// surface, so a dropped-in @data/@rest file is compiled even when it is not listed. Non-surface
|
|
42
|
+
// helpers stay out of the entry list - they are still compiled when imported - which also avoids
|
|
43
|
+
// toilscript's "class is not a WebAssembly export" warning for handler classes.
|
|
44
|
+
const result = new Set<string>(entries.map((e) => path.relative(root, path.resolve(root, e))));
|
|
45
|
+
|
|
46
|
+
const dirs = new Set<string>();
|
|
47
|
+
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
48
|
+
if (dirs.size === 0) dirs.add(path.join(root, 'server'));
|
|
49
|
+
|
|
50
|
+
let scanned = 0;
|
|
51
|
+
const cap = 500;
|
|
52
|
+
const visit = (dir: string, depth: number): void => {
|
|
53
|
+
if (scanned >= cap || depth > 16) return;
|
|
54
|
+
let listing: fs.Dirent[];
|
|
55
|
+
try {
|
|
56
|
+
listing = fs.readdirSync(dir, { withFileTypes: true });
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const entry of listing) {
|
|
61
|
+
if (scanned >= cap) break;
|
|
62
|
+
const full = path.join(dir, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
if (entry.name !== 'node_modules') visit(full, depth + 1);
|
|
65
|
+
} else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
66
|
+
scanned++;
|
|
67
|
+
try {
|
|
68
|
+
if (SURFACE_DECORATOR.test(fs.readFileSync(full, 'utf8'))) {
|
|
69
|
+
result.add(path.relative(root, full));
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// unreadable: skip
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
for (const dir of dirs) visit(dir, 0);
|
|
78
|
+
return [...result].sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
14
81
|
/**
|
|
15
82
|
* Builds the toilscript server target (which also regenerates `shared/server.ts` via
|
|
16
83
|
* `--rpcModule`) when the project has one, signalled by a `toilconfig.json` at the root. This
|
|
17
84
|
* runs before the client build/dev so the generated `@data` + `Server` module the client
|
|
18
85
|
* imports is always current; without it a stale or missing `shared/server.ts` breaks the
|
|
19
|
-
* client build. A no-op for client-only projects.
|
|
20
|
-
*
|
|
86
|
+
* client build. A no-op for client-only projects. Compiles every server `.ts` file (not just the
|
|
87
|
+
* toilconfig entries) so dropped-in `@data`/`@rest` files are picked up. Runs the locally
|
|
88
|
+
* installed `toilscript`, resolved + invoked via Node (no `.bin` shim / PATH assumptions).
|
|
21
89
|
*/
|
|
22
90
|
async function buildServer(root: string): Promise<void> {
|
|
23
91
|
if (!fs.existsSync(path.join(root, 'toilconfig.json'))) return;
|
|
@@ -39,12 +107,13 @@ async function buildServer(root: string): Promise<void> {
|
|
|
39
107
|
);
|
|
40
108
|
}
|
|
41
109
|
|
|
110
|
+
// Explicit entries (every server file) override the toilconfig entries; the target options
|
|
111
|
+
// (optimization, features, runtime) still come from the toilconfig's `release` target.
|
|
112
|
+
const files = serverEntryFiles(root);
|
|
113
|
+
const args = [binJs, ...files, '--target', 'release', '--rpcModule', 'shared/server.ts'];
|
|
114
|
+
|
|
42
115
|
await new Promise<void>((resolve, reject) => {
|
|
43
|
-
const child = spawn(
|
|
44
|
-
process.execPath,
|
|
45
|
-
[binJs, '--target', 'release', '--rpcModule', 'shared/server.ts'],
|
|
46
|
-
{ cwd: root, stdio: 'inherit' },
|
|
47
|
-
);
|
|
116
|
+
const child = spawn(process.execPath, args, { cwd: root, stdio: 'inherit' });
|
|
48
117
|
child.on('error', reject);
|
|
49
118
|
child.on('close', (code) =>
|
|
50
119
|
code === 0
|
|
@@ -59,6 +128,8 @@ export interface ToilCommandOptions {
|
|
|
59
128
|
readonly port?: number;
|
|
60
129
|
/** Bind host for `start`. Defaults to loopback (`127.0.0.1`); pass `0.0.0.0` to expose. */
|
|
61
130
|
readonly host?: string;
|
|
131
|
+
/** `build` only: build the server (regenerate `shared/server.ts` + the wasm) and skip the client. */
|
|
132
|
+
readonly serverOnly?: boolean;
|
|
62
133
|
}
|
|
63
134
|
|
|
64
135
|
/** Starts the Vite dev server (HMR + transforms) for the client app. Returns the running server. */
|
|
@@ -72,10 +143,12 @@ export async function dev(opts: ToilCommandOptions = {}): Promise<ViteDevServer>
|
|
|
72
143
|
return server;
|
|
73
144
|
}
|
|
74
145
|
|
|
75
|
-
/** Produces an optimized production SPA bundle in the configured `outDir`.
|
|
146
|
+
/** Produces an optimized production SPA bundle in the configured `outDir`. With `serverOnly`,
|
|
147
|
+
* builds just the server (regenerates `shared/server.ts` + the wasm) and skips the client. */
|
|
76
148
|
export async function build(opts: ToilCommandOptions = {}): Promise<void> {
|
|
77
149
|
const cfg = await loadConfig(opts);
|
|
78
150
|
await buildServer(cfg.root);
|
|
151
|
+
if (opts.serverOnly) return;
|
|
79
152
|
generate(cfg);
|
|
80
153
|
await viteBuild(await createViteConfig(cfg));
|
|
81
154
|
// SSG: bake per-URL HTML + sitemap for dynamic routes that opt in via `generateStaticParams`.
|