toiljs 0.0.30 → 0.0.32
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/.github/workflows/ci.yml +1 -1
- package/CHANGELOG.md +22 -0
- package/build/backend/.tsbuildinfo +1 -1
- package/build/backend/index.js +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +170 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/index.d.ts +1 -1
- package/build/compiler/index.js +2 -2
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/index.js +1 -1
- package/build/devserver/proxy.d.ts +1 -1
- package/package.json +5 -6
- package/src/backend/index.ts +2 -2
- package/src/cli/index.ts +9 -0
- package/src/cli/notify.ts +125 -0
- package/src/cli/update.ts +2 -2
- package/src/cli/version-check.ts +121 -0
- package/src/compiler/index.ts +6 -2
- package/src/devserver/index.ts +2 -2
- package/src/devserver/proxy.ts +1 -1
- package/test/version-check.test.ts +130 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the automatic toiljs update check that runs on every CLI invocation
|
|
3
|
+
* (and therefore on `npm run dev` / `npm run build`, which call the CLI). IO-free so it
|
|
4
|
+
* can be unit-tested; the registry fetch, cache file, and printing live in `notify.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** How long a registry answer is trusted before we ask npm again. */
|
|
8
|
+
export const CHECK_TTL_MS = 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
/** What we persist between runs: the latest known version and when we asked. */
|
|
11
|
+
export interface CheckCache {
|
|
12
|
+
readonly latest: string | null;
|
|
13
|
+
readonly checkedAt: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Parses the cache file contents; returns null when malformed (forces a fresh check). */
|
|
17
|
+
export function parseCheckCache(raw: string): CheckCache | null {
|
|
18
|
+
try {
|
|
19
|
+
const parsed: unknown = JSON.parse(raw);
|
|
20
|
+
if (typeof parsed !== 'object' || parsed === null) return null;
|
|
21
|
+
const o = parsed as Record<string, unknown>;
|
|
22
|
+
const latest = typeof o.latest === 'string' ? o.latest : null;
|
|
23
|
+
const checkedAt = typeof o.checkedAt === 'number' ? o.checkedAt : NaN;
|
|
24
|
+
if (!Number.isFinite(checkedAt)) return null;
|
|
25
|
+
return { latest, checkedAt };
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the cached answer is still trustworthy (also stale if the clock went backwards). */
|
|
32
|
+
export function isCacheFresh(cache: CheckCache, now: number, ttlMs: number = CHECK_TTL_MS): boolean {
|
|
33
|
+
return cache.checkedAt <= now && now - cache.checkedAt < ttlMs;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Parsed {
|
|
37
|
+
readonly nums: readonly [number, number, number];
|
|
38
|
+
readonly pre: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseSemver(v: string): Parsed {
|
|
42
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v.trim());
|
|
43
|
+
if (!m) return { nums: [0, 0, 0], pre: null };
|
|
44
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compares two semver strings: negative when `a < b`, 0 when equal, positive when `a > b`.
|
|
49
|
+
* A prerelease sorts below its release (`0.1.0-beta.1 < 0.1.0`); two prereleases compare
|
|
50
|
+
* lexicographically, which is enough for an update nudge.
|
|
51
|
+
*/
|
|
52
|
+
export function compareSemver(a: string, b: string): number {
|
|
53
|
+
const pa = parseSemver(a);
|
|
54
|
+
const pb = parseSemver(b);
|
|
55
|
+
for (let i = 0; i < 3; i++) {
|
|
56
|
+
if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] < pb.nums[i] ? -1 : 1;
|
|
57
|
+
}
|
|
58
|
+
if (pa.pre === pb.pre) return 0;
|
|
59
|
+
if (pa.pre === null) return 1;
|
|
60
|
+
if (pb.pre === null) return -1;
|
|
61
|
+
return pa.pre < pb.pre ? -1 : 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** True when `installed` is behind `latest`. */
|
|
65
|
+
export function isOutdated(installed: string, latest: string): boolean {
|
|
66
|
+
return compareSemver(installed, latest) < 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The command that updates toiljs for the project's package manager, or globally. */
|
|
70
|
+
export function installCommand(pm: string, scope: 'project' | 'global'): string {
|
|
71
|
+
if (scope === 'global') {
|
|
72
|
+
if (pm === 'pnpm') return 'pnpm add -g toiljs@latest';
|
|
73
|
+
if (pm === 'yarn') return 'yarn global add toiljs@latest';
|
|
74
|
+
if (pm === 'bun') return 'bun add -g toiljs@latest';
|
|
75
|
+
return 'npm install -g toiljs@latest';
|
|
76
|
+
}
|
|
77
|
+
if (pm === 'pnpm') return 'pnpm add toiljs@latest';
|
|
78
|
+
if (pm === 'yarn') return 'yarn add toiljs@latest';
|
|
79
|
+
if (pm === 'bun') return 'bun add toiljs@latest';
|
|
80
|
+
return 'npm install toiljs@latest';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One out-of-date install we want to nag about. */
|
|
84
|
+
export interface OutdatedRow {
|
|
85
|
+
/** Where the stale copy lives: the project's node_modules or the global CLI. */
|
|
86
|
+
readonly scope: 'project' | 'global';
|
|
87
|
+
readonly installed: string;
|
|
88
|
+
readonly command: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Decides what (if anything) to warn about. `projectVersion` is the toiljs resolved in the
|
|
93
|
+
* project's node_modules (null when not installed there), `cliVersion` is the copy of the CLI
|
|
94
|
+
* actually running, and `cliIsLocal` tells us whether those are the same install.
|
|
95
|
+
*/
|
|
96
|
+
export function findOutdated(
|
|
97
|
+
latest: string,
|
|
98
|
+
projectVersion: string | null,
|
|
99
|
+
cliVersion: string,
|
|
100
|
+
cliIsLocal: boolean,
|
|
101
|
+
pm: string,
|
|
102
|
+
): OutdatedRow[] {
|
|
103
|
+
const rows: OutdatedRow[] = [];
|
|
104
|
+
if (projectVersion !== null && isOutdated(projectVersion, latest)) {
|
|
105
|
+
rows.push({
|
|
106
|
+
scope: 'project',
|
|
107
|
+
installed: projectVersion,
|
|
108
|
+
command: installCommand(pm, 'project'),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// The running CLI only gets its own row when it is not the project install we already
|
|
112
|
+
// reported (global installs, or `npx toiljs` outside a project).
|
|
113
|
+
if (!cliIsLocal && isOutdated(cliVersion, latest)) {
|
|
114
|
+
rows.push({
|
|
115
|
+
scope: 'global',
|
|
116
|
+
installed: cliVersion,
|
|
117
|
+
command: installCommand('npm', 'global'),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return rows;
|
|
121
|
+
}
|
package/src/compiler/index.ts
CHANGED
|
@@ -6,8 +6,10 @@ import path from 'node:path';
|
|
|
6
6
|
|
|
7
7
|
import pc from 'picocolors';
|
|
8
8
|
import { build as viteBuild, createServer, mergeConfig, type ViteDevServer } from 'vite';
|
|
9
|
-
|
|
10
|
-
import
|
|
9
|
+
// The server modules pull in @dacely/hyper-express, whose uWebSockets.js native
|
|
10
|
+
// addon loads at import time. Only `dev`/`start` need them, so they are imported
|
|
11
|
+
// lazily; `create`/`build`/`doctor` must never touch the native binary.
|
|
12
|
+
import type { RunningBackend } from 'toiljs/backend';
|
|
11
13
|
|
|
12
14
|
import { loadConfig } from './config.js';
|
|
13
15
|
import { generate } from './generate.js';
|
|
@@ -263,6 +265,7 @@ export async function dev(opts: ToilCommandOptions = {}): Promise<ViteDevServer>
|
|
|
263
265
|
const server = await createServer(viteConfig);
|
|
264
266
|
await server.listen();
|
|
265
267
|
|
|
268
|
+
const { startDevServer } = await import('toiljs/devserver');
|
|
266
269
|
const front = await startDevServer({
|
|
267
270
|
root: cfg.root,
|
|
268
271
|
port: cfg.port,
|
|
@@ -322,6 +325,7 @@ export async function start(opts: ToilCommandOptions = {}): Promise<RunningBacke
|
|
|
322
325
|
if (!fs.existsSync(path.join(outDir, 'index.html'))) {
|
|
323
326
|
throw new Error(`No build found in ${outDir}. Run \`toiljs build\` first.`);
|
|
324
327
|
}
|
|
328
|
+
const { startBackend } = await import('toiljs/backend');
|
|
325
329
|
return startBackend({ root: outDir, port: cfg.port, host: opts.host });
|
|
326
330
|
}
|
|
327
331
|
|
package/src/devserver/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The toiljs WASM dev server: a uWebSockets.js front (via @
|
|
2
|
+
* The toiljs WASM dev server: a uWebSockets.js front (via @dacely/hyper-express,
|
|
3
3
|
* the same stack as `toiljs/backend`) that dispatches HTTP requests into the
|
|
4
4
|
* ToilScript-compiled server wasm exactly like the production edge does, and
|
|
5
5
|
* proxies everything the server does not claim to an internal Vite dev server,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
|
|
25
|
-
import { Server, type Request, type Response } from '@
|
|
25
|
+
import { Server, type Request, type Response } from '@dacely/hyper-express';
|
|
26
26
|
import pc from 'picocolors';
|
|
27
27
|
|
|
28
28
|
import { METHOD_CODES, type EnvelopeRequest } from './envelope.js';
|
package/src/devserver/proxy.ts
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CHECK_TTL_MS,
|
|
5
|
+
compareSemver,
|
|
6
|
+
findOutdated,
|
|
7
|
+
installCommand,
|
|
8
|
+
isCacheFresh,
|
|
9
|
+
isOutdated,
|
|
10
|
+
parseCheckCache,
|
|
11
|
+
} from '../src/cli/version-check';
|
|
12
|
+
|
|
13
|
+
describe('compareSemver', () => {
|
|
14
|
+
it('orders plain versions', () => {
|
|
15
|
+
expect(compareSemver('0.0.31', '0.0.32')).toBeLessThan(0);
|
|
16
|
+
expect(compareSemver('0.0.32', '0.0.31')).toBeGreaterThan(0);
|
|
17
|
+
expect(compareSemver('0.0.31', '0.0.31')).toBe(0);
|
|
18
|
+
expect(compareSemver('0.9.9', '0.10.0')).toBeLessThan(0);
|
|
19
|
+
expect(compareSemver('1.0.0', '0.99.99')).toBeGreaterThan(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('treats a prerelease as older than its release', () => {
|
|
23
|
+
expect(compareSemver('0.1.0-beta.1', '0.1.0')).toBeLessThan(0);
|
|
24
|
+
expect(compareSemver('0.1.0', '0.1.0-beta.1')).toBeGreaterThan(0);
|
|
25
|
+
expect(compareSemver('0.1.0-alpha', '0.1.0-beta')).toBeLessThan(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('tolerates a leading v and garbage input', () => {
|
|
29
|
+
expect(compareSemver('v1.2.3', '1.2.3')).toBe(0);
|
|
30
|
+
expect(compareSemver('garbage', '0.0.0')).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('isOutdated', () => {
|
|
35
|
+
it('is true only when installed is behind latest', () => {
|
|
36
|
+
expect(isOutdated('0.0.31', '0.0.32')).toBe(true);
|
|
37
|
+
expect(isOutdated('0.0.32', '0.0.32')).toBe(false);
|
|
38
|
+
// A local build ahead of the registry must not warn.
|
|
39
|
+
expect(isOutdated('0.0.33', '0.0.32')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('parseCheckCache', () => {
|
|
44
|
+
it('parses a valid cache entry', () => {
|
|
45
|
+
expect(parseCheckCache('{"latest":"0.0.32","checkedAt":1000}')).toEqual({
|
|
46
|
+
latest: '0.0.32',
|
|
47
|
+
checkedAt: 1000,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps a cached failure (latest null) so offline machines back off', () => {
|
|
52
|
+
expect(parseCheckCache('{"latest":null,"checkedAt":1000}')).toEqual({
|
|
53
|
+
latest: null,
|
|
54
|
+
checkedAt: 1000,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects malformed contents', () => {
|
|
59
|
+
expect(parseCheckCache('not json')).toBeNull();
|
|
60
|
+
expect(parseCheckCache('[]')).toBeNull();
|
|
61
|
+
expect(parseCheckCache('{"latest":"0.0.32"}')).toBeNull();
|
|
62
|
+
expect(parseCheckCache('{"latest":42,"checkedAt":"soon"}')).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('isCacheFresh', () => {
|
|
67
|
+
const cache = { latest: '0.0.32', checkedAt: 10_000 };
|
|
68
|
+
|
|
69
|
+
it('is fresh within the TTL and stale after it', () => {
|
|
70
|
+
expect(isCacheFresh(cache, 10_000 + CHECK_TTL_MS - 1)).toBe(true);
|
|
71
|
+
expect(isCacheFresh(cache, 10_000 + CHECK_TTL_MS)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('is stale when the clock went backwards past checkedAt', () => {
|
|
75
|
+
expect(isCacheFresh(cache, 9_999)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('installCommand', () => {
|
|
80
|
+
it('targets the project with the detected package manager', () => {
|
|
81
|
+
expect(installCommand('npm', 'project')).toBe('npm install toiljs@latest');
|
|
82
|
+
expect(installCommand('pnpm', 'project')).toBe('pnpm add toiljs@latest');
|
|
83
|
+
expect(installCommand('yarn', 'project')).toBe('yarn add toiljs@latest');
|
|
84
|
+
expect(installCommand('bun', 'project')).toBe('bun add toiljs@latest');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('targets the global install', () => {
|
|
88
|
+
expect(installCommand('npm', 'global')).toBe('npm install -g toiljs@latest');
|
|
89
|
+
expect(installCommand('pnpm', 'global')).toBe('pnpm add -g toiljs@latest');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('findOutdated', () => {
|
|
94
|
+
it('flags an outdated project install with the project command', () => {
|
|
95
|
+
const rows = findOutdated('0.0.32', '0.0.31', '0.0.31', true, 'pnpm');
|
|
96
|
+
expect(rows).toEqual([
|
|
97
|
+
{ scope: 'project', installed: '0.0.31', command: 'pnpm add toiljs@latest' },
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('flags an outdated global CLI even when the project is current', () => {
|
|
102
|
+
const rows = findOutdated('0.0.32', '0.0.32', '0.0.30', false, 'npm');
|
|
103
|
+
expect(rows).toEqual([
|
|
104
|
+
{ scope: 'global', installed: '0.0.30', command: 'npm install -g toiljs@latest' },
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('flags both when a stale global CLI runs inside a stale project', () => {
|
|
109
|
+
const rows = findOutdated('0.0.32', '0.0.31', '0.0.30', false, 'npm');
|
|
110
|
+
expect(rows.map((r) => r.scope)).toEqual(['project', 'global']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not double-report when the running CLI is the project install', () => {
|
|
114
|
+
const rows = findOutdated('0.0.32', '0.0.31', '0.0.31', true, 'npm');
|
|
115
|
+
expect(rows).toHaveLength(1);
|
|
116
|
+
expect(rows[0].scope).toBe('project');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('reports nothing when everything is current or ahead', () => {
|
|
120
|
+
expect(findOutdated('0.0.32', '0.0.32', '0.0.32', true, 'npm')).toEqual([]);
|
|
121
|
+
expect(findOutdated('0.0.32', null, '0.0.33', false, 'npm')).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('handles a missing project install (npx outside a project)', () => {
|
|
125
|
+
const rows = findOutdated('0.0.32', null, '0.0.31', false, 'npm');
|
|
126
|
+
expect(rows).toEqual([
|
|
127
|
+
{ scope: 'global', installed: '0.0.31', command: 'npm install -g toiljs@latest' },
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
});
|