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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
function parseVersion(v) {
|
|
2
|
+
const m = /(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(v);
|
|
3
|
+
if (!m)
|
|
4
|
+
return [0, 0, 0];
|
|
5
|
+
return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
|
|
6
|
+
}
|
|
7
|
+
export function satisfiesMin(version, range) {
|
|
8
|
+
const [a, b, c] = parseVersion(version);
|
|
9
|
+
const [x, y, z] = parseVersion(range);
|
|
10
|
+
if (a !== x)
|
|
11
|
+
return a > x;
|
|
12
|
+
if (b !== y)
|
|
13
|
+
return b > y;
|
|
14
|
+
return c >= z;
|
|
15
|
+
}
|
|
16
|
+
export function checkNode(current, requiredRange) {
|
|
17
|
+
const ok = satisfiesMin(current, requiredRange);
|
|
18
|
+
return {
|
|
19
|
+
id: 'node',
|
|
20
|
+
label: 'Node.js',
|
|
21
|
+
status: ok ? 'pass' : 'fail',
|
|
22
|
+
detail: `${current} (requires ${requiredRange})`,
|
|
23
|
+
fix: ok ? undefined : `Upgrade Node to ${requiredRange}.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function checkPeer(name, installed, range) {
|
|
27
|
+
if (installed === null) {
|
|
28
|
+
return {
|
|
29
|
+
id: `peer:${name}`,
|
|
30
|
+
label: name,
|
|
31
|
+
status: 'fail',
|
|
32
|
+
detail: `not installed (requires ${range})`,
|
|
33
|
+
fix: `Install ${name}@"${range}".`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const ok = satisfiesMin(installed, range);
|
|
37
|
+
return {
|
|
38
|
+
id: `peer:${name}`,
|
|
39
|
+
label: name,
|
|
40
|
+
status: ok ? 'pass' : 'warn',
|
|
41
|
+
detail: `${installed} (requires ${range})`,
|
|
42
|
+
fix: ok ? undefined : `Update ${name} to ${range}.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function checkPackageManager(lockfiles) {
|
|
46
|
+
if (lockfiles.length === 0) {
|
|
47
|
+
return {
|
|
48
|
+
id: 'pm',
|
|
49
|
+
label: 'Package manager',
|
|
50
|
+
status: 'warn',
|
|
51
|
+
detail: 'no lockfile found',
|
|
52
|
+
fix: 'Run an install (npm/pnpm/yarn/bun) to create a lockfile.',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { id: 'pm', label: 'Package manager', status: 'pass', detail: lockfiles.join(', ') };
|
|
56
|
+
}
|
|
57
|
+
export function checkToiljsInstalled(version) {
|
|
58
|
+
return version
|
|
59
|
+
? { id: 'toiljs', label: 'toiljs', status: 'pass', detail: version }
|
|
60
|
+
: {
|
|
61
|
+
id: 'toiljs',
|
|
62
|
+
label: 'toiljs',
|
|
63
|
+
status: 'fail',
|
|
64
|
+
detail: 'not a dependency of this project',
|
|
65
|
+
fix: 'Add toiljs to dependencies, or run from the project root with --root.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function checkDir(id, label, exists, fix) {
|
|
69
|
+
return exists
|
|
70
|
+
? { id, label, status: 'pass' }
|
|
71
|
+
: { id, label, status: 'fail', detail: 'missing', fix };
|
|
72
|
+
}
|
|
73
|
+
export function checkMountSlots(entrySource) {
|
|
74
|
+
const label = 'App entry mount()';
|
|
75
|
+
if (entrySource === null) {
|
|
76
|
+
return {
|
|
77
|
+
id: 'mount',
|
|
78
|
+
label,
|
|
79
|
+
status: 'warn',
|
|
80
|
+
detail: 'entry file not found',
|
|
81
|
+
fix: 'Ensure client/toil.tsx calls Toil.mount(...).',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const call = /\bmount\s*\(([^)]*)\)/.exec(entrySource);
|
|
85
|
+
if (!call) {
|
|
86
|
+
return {
|
|
87
|
+
id: 'mount',
|
|
88
|
+
label,
|
|
89
|
+
status: 'warn',
|
|
90
|
+
detail: 'no mount() call found',
|
|
91
|
+
fix: 'Call Toil.mount(routes, layout, notFound, globalError, slots).',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const hasSlots = /\bslots\b/.test(call[1]);
|
|
95
|
+
return hasSlots
|
|
96
|
+
? { id: 'mount', label, status: 'pass', detail: 'passes slots' }
|
|
97
|
+
: {
|
|
98
|
+
id: 'mount',
|
|
99
|
+
label,
|
|
100
|
+
status: 'warn',
|
|
101
|
+
detail: 'mount() is missing the slots argument',
|
|
102
|
+
fix: 'Pass slots last: mount(routes, layout, notFound, globalError, slots). Without it, parallel and intercepting routes are ignored.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function checkRootElement(indexHtml) {
|
|
106
|
+
const label = 'index.html mount target';
|
|
107
|
+
if (indexHtml === null) {
|
|
108
|
+
return {
|
|
109
|
+
id: 'root-el',
|
|
110
|
+
label,
|
|
111
|
+
status: 'fail',
|
|
112
|
+
detail: 'index.html not found',
|
|
113
|
+
fix: 'Add public/index.html with <div id="root"></div>.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const ok = /id\s*=\s*["']root["']/.test(indexHtml);
|
|
117
|
+
return ok
|
|
118
|
+
? { id: 'root-el', label, status: 'pass' }
|
|
119
|
+
: {
|
|
120
|
+
id: 'root-el',
|
|
121
|
+
label,
|
|
122
|
+
status: 'fail',
|
|
123
|
+
detail: 'no element with id="root"',
|
|
124
|
+
fix: 'Add <div id="root"></div> to public/index.html.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function checkRoutesPresent(routeCount) {
|
|
128
|
+
return routeCount > 0
|
|
129
|
+
? {
|
|
130
|
+
id: 'routes',
|
|
131
|
+
label: 'Routes',
|
|
132
|
+
status: 'pass',
|
|
133
|
+
detail: `${routeCount} route${routeCount === 1 ? '' : 's'}`,
|
|
134
|
+
}
|
|
135
|
+
: {
|
|
136
|
+
id: 'routes',
|
|
137
|
+
label: 'Routes',
|
|
138
|
+
status: 'fail',
|
|
139
|
+
detail: 'no routes found',
|
|
140
|
+
fix: 'Add a page, e.g. client/routes/index.tsx.',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function checkDuplicatePatterns(patterns) {
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
const dupes = new Set();
|
|
146
|
+
for (const p of patterns) {
|
|
147
|
+
if (seen.has(p))
|
|
148
|
+
dupes.add(p);
|
|
149
|
+
else
|
|
150
|
+
seen.add(p);
|
|
151
|
+
}
|
|
152
|
+
return dupes.size === 0
|
|
153
|
+
? { id: 'route-dupes', label: 'Unique route patterns', status: 'pass' }
|
|
154
|
+
: {
|
|
155
|
+
id: 'route-dupes',
|
|
156
|
+
label: 'Unique route patterns',
|
|
157
|
+
status: 'warn',
|
|
158
|
+
detail: `duplicate: ${[...dupes].join(', ')}`,
|
|
159
|
+
fix: 'Two route files map to the same URL; rename or remove one.',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function isBrokenRelativeAsset(value) {
|
|
163
|
+
if (value === '')
|
|
164
|
+
return false;
|
|
165
|
+
if (value.startsWith('/'))
|
|
166
|
+
return false;
|
|
167
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value))
|
|
168
|
+
return false;
|
|
169
|
+
if (value.startsWith('#') || value.startsWith('?'))
|
|
170
|
+
return false;
|
|
171
|
+
return /\.(svgz?|png|jpe?g|gif|webp|avif|ico|css|m?js|woff2?|ttf|otf|eot|mp4|webm|json)$/i.test(value);
|
|
172
|
+
}
|
|
173
|
+
export function findRelativeAssets(files) {
|
|
174
|
+
const issues = [];
|
|
175
|
+
const attr = /\b(?:src|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
176
|
+
for (const file of files) {
|
|
177
|
+
const lines = file.source.split('\n');
|
|
178
|
+
for (let i = 0; i < lines.length; i++) {
|
|
179
|
+
attr.lastIndex = 0;
|
|
180
|
+
let m;
|
|
181
|
+
while ((m = attr.exec(lines[i])) !== null) {
|
|
182
|
+
const value = m[1] ?? m[2] ?? '';
|
|
183
|
+
if (isBrokenRelativeAsset(value)) {
|
|
184
|
+
issues.push({ file: file.path, line: i + 1, value });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return issues;
|
|
190
|
+
}
|
|
191
|
+
export function checkRelativeAssets(issues) {
|
|
192
|
+
if (issues.length === 0)
|
|
193
|
+
return { id: 'rel-assets', label: 'Asset paths', status: 'pass' };
|
|
194
|
+
const shown = issues
|
|
195
|
+
.slice(0, 5)
|
|
196
|
+
.map((i) => `${i.file}:${String(i.line)} "${i.value}"`)
|
|
197
|
+
.join('; ');
|
|
198
|
+
const more = issues.length > 5 ? `, and ${String(issues.length - 5)} more` : '';
|
|
199
|
+
return {
|
|
200
|
+
id: 'rel-assets',
|
|
201
|
+
label: 'Asset paths',
|
|
202
|
+
status: 'warn',
|
|
203
|
+
detail: `${String(issues.length)} relative reference(s): ${shown}${more}`,
|
|
204
|
+
fix: 'Use a root-absolute path (e.g. "/images/logo.svg") or import the asset; relative paths 404 on nested routes.',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export function checkConfigLoads(loaded, error) {
|
|
208
|
+
return loaded
|
|
209
|
+
? { id: 'config', label: 'toil.config loads', status: 'pass' }
|
|
210
|
+
: {
|
|
211
|
+
id: 'config',
|
|
212
|
+
label: 'toil.config loads',
|
|
213
|
+
status: 'fail',
|
|
214
|
+
detail: error ?? 'failed to load',
|
|
215
|
+
fix: 'Fix the error in your toil.config.* so dev/build can read it.',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export function checkBasePath(base) {
|
|
219
|
+
const ok = base === '/' || (base.startsWith('/') && base.endsWith('/'));
|
|
220
|
+
return ok
|
|
221
|
+
? { id: 'base', label: 'Base path', status: 'pass', detail: base }
|
|
222
|
+
: {
|
|
223
|
+
id: 'base',
|
|
224
|
+
label: 'Base path',
|
|
225
|
+
status: 'warn',
|
|
226
|
+
detail: `"${base}"`,
|
|
227
|
+
fix: 'A non-root base should start and end with "/" (e.g. "/app/").',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function checkSeoUrl(seoConfigured, hasUrl) {
|
|
231
|
+
if (!seoConfigured) {
|
|
232
|
+
return {
|
|
233
|
+
id: 'seo-url',
|
|
234
|
+
label: 'SEO site url',
|
|
235
|
+
status: 'pass',
|
|
236
|
+
detail: 'SEO not configured',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return hasUrl
|
|
240
|
+
? { id: 'seo-url', label: 'SEO site url', status: 'pass' }
|
|
241
|
+
: {
|
|
242
|
+
id: 'seo-url',
|
|
243
|
+
label: 'SEO site url',
|
|
244
|
+
status: 'warn',
|
|
245
|
+
detail: 'seo is set without a url',
|
|
246
|
+
fix: 'Set client.seo.url so sitemap.xml and canonical links are absolute.',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export function checkStyling(f) {
|
|
250
|
+
const label = 'Styling';
|
|
251
|
+
if (f.preprocessorImported && f.preprocessorImported !== 'css' && !f.preprocessorInstalled) {
|
|
252
|
+
return {
|
|
253
|
+
id: 'styling',
|
|
254
|
+
label,
|
|
255
|
+
status: 'fail',
|
|
256
|
+
detail: `${f.preprocessorImported} stylesheet imported but ${f.preprocessorImported} is not installed`,
|
|
257
|
+
fix: `Install ${f.preprocessorImported}, or run toiljs configure.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (f.tailwindImported && !f.tailwindInstalled) {
|
|
261
|
+
return {
|
|
262
|
+
id: 'styling',
|
|
263
|
+
label,
|
|
264
|
+
status: 'fail',
|
|
265
|
+
detail: 'Tailwind entry imported but @tailwindcss/vite is not installed',
|
|
266
|
+
fix: 'Run toiljs configure --tailwind, or install @tailwindcss/vite.',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { id: 'styling', label, status: 'pass' };
|
|
270
|
+
}
|
|
271
|
+
export function checkToilconfig(present) {
|
|
272
|
+
return present
|
|
273
|
+
? { id: 'toilconfig', label: 'Server target (toilconfig.json)', status: 'pass' }
|
|
274
|
+
: {
|
|
275
|
+
id: 'toilconfig',
|
|
276
|
+
label: 'Server target (toilconfig.json)',
|
|
277
|
+
status: 'warn',
|
|
278
|
+
detail: 'no toilconfig.json (no WebAssembly server)',
|
|
279
|
+
fix: 'Add toilconfig.json + a server/ entry if you want a WASM backend.',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export function checkServerEntry(missing) {
|
|
283
|
+
return missing.length === 0
|
|
284
|
+
? { id: 'server-entry', label: 'Server entries', status: 'pass' }
|
|
285
|
+
: {
|
|
286
|
+
id: 'server-entry',
|
|
287
|
+
label: 'Server entries',
|
|
288
|
+
status: 'fail',
|
|
289
|
+
detail: `missing: ${missing.join(', ')}`,
|
|
290
|
+
fix: 'Create the entry file(s) listed in toilconfig.json "entries", or update them.',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
export function checkToilscriptInstalled(installed) {
|
|
294
|
+
return installed
|
|
295
|
+
? { id: 'toilscript', label: 'toilscript compiler', status: 'pass' }
|
|
296
|
+
: {
|
|
297
|
+
id: 'toilscript',
|
|
298
|
+
label: 'toilscript compiler',
|
|
299
|
+
status: 'warn',
|
|
300
|
+
detail: 'toilconfig.json present but toilscript is not installed',
|
|
301
|
+
fix: 'Install toilscript to compile the server to WebAssembly.',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
export function checkWasmBuilt(exists) {
|
|
305
|
+
return exists
|
|
306
|
+
? { id: 'wasm', label: 'Server build', status: 'pass' }
|
|
307
|
+
: {
|
|
308
|
+
id: 'wasm',
|
|
309
|
+
label: 'Server build',
|
|
310
|
+
status: 'warn',
|
|
311
|
+
detail: 'no compiled .wasm found',
|
|
312
|
+
fix: 'Run your server build (toilscript) before toiljs start.',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
export function summarize(groups) {
|
|
316
|
+
let pass = 0;
|
|
317
|
+
let warn = 0;
|
|
318
|
+
let fail = 0;
|
|
319
|
+
for (const group of groups) {
|
|
320
|
+
for (const check of group.checks) {
|
|
321
|
+
if (check.status === 'pass')
|
|
322
|
+
pass++;
|
|
323
|
+
else if (check.status === 'warn')
|
|
324
|
+
warn++;
|
|
325
|
+
else
|
|
326
|
+
fail++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { pass, warn, fail };
|
|
330
|
+
}
|
|
331
|
+
export function hasFailures(summary) {
|
|
332
|
+
return summary.fail > 0;
|
|
333
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { loadConfig, scanRoutes } from 'toiljs/compiler';
|
|
6
|
+
import { checkBasePath, checkConfigLoads, checkDir, checkDuplicatePatterns, checkMountSlots, checkNode, checkPackageManager, checkPeer, checkRelativeAssets, checkRootElement, checkRoutesPresent, checkSeoUrl, checkServerEntry, checkStyling, checkToilconfig, checkToiljsInstalled, checkToilscriptInstalled, checkWasmBuilt, findRelativeAssets, hasFailures, summarize, } from './diagnostics.js';
|
|
7
|
+
import { detectTailwind, PREPROCESSOR_PKG, preprocessorForExt, TAILWIND_ENTRY, } from './features.js';
|
|
8
|
+
import { accent, bold, danger, dim, success, version, warn } from './ui.js';
|
|
9
|
+
function readJsonObject(file) {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
12
|
+
return typeof parsed === 'object' && parsed !== null
|
|
13
|
+
? parsed
|
|
14
|
+
: null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function stringRecord(value) {
|
|
21
|
+
if (typeof value !== 'object' || value === null)
|
|
22
|
+
return {};
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [k, v] of Object.entries(value))
|
|
25
|
+
if (typeof v === 'string')
|
|
26
|
+
out[k] = v;
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function readFile(file) {
|
|
30
|
+
try {
|
|
31
|
+
return fs.readFileSync(file, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function frameworkMeta() {
|
|
38
|
+
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
39
|
+
const pkg = readJsonObject(pkgPath);
|
|
40
|
+
const engines = pkg ? stringRecord(pkg.engines) : {};
|
|
41
|
+
const peers = pkg ? stringRecord(pkg.peerDependencies) : {};
|
|
42
|
+
return { node: engines.node ?? '>=24.0.0', peers };
|
|
43
|
+
}
|
|
44
|
+
const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb'];
|
|
45
|
+
function readEntry(clientAbsDir) {
|
|
46
|
+
for (const name of ['toil.tsx', 'toil.jsx', 'main.tsx', 'main.jsx']) {
|
|
47
|
+
const source = readFile(path.join(clientAbsDir, name));
|
|
48
|
+
if (source !== null && /toiljs\/routes|\bmount\s*\(/.test(source))
|
|
49
|
+
return source;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function collectSources(root, dir, cap) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const visit = (current) => {
|
|
56
|
+
if (out.length >= cap)
|
|
57
|
+
return;
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (out.length >= cap)
|
|
67
|
+
break;
|
|
68
|
+
const full = path.join(current, entry.name);
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
if (entry.name !== 'node_modules')
|
|
71
|
+
visit(full);
|
|
72
|
+
}
|
|
73
|
+
else if (/\.(tsx|jsx)$/.test(entry.name)) {
|
|
74
|
+
const source = readFile(full);
|
|
75
|
+
if (source !== null)
|
|
76
|
+
out.push({ path: path.relative(root, full), source });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
visit(dir);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function glyph(status) {
|
|
84
|
+
if (status === 'pass')
|
|
85
|
+
return success('✓');
|
|
86
|
+
if (status === 'warn')
|
|
87
|
+
return warn('⚠');
|
|
88
|
+
return danger('✗');
|
|
89
|
+
}
|
|
90
|
+
function renderHuman(groups) {
|
|
91
|
+
const summary = summarize(groups);
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const group of groups) {
|
|
94
|
+
out.push(' ' + bold(group.title));
|
|
95
|
+
for (const check of group.checks) {
|
|
96
|
+
let line = ` ${glyph(check.status)} ${check.label}`;
|
|
97
|
+
if (check.detail)
|
|
98
|
+
line += dim(` ${check.detail}`);
|
|
99
|
+
out.push(line);
|
|
100
|
+
if (check.fix && check.status !== 'pass')
|
|
101
|
+
out.push(' ' + dim(`fix: ${check.fix}`));
|
|
102
|
+
}
|
|
103
|
+
out.push('');
|
|
104
|
+
}
|
|
105
|
+
const parts = [success(`${String(summary.pass)} passed`)];
|
|
106
|
+
if (summary.warn > 0) {
|
|
107
|
+
parts.push(warn(`${String(summary.warn)} warning${summary.warn === 1 ? '' : 's'}`));
|
|
108
|
+
}
|
|
109
|
+
if (summary.fail > 0)
|
|
110
|
+
parts.push(danger(`${String(summary.fail)} failed`));
|
|
111
|
+
out.push(' ' + parts.join(dim(', ')));
|
|
112
|
+
out.push('');
|
|
113
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
114
|
+
}
|
|
115
|
+
export async function runDoctor(opts) {
|
|
116
|
+
const root = path.resolve(opts.root ?? opts.cwd);
|
|
117
|
+
const meta = frameworkMeta();
|
|
118
|
+
const projectPkg = readJsonObject(path.join(root, 'package.json'));
|
|
119
|
+
const deps = {
|
|
120
|
+
...(projectPkg ? stringRecord(projectPkg.dependencies) : {}),
|
|
121
|
+
...(projectPkg ? stringRecord(projectPkg.devDependencies) : {}),
|
|
122
|
+
};
|
|
123
|
+
let cfg = null;
|
|
124
|
+
let configError;
|
|
125
|
+
try {
|
|
126
|
+
cfg = await loadConfig({ root });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
configError = err instanceof Error ? err.message : String(err);
|
|
130
|
+
}
|
|
131
|
+
const clientAbsDir = cfg ? cfg.clientAbsDir : path.join(root, 'client');
|
|
132
|
+
const routesAbsDir = cfg ? cfg.routesAbsDir : path.join(clientAbsDir, 'routes');
|
|
133
|
+
const publicDir = cfg ? cfg.publicDir : path.join(clientAbsDir, 'public');
|
|
134
|
+
const entrySource = readEntry(clientAbsDir);
|
|
135
|
+
const indexHtml = readFile(path.join(publicDir, 'index.html'));
|
|
136
|
+
const routes = scanRoutes(routesAbsDir);
|
|
137
|
+
const mainPatterns = routes.filter((r) => r.slot === undefined).map((r) => r.pattern);
|
|
138
|
+
const assetIssues = findRelativeAssets(collectSources(root, clientAbsDir, 200));
|
|
139
|
+
let preprocessorImported = null;
|
|
140
|
+
let tailwindImported = false;
|
|
141
|
+
if (entrySource) {
|
|
142
|
+
const styleImport = /import\s+['"]\.\/styles\/main\.([a-z]+)['"]/.exec(entrySource);
|
|
143
|
+
if (styleImport)
|
|
144
|
+
preprocessorImported = preprocessorForExt(styleImport[1]);
|
|
145
|
+
tailwindImported = entrySource.includes(TAILWIND_ENTRY);
|
|
146
|
+
}
|
|
147
|
+
const ppPkg = preprocessorImported ? PREPROCESSOR_PKG[preprocessorImported] : null;
|
|
148
|
+
const preprocessorInstalled = ppPkg === null || ppPkg in deps;
|
|
149
|
+
const toilconfig = readJsonObject(path.join(root, 'toilconfig.json'));
|
|
150
|
+
const serverPresent = toilconfig !== null;
|
|
151
|
+
let missingEntries = [];
|
|
152
|
+
let toilscriptInstalled = false;
|
|
153
|
+
let wasmExists = false;
|
|
154
|
+
if (toilconfig) {
|
|
155
|
+
const entries = Array.isArray(toilconfig.entries)
|
|
156
|
+
? toilconfig.entries.filter((e) => typeof e === 'string')
|
|
157
|
+
: [];
|
|
158
|
+
missingEntries = entries.filter((e) => !fs.existsSync(path.join(root, e)));
|
|
159
|
+
try {
|
|
160
|
+
createRequire(path.join(root, 'package.json')).resolve('toilscript');
|
|
161
|
+
toilscriptInstalled = true;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
toilscriptInstalled = false;
|
|
165
|
+
}
|
|
166
|
+
const targets = typeof toilconfig.targets === 'object' && toilconfig.targets !== null
|
|
167
|
+
? toilconfig.targets
|
|
168
|
+
: {};
|
|
169
|
+
const outFiles = [];
|
|
170
|
+
for (const target of Object.values(targets)) {
|
|
171
|
+
if (typeof target === 'object' && target !== null) {
|
|
172
|
+
const outFile = target.outFile;
|
|
173
|
+
if (typeof outFile === 'string')
|
|
174
|
+
outFiles.push(outFile);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
wasmExists = outFiles.some((f) => fs.existsSync(path.join(root, f)));
|
|
178
|
+
if (!wasmExists && outFiles.length === 0) {
|
|
179
|
+
try {
|
|
180
|
+
wasmExists = fs
|
|
181
|
+
.readdirSync(path.join(root, 'build', 'server'))
|
|
182
|
+
.some((f) => f.endsWith('.wasm'));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
wasmExists = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const peerName = (n) => checkPeer(n, deps[n] ?? null, meta.peers[n] ?? '*');
|
|
190
|
+
const peerChecks = Object.keys(meta.peers).map(peerName);
|
|
191
|
+
const groups = [
|
|
192
|
+
{
|
|
193
|
+
title: 'Environment',
|
|
194
|
+
checks: [
|
|
195
|
+
checkNode(process.versions.node, meta.node),
|
|
196
|
+
checkToiljsInstalled('toiljs' in deps ? version() : null),
|
|
197
|
+
...peerChecks,
|
|
198
|
+
checkPackageManager(LOCKFILES.filter((f) => fs.existsSync(path.join(root, f)))),
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
title: 'Project + routing',
|
|
203
|
+
checks: [
|
|
204
|
+
checkDir('client-dir', 'client/ directory', fs.existsSync(clientAbsDir), 'Create a client/ directory for your app.'),
|
|
205
|
+
checkDir('routes-dir', 'routes/ directory', fs.existsSync(routesAbsDir), 'Create client/routes/ and add an index.tsx.'),
|
|
206
|
+
checkRootElement(indexHtml),
|
|
207
|
+
checkMountSlots(entrySource),
|
|
208
|
+
checkRoutesPresent(routes.length),
|
|
209
|
+
checkDuplicatePatterns(mainPatterns),
|
|
210
|
+
checkRelativeAssets(assetIssues),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
title: 'Config + assets',
|
|
215
|
+
checks: [
|
|
216
|
+
checkConfigLoads(cfg !== null, configError),
|
|
217
|
+
checkBasePath(cfg ? cfg.base : '/'),
|
|
218
|
+
checkSeoUrl(cfg?.seo != null, cfg?.seo?.url != null),
|
|
219
|
+
checkStyling({
|
|
220
|
+
preprocessorImported,
|
|
221
|
+
preprocessorInstalled,
|
|
222
|
+
tailwindImported,
|
|
223
|
+
tailwindInstalled: detectTailwind(deps),
|
|
224
|
+
}),
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
title: 'Server / WASM',
|
|
229
|
+
checks: serverPresent
|
|
230
|
+
? [
|
|
231
|
+
checkToilconfig(true),
|
|
232
|
+
checkServerEntry(missingEntries),
|
|
233
|
+
checkToilscriptInstalled(toilscriptInstalled),
|
|
234
|
+
checkWasmBuilt(wasmExists),
|
|
235
|
+
]
|
|
236
|
+
: [checkToilconfig(false)],
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
const summary = summarize(groups);
|
|
240
|
+
if (opts.json) {
|
|
241
|
+
process.stdout.write(JSON.stringify({ groups, summary }, null, 2) + '\n');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
process.stdout.write('\n' + accent(' Doctor') + dim(` ${root}`) + '\n\n');
|
|
245
|
+
renderHuman(groups);
|
|
246
|
+
}
|
|
247
|
+
if (hasFailures(summary))
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
}
|
package/build/cli/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { build, dev, start } from 'toiljs/compiler';
|
|
3
3
|
import { runConfigure } from './configure.js';
|
|
4
4
|
import { runCreate } from './create.js';
|
|
5
|
+
import { runDoctor } from './doctor.js';
|
|
6
|
+
import { runUpdate } from './update.js';
|
|
5
7
|
import { PREPROCESSORS } from './features.js';
|
|
6
8
|
import { accent, banner, bold, danger, dim, success, version } from './ui.js';
|
|
7
9
|
function parseArgs(argv) {
|
|
@@ -68,6 +70,12 @@ function parseArgs(argv) {
|
|
|
68
70
|
case '--yes':
|
|
69
71
|
flags.yes = true;
|
|
70
72
|
break;
|
|
73
|
+
case '--json':
|
|
74
|
+
flags.json = true;
|
|
75
|
+
break;
|
|
76
|
+
case '--target':
|
|
77
|
+
flags.target = argv[++i];
|
|
78
|
+
break;
|
|
71
79
|
default:
|
|
72
80
|
if (!arg.startsWith('-') && flags.name === undefined)
|
|
73
81
|
flags.name = arg;
|
|
@@ -86,6 +94,8 @@ function printHelp() {
|
|
|
86
94
|
cmd('dev', 'start the dev server with HMR'),
|
|
87
95
|
cmd('build', 'build the optimized production bundle'),
|
|
88
96
|
cmd('start', 'self-host the built app (hyper-express / uWS)'),
|
|
97
|
+
cmd('doctor', 'diagnose project setup and dependencies'),
|
|
98
|
+
cmd('update', 'check for and apply dependency updates'),
|
|
89
99
|
'',
|
|
90
100
|
bold('Options'),
|
|
91
101
|
cmd('--root <dir>', 'project root (default: current directory)'),
|
|
@@ -96,6 +106,8 @@ function printHelp() {
|
|
|
96
106
|
cmd('--no-ai', 'create: skip AI assistant files (CLAUDE.md, etc.)'),
|
|
97
107
|
cmd('-y, --yes', 'create: accept defaults (non-interactive)'),
|
|
98
108
|
cmd('--no-install', "create: don't install dependencies"),
|
|
109
|
+
cmd('--json', 'doctor: machine-readable output'),
|
|
110
|
+
cmd('--target <t>', 'update: latest | minor | patch | newest | greatest'),
|
|
99
111
|
cmd('-v, --version', 'print the toiljs version'),
|
|
100
112
|
'',
|
|
101
113
|
].join('\n') + '\n');
|
|
@@ -156,6 +168,20 @@ async function main() {
|
|
|
156
168
|
'\n\n');
|
|
157
169
|
break;
|
|
158
170
|
}
|
|
171
|
+
case 'doctor':
|
|
172
|
+
if (!flags.json)
|
|
173
|
+
banner();
|
|
174
|
+
await runDoctor({ root: flags.root, cwd: process.cwd(), json: flags.json });
|
|
175
|
+
break;
|
|
176
|
+
case 'update':
|
|
177
|
+
banner();
|
|
178
|
+
await runUpdate({
|
|
179
|
+
root: flags.root,
|
|
180
|
+
cwd: process.cwd(),
|
|
181
|
+
yes: flags.yes,
|
|
182
|
+
target: flags.target,
|
|
183
|
+
});
|
|
184
|
+
break;
|
|
159
185
|
case 'help':
|
|
160
186
|
case '--help':
|
|
161
187
|
case '-h':
|
package/build/cli/proc.d.ts
CHANGED
package/build/cli/proc.js
CHANGED
|
@@ -9,3 +9,23 @@ export function run(cmd, args, cwd) {
|
|
|
9
9
|
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${String(code)}`)));
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
+
export function capture(cmd, args, cwd) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const onWindows = process.platform === 'win32';
|
|
15
|
+
const child = onWindows
|
|
16
|
+
? spawn([cmd, ...args].join(' '), { cwd, shell: true })
|
|
17
|
+
: spawn(cmd, args, { cwd });
|
|
18
|
+
let stdout = '';
|
|
19
|
+
let stderr = '';
|
|
20
|
+
child.stdout?.on('data', (d) => {
|
|
21
|
+
stdout += d.toString();
|
|
22
|
+
});
|
|
23
|
+
child.stderr?.on('data', (d) => {
|
|
24
|
+
stderr += d.toString();
|
|
25
|
+
});
|
|
26
|
+
child.on('error', reject);
|
|
27
|
+
child.on('close', (code) => {
|
|
28
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
package/build/cli/ui.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export declare function brand(s: string): string;
|
|
|
4
4
|
export declare const accent: typeof brand;
|
|
5
5
|
export declare function success(s: string): string;
|
|
6
6
|
export declare const danger: import("picocolors/types").Formatter;
|
|
7
|
+
export declare const warn: import("picocolors/types").Formatter;
|
|
7
8
|
export declare function version(): string;
|
|
8
9
|
export declare function banner(): void;
|