toiljs 0.0.48 → 0.0.50
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/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +17 -9
- package/build/client/.tsbuildinfo +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +2 -2
- package/build/compiler/email-preview.js +4 -2
- package/build/compiler/generate.js +0 -1
- package/build/compiler/index.js +13 -1
- package/build/compiler/vite.js +14 -1
- package/examples/basic/client/layout.tsx +1 -1
- package/examples/basic/client/routes/pq.tsx +2 -2
- package/examples/basic/client/routes/rpc.tsx +1 -1
- package/examples/basic/client/routes/search.tsx +1 -1
- package/package.json +2 -2
- package/src/cli/create.ts +1 -2
- package/src/cli/diagnostics.ts +2 -2
- package/src/cli/proc.ts +9 -3
- package/src/cli/ui.ts +0 -1
- package/src/cli/update.ts +16 -3
- package/src/client/head/head.ts +1 -1
- package/src/client/head/metadata.ts +1 -1
- package/src/compiler/docs.ts +2 -2
- package/src/compiler/email-preview.ts +4 -2
- package/src/compiler/generate.ts +2 -1
- package/src/compiler/index.ts +20 -2
- package/src/compiler/vite.ts +21 -1
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.50",
|
|
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": {
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"nodemailer": "^9.0.0",
|
|
131
131
|
"picocolors": "^1.1.1",
|
|
132
132
|
"sharp": "^0.35.0",
|
|
133
|
-
"toilscript": "^0.1.
|
|
133
|
+
"toilscript": "^0.1.27",
|
|
134
134
|
"typescript-eslint": "^8.60.0",
|
|
135
135
|
"vite": "^8.0.14",
|
|
136
136
|
"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.27',
|
|
118
118
|
typescript: '^6.0.3',
|
|
119
119
|
};
|
|
120
120
|
for (const dep of requiredPackages(features).sort()) {
|
|
@@ -302,7 +302,6 @@ declare enum EmailStatus { Sent, Disabled, Budget, RecipientCapped, Deduped, Try
|
|
|
302
302
|
declare namespace EmailService { function send(to: string, subject: string, body: string, purpose?: string, html?: string): EmailStatus; }
|
|
303
303
|
declare class RenderedEmail { subject: string; body: string; html: string; constructor(subject: string, body: string, html: string); }
|
|
304
304
|
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; }
|
|
305
|
-
declare enum RateLimit { FixedWindow, SlidingWindow, TokenBucket }
|
|
306
305
|
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; }
|
|
307
306
|
declare namespace Environment { function get(key: string): string | null; function getSecure(key: string): string | null; }
|
|
308
307
|
declare class TwoFactorIssue { code: string; token: string; constructor(code: string, token: string); }
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -402,8 +402,8 @@ export function checkWasmBuilt(exists: boolean): Check {
|
|
|
402
402
|
|
|
403
403
|
// --- Typed RPC (@data / @remote / @service) -------------------------------------------------------
|
|
404
404
|
|
|
405
|
-
/** Minimum toilscript: @rest/@route
|
|
406
|
-
export const RPC_TOILSCRIPT_MIN = '0.1.
|
|
405
|
+
/** Minimum toilscript: @rest/@route + RPC codegen + hardened decoders + @data editor decls (TS2395 fix) + RateLimit-enum @ratelimit typing. */
|
|
406
|
+
export const RPC_TOILSCRIPT_MIN = '0.1.27';
|
|
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/proc.ts
CHANGED
|
@@ -6,12 +6,18 @@ import { spawn } from 'node:child_process';
|
|
|
6
6
|
* `shell: true` is deprecated (DEP0190), so the whole command is passed as one string there
|
|
7
7
|
* (args are fixed/allowlisted, never raw user input). POSIX spawns directly.
|
|
8
8
|
*/
|
|
9
|
-
export function run(
|
|
9
|
+
export function run(
|
|
10
|
+
cmd: string,
|
|
11
|
+
args: string[],
|
|
12
|
+
cwd: string,
|
|
13
|
+
opts: { stdio?: 'ignore' | 'inherit' } = {},
|
|
14
|
+
): Promise<void> {
|
|
10
15
|
return new Promise((resolve, reject) => {
|
|
11
16
|
const onWindows = process.platform === 'win32';
|
|
17
|
+
const stdio = opts.stdio ?? 'ignore';
|
|
12
18
|
const child = onWindows
|
|
13
|
-
? spawn([cmd, ...args].join(' '), { cwd, stdio
|
|
14
|
-
: spawn(cmd, args, { cwd, stdio
|
|
19
|
+
? spawn([cmd, ...args].join(' '), { cwd, stdio, shell: true })
|
|
20
|
+
: spawn(cmd, args, { cwd, stdio });
|
|
15
21
|
child.on('error', reject);
|
|
16
22
|
child.on('close', (code) =>
|
|
17
23
|
code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${String(code)}`)),
|
package/src/cli/ui.ts
CHANGED
|
@@ -134,7 +134,6 @@ export function box(lines: readonly string[], paint: (s: string) => string = (s)
|
|
|
134
134
|
* first full-stack framework for a globally distributed application delivery network.
|
|
135
135
|
*/
|
|
136
136
|
const TAGLINES: ReadonlyArray<(a: (s: string) => string) => string> = [
|
|
137
|
-
(a) => `the most performant ${a('react')} framework`,
|
|
138
137
|
(a) => `bringing ${a('hyper scale')} to anyone`,
|
|
139
138
|
(a) => `the first full-stack ${a('application delivery network')}`,
|
|
140
139
|
(a) => `your app, ${a('globally distributed')} by default`,
|
package/src/cli/update.ts
CHANGED
|
@@ -140,9 +140,22 @@ export async function runUpdate(opts: UpdateOptions): Promise<void> {
|
|
|
140
140
|
await run('npx', ncuArgs(applyAll ? ['-u'] : ['-u', '--filter', selected.join(' ')]), root);
|
|
141
141
|
s.stop('package.json updated');
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
// Run the install with VISIBLE output (so a failure is diagnosable — npm
|
|
144
|
+
// prints the real error) and handle a non-zero exit gracefully, instead of
|
|
145
|
+
// leaving the spinner spinning forever on a failed install.
|
|
146
|
+
note(dim(`Running ${pm.name} install…`), 'Install');
|
|
147
|
+
try {
|
|
148
|
+
await run(pm.name, ['install'], root, { stdio: 'inherit' });
|
|
149
|
+
} catch {
|
|
150
|
+
outro(
|
|
151
|
+
danger(
|
|
152
|
+
`${pm.name} install failed — package.json was updated to the new versions, but the ` +
|
|
153
|
+
`install did not finish. Fix the error printed above, then run \`${pm.name} install\`.`,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
146
159
|
|
|
147
160
|
outro(
|
|
148
161
|
success(`Updated ${String(selected.length)} package${selected.length === 1 ? '' : 's'}.`),
|
package/src/client/head/head.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface LinkTag {
|
|
|
25
25
|
export interface HeadSpec {
|
|
26
26
|
/** Document title. */
|
|
27
27
|
readonly title?: string;
|
|
28
|
-
/** Template applied to a child's title, `%s` = the title (e.g. `'%s
|
|
28
|
+
/** Template applied to a child's title, `%s` = the title (e.g. `'%s, toiljs'`). */
|
|
29
29
|
readonly titleTemplate?: string;
|
|
30
30
|
readonly meta?: readonly MetaTag[];
|
|
31
31
|
readonly link?: readonly LinkTag[];
|
|
@@ -21,7 +21,7 @@ export interface OpenGraph {
|
|
|
21
21
|
export interface Metadata {
|
|
22
22
|
/** Document title. */
|
|
23
23
|
readonly title?: string;
|
|
24
|
-
/** Template applied to the title (`%s` = the title), e.g. `'%s
|
|
24
|
+
/** Template applied to the title (`%s` = the title), e.g. `'%s, toiljs'`. */
|
|
25
25
|
readonly titleTemplate?: string;
|
|
26
26
|
/** `<meta name="description">`. */
|
|
27
27
|
readonly description?: string;
|
package/src/compiler/docs.ts
CHANGED
|
@@ -8,7 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
|
|
10
10
|
/** Shared body for the per-tool pointer files. */
|
|
11
|
-
const POINTER_BODY = `# toiljs
|
|
11
|
+
const POINTER_BODY = `# toiljs, AI assistant guide
|
|
12
12
|
|
|
13
13
|
This is a **toiljs** project, a full-stack React framework (React + Vite client, file-based
|
|
14
14
|
routing, and a toilscript→WebAssembly server).
|
|
@@ -183,7 +183,7 @@ export const TOIL_DOCS: Record<string, string> = {
|
|
|
183
183
|
'',
|
|
184
184
|
' Toil.useHead({',
|
|
185
185
|
' title: "Blog",',
|
|
186
|
-
' titleTemplate: "%s
|
|
186
|
+
' titleTemplate: "%s, MyApp",',
|
|
187
187
|
' meta: [{ name: "description", content: "..." }],',
|
|
188
188
|
' });',
|
|
189
189
|
]),
|
|
@@ -120,7 +120,7 @@ export function previewShellHtml(): string {
|
|
|
120
120
|
<head>
|
|
121
121
|
<meta charset="utf-8" />
|
|
122
122
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
123
|
-
<title>Email preview
|
|
123
|
+
<title>Email preview, toiljs</title>
|
|
124
124
|
<style>
|
|
125
125
|
/* Matches the toiljs demo brand (examples/basic/client/styles/main.css). */
|
|
126
126
|
:root {
|
|
@@ -213,7 +213,9 @@ export function previewShellHtml(): string {
|
|
|
213
213
|
subjectEl.textContent = fill(rendered.subject);
|
|
214
214
|
if (format === 'html') {
|
|
215
215
|
frame.hidden = false; textEl.hidden = true;
|
|
216
|
-
|
|
216
|
+
// Wrap the email FRAGMENT in a minimal dark document so the iframe doesn't
|
|
217
|
+
// show the browser-default white body/margin around and below the email.
|
|
218
|
+
frame.srcdoc = '<!doctype html><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:#080d11}</style>' + fill(rendered.html);
|
|
217
219
|
} else {
|
|
218
220
|
frame.hidden = true; textEl.hidden = false;
|
|
219
221
|
textEl.textContent = fill(rendered.text);
|
package/src/compiler/generate.ts
CHANGED
|
@@ -115,7 +115,8 @@ export const TOIL_SERVER_ENV_DTS =
|
|
|
115
115
|
`declare namespace EmailService { function send(to: string, subject: string, body: string, purpose?: string, html?: string): EmailStatus; }\n` +
|
|
116
116
|
`declare class RenderedEmail { subject: string; body: string; html: string; constructor(subject: string, body: string, html: string); }\n` +
|
|
117
117
|
`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; }\n` +
|
|
118
|
-
`
|
|
118
|
+
// `RateLimit` (the @ratelimit strategy enum) is declared by toilscript's std d.ts,
|
|
119
|
+
// which owns the @ratelimit decorator; declaring it here too would collide.
|
|
119
120
|
`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; }\n` +
|
|
120
121
|
`declare namespace Environment { function get(key: string): string | null; function getSecure(key: string): string | null; }\n` +
|
|
121
122
|
`declare class TwoFactorIssue { code: string; token: string; constructor(code: string, token: string); }\n` +
|
package/src/compiler/index.ts
CHANGED
|
@@ -220,6 +220,14 @@ function watchServer(cfg: ResolvedToilConfig, watcher: ViteDevServer['watcher'])
|
|
|
220
220
|
dirs.some((dir) => file === dir || file.startsWith(dir + path.sep));
|
|
221
221
|
const isEmailSource = (file: string): boolean =>
|
|
222
222
|
/\.(tsx|jsx)$/.test(file) && (file === emailsDir || file.startsWith(emailsDir + path.sep));
|
|
223
|
+
// A transient watch error must NOT crash the dev server: an unhandled 'error'
|
|
224
|
+
// on the chokidar watcher takes down the whole process. Windows throws EBUSY /
|
|
225
|
+
// EPERM when a file is momentarily locked (an editor save, a formatter, our own
|
|
226
|
+
// rebuild, a just-written file). Swallow it — the next change still fires.
|
|
227
|
+
watcher.on('error', (err: unknown) => {
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
process.stdout.write(pc.yellow(' ! ') + pc.dim(`file watcher: ${msg}`) + '\n');
|
|
230
|
+
});
|
|
223
231
|
watcher.add([...dirs, emailsDir]);
|
|
224
232
|
watcher.on('all', (_event, file) => {
|
|
225
233
|
if (!isServerSource(file) && !isEmailSource(file)) return;
|
|
@@ -237,12 +245,22 @@ function watchServer(cfg: ResolvedToilConfig, watcher: ViteDevServer['watcher'])
|
|
|
237
245
|
* close the servers, and force-exit after a short grace period no matter what.
|
|
238
246
|
*/
|
|
239
247
|
function installDevShutdown(close: () => Promise<void> | void): void {
|
|
248
|
+
// Final, SYNCHRONOUS cursor + style restore — `exit` runs no matter how we go
|
|
249
|
+
// (signal, throw, normal), so the terminal can't be left without a cursor.
|
|
250
|
+
const restoreTerminal = (): void => {
|
|
251
|
+
try {
|
|
252
|
+
process.stdout.write('\x1b[0m\x1b[?25h');
|
|
253
|
+
} catch {
|
|
254
|
+
/* stream already closed */
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
process.on('exit', restoreTerminal);
|
|
258
|
+
|
|
240
259
|
let closing = false;
|
|
241
260
|
const shutdown = (): void => {
|
|
242
261
|
if (closing) return;
|
|
243
262
|
closing = true;
|
|
244
|
-
|
|
245
|
-
process.stdout.write('\x1b[?25h');
|
|
263
|
+
restoreTerminal();
|
|
246
264
|
process.stdout.write(pc.dim('\n shutting down dev server…') + '\n');
|
|
247
265
|
// Force-exit even if a server hangs on close (the orphan-prevention).
|
|
248
266
|
const hard = setTimeout(() => process.exit(0), 1500);
|
package/src/compiler/vite.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
|
|
6
|
+
import pc from 'picocolors';
|
|
6
7
|
import react from '@vitejs/plugin-react';
|
|
7
8
|
import { imagetools } from 'vite-imagetools';
|
|
8
9
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
|
9
|
-
import { mergeConfig, type InlineConfig, type PluginOption } from 'vite';
|
|
10
|
+
import { createLogger, mergeConfig, type InlineConfig, type Logger, type PluginOption } from 'vite';
|
|
10
11
|
|
|
11
12
|
import { type ResolvedToilConfig } from './config.js';
|
|
12
13
|
import { fontPreloadPlugin } from './fonts.js';
|
|
@@ -120,6 +121,24 @@ function manualChunks(id: string): string | undefined {
|
|
|
120
121
|
* splitting and tuned build options, is applied here; `toiljs/client` is aliased to the
|
|
121
122
|
* runtime, and the user's `client.vite` overrides deep-merge on top.
|
|
122
123
|
*/
|
|
124
|
+
/**
|
|
125
|
+
* Vite's `(ssr)` environment label is dark blue — nearly invisible on a black
|
|
126
|
+
* terminal. Wrap the default logger to recolor `(ssr)` magenta (purple) and
|
|
127
|
+
* `(client)` cyan, so the dev reload logs are readable.
|
|
128
|
+
*/
|
|
129
|
+
function brandedLogger(): Logger {
|
|
130
|
+
const logger = createLogger();
|
|
131
|
+
const recolor = (msg: string): string =>
|
|
132
|
+
msg
|
|
133
|
+
.replace(/(?:\[[0-9;]*m)*\(ssr\)(?:\[[0-9;]*m)*/g, pc.magenta('(ssr)'))
|
|
134
|
+
.replace(/(?:\[[0-9;]*m)*\(client\)(?:\[[0-9;]*m)*/g, pc.cyan('(client)'));
|
|
135
|
+
const info = logger.info.bind(logger);
|
|
136
|
+
const warn = logger.warn.bind(logger);
|
|
137
|
+
logger.info = (msg, opts) => info(recolor(msg), opts);
|
|
138
|
+
logger.warn = (msg, opts) => warn(recolor(msg), opts);
|
|
139
|
+
return logger;
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
export async function createViteConfig(cfg: ResolvedToilConfig): Promise<InlineConfig> {
|
|
124
143
|
const frameworkRoot = path.resolve(path.dirname(cfg.runtimePath), '..', '..');
|
|
125
144
|
const tailwind = await tailwindPlugin(cfg.root);
|
|
@@ -128,6 +147,7 @@ export async function createViteConfig(cfg: ResolvedToilConfig): Promise<InlineC
|
|
|
128
147
|
root: cfg.toilDir,
|
|
129
148
|
base: cfg.base,
|
|
130
149
|
configFile: false,
|
|
150
|
+
customLogger: brandedLogger(),
|
|
131
151
|
plugins: [
|
|
132
152
|
tailwind,
|
|
133
153
|
// Build-time image resize/optimization. Every *imported* raster image is compressed to
|