toiljs 0.0.113 → 0.0.115
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 +18 -0
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +198 -41
- package/build/client/.tsbuildinfo +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/index.js +1 -1
- package/build/compiler/pages.js +1 -13
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +39 -2
- package/build/compiler/toil-docs.generated.js +3 -3
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/host.d.ts +2 -1
- package/build/devserver/daemon/host.js +12 -12
- package/build/devserver/daemon/index.js +3 -3
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/docs/background/daemons.md +49 -3
- package/docs/cli/README.md +4 -2
- package/docs/getting-started/installation.md +18 -0
- package/package.json +15 -15
- package/src/cli/create.ts +171 -25
- package/src/cli/diagnostics.ts +74 -0
- package/src/cli/doctor.ts +105 -27
- package/src/cli/features.ts +7 -3
- package/src/cli/index.ts +2 -2
- package/src/cli/update.ts +30 -3
- package/src/cli/updates.ts +23 -0
- package/src/compiler/index.ts +11 -2
- package/src/compiler/pages.ts +1 -22
- package/src/compiler/prerender.ts +57 -3
- package/src/compiler/toil-docs.generated.ts +3 -3
- package/src/devserver/daemon/host.ts +29 -19
- package/src/devserver/daemon/index.ts +12 -5
- package/test/daemon-emulation.test.ts +44 -2
- package/test/doctor.test.ts +28 -0
- package/test/fixtures/daemon-app.ts +9 -4
- package/test/prerender.test.ts +64 -2
- package/test/update.test.ts +15 -1
- package/tsconfig.base.json +0 -1
package/src/cli/create.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { run } from './proc.js';
|
|
|
38
38
|
import { accent, dim, version } from './ui.js';
|
|
39
39
|
import { isPackageManager, isValidName, resolveProjectDir } from './validate.js';
|
|
40
40
|
|
|
41
|
-
export type Template = 'app' | 'minimal';
|
|
41
|
+
export type Template = 'app' | 'minimal' | 'agent';
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The `server/migrations/` README, scaffolded by `create` and ensured by `doctor`/`update`. ToilDB
|
|
@@ -157,6 +157,7 @@ function scaffold(
|
|
|
157
157
|
private: true,
|
|
158
158
|
type: 'module',
|
|
159
159
|
scripts: {
|
|
160
|
+
start: 'toiljs start',
|
|
160
161
|
dev: 'toiljs dev',
|
|
161
162
|
build: 'toiljs build',
|
|
162
163
|
'build:server': 'toiljs build --server',
|
|
@@ -297,11 +298,16 @@ function scaffold(
|
|
|
297
298
|
'server/migrations/README.md': MIGRATIONS_README,
|
|
298
299
|
};
|
|
299
300
|
|
|
300
|
-
// The `app` template's client UI + server are copied from examples/basic at runtime;
|
|
301
|
-
//
|
|
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.
|
|
302
305
|
if (template === 'minimal') {
|
|
303
306
|
Object.assign(files, minimalClient(name, features));
|
|
304
307
|
Object.assign(files, minimalServer());
|
|
308
|
+
} else if (template === 'agent') {
|
|
309
|
+
Object.assign(files, agentClient(name, features));
|
|
310
|
+
Object.assign(files, agentServer());
|
|
305
311
|
}
|
|
306
312
|
|
|
307
313
|
// Selected AI-assistant pointer files at the root (committed). The real docs are always seeded
|
|
@@ -362,6 +368,22 @@ declare namespace AuthService { const SESSION_COOKIE: string; const USER_COOKIE:
|
|
|
362
368
|
interface __ToilAuthUser {}
|
|
363
369
|
`;
|
|
364
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
|
+
|
|
365
387
|
/**
|
|
366
388
|
* A minimal but working server for the `minimal` template (the `app` template copies
|
|
367
389
|
* examples/basic/server). Same folder conventions as the full starter, just fewer files:
|
|
@@ -405,25 +427,86 @@ function minimalServer(): Record<string, string> {
|
|
|
405
427
|
'export function abort(message: string, fileName: string, line: u32, column: u32): void {\n' +
|
|
406
428
|
' revertOnError(message, fileName, line, column);\n' +
|
|
407
429
|
'}\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
|
-
'| `migrations/` | ToilDB schema migrations: a `<Type>.migration.ts` per evolving `@data` value type, holding the old shapes + the `@migrate` transform. |\n' +
|
|
417
|
-
'| `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |\n' +
|
|
418
|
-
'| `services/` | `@service` classes and free `@remote` functions (typed RPC). |\n' +
|
|
419
|
-
'| `scheduled/` | Reserved for scheduled tasks (not shipped yet). |\n\n' +
|
|
420
|
-
'New decorated files are picked up automatically by `toiljs build`/`dev`; also add an import\n' +
|
|
421
|
-
'in `main.ts` so a direct `toilscript` run builds the same server.\n',
|
|
430
|
+
'server/README.md': SERVER_README,
|
|
422
431
|
};
|
|
423
432
|
}
|
|
424
433
|
|
|
425
|
-
/**
|
|
426
|
-
|
|
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> {
|
|
427
510
|
const files: Record<string, string> = {
|
|
428
511
|
'client/public/index.html':
|
|
429
512
|
'<!doctype html>\n<html lang="en">\n <head>\n' +
|
|
@@ -471,7 +554,17 @@ function minimalClient(name: string, features: StyleFeatures): Record<string, st
|
|
|
471
554
|
'Toil.mount(routes, layout, notFound, globalError, slots);\n',
|
|
472
555
|
[`client/${styleEntry(features.preprocessor)}`]: DEFAULT_STYLE_CONTENT,
|
|
473
556
|
'client/components/.gitkeep': '# Place shared React components here.\n',
|
|
474
|
-
|
|
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';
|
|
475
568
|
|
|
476
569
|
export default function Layout({ children }: { children?: ReactNode }) {
|
|
477
570
|
return (
|
|
@@ -487,14 +580,21 @@ export default function Layout({ children }: { children?: ReactNode }) {
|
|
|
487
580
|
}}>
|
|
488
581
|
<strong style={{ color: '#2563FF', fontSize: '1.1rem' }}>${path.basename(name)}</strong>
|
|
489
582
|
<nav style={{ display: 'flex', gap: '1rem' }}>
|
|
490
|
-
|
|
583
|
+
${links}
|
|
491
584
|
</nav>
|
|
492
585
|
</header>
|
|
493
586
|
{children}
|
|
494
587
|
</div>
|
|
495
588
|
);
|
|
496
589
|
}
|
|
497
|
-
|
|
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' }]),
|
|
498
598
|
'client/routes/index.tsx':
|
|
499
599
|
'export default function Home() {\n' +
|
|
500
600
|
' return (\n <main>\n' +
|
|
@@ -502,8 +602,49 @@ export default function Layout({ children }: { children?: ReactNode }) {
|
|
|
502
602
|
' <p>File-based routing, bundled by Vite, zero config.</p>\n' +
|
|
503
603
|
' </main>\n );\n}\n',
|
|
504
604
|
};
|
|
505
|
-
|
|
506
|
-
|
|
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
|
+
};
|
|
507
648
|
}
|
|
508
649
|
|
|
509
650
|
/**
|
|
@@ -624,6 +765,11 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
624
765
|
hint: 'the full ToilJS starter, landing page, layout, styles, demo routes',
|
|
625
766
|
},
|
|
626
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
|
+
},
|
|
627
773
|
];
|
|
628
774
|
const choice = await select({
|
|
629
775
|
message: 'Which template?',
|
|
@@ -631,7 +777,7 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
631
777
|
initialValue: 'app',
|
|
632
778
|
});
|
|
633
779
|
bail(choice);
|
|
634
|
-
template = choice === 'minimal'
|
|
780
|
+
template = choice === 'minimal' || choice === 'agent' ? choice : 'app';
|
|
635
781
|
}
|
|
636
782
|
|
|
637
783
|
let preprocessor: Preprocessor = opts.preprocessor ?? 'css';
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -83,6 +83,80 @@ export function checkPeer(name: string, installed: string | null, range: string)
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** The TypeScript range toiljs supports, and the range `doctor --fix` pins an unsupported one to. */
|
|
87
|
+
export const TYPESCRIPT_SUPPORTED = '>=6.0.0 <7.0.0';
|
|
88
|
+
export const TYPESCRIPT_FIX_RANGE = '^6.0.3';
|
|
89
|
+
|
|
90
|
+
/** First TypeScript major that dropped the JavaScript compiler API (the native port). */
|
|
91
|
+
const TYPESCRIPT_NATIVE_MAJOR = 7;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The major a dependency range floats to: the first number in the range, once the comparators are
|
|
95
|
+
* stripped (`^7.0.2` -> 7, `>=6.0.0 <7.0.0` -> 6). Returns null for a range that names no version
|
|
96
|
+
* (`*`, `latest`), which is treated as "unpinned" rather than "unsupported".
|
|
97
|
+
*/
|
|
98
|
+
export function rangeMajor(range: string): number | null {
|
|
99
|
+
const m = /(\d+)/.exec(range.replace(/^[\s^~>=<v]*/, ''));
|
|
100
|
+
return m ? Number(m[1]) : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* TypeScript 7 is the native (Go) port: it publishes `tsc` but no longer exports the JavaScript
|
|
105
|
+
* compiler API, whose main entry is now just `{ version, versionMajorMinor }`. toiljs reads each
|
|
106
|
+
* route's static `metadata` through that API to bake SEO tags into the built HTML, and the eslint
|
|
107
|
+
* and typedoc presets load it too. So a TS 7 install does not fail loudly, it silently stops baking
|
|
108
|
+
* metadata (and hard-crashes typescript-eslint), which is worth a `fail` rather than a warning.
|
|
109
|
+
*
|
|
110
|
+
* `installed` is the version resolved from node_modules (authoritative, it is what actually runs);
|
|
111
|
+
* `declared` is the project's package.json range, which is flagged when it *admits* 7 even if the
|
|
112
|
+
* currently-installed copy is older, since the next install would break.
|
|
113
|
+
*/
|
|
114
|
+
export function checkTypeScript(installed: string | null, declared: string | null): Check {
|
|
115
|
+
const id = 'peer:typescript';
|
|
116
|
+
const label = 'typescript';
|
|
117
|
+
const unsupported = (version: string, detail: string): Check => ({
|
|
118
|
+
id,
|
|
119
|
+
label,
|
|
120
|
+
status: 'fail',
|
|
121
|
+
detail,
|
|
122
|
+
fix:
|
|
123
|
+
`TypeScript ${version} removed the JavaScript compiler API (it moved to ` +
|
|
124
|
+
`'typescript/unstable/*'), which toiljs's route-metadata extractor, the eslint preset, ` +
|
|
125
|
+
`and typedoc all load. Pin typescript to "${TYPESCRIPT_FIX_RANGE}" and reinstall ` +
|
|
126
|
+
`(\`toiljs doctor --fix\` does this).`,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (installed === null) {
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
label,
|
|
133
|
+
status: 'fail',
|
|
134
|
+
detail: `not installed (requires ${TYPESCRIPT_SUPPORTED})`,
|
|
135
|
+
fix: `Install typescript@"${TYPESCRIPT_FIX_RANGE}".`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const installedMajor = rangeMajor(installed);
|
|
140
|
+
if (installedMajor !== null && installedMajor >= TYPESCRIPT_NATIVE_MAJOR) {
|
|
141
|
+
return unsupported(String(installedMajor), `${installed} installed, unsupported`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Installed copy is fine, but the declared range would pull an unsupported major next install.
|
|
145
|
+
const declaredMajor = declared === null ? null : rangeMajor(declared);
|
|
146
|
+
if (declaredMajor !== null && declaredMajor >= TYPESCRIPT_NATIVE_MAJOR) {
|
|
147
|
+
return unsupported(String(declaredMajor), `package.json declares ${declared ?? ''}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const ok = satisfiesMin(installed, TYPESCRIPT_SUPPORTED);
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
label,
|
|
154
|
+
status: ok ? 'pass' : 'warn',
|
|
155
|
+
detail: `${installed} (requires ${TYPESCRIPT_SUPPORTED})`,
|
|
156
|
+
fix: ok ? undefined : `Update typescript to ${TYPESCRIPT_SUPPORTED}.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
86
160
|
export function checkPackageManager(lockfiles: readonly string[]): Check {
|
|
87
161
|
if (lockfiles.length === 0) {
|
|
88
162
|
return {
|
package/src/cli/doctor.ts
CHANGED
|
@@ -46,15 +46,18 @@ import {
|
|
|
46
46
|
checkToilconfig,
|
|
47
47
|
checkToiljsInstalled,
|
|
48
48
|
checkToilscriptInstalled,
|
|
49
|
+
checkTypeScript,
|
|
49
50
|
checkWasmBuilt,
|
|
50
51
|
findRelativeAssets,
|
|
51
52
|
hasFailures,
|
|
53
|
+
rangeMajor,
|
|
52
54
|
type RestFacts,
|
|
53
55
|
RPC_TOILSCRIPT_MIN,
|
|
54
56
|
type RpcFacts,
|
|
55
57
|
satisfiesMin,
|
|
56
58
|
type SourceFile,
|
|
57
59
|
summarize,
|
|
60
|
+
TYPESCRIPT_FIX_RANGE,
|
|
58
61
|
} from './diagnostics.js';
|
|
59
62
|
import {
|
|
60
63
|
detectTailwind,
|
|
@@ -70,7 +73,7 @@ export interface DoctorOptions {
|
|
|
70
73
|
readonly cwd: string;
|
|
71
74
|
/** Emit machine-readable JSON instead of the human report. */
|
|
72
75
|
readonly json?: boolean;
|
|
73
|
-
/** Auto-fix what can be fixed in place (
|
|
76
|
+
/** Auto-fix what can be fixed in place (the typed-RPC wiring, and an unsupported typescript). */
|
|
74
77
|
readonly fix?: boolean;
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -129,6 +132,72 @@ function isPackageInstalled(root: string, name: string): boolean {
|
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The version of `name` actually installed for the project at `root`, or null when absent. Reads the
|
|
137
|
+
* resolved package.json rather than trusting the declared range, since the installed copy is what
|
|
138
|
+
* the compiler API consumers load. Falls back to walking `node_modules` (see `isPackageInstalled`).
|
|
139
|
+
*/
|
|
140
|
+
function installedVersion(root: string, name: string): string | null {
|
|
141
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
142
|
+
let pkgPath: string | null = null;
|
|
143
|
+
try {
|
|
144
|
+
pkgPath = require.resolve(`${name}/package.json`);
|
|
145
|
+
} catch {
|
|
146
|
+
for (let dir = root; pkgPath === null; ) {
|
|
147
|
+
const candidate = path.join(dir, 'node_modules', name, 'package.json');
|
|
148
|
+
if (fs.existsSync(candidate)) pkgPath = candidate;
|
|
149
|
+
const parent = path.dirname(dir);
|
|
150
|
+
if (parent === dir) break;
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (pkgPath === null) return null;
|
|
155
|
+
const pkg = readJsonObject(pkgPath);
|
|
156
|
+
return pkg && typeof pkg.version === 'string' ? pkg.version : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Pins an unsupported TypeScript (the native 7.x, which ships no JavaScript compiler API) back to a
|
|
161
|
+
* range toiljs can drive. Rewrites whichever of `devDependencies`/`dependencies` declares it; a
|
|
162
|
+
* project with no declaration at all is left alone (nothing to pin), and the reinstall is left to
|
|
163
|
+
* the user since only they know their package manager.
|
|
164
|
+
*/
|
|
165
|
+
function applyTypeScriptFix(root: string): RpcFixResult {
|
|
166
|
+
const changed: string[] = [];
|
|
167
|
+
const skipped: string[] = [];
|
|
168
|
+
|
|
169
|
+
const pkgPath = path.join(root, 'package.json');
|
|
170
|
+
const pkg = readJsonObject(pkgPath);
|
|
171
|
+
if (pkg === null) return { changed, skipped };
|
|
172
|
+
|
|
173
|
+
const field = (['devDependencies', 'dependencies'] as const).find((f) => {
|
|
174
|
+
const deps = asRecord(pkg[f]);
|
|
175
|
+
return deps !== null && typeof deps.typescript === 'string';
|
|
176
|
+
});
|
|
177
|
+
if (field === undefined) {
|
|
178
|
+
// Nothing declares typescript: it is either absent or hoisted from a workspace root.
|
|
179
|
+
if (installedVersion(root, 'typescript') !== null) {
|
|
180
|
+
skipped.push('typescript (installed but not declared in package.json; pin it by hand)');
|
|
181
|
+
}
|
|
182
|
+
return { changed, skipped };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const deps = asRecord(pkg[field]);
|
|
186
|
+
if (deps === null) return { changed, skipped };
|
|
187
|
+
const declared = deps.typescript as string;
|
|
188
|
+
const declaredMajor = rangeMajor(declared);
|
|
189
|
+
const installedMajor = rangeMajor(installedVersion(root, 'typescript') ?? '');
|
|
190
|
+
const unsupported =
|
|
191
|
+
(declaredMajor !== null && declaredMajor >= 7) ||
|
|
192
|
+
(installedMajor !== null && installedMajor >= 7);
|
|
193
|
+
if (!unsupported) return { changed, skipped };
|
|
194
|
+
|
|
195
|
+
deps.typescript = TYPESCRIPT_FIX_RANGE;
|
|
196
|
+
writeFile(pkgPath, JSON.stringify(pkg, null, 4) + '\n');
|
|
197
|
+
changed.push(`package.json (typescript ${declared} -> ${TYPESCRIPT_FIX_RANGE})`);
|
|
198
|
+
return { changed, skipped };
|
|
199
|
+
}
|
|
200
|
+
|
|
132
201
|
/** Narrows a value to a plain (non-array) object, or null. */
|
|
133
202
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
134
203
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
@@ -764,7 +833,26 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
764
833
|
}
|
|
765
834
|
|
|
766
835
|
const peerName = (n: string): Check => checkPeer(n, deps[n] ?? null, meta.peers[n] ?? '*');
|
|
767
|
-
|
|
836
|
+
// typescript gets its own check: its peer range is the only one whose upper bound matters (7.x
|
|
837
|
+
// clears the `>=6.0.0` floor but ships no compiler API), and it is fixable in place.
|
|
838
|
+
const peerChecks = Object.keys(meta.peers)
|
|
839
|
+
.filter((n) => n !== 'typescript')
|
|
840
|
+
.map(peerName);
|
|
841
|
+
const typeScriptFix = opts.fix ? applyTypeScriptFix(root) : null;
|
|
842
|
+
// Re-read the declared range: `--fix` may have just rewritten it.
|
|
843
|
+
const declaredTypeScript = (() => {
|
|
844
|
+
const pkg = readJsonObject(path.join(root, 'package.json'));
|
|
845
|
+
if (pkg === null) return deps.typescript ?? null;
|
|
846
|
+
for (const field of ['devDependencies', 'dependencies'] as const) {
|
|
847
|
+
const range = asRecord(pkg[field])?.typescript;
|
|
848
|
+
if (typeof range === 'string') return range;
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
})();
|
|
852
|
+
const typeScriptCheck = checkTypeScript(
|
|
853
|
+
installedVersion(root, 'typescript'),
|
|
854
|
+
declaredTypeScript,
|
|
855
|
+
);
|
|
768
856
|
|
|
769
857
|
// Server tooling (RPC wiring + the prettier plugin + the editor LS plugin): optionally fix in
|
|
770
858
|
// place, then re-read.
|
|
@@ -784,19 +872,15 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
784
872
|
const serverTsParsed = serverTsPath ? readJsonObject(serverTsPath) : null;
|
|
785
873
|
const serverTsPluginPresent =
|
|
786
874
|
serverTsPath === null || serverTsParsed === null ? true : tsconfigHasToilPlugin(serverTsParsed);
|
|
875
|
+
// The typescript pin is fixable without a server; the rest only apply to one.
|
|
876
|
+
const applied = [typeScriptFix, rpcFix, prettierFix, editorFix].filter(
|
|
877
|
+
(fix): fix is RpcFixResult => fix !== null,
|
|
878
|
+
);
|
|
787
879
|
const serverFix =
|
|
788
|
-
|
|
880
|
+
applied.length > 0
|
|
789
881
|
? {
|
|
790
|
-
changed:
|
|
791
|
-
|
|
792
|
-
...(prettierFix?.changed ?? []),
|
|
793
|
-
...(editorFix?.changed ?? []),
|
|
794
|
-
],
|
|
795
|
-
skipped: [
|
|
796
|
-
...(rpcFix?.skipped ?? []),
|
|
797
|
-
...(prettierFix?.skipped ?? []),
|
|
798
|
-
...(editorFix?.skipped ?? []),
|
|
799
|
-
],
|
|
882
|
+
changed: applied.flatMap((fix) => fix.changed),
|
|
883
|
+
skipped: applied.flatMap((fix) => fix.skipped),
|
|
800
884
|
}
|
|
801
885
|
: null;
|
|
802
886
|
|
|
@@ -807,6 +891,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
807
891
|
checkNode(process.versions.node, meta.node),
|
|
808
892
|
checkToiljsInstalled('toiljs' in deps ? version() : null),
|
|
809
893
|
...peerChecks,
|
|
894
|
+
typeScriptCheck,
|
|
810
895
|
checkPackageManager(LOCKFILES.filter((f) => fs.existsSync(path.join(root, f)))),
|
|
811
896
|
],
|
|
812
897
|
},
|
|
@@ -876,28 +961,21 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
876
961
|
} else {
|
|
877
962
|
process.stdout.write('\n' + accent(' Doctor') + dim(` ${root}`) + '\n\n');
|
|
878
963
|
renderHuman(groups);
|
|
879
|
-
if (serverFix)
|
|
880
|
-
else if (opts.fix && !serverPresent) {
|
|
881
|
-
process.stdout.write(
|
|
882
|
-
' ' + dim('--fix: no server (toilconfig.json) found, nothing to wire.') + '\n\n',
|
|
883
|
-
);
|
|
884
|
-
}
|
|
964
|
+
if (serverFix) renderFix(serverFix);
|
|
885
965
|
}
|
|
886
966
|
if (hasFailures(summary)) process.exitCode = 1;
|
|
887
967
|
}
|
|
888
968
|
|
|
889
|
-
/** Prints the result of `--fix`, and whether a reinstall is needed (
|
|
890
|
-
function
|
|
969
|
+
/** Prints the result of `--fix`, and whether a reinstall is needed (a changed dependency range). */
|
|
970
|
+
function renderFix(result: RpcFixResult): void {
|
|
891
971
|
const out: string[] = [];
|
|
892
972
|
if (result.changed.length > 0) {
|
|
893
|
-
out.push(' ' + success('fixed
|
|
894
|
-
if (result.changed.
|
|
895
|
-
out.push(
|
|
896
|
-
' ' + dim('run your installer (npm/pnpm/yarn) if the toilscript version changed.'),
|
|
897
|
-
);
|
|
973
|
+
out.push(' ' + success('fixed') + dim(` ${result.changed.join(', ')}`));
|
|
974
|
+
if (result.changed.some((entry) => entry.startsWith('package.json'))) {
|
|
975
|
+
out.push(' ' + dim('run your installer (npm/pnpm/yarn) to apply the version changes.'));
|
|
898
976
|
}
|
|
899
977
|
} else {
|
|
900
|
-
out.push(' ' + dim('
|
|
978
|
+
out.push(' ' + dim('nothing to fix.'));
|
|
901
979
|
}
|
|
902
980
|
for (const item of result.skipped) out.push(' ' + warn('skipped') + dim(` ${item}`));
|
|
903
981
|
process.stdout.write(out.join('\n') + '\n\n');
|
package/src/cli/features.ts
CHANGED
|
@@ -36,13 +36,17 @@ export const PREPROCESSOR_PKG: Record<Preprocessor, string | null> = {
|
|
|
36
36
|
/** Tailwind v4 packages. The framework auto-wires the Vite plugin when `@tailwindcss/vite` resolves. */
|
|
37
37
|
export const TAILWIND_PKGS: readonly string[] = ['tailwindcss', '@tailwindcss/vite'];
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Pinned versions for every package these features may install. `@tailwindcss/vite` must be
|
|
41
|
+
* `>= 4.3.0`: earlier 4.x releases cap their `vite` peer at `^7`, so they ERESOLVE against the
|
|
42
|
+
* Vite 8 toiljs ships (the "Tailwind broken on install" error). Keep both Tailwind pins in lockstep.
|
|
43
|
+
*/
|
|
40
44
|
export const PKG_VERSION: Record<string, string> = {
|
|
41
45
|
sass: '^1.83.0',
|
|
42
46
|
less: '^4.2.1',
|
|
43
47
|
stylus: '^0.64.0',
|
|
44
|
-
tailwindcss: '^4.
|
|
45
|
-
'@tailwindcss/vite': '^4.
|
|
48
|
+
tailwindcss: '^4.3.0',
|
|
49
|
+
'@tailwindcss/vite': '^4.3.0',
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
/** Dedicated Tailwind entry (kept `.css` so no preprocessor touches its `@import`). */
|
package/src/cli/index.ts
CHANGED
|
@@ -74,7 +74,7 @@ function parseArgs(argv: string[]): Flags {
|
|
|
74
74
|
case '--template':
|
|
75
75
|
case '-t': {
|
|
76
76
|
const t = argv[++i];
|
|
77
|
-
if (t === 'app' || t === 'minimal') flags.template = t;
|
|
77
|
+
if (t === 'app' || t === 'minimal' || t === 'agent') flags.template = t;
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
80
|
case '--pm':
|
|
@@ -160,7 +160,7 @@ function printHelp(): void {
|
|
|
160
160
|
cmd('--port <n>', 'dev/start: listen port (or PORT env / client.port; default 3000)'),
|
|
161
161
|
cmd('--host <h>', 'dev/start: bind host, e.g. 0.0.0.0 (or HOST env / client.host; default 127.0.0.1)'),
|
|
162
162
|
cmd('--threads <n>', 'start: production HTTP worker count'),
|
|
163
|
-
cmd('-t, --template', 'create: app | minimal'),
|
|
163
|
+
cmd('-t, --template', 'create: app | minimal | agent'),
|
|
164
164
|
cmd('--style <name>', 'create/configure: css | sass | less | stylus'),
|
|
165
165
|
cmd('--tailwind', 'create/configure: enable Tailwind (--no-tailwind to remove)'),
|
|
166
166
|
cmd('--no-ai', 'create: skip AI assistant files (CLAUDE.md, etc.)'),
|
package/src/cli/update.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { cancel, intro, isCancel, multiselect, note, outro, spinner } from '@cla
|
|
|
11
11
|
|
|
12
12
|
import { MIGRATIONS_README } from './create.js';
|
|
13
13
|
import { capture, run } from './proc.js';
|
|
14
|
-
import { buildRows, type Bump, parseNcuJson, type UpdateRow } from './updates.js';
|
|
14
|
+
import { buildRows, type Bump, parseNcuJson, type UpdateRow, withheldUpgrades } from './updates.js';
|
|
15
15
|
import { accent, danger, dim, success, warn } from './ui.js';
|
|
16
16
|
|
|
17
17
|
export interface UpdateOptions {
|
|
@@ -109,6 +109,20 @@ function rowLine(row: UpdateRow): string {
|
|
|
109
109
|
return `${row.name} ${dim(row.from)} ${dim('->')} ${bumpColor(row.bump, row.to)}`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/** Tells the user which upgrades were held back, and why they are not simply missing. */
|
|
113
|
+
function noteWithheld(names: readonly string[]): void {
|
|
114
|
+
note(
|
|
115
|
+
names.map((n) => `${dim('-')} ${n}`).join('\n') +
|
|
116
|
+
'\n\n' +
|
|
117
|
+
dim(
|
|
118
|
+
'Held back: this major is not supported by toiljs yet. TypeScript 7 is the native\n' +
|
|
119
|
+
'port and ships no JavaScript compiler API, so route metadata would stop being\n' +
|
|
120
|
+
'baked into the built HTML and typescript-eslint would not load.',
|
|
121
|
+
),
|
|
122
|
+
warn('Not upgraded'),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
112
126
|
const TARGETS = new Set(['latest', 'minor', 'patch', 'newest', 'greatest']);
|
|
113
127
|
|
|
114
128
|
export async function runUpdate(opts: UpdateOptions): Promise<void> {
|
|
@@ -152,13 +166,21 @@ export async function runUpdate(opts: UpdateOptions): Promise<void> {
|
|
|
152
166
|
process.exitCode = 1;
|
|
153
167
|
return;
|
|
154
168
|
}
|
|
155
|
-
|
|
169
|
+
// Hold back upgrades into a major toiljs cannot run on (typescript 7), so neither the picker nor
|
|
170
|
+
// a `-y` run can install one. `--reject` keeps ncu from applying them during `-u` below.
|
|
171
|
+
const upgraded = parseNcuJson(res.stdout);
|
|
172
|
+
const withheld = withheldUpgrades(upgraded);
|
|
173
|
+
for (const name of withheld) delete upgraded[name];
|
|
174
|
+
|
|
175
|
+
const rows = buildRows(upgraded, currentDeps);
|
|
156
176
|
if (rows.length === 0) {
|
|
157
177
|
s.stop('Everything is up to date');
|
|
178
|
+
if (withheld.length > 0) noteWithheld(withheld);
|
|
158
179
|
outro(success('Nothing to update.'));
|
|
159
180
|
return;
|
|
160
181
|
}
|
|
161
182
|
s.stop(`${String(rows.length)} update${rows.length === 1 ? '' : 's'} available`);
|
|
183
|
+
if (withheld.length > 0) noteWithheld(withheld);
|
|
162
184
|
|
|
163
185
|
const counts = { major: 0, minor: 0, patch: 0, other: 0 };
|
|
164
186
|
for (const r of rows) counts[r.bump]++;
|
|
@@ -195,7 +217,12 @@ export async function runUpdate(opts: UpdateOptions): Promise<void> {
|
|
|
195
217
|
|
|
196
218
|
s.start('Updating package.json');
|
|
197
219
|
const applyAll = selected.length === rows.length;
|
|
198
|
-
|
|
220
|
+
const reject = withheld.length > 0 ? ['--reject', withheld.join(' ')] : [];
|
|
221
|
+
await run(
|
|
222
|
+
'npx',
|
|
223
|
+
ncuArgs(applyAll ? ['-u', ...reject] : ['-u', '--filter', selected.join(' '), ...reject]),
|
|
224
|
+
root,
|
|
225
|
+
);
|
|
199
226
|
s.stop('package.json updated');
|
|
200
227
|
|
|
201
228
|
// Run the install with VISIBLE output (so a failure is diagnosable — npm
|