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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.48",
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.26",
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.26',
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); }
@@ -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 HTTP layer + RPC codegen + hardened decoders + correct @data editor decls (TS2395 fix). */
406
- export const RPC_TOILSCRIPT_MIN = '0.1.26';
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(cmd: string, args: string[], cwd: string): Promise<void> {
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: 'ignore', shell: true })
14
- : spawn(cmd, args, { cwd, stdio: 'ignore' });
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
- s.start(`Installing with ${pm.name}`);
144
- await run(pm.name, ['install'], root);
145
- s.stop('Dependencies installed');
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'}.`),
@@ -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 · toiljs'`). */
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 · toiljs'`. */
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;
@@ -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 · AI assistant guide
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 · MyApp",',
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 · toiljs</title>
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
- frame.srcdoc = fill(rendered.html);
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);
@@ -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
- `declare enum RateLimit { FixedWindow, SlidingWindow, TokenBucket }\n` +
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` +
@@ -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
- // Restore the cursor (anything that hid it leaves the terminal odd on exit).
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);
@@ -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