toiljs 0.0.11 → 0.0.12
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/README.md +2 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.js +10 -4
- package/build/cli/create.js +58 -30
- package/build/cli/diagnostics.d.ts +55 -0
- package/build/cli/diagnostics.js +333 -0
- package/build/cli/doctor.d.ts +6 -0
- package/build/cli/doctor.js +249 -0
- package/build/cli/index.js +26 -0
- package/build/cli/proc.d.ts +5 -0
- package/build/cli/proc.js +20 -0
- package/build/cli/ui.d.ts +1 -0
- package/build/cli/ui.js +1 -0
- package/build/cli/update.d.ts +7 -0
- package/build/cli/update.js +117 -0
- package/build/cli/updates.d.ts +10 -0
- package/build/cli/updates.js +45 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/head/metadata.js +3 -1
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +2 -0
- package/build/client/navigation/navigation.js +1 -1
- package/build/client/routing/Router.js +2 -2
- package/build/client/search/search.d.ts +26 -0
- package/build/client/search/search.js +101 -0
- package/build/client/search/use-page-search.d.ts +8 -0
- package/build/client/search/use-page-search.js +21 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +26 -23
- package/build/compiler/index.d.ts +2 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/pages.d.ts +8 -0
- package/build/compiler/pages.js +37 -0
- package/build/compiler/plugin.js +3 -1
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +11 -5
- package/build/compiler/seo.js +10 -3
- package/build/io/.tsbuildinfo +1 -1
- package/examples/basic/client/components/Header.tsx +43 -41
- package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
- package/examples/basic/client/public/index.html +18 -16
- package/examples/basic/client/routes/(legal)/privacy.tsx +18 -19
- package/examples/basic/client/routes/(legal)/terms.tsx +15 -16
- package/examples/basic/client/routes/about.tsx +21 -22
- package/examples/basic/client/routes/blog/[id].tsx +26 -18
- package/examples/basic/client/routes/features/actions.tsx +67 -67
- package/examples/basic/client/routes/features/error/index.tsx +27 -27
- package/examples/basic/client/routes/features/head.tsx +38 -38
- package/examples/basic/client/routes/features/index.tsx +83 -75
- package/examples/basic/client/routes/features/realtime.tsx +34 -32
- package/examples/basic/client/routes/features/script.tsx +31 -31
- package/examples/basic/client/routes/features/seo.tsx +39 -39
- package/examples/basic/client/routes/features/template/index.tsx +20 -20
- package/examples/basic/client/routes/features/template/template.tsx +16 -18
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -23
- package/examples/basic/client/routes/gallery/index.tsx +42 -42
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -18
- package/examples/basic/client/routes/get-started.tsx +157 -84
- package/examples/basic/client/routes/index.tsx +137 -96
- package/examples/basic/client/routes/loader-demo/index.tsx +59 -52
- package/examples/basic/client/routes/search.tsx +61 -0
- package/examples/basic/client/routes/test.tsx +7 -8
- package/examples/basic/client/styles/main.css +624 -552
- package/package.json +2 -2
- package/presets/eslint.js +10 -3
- package/src/cli/configure.ts +363 -353
- package/src/cli/create.ts +563 -530
- package/src/cli/diagnostics.ts +421 -0
- package/src/cli/doctor.ts +318 -0
- package/src/cli/features.ts +166 -160
- package/src/cli/index.ts +242 -211
- package/src/cli/proc.ts +30 -0
- package/src/cli/ui.ts +111 -103
- package/src/cli/update.ts +150 -0
- package/src/cli/updates.ts +69 -0
- package/src/client/components/Image.tsx +91 -89
- package/src/client/dev/error-overlay.tsx +193 -197
- package/src/client/head/metadata.ts +94 -92
- package/src/client/index.ts +79 -64
- package/src/client/navigation/Link.tsx +94 -100
- package/src/client/navigation/navigation.ts +215 -218
- package/src/client/routing/Router.tsx +210 -193
- package/src/client/routing/hooks.ts +110 -114
- package/src/client/routing/lazy.ts +77 -81
- package/src/client/search/search.ts +189 -0
- package/src/client/search/use-page-search.ts +73 -0
- package/src/compiler/config.ts +173 -171
- package/src/compiler/fonts.ts +89 -87
- package/src/compiler/generate.ts +378 -373
- package/src/compiler/image-report.ts +88 -85
- package/src/compiler/index.ts +2 -0
- package/src/compiler/pages.ts +70 -0
- package/src/compiler/plugin.ts +51 -47
- package/src/compiler/prerender.ts +152 -130
- package/src/compiler/routes.ts +132 -131
- package/src/compiler/seo.ts +381 -356
- package/src/compiler/vite.ts +155 -145
- package/src/io/FastSet.ts +99 -96
- package/test/configure.test.ts +94 -90
- package/test/doctor.test.ts +140 -0
- package/test/dom/Image.test.tsx +73 -46
- package/test/dom/Script.test.tsx +48 -45
- package/test/dom/action.test.tsx +146 -129
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +2 -2
- package/test/dom/revalidate.test.tsx +1 -1
- package/test/dom/route-head.test.tsx +1 -2
- package/test/dom/slot.test.tsx +131 -109
- package/test/dom/view-transitions.test.tsx +53 -51
- package/test/features.test.ts +149 -142
- package/test/fonts.test.ts +28 -26
- package/test/head.test.ts +45 -35
- package/test/metadata.test.ts +42 -41
- package/test/pages.test.ts +105 -0
- package/test/prerender.test.ts +54 -46
- package/test/search.test.ts +114 -0
- package/test/seo.test.ts +164 -142
- package/test/update.test.ts +44 -0
package/src/cli/ui.ts
CHANGED
|
@@ -1,103 +1,111 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared CLI presentation: the toiljs brand banner, gradient text, and small helpers.
|
|
3
|
-
* Kept dependency-light (only picocolors); the gradient is hand-rolled truecolor ANSI so
|
|
4
|
-
* the logo pops without pulling in a gradient library.
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
import pc from 'picocolors';
|
|
11
|
-
|
|
12
|
-
type RGB = readonly [number, number, number];
|
|
13
|
-
|
|
14
|
-
/** toiljs brand palette. */
|
|
15
|
-
const PRIMARY: RGB = [37, 99, 255];
|
|
16
|
-
const SECONDARY: RGB = [124, 58, 237];
|
|
17
|
-
const ACCENT: RGB = [34, 227, 171];
|
|
18
|
-
|
|
19
|
-
/** Logo gradient stops: blue → purple → teal. */
|
|
20
|
-
const GRADIENT: readonly RGB[] = [PRIMARY, SECONDARY, ACCENT];
|
|
21
|
-
|
|
22
|
-
/** ANSI-shadow "TOIL" wordmark. */
|
|
23
|
-
const ART: readonly string[] = [
|
|
24
|
-
'████████╗ ██████╗ ██╗ ██╗ ',
|
|
25
|
-
'╚══██╔══╝ ██╔═══██╗ ██║ ██║ ',
|
|
26
|
-
' ██║ ██║ ██║ ██║ ██║ ',
|
|
27
|
-
' ██║ ██║ ██║ ██║ ██║ ',
|
|
28
|
-
' ██║ ╚██████╔╝ ██║ ███████╗',
|
|
29
|
-
' ╚═╝ ╚═════╝ ╚═╝ ╚══════╝',
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
export const dim = pc.dim;
|
|
33
|
-
export const bold = pc.bold;
|
|
34
|
-
|
|
35
|
-
/** True when we should emit ANSI color: a TTY (or FORCE_COLOR), and not disabled via NO_COLOR. */
|
|
36
|
-
function colorEnabled(): boolean {
|
|
37
|
-
if (process.env.NO_COLOR) return false;
|
|
38
|
-
if (process.env.FORCE_COLOR) return true;
|
|
39
|
-
return process.stdout.isTTY;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function rgb(color: RGB, s: string): string {
|
|
43
|
-
return colorEnabled() ? `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${s}\x1b[39m` : s;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** The primary brand accent (blue). No-ops to plain text when color is disabled. */
|
|
47
|
-
export function brand(s: string): string {
|
|
48
|
-
return rgb(PRIMARY, s);
|
|
49
|
-
}
|
|
50
|
-
export const accent = brand;
|
|
51
|
-
|
|
52
|
-
/** Success/positive accent (teal). */
|
|
53
|
-
export function success(s: string): string {
|
|
54
|
-
return rgb(ACCENT, s);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Error accent (red, kept outside the brand palette since errors should read as errors). */
|
|
58
|
-
export const danger = pc.red;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI presentation: the toiljs brand banner, gradient text, and small helpers.
|
|
3
|
+
* Kept dependency-light (only picocolors); the gradient is hand-rolled truecolor ANSI so
|
|
4
|
+
* the logo pops without pulling in a gradient library.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
|
|
12
|
+
type RGB = readonly [number, number, number];
|
|
13
|
+
|
|
14
|
+
/** toiljs brand palette. */
|
|
15
|
+
const PRIMARY: RGB = [37, 99, 255];
|
|
16
|
+
const SECONDARY: RGB = [124, 58, 237];
|
|
17
|
+
const ACCENT: RGB = [34, 227, 171];
|
|
18
|
+
|
|
19
|
+
/** Logo gradient stops: blue → purple → teal. */
|
|
20
|
+
const GRADIENT: readonly RGB[] = [PRIMARY, SECONDARY, ACCENT];
|
|
21
|
+
|
|
22
|
+
/** ANSI-shadow "TOIL" wordmark. */
|
|
23
|
+
const ART: readonly string[] = [
|
|
24
|
+
'████████╗ ██████╗ ██╗ ██╗ ',
|
|
25
|
+
'╚══██╔══╝ ██╔═══██╗ ██║ ██║ ',
|
|
26
|
+
' ██║ ██║ ██║ ██║ ██║ ',
|
|
27
|
+
' ██║ ██║ ██║ ██║ ██║ ',
|
|
28
|
+
' ██║ ╚██████╔╝ ██║ ███████╗',
|
|
29
|
+
' ╚═╝ ╚═════╝ ╚═╝ ╚══════╝',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const dim = pc.dim;
|
|
33
|
+
export const bold = pc.bold;
|
|
34
|
+
|
|
35
|
+
/** True when we should emit ANSI color: a TTY (or FORCE_COLOR), and not disabled via NO_COLOR. */
|
|
36
|
+
function colorEnabled(): boolean {
|
|
37
|
+
if (process.env.NO_COLOR) return false;
|
|
38
|
+
if (process.env.FORCE_COLOR) return true;
|
|
39
|
+
return process.stdout.isTTY;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rgb(color: RGB, s: string): string {
|
|
43
|
+
return colorEnabled() ? `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${s}\x1b[39m` : s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The primary brand accent (blue). No-ops to plain text when color is disabled. */
|
|
47
|
+
export function brand(s: string): string {
|
|
48
|
+
return rgb(PRIMARY, s);
|
|
49
|
+
}
|
|
50
|
+
export const accent = brand;
|
|
51
|
+
|
|
52
|
+
/** Success/positive accent (teal). */
|
|
53
|
+
export function success(s: string): string {
|
|
54
|
+
return rgb(ACCENT, s);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Error accent (red, kept outside the brand palette since errors should read as errors). */
|
|
58
|
+
export const danger = pc.red;
|
|
59
|
+
|
|
60
|
+
/** Warning accent (yellow, outside the brand palette so warnings read as warnings). */
|
|
61
|
+
export const warn = pc.yellow;
|
|
62
|
+
|
|
63
|
+
function lerp(a: number, b: number, t: number): number {
|
|
64
|
+
return Math.round(a + (b - a) * t);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Samples the multi-stop brand gradient at `t` in [0, 1]. */
|
|
68
|
+
function gradientAt(t: number): RGB {
|
|
69
|
+
const segments = GRADIENT.length - 1;
|
|
70
|
+
const scaled = t * segments;
|
|
71
|
+
const i = Math.min(Math.floor(scaled), segments - 1);
|
|
72
|
+
const a = GRADIENT[i];
|
|
73
|
+
const b = GRADIENT[i + 1];
|
|
74
|
+
const localT = scaled - i;
|
|
75
|
+
return [lerp(a[0], b[0], localT), lerp(a[1], b[1], localT), lerp(a[2], b[2], localT)];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Colors each character of `line` along the left→right brand gradient. */
|
|
79
|
+
function gradientLine(line: string): string {
|
|
80
|
+
const n = line.length;
|
|
81
|
+
let out = '';
|
|
82
|
+
for (let i = 0; i < n; i++) {
|
|
83
|
+
const [r, g, b] = gradientAt(n > 1 ? i / (n - 1) : 0);
|
|
84
|
+
out += `\x1b[38;2;${r};${g};${b}m${line[i]}`;
|
|
85
|
+
}
|
|
86
|
+
return out + '\x1b[39m';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Reads the toiljs package version (CLI lives at build/cli/, package root is two up). */
|
|
90
|
+
export function version(): string {
|
|
91
|
+
try {
|
|
92
|
+
const pkgPath = path.resolve(
|
|
93
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
94
|
+
'..',
|
|
95
|
+
'..',
|
|
96
|
+
'package.json',
|
|
97
|
+
);
|
|
98
|
+
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
99
|
+
const match = /"version"\s*:\s*"([^"]+)"/.exec(raw);
|
|
100
|
+
if (match && match[1]) return match[1];
|
|
101
|
+
} catch {}
|
|
102
|
+
return '0.0.0';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Prints the brand banner: gradient logo + tagline + version. */
|
|
106
|
+
export function banner(): void {
|
|
107
|
+
const lines = colorEnabled() ? ART.map(gradientLine) : ART.slice();
|
|
108
|
+
const tagline = ` the most performant ${brand('react')} framework`;
|
|
109
|
+
const ver = `${dim(' v')}${brand(version())}`;
|
|
110
|
+
process.stdout.write('\n' + lines.join('\n') + '\n\n' + tagline + ' ' + ver + '\n\n');
|
|
111
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toiljs update`, a friendly wrapper over `npm-check-updates`: checks the registry for newer
|
|
3
|
+
* dependency versions, shows them grouped by semver bump (major / minor / patch) with colors, lets
|
|
4
|
+
* you pick which to apply (or `-y` for all), bumps package.json, and runs the project's package
|
|
5
|
+
* manager install. ncu is invoked via `npx --yes` so it isn't a permanent dependency of toiljs.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { cancel, intro, isCancel, multiselect, note, outro, spinner } from '@clack/prompts';
|
|
11
|
+
|
|
12
|
+
import { capture, run } from './proc.js';
|
|
13
|
+
import { buildRows, type Bump, parseNcuJson, type UpdateRow } from './updates.js';
|
|
14
|
+
import { accent, danger, dim, success, warn } from './ui.js';
|
|
15
|
+
|
|
16
|
+
export interface UpdateOptions {
|
|
17
|
+
readonly root?: string;
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
/** Apply all available updates without the interactive picker. */
|
|
20
|
+
readonly yes?: boolean;
|
|
21
|
+
/** ncu `--target` (latest | minor | patch | newest | greatest). Default latest. */
|
|
22
|
+
readonly target?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PackageManager {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly ncuName: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Detects the package manager from the project's lockfile (defaults to npm). */
|
|
31
|
+
function detectPackageManager(root: string): PackageManager {
|
|
32
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) return { name: 'pnpm', ncuName: 'pnpm' };
|
|
33
|
+
if (fs.existsSync(path.join(root, 'yarn.lock'))) return { name: 'yarn', ncuName: 'yarn' };
|
|
34
|
+
if (fs.existsSync(path.join(root, 'bun.lockb'))) return { name: 'bun', ncuName: 'bun' };
|
|
35
|
+
return { name: 'npm', ncuName: 'npm' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reads the merged dependency ranges (deps + devDeps) from a package.json. */
|
|
39
|
+
function readDependencies(pkgPath: string): Record<string, string> {
|
|
40
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
41
|
+
if (typeof parsed !== 'object' || parsed === null) return {};
|
|
42
|
+
const pkg = parsed as Record<string, unknown>;
|
|
43
|
+
const merge = (v: unknown): Record<string, string> => {
|
|
44
|
+
if (typeof v !== 'object' || v === null) return {};
|
|
45
|
+
const out: Record<string, string> = {};
|
|
46
|
+
for (const [k, val] of Object.entries(v)) if (typeof val === 'string') out[k] = val;
|
|
47
|
+
return out;
|
|
48
|
+
};
|
|
49
|
+
return { ...merge(pkg.dependencies), ...merge(pkg.devDependencies) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function bumpColor(bump: Bump, text: string): string {
|
|
53
|
+
if (bump === 'major') return danger(text);
|
|
54
|
+
if (bump === 'minor') return warn(text);
|
|
55
|
+
if (bump === 'patch') return success(text);
|
|
56
|
+
return dim(text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Renders a row as `name from -> to`, the version part colored by bump. */
|
|
60
|
+
function rowLine(row: UpdateRow): string {
|
|
61
|
+
return `${row.name} ${dim(row.from)} ${dim('->')} ${bumpColor(row.bump, row.to)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const TARGETS = new Set(['latest', 'minor', 'patch', 'newest', 'greatest']);
|
|
65
|
+
|
|
66
|
+
export async function runUpdate(opts: UpdateOptions): Promise<void> {
|
|
67
|
+
const root = path.resolve(opts.root ?? opts.cwd);
|
|
68
|
+
const pkgPath = path.join(root, 'package.json');
|
|
69
|
+
if (!fs.existsSync(pkgPath)) {
|
|
70
|
+
throw new Error('No package.json here. Run from your project root or pass --root <dir>.');
|
|
71
|
+
}
|
|
72
|
+
const currentDeps = readDependencies(pkgPath);
|
|
73
|
+
const pm = detectPackageManager(root);
|
|
74
|
+
const target = opts.target && TARGETS.has(opts.target) ? opts.target : 'latest';
|
|
75
|
+
|
|
76
|
+
const ncuArgs = (extra: string[]): string[] => [
|
|
77
|
+
'--yes',
|
|
78
|
+
'npm-check-updates',
|
|
79
|
+
'--packageManager',
|
|
80
|
+
pm.ncuName,
|
|
81
|
+
'--target',
|
|
82
|
+
target,
|
|
83
|
+
...extra,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
intro(accent('toiljs update'));
|
|
87
|
+
|
|
88
|
+
const s = spinner();
|
|
89
|
+
s.start('Checking the registry for updates');
|
|
90
|
+
const res = await capture('npx', ncuArgs(['--jsonUpgraded']), root);
|
|
91
|
+
if (res.code !== 0 && res.stdout.indexOf('{') === -1) {
|
|
92
|
+
s.stop('Could not check for updates');
|
|
93
|
+
note(dim(res.stderr.trim() || 'npm-check-updates failed.'), 'Error');
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const rows = buildRows(parseNcuJson(res.stdout), currentDeps);
|
|
98
|
+
if (rows.length === 0) {
|
|
99
|
+
s.stop('Everything is up to date');
|
|
100
|
+
outro(success('Nothing to update.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
s.stop(`${String(rows.length)} update${rows.length === 1 ? '' : 's'} available`);
|
|
104
|
+
|
|
105
|
+
const counts = { major: 0, minor: 0, patch: 0, other: 0 };
|
|
106
|
+
for (const r of rows) counts[r.bump]++;
|
|
107
|
+
note(
|
|
108
|
+
rows.map(rowLine).join('\n'),
|
|
109
|
+
`${danger(`${String(counts.major)} major`)} ${warn(`${String(counts.minor)} minor`)} ${success(`${String(counts.patch)} patch`)}`,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
let selected: string[];
|
|
113
|
+
if (opts.yes) {
|
|
114
|
+
selected = rows.map((r) => r.name);
|
|
115
|
+
} else {
|
|
116
|
+
const answer = await multiselect<string>({
|
|
117
|
+
message: 'Select packages to update (space to toggle, enter to confirm)',
|
|
118
|
+
options: rows.map((r) => ({
|
|
119
|
+
value: r.name,
|
|
120
|
+
label: r.name,
|
|
121
|
+
hint: `${r.from} -> ${r.to}`,
|
|
122
|
+
})),
|
|
123
|
+
initialValues: rows.map((r) => r.name),
|
|
124
|
+
required: false,
|
|
125
|
+
});
|
|
126
|
+
if (isCancel(answer)) {
|
|
127
|
+
cancel('Update cancelled.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
selected = answer;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (selected.length === 0) {
|
|
134
|
+
outro(dim('No packages selected.'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
s.start('Updating package.json');
|
|
139
|
+
const applyAll = selected.length === rows.length;
|
|
140
|
+
await run('npx', ncuArgs(applyAll ? ['-u'] : ['-u', '--filter', selected.join(' ')]), root);
|
|
141
|
+
s.stop('package.json updated');
|
|
142
|
+
|
|
143
|
+
s.start(`Installing with ${pm.name}`);
|
|
144
|
+
await run(pm.name, ['install'], root);
|
|
145
|
+
s.stop('Dependencies installed');
|
|
146
|
+
|
|
147
|
+
outro(
|
|
148
|
+
success(`Updated ${String(selected.length)} package${selected.length === 1 ? '' : 's'}.`),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for `toiljs update`: parse `npm-check-updates --jsonUpgraded` output and classify the
|
|
3
|
+
* semver bump of each upgrade. IO-free so it can be unit-tested; the spawn/UI live in `update.ts`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The kind of version jump an upgrade represents. */
|
|
7
|
+
export type Bump = 'major' | 'minor' | 'patch' | 'other';
|
|
8
|
+
|
|
9
|
+
/** One available upgrade: package, current range, target range, and bump kind. */
|
|
10
|
+
export interface UpdateRow {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly from: string;
|
|
13
|
+
readonly to: string;
|
|
14
|
+
readonly bump: Bump;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Extracts a version's leading `x.y.z` (ignoring `^`, `~`, `>=`, etc.); missing parts become 0. */
|
|
18
|
+
function parseVersion(v: string): [number, number, number] {
|
|
19
|
+
const m = /(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(v);
|
|
20
|
+
if (!m) return [0, 0, 0];
|
|
21
|
+
return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Classifies the bump from `from` to `to` (both may be ranges like `^1.2.3`). */
|
|
25
|
+
export function classifyBump(from: string, to: string): Bump {
|
|
26
|
+
const [fa, fb, fc] = parseVersion(from);
|
|
27
|
+
const [ta, tb, tc] = parseVersion(to);
|
|
28
|
+
if (ta !== fa) return 'major';
|
|
29
|
+
if (tb !== fb) return 'minor';
|
|
30
|
+
if (tc !== fc) return 'patch';
|
|
31
|
+
return 'other';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses the JSON object `npm-check-updates --jsonUpgraded` prints (a `{ name: range }` map). Tolerant
|
|
36
|
+
* of leading/trailing noise (npx banners) by slicing to the outermost braces. Returns `{}` on failure.
|
|
37
|
+
*/
|
|
38
|
+
export function parseNcuJson(stdout: string): Record<string, string> {
|
|
39
|
+
const start = stdout.indexOf('{');
|
|
40
|
+
const end = stdout.lastIndexOf('}');
|
|
41
|
+
if (start === -1 || end <= start) return {};
|
|
42
|
+
try {
|
|
43
|
+
const parsed: unknown = JSON.parse(stdout.slice(start, end + 1));
|
|
44
|
+
if (typeof parsed !== 'object' || parsed === null) return {};
|
|
45
|
+
const out: Record<string, string> = {};
|
|
46
|
+
for (const [k, v] of Object.entries(parsed)) if (typeof v === 'string') out[k] = v;
|
|
47
|
+
return out;
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SEVERITY: Record<Bump, number> = { major: 0, minor: 1, patch: 2, other: 3 };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds the upgrade rows from the ncu map and the project's current dependency ranges, sorted by
|
|
57
|
+
* bump severity (major first) then name.
|
|
58
|
+
*/
|
|
59
|
+
export function buildRows(
|
|
60
|
+
upgraded: Record<string, string>,
|
|
61
|
+
currentDeps: Record<string, string>,
|
|
62
|
+
): UpdateRow[] {
|
|
63
|
+
return Object.entries(upgraded)
|
|
64
|
+
.map(([name, to]) => {
|
|
65
|
+
const from = currentDeps[name] ?? '?';
|
|
66
|
+
return { name, from, to, bump: classifyBump(from, to) };
|
|
67
|
+
})
|
|
68
|
+
.sort((a, b) => SEVERITY[a.bump] - SEVERITY[b.bump] || a.name.localeCompare(b.name));
|
|
69
|
+
}
|
|
@@ -1,89 +1,91 @@
|
|
|
1
|
-
import { useState, type CSSProperties, type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Props for {@link Image}: every standard `<img>` attribute, plus toil's layout/loading controls.
|
|
5
|
-
* `src` and `alt` are required (`alt` is enforced for accessibility, pass `alt=""` for decorative
|
|
6
|
-
* images). `width`/`height` (or `fill`) reserve space to prevent layout shift.
|
|
7
|
-
*/
|
|
8
|
-
export interface ImageProps
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/** Intrinsic
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1
|
+
import { useState, type CSSProperties, type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for {@link Image}: every standard `<img>` attribute, plus toil's layout/loading controls.
|
|
5
|
+
* `src` and `alt` are required (`alt` is enforced for accessibility, pass `alt=""` for decorative
|
|
6
|
+
* images). `width`/`height` (or `fill`) reserve space to prevent layout shift.
|
|
7
|
+
*/
|
|
8
|
+
export interface ImageProps extends Omit<
|
|
9
|
+
ComponentPropsWithRef<'img'>,
|
|
10
|
+
'loading' | 'placeholder' | 'width' | 'height'
|
|
11
|
+
> {
|
|
12
|
+
src: string;
|
|
13
|
+
alt: string;
|
|
14
|
+
/** Intrinsic width in px. Set together with `height` to reserve space (avoids layout shift). */
|
|
15
|
+
width?: number;
|
|
16
|
+
/** Intrinsic height in px. Set together with `width` to reserve space (avoids layout shift). */
|
|
17
|
+
height?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Fill the nearest positioned ancestor (the parent must be `position: relative|absolute|fixed`).
|
|
20
|
+
* The image is absolutely positioned at 100% × 100%; `width`/`height` are ignored. Pair with
|
|
21
|
+
* `objectFit` to control cropping.
|
|
22
|
+
*/
|
|
23
|
+
fill?: boolean;
|
|
24
|
+
/** `object-fit` for the rendered image (handy with `fill`). */
|
|
25
|
+
objectFit?: CSSProperties['objectFit'];
|
|
26
|
+
/**
|
|
27
|
+
* Mark this as a high-priority (LCP) image: eager load + `fetchpriority="high"` and no lazy
|
|
28
|
+
* loading. Use for above-the-fold hero images; everything else stays lazy. Default `false`.
|
|
29
|
+
*/
|
|
30
|
+
priority?: boolean;
|
|
31
|
+
/** Placeholder shown until the image loads: `'empty'` (default) or `'blur'` (needs `blurDataURL`). */
|
|
32
|
+
placeholder?: 'empty' | 'blur';
|
|
33
|
+
/** A tiny (base64) image shown blurred behind the image while it loads, when `placeholder="blur"`. */
|
|
34
|
+
blurDataURL?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A drop-in `<img>` replacement that prevents layout shift and lazy-loads by default. It reserves
|
|
39
|
+
* space from `width`/`height` (or fills its container with `fill`), decodes async, lazy-loads unless
|
|
40
|
+
* `priority`, and can fade in from a `blur` placeholder. This is a client-only component, there is
|
|
41
|
+
* no server-side resizing; pass an already-optimized `src` (Vite hashes imported assets for you).
|
|
42
|
+
*/
|
|
43
|
+
export function Image(props: ImageProps): ReactNode {
|
|
44
|
+
const {
|
|
45
|
+
src,
|
|
46
|
+
alt,
|
|
47
|
+
width,
|
|
48
|
+
height,
|
|
49
|
+
fill = false,
|
|
50
|
+
objectFit,
|
|
51
|
+
priority = false,
|
|
52
|
+
placeholder = 'empty',
|
|
53
|
+
blurDataURL,
|
|
54
|
+
style,
|
|
55
|
+
onLoad,
|
|
56
|
+
...rest
|
|
57
|
+
} = props;
|
|
58
|
+
|
|
59
|
+
const [loaded, setLoaded] = useState(false);
|
|
60
|
+
const showBlur = placeholder === 'blur' && blurDataURL !== undefined && !loaded;
|
|
61
|
+
|
|
62
|
+
const layoutStyle: CSSProperties = fill
|
|
63
|
+
? { position: 'absolute', inset: 0, width: '100%', height: '100%' }
|
|
64
|
+
: {};
|
|
65
|
+
const blurStyle: CSSProperties = showBlur
|
|
66
|
+
? {
|
|
67
|
+
backgroundImage: `url(${blurDataURL})`,
|
|
68
|
+
backgroundSize: 'cover',
|
|
69
|
+
backgroundPosition: 'center',
|
|
70
|
+
filter: 'blur(20px)',
|
|
71
|
+
}
|
|
72
|
+
: {};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<img
|
|
76
|
+
{...rest}
|
|
77
|
+
src={src}
|
|
78
|
+
alt={alt}
|
|
79
|
+
width={fill ? undefined : width}
|
|
80
|
+
height={fill ? undefined : height}
|
|
81
|
+
loading={priority ? 'eager' : 'lazy'}
|
|
82
|
+
decoding="async"
|
|
83
|
+
fetchPriority={priority ? 'high' : 'auto'}
|
|
84
|
+
onLoad={(event) => {
|
|
85
|
+
setLoaded(true);
|
|
86
|
+
onLoad?.(event);
|
|
87
|
+
}}
|
|
88
|
+
style={{ ...layoutStyle, objectFit, ...blurStyle, ...style }}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|