toiljs 0.0.10 → 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 +315 -1
- package/assets/logo.svg +37 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.js +10 -4
- package/build/cli/create.js +60 -32
- 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 +35 -26
- 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/compiler/vite.js +7 -0
- package/build/io/.tsbuildinfo +1 -1
- package/examples/basic/client/components/Header.tsx +43 -38
- package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
- package/examples/basic/client/layout.tsx +4 -1
- package/examples/basic/client/public/index.html +18 -16
- package/examples/basic/client/routes/(legal)/privacy.tsx +18 -0
- package/examples/basic/client/routes/(legal)/terms.tsx +15 -0
- package/examples/basic/client/routes/about.tsx +21 -19
- package/examples/basic/client/routes/blog/[id].tsx +26 -12
- package/examples/basic/client/routes/features/actions.tsx +67 -0
- package/examples/basic/client/routes/features/error/error.tsx +16 -0
- package/examples/basic/client/routes/features/error/index.tsx +27 -0
- package/examples/basic/client/routes/features/head.tsx +38 -0
- package/examples/basic/client/routes/features/index.tsx +83 -0
- package/examples/basic/client/routes/features/realtime.tsx +34 -0
- package/examples/basic/client/routes/features/script.tsx +31 -0
- package/examples/basic/client/routes/features/seo.tsx +39 -0
- package/examples/basic/client/routes/features/template/b.tsx +14 -0
- package/examples/basic/client/routes/features/template/index.tsx +20 -0
- package/examples/basic/client/routes/features/template/template.tsx +16 -0
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -0
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -0
- package/examples/basic/client/routes/gallery/index.tsx +42 -0
- package/examples/basic/client/routes/gallery/layout.tsx +13 -0
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -0
- package/examples/basic/client/routes/get-started.tsx +157 -84
- package/examples/basic/client/routes/index.tsx +137 -87
- package/examples/basic/client/routes/loader-demo/index.tsx +59 -50
- 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/examples/basic/client/toil.tsx +2 -4
- package/package.json +3 -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 -364
- 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 -130
- 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 +35 -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/slot-layouts.test.ts +69 -0
- package/test/update.test.ts +44 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toiljs doctor`, read-only project diagnostics. Gathers facts from disk (package.json, lockfiles,
|
|
3
|
+
* the resolved `toil.config`, the app entry, `index.html`, the scanned routes, client source files,
|
|
4
|
+
* and the server target), runs the pure checks in `diagnostics.ts`, and prints a grouped human
|
|
5
|
+
* report (or `--json` for CI). Never throws on a partial/non-toiljs project: missing inputs become
|
|
6
|
+
* fail/warn checks. Sets a non-zero exit code when any check fails.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
import { loadConfig, type ResolvedToilConfig, scanRoutes } from 'toiljs/compiler';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type Check,
|
|
17
|
+
checkBasePath,
|
|
18
|
+
checkConfigLoads,
|
|
19
|
+
checkDir,
|
|
20
|
+
checkDuplicatePatterns,
|
|
21
|
+
type CheckGroup,
|
|
22
|
+
checkMountSlots,
|
|
23
|
+
checkNode,
|
|
24
|
+
checkPackageManager,
|
|
25
|
+
checkPeer,
|
|
26
|
+
checkRelativeAssets,
|
|
27
|
+
checkRootElement,
|
|
28
|
+
checkRoutesPresent,
|
|
29
|
+
checkSeoUrl,
|
|
30
|
+
checkServerEntry,
|
|
31
|
+
type CheckStatus,
|
|
32
|
+
checkStyling,
|
|
33
|
+
checkToilconfig,
|
|
34
|
+
checkToiljsInstalled,
|
|
35
|
+
checkToilscriptInstalled,
|
|
36
|
+
checkWasmBuilt,
|
|
37
|
+
findRelativeAssets,
|
|
38
|
+
hasFailures,
|
|
39
|
+
type SourceFile,
|
|
40
|
+
summarize,
|
|
41
|
+
} from './diagnostics.js';
|
|
42
|
+
import {
|
|
43
|
+
detectTailwind,
|
|
44
|
+
type Preprocessor,
|
|
45
|
+
PREPROCESSOR_PKG,
|
|
46
|
+
preprocessorForExt,
|
|
47
|
+
TAILWIND_ENTRY,
|
|
48
|
+
} from './features.js';
|
|
49
|
+
import { accent, bold, danger, dim, success, version, warn } from './ui.js';
|
|
50
|
+
|
|
51
|
+
export interface DoctorOptions {
|
|
52
|
+
readonly root?: string;
|
|
53
|
+
readonly cwd: string;
|
|
54
|
+
/** Emit machine-readable JSON instead of the human report. */
|
|
55
|
+
readonly json?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parses a JSON file into a plain object, or null on any error / non-object. */
|
|
59
|
+
function readJsonObject(file: string): Record<string, unknown> | null {
|
|
60
|
+
try {
|
|
61
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
62
|
+
return typeof parsed === 'object' && parsed !== null
|
|
63
|
+
? (parsed as Record<string, unknown>)
|
|
64
|
+
: null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Coerces a value to a `Record<string, string>` (drops non-string entries). */
|
|
71
|
+
function stringRecord(value: unknown): Record<string, string> {
|
|
72
|
+
if (typeof value !== 'object' || value === null) return {};
|
|
73
|
+
const out: Record<string, string> = {};
|
|
74
|
+
for (const [k, v] of Object.entries(value)) if (typeof v === 'string') out[k] = v;
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readFile(file: string): string | null {
|
|
79
|
+
try {
|
|
80
|
+
return fs.readFileSync(file, 'utf8');
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Reads the framework's own package.json (engines + peerDependencies) for the requirements. */
|
|
87
|
+
function frameworkMeta(): { node: string; peers: Record<string, string> } {
|
|
88
|
+
const pkgPath = path.resolve(
|
|
89
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
90
|
+
'..',
|
|
91
|
+
'..',
|
|
92
|
+
'package.json',
|
|
93
|
+
);
|
|
94
|
+
const pkg = readJsonObject(pkgPath);
|
|
95
|
+
const engines = pkg ? stringRecord(pkg.engines) : {};
|
|
96
|
+
const peers = pkg ? stringRecord(pkg.peerDependencies) : {};
|
|
97
|
+
return { node: engines.node ?? '>=24.0.0', peers };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb'];
|
|
101
|
+
|
|
102
|
+
/** Reads the app entry source (the file that mounts the app), or null if none is found. */
|
|
103
|
+
function readEntry(clientAbsDir: string): string | null {
|
|
104
|
+
for (const name of ['toil.tsx', 'toil.jsx', 'main.tsx', 'main.jsx']) {
|
|
105
|
+
const source = readFile(path.join(clientAbsDir, name));
|
|
106
|
+
if (source !== null && /toiljs\/routes|\bmount\s*\(/.test(source)) return source;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Collects client `.tsx`/`.jsx` sources (capped) for the relative-asset scan. */
|
|
112
|
+
function collectSources(root: string, dir: string, cap: number): SourceFile[] {
|
|
113
|
+
const out: SourceFile[] = [];
|
|
114
|
+
const visit = (current: string): void => {
|
|
115
|
+
if (out.length >= cap) return;
|
|
116
|
+
let entries: fs.Dirent[];
|
|
117
|
+
try {
|
|
118
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
119
|
+
} catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (out.length >= cap) break;
|
|
124
|
+
const full = path.join(current, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
if (entry.name !== 'node_modules') visit(full);
|
|
127
|
+
} else if (/\.(tsx|jsx)$/.test(entry.name)) {
|
|
128
|
+
const source = readFile(full);
|
|
129
|
+
if (source !== null) out.push({ path: path.relative(root, full), source });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
visit(dir);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Picks a check glyph in the brand palette. */
|
|
138
|
+
function glyph(status: CheckStatus): string {
|
|
139
|
+
if (status === 'pass') return success('✓');
|
|
140
|
+
if (status === 'warn') return warn('⚠');
|
|
141
|
+
return danger('✗');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderHuman(groups: readonly CheckGroup[]): void {
|
|
145
|
+
const summary = summarize(groups);
|
|
146
|
+
const out: string[] = [];
|
|
147
|
+
for (const group of groups) {
|
|
148
|
+
out.push(' ' + bold(group.title));
|
|
149
|
+
for (const check of group.checks) {
|
|
150
|
+
let line = ` ${glyph(check.status)} ${check.label}`;
|
|
151
|
+
if (check.detail) line += dim(` ${check.detail}`);
|
|
152
|
+
out.push(line);
|
|
153
|
+
if (check.fix && check.status !== 'pass')
|
|
154
|
+
out.push(' ' + dim(`fix: ${check.fix}`));
|
|
155
|
+
}
|
|
156
|
+
out.push('');
|
|
157
|
+
}
|
|
158
|
+
const parts = [success(`${String(summary.pass)} passed`)];
|
|
159
|
+
if (summary.warn > 0) {
|
|
160
|
+
parts.push(warn(`${String(summary.warn)} warning${summary.warn === 1 ? '' : 's'}`));
|
|
161
|
+
}
|
|
162
|
+
if (summary.fail > 0) parts.push(danger(`${String(summary.fail)} failed`));
|
|
163
|
+
out.push(' ' + parts.join(dim(', ')));
|
|
164
|
+
out.push('');
|
|
165
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
169
|
+
const root = path.resolve(opts.root ?? opts.cwd);
|
|
170
|
+
const meta = frameworkMeta();
|
|
171
|
+
|
|
172
|
+
const projectPkg = readJsonObject(path.join(root, 'package.json'));
|
|
173
|
+
const deps: Record<string, string> = {
|
|
174
|
+
...(projectPkg ? stringRecord(projectPkg.dependencies) : {}),
|
|
175
|
+
...(projectPkg ? stringRecord(projectPkg.devDependencies) : {}),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Config (the only async fact). On failure, downstream paths fall back to conventional defaults.
|
|
179
|
+
let cfg: ResolvedToilConfig | null = null;
|
|
180
|
+
let configError: string | undefined;
|
|
181
|
+
try {
|
|
182
|
+
cfg = await loadConfig({ root });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
configError = err instanceof Error ? err.message : String(err);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const clientAbsDir = cfg ? cfg.clientAbsDir : path.join(root, 'client');
|
|
188
|
+
const routesAbsDir = cfg ? cfg.routesAbsDir : path.join(clientAbsDir, 'routes');
|
|
189
|
+
const publicDir = cfg ? cfg.publicDir : path.join(clientAbsDir, 'public');
|
|
190
|
+
|
|
191
|
+
const entrySource = readEntry(clientAbsDir);
|
|
192
|
+
const indexHtml = readFile(path.join(publicDir, 'index.html'));
|
|
193
|
+
const routes = scanRoutes(routesAbsDir);
|
|
194
|
+
const mainPatterns = routes.filter((r) => r.slot === undefined).map((r) => r.pattern);
|
|
195
|
+
const assetIssues = findRelativeAssets(collectSources(root, clientAbsDir, 200));
|
|
196
|
+
|
|
197
|
+
// Styling facts from the entry's style imports.
|
|
198
|
+
let preprocessorImported: Preprocessor | null = null;
|
|
199
|
+
let tailwindImported = false;
|
|
200
|
+
if (entrySource) {
|
|
201
|
+
const styleImport = /import\s+['"]\.\/styles\/main\.([a-z]+)['"]/.exec(entrySource);
|
|
202
|
+
if (styleImport) preprocessorImported = preprocessorForExt(styleImport[1]);
|
|
203
|
+
tailwindImported = entrySource.includes(TAILWIND_ENTRY);
|
|
204
|
+
}
|
|
205
|
+
const ppPkg = preprocessorImported ? PREPROCESSOR_PKG[preprocessorImported] : null;
|
|
206
|
+
const preprocessorInstalled = ppPkg === null || ppPkg in deps;
|
|
207
|
+
|
|
208
|
+
// Server / WASM facts.
|
|
209
|
+
const toilconfig = readJsonObject(path.join(root, 'toilconfig.json'));
|
|
210
|
+
const serverPresent = toilconfig !== null;
|
|
211
|
+
let missingEntries: string[] = [];
|
|
212
|
+
let toilscriptInstalled = false;
|
|
213
|
+
let wasmExists = false;
|
|
214
|
+
if (toilconfig) {
|
|
215
|
+
const entries = Array.isArray(toilconfig.entries)
|
|
216
|
+
? toilconfig.entries.filter((e): e is string => typeof e === 'string')
|
|
217
|
+
: [];
|
|
218
|
+
missingEntries = entries.filter((e) => !fs.existsSync(path.join(root, e)));
|
|
219
|
+
try {
|
|
220
|
+
createRequire(path.join(root, 'package.json')).resolve('toilscript');
|
|
221
|
+
toilscriptInstalled = true;
|
|
222
|
+
} catch {
|
|
223
|
+
toilscriptInstalled = false;
|
|
224
|
+
}
|
|
225
|
+
const targets =
|
|
226
|
+
typeof toilconfig.targets === 'object' && toilconfig.targets !== null
|
|
227
|
+
? (toilconfig.targets as Record<string, unknown>)
|
|
228
|
+
: {};
|
|
229
|
+
const outFiles: string[] = [];
|
|
230
|
+
for (const target of Object.values(targets)) {
|
|
231
|
+
if (typeof target === 'object' && target !== null) {
|
|
232
|
+
const outFile = (target as Record<string, unknown>).outFile;
|
|
233
|
+
if (typeof outFile === 'string') outFiles.push(outFile);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
wasmExists = outFiles.some((f) => fs.existsSync(path.join(root, f)));
|
|
237
|
+
if (!wasmExists && outFiles.length === 0) {
|
|
238
|
+
try {
|
|
239
|
+
wasmExists = fs
|
|
240
|
+
.readdirSync(path.join(root, 'build', 'server'))
|
|
241
|
+
.some((f) => f.endsWith('.wasm'));
|
|
242
|
+
} catch {
|
|
243
|
+
wasmExists = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const peerName = (n: string): Check => checkPeer(n, deps[n] ?? null, meta.peers[n] ?? '*');
|
|
249
|
+
const peerChecks = Object.keys(meta.peers).map(peerName);
|
|
250
|
+
|
|
251
|
+
const groups: CheckGroup[] = [
|
|
252
|
+
{
|
|
253
|
+
title: 'Environment',
|
|
254
|
+
checks: [
|
|
255
|
+
checkNode(process.versions.node, meta.node),
|
|
256
|
+
checkToiljsInstalled('toiljs' in deps ? version() : null),
|
|
257
|
+
...peerChecks,
|
|
258
|
+
checkPackageManager(LOCKFILES.filter((f) => fs.existsSync(path.join(root, f)))),
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
title: 'Project + routing',
|
|
263
|
+
checks: [
|
|
264
|
+
checkDir(
|
|
265
|
+
'client-dir',
|
|
266
|
+
'client/ directory',
|
|
267
|
+
fs.existsSync(clientAbsDir),
|
|
268
|
+
'Create a client/ directory for your app.',
|
|
269
|
+
),
|
|
270
|
+
checkDir(
|
|
271
|
+
'routes-dir',
|
|
272
|
+
'routes/ directory',
|
|
273
|
+
fs.existsSync(routesAbsDir),
|
|
274
|
+
'Create client/routes/ and add an index.tsx.',
|
|
275
|
+
),
|
|
276
|
+
checkRootElement(indexHtml),
|
|
277
|
+
checkMountSlots(entrySource),
|
|
278
|
+
checkRoutesPresent(routes.length),
|
|
279
|
+
checkDuplicatePatterns(mainPatterns),
|
|
280
|
+
checkRelativeAssets(assetIssues),
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
title: 'Config + assets',
|
|
285
|
+
checks: [
|
|
286
|
+
checkConfigLoads(cfg !== null, configError),
|
|
287
|
+
checkBasePath(cfg ? cfg.base : '/'),
|
|
288
|
+
checkSeoUrl(cfg?.seo != null, cfg?.seo?.url != null),
|
|
289
|
+
checkStyling({
|
|
290
|
+
preprocessorImported,
|
|
291
|
+
preprocessorInstalled,
|
|
292
|
+
tailwindImported,
|
|
293
|
+
tailwindInstalled: detectTailwind(deps),
|
|
294
|
+
}),
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
title: 'Server / WASM',
|
|
299
|
+
checks: serverPresent
|
|
300
|
+
? [
|
|
301
|
+
checkToilconfig(true),
|
|
302
|
+
checkServerEntry(missingEntries),
|
|
303
|
+
checkToilscriptInstalled(toilscriptInstalled),
|
|
304
|
+
checkWasmBuilt(wasmExists),
|
|
305
|
+
]
|
|
306
|
+
: [checkToilconfig(false)],
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
const summary = summarize(groups);
|
|
311
|
+
if (opts.json) {
|
|
312
|
+
process.stdout.write(JSON.stringify({ groups, summary }, null, 2) + '\n');
|
|
313
|
+
} else {
|
|
314
|
+
process.stdout.write('\n' + accent(' Doctor') + dim(` ${root}`) + '\n\n');
|
|
315
|
+
renderHuman(groups);
|
|
316
|
+
}
|
|
317
|
+
if (hasFailures(summary)) process.exitCode = 1;
|
|
318
|
+
}
|
package/src/cli/features.ts
CHANGED
|
@@ -1,160 +1,166 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure description of toiljs's optional client styling features, a CSS preprocessor and Tailwind ,
|
|
3
|
-
* shared by `create` (scaffold) and `configure` (toggle on existing projects). Dependency-light
|
|
4
|
-
* (no node IO) so it can be unit-tested; the file writes and package-manager calls live in the
|
|
5
|
-
* commands. Preprocessor and Tailwind are independent: Tailwind lives in its own `.css` entry so
|
|
6
|
-
* it never passes through (and breaks on) a preprocessor's `@import` resolution.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Supported CSS preprocessor (`css` = none). */
|
|
10
|
-
export type Preprocessor = 'css' | 'sass' | 'less' | 'stylus';
|
|
11
|
-
|
|
12
|
-
/** The two independently-toggleable styling features of a project. */
|
|
13
|
-
export interface StyleFeatures {
|
|
14
|
-
readonly preprocessor: Preprocessor;
|
|
15
|
-
readonly tailwind: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const PREPROCESSORS: readonly Preprocessor[] = ['css', 'sass', 'less', 'stylus'];
|
|
19
|
-
|
|
20
|
-
/** Main stylesheet extension for each preprocessor. */
|
|
21
|
-
export const STYLE_EXT: Record<Preprocessor, string> = {
|
|
22
|
-
css: 'css',
|
|
23
|
-
sass: 'scss',
|
|
24
|
-
less: 'less',
|
|
25
|
-
stylus: 'styl',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** npm package that enables each preprocessor in Vite (plain CSS needs none). */
|
|
29
|
-
export const PREPROCESSOR_PKG: Record<Preprocessor, string | null> = {
|
|
30
|
-
css: null,
|
|
31
|
-
sass: 'sass',
|
|
32
|
-
less: 'less',
|
|
33
|
-
stylus: 'stylus',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/** Tailwind v4 packages. The framework auto-wires the Vite plugin when `@tailwindcss/vite` resolves. */
|
|
37
|
-
export const TAILWIND_PKGS: readonly string[] = ['tailwindcss', '@tailwindcss/vite'];
|
|
38
|
-
|
|
39
|
-
/** Pinned versions for every package these features may install. */
|
|
40
|
-
export const PKG_VERSION: Record<string, string> = {
|
|
41
|
-
sass: '^1.83.0',
|
|
42
|
-
less: '^4.2.1',
|
|
43
|
-
stylus: '^0.64.0',
|
|
44
|
-
tailwindcss: '^4.0.0',
|
|
45
|
-
'@tailwindcss/vite': '^4.0.0',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** Dedicated Tailwind entry (kept `.css` so no preprocessor touches its `@import`). */
|
|
49
|
-
export const TAILWIND_ENTRY = 'styles/tailwind.css';
|
|
50
|
-
export const TAILWIND_CSS = `@import 'tailwindcss';\n`;
|
|
51
|
-
|
|
52
|
-
/** Path (relative to `client/`) of the main stylesheet for a preprocessor. */
|
|
53
|
-
export function styleEntry(p: Preprocessor): string {
|
|
54
|
-
return `styles/main.${STYLE_EXT[p]}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The preprocessor whose main stylesheet uses `ext` (with or without a leading dot), or null. */
|
|
58
|
-
export function preprocessorForExt(ext: string): Preprocessor | null {
|
|
59
|
-
const e = ext.replace(/^\./, '');
|
|
60
|
-
if (e === 'sass') return 'sass';
|
|
61
|
-
return PREPROCESSORS.find((p) => STYLE_EXT[p] === e) ?? null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Packages required by a feature set (preprocessor package + Tailwind packages). */
|
|
65
|
-
export function requiredPackages(f: StyleFeatures): string[] {
|
|
66
|
-
const pkgs: string[] = [];
|
|
67
|
-
const pp = PREPROCESSOR_PKG[f.preprocessor];
|
|
68
|
-
if (pp) pkgs.push(pp);
|
|
69
|
-
if (f.tailwind) pkgs.push(...TAILWIND_PKGS);
|
|
70
|
-
return pkgs;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Managed packages to add and remove when moving from `from` to `to`. */
|
|
74
|
-
export function packageDiff(
|
|
75
|
-
from: StyleFeatures,
|
|
76
|
-
to: StyleFeatures,
|
|
77
|
-
): { add: string[]; remove: string[] } {
|
|
78
|
-
const want = new Set(requiredPackages(to));
|
|
79
|
-
const had = new Set(requiredPackages(from));
|
|
80
|
-
return {
|
|
81
|
-
add: [...want].filter((p) => !had.has(p)),
|
|
82
|
-
remove: [...had].filter((p) => !want.has(p)),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** The side-effect style imports for the app entry, Tailwind first so app CSS can override it. */
|
|
87
|
-
export function styleImportLines(f: StyleFeatures): string[] {
|
|
88
|
-
const lines: string[] = [];
|
|
89
|
-
if (f.tailwind) lines.push(`import './${TAILWIND_ENTRY}';`);
|
|
90
|
-
lines.push(`import './${styleEntry(f.preprocessor)}';`);
|
|
91
|
-
return lines;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Rewrites the `./styles/*` side-effect imports in an app entry (`client/toil.tsx`) to match
|
|
96
|
-
* `features`, preserving the rest of the file. Existing style imports are removed and the new
|
|
97
|
-
* block is placed after the `toiljs/routes` import (or the last import, or the top).
|
|
98
|
-
*/
|
|
99
|
-
export function setStyleImports(source: string, f: StyleFeatures): string {
|
|
100
|
-
const stripped = source.replace(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
insertAt =
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (/\
|
|
141
|
-
return source.replace(/\
|
|
142
|
-
}
|
|
143
|
-
if (
|
|
144
|
-
return source.replace(
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pure description of toiljs's optional client styling features, a CSS preprocessor and Tailwind ,
|
|
3
|
+
* shared by `create` (scaffold) and `configure` (toggle on existing projects). Dependency-light
|
|
4
|
+
* (no node IO) so it can be unit-tested; the file writes and package-manager calls live in the
|
|
5
|
+
* commands. Preprocessor and Tailwind are independent: Tailwind lives in its own `.css` entry so
|
|
6
|
+
* it never passes through (and breaks on) a preprocessor's `@import` resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Supported CSS preprocessor (`css` = none). */
|
|
10
|
+
export type Preprocessor = 'css' | 'sass' | 'less' | 'stylus';
|
|
11
|
+
|
|
12
|
+
/** The two independently-toggleable styling features of a project. */
|
|
13
|
+
export interface StyleFeatures {
|
|
14
|
+
readonly preprocessor: Preprocessor;
|
|
15
|
+
readonly tailwind: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PREPROCESSORS: readonly Preprocessor[] = ['css', 'sass', 'less', 'stylus'];
|
|
19
|
+
|
|
20
|
+
/** Main stylesheet extension for each preprocessor. */
|
|
21
|
+
export const STYLE_EXT: Record<Preprocessor, string> = {
|
|
22
|
+
css: 'css',
|
|
23
|
+
sass: 'scss',
|
|
24
|
+
less: 'less',
|
|
25
|
+
stylus: 'styl',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** npm package that enables each preprocessor in Vite (plain CSS needs none). */
|
|
29
|
+
export const PREPROCESSOR_PKG: Record<Preprocessor, string | null> = {
|
|
30
|
+
css: null,
|
|
31
|
+
sass: 'sass',
|
|
32
|
+
less: 'less',
|
|
33
|
+
stylus: 'stylus',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Tailwind v4 packages. The framework auto-wires the Vite plugin when `@tailwindcss/vite` resolves. */
|
|
37
|
+
export const TAILWIND_PKGS: readonly string[] = ['tailwindcss', '@tailwindcss/vite'];
|
|
38
|
+
|
|
39
|
+
/** Pinned versions for every package these features may install. */
|
|
40
|
+
export const PKG_VERSION: Record<string, string> = {
|
|
41
|
+
sass: '^1.83.0',
|
|
42
|
+
less: '^4.2.1',
|
|
43
|
+
stylus: '^0.64.0',
|
|
44
|
+
tailwindcss: '^4.0.0',
|
|
45
|
+
'@tailwindcss/vite': '^4.0.0',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Dedicated Tailwind entry (kept `.css` so no preprocessor touches its `@import`). */
|
|
49
|
+
export const TAILWIND_ENTRY = 'styles/tailwind.css';
|
|
50
|
+
export const TAILWIND_CSS = `@import 'tailwindcss';\n`;
|
|
51
|
+
|
|
52
|
+
/** Path (relative to `client/`) of the main stylesheet for a preprocessor. */
|
|
53
|
+
export function styleEntry(p: Preprocessor): string {
|
|
54
|
+
return `styles/main.${STYLE_EXT[p]}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The preprocessor whose main stylesheet uses `ext` (with or without a leading dot), or null. */
|
|
58
|
+
export function preprocessorForExt(ext: string): Preprocessor | null {
|
|
59
|
+
const e = ext.replace(/^\./, '');
|
|
60
|
+
if (e === 'sass') return 'sass';
|
|
61
|
+
return PREPROCESSORS.find((p) => STYLE_EXT[p] === e) ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Packages required by a feature set (preprocessor package + Tailwind packages). */
|
|
65
|
+
export function requiredPackages(f: StyleFeatures): string[] {
|
|
66
|
+
const pkgs: string[] = [];
|
|
67
|
+
const pp = PREPROCESSOR_PKG[f.preprocessor];
|
|
68
|
+
if (pp) pkgs.push(pp);
|
|
69
|
+
if (f.tailwind) pkgs.push(...TAILWIND_PKGS);
|
|
70
|
+
return pkgs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Managed packages to add and remove when moving from `from` to `to`. */
|
|
74
|
+
export function packageDiff(
|
|
75
|
+
from: StyleFeatures,
|
|
76
|
+
to: StyleFeatures,
|
|
77
|
+
): { add: string[]; remove: string[] } {
|
|
78
|
+
const want = new Set(requiredPackages(to));
|
|
79
|
+
const had = new Set(requiredPackages(from));
|
|
80
|
+
return {
|
|
81
|
+
add: [...want].filter((p) => !had.has(p)),
|
|
82
|
+
remove: [...had].filter((p) => !want.has(p)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The side-effect style imports for the app entry, Tailwind first so app CSS can override it. */
|
|
87
|
+
export function styleImportLines(f: StyleFeatures): string[] {
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
if (f.tailwind) lines.push(`import './${TAILWIND_ENTRY}';`);
|
|
90
|
+
lines.push(`import './${styleEntry(f.preprocessor)}';`);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rewrites the `./styles/*` side-effect imports in an app entry (`client/toil.tsx`) to match
|
|
96
|
+
* `features`, preserving the rest of the file. Existing style imports are removed and the new
|
|
97
|
+
* block is placed after the `toiljs/routes` import (or the last import, or the top).
|
|
98
|
+
*/
|
|
99
|
+
export function setStyleImports(source: string, f: StyleFeatures): string {
|
|
100
|
+
const stripped = source.replace(
|
|
101
|
+
/^[ \t]*import\s+['"]\.\/styles\/[^'"]+['"];?[ \t]*\r?\n/gm,
|
|
102
|
+
'',
|
|
103
|
+
);
|
|
104
|
+
const block = styleImportLines(f).join('\n') + '\n';
|
|
105
|
+
|
|
106
|
+
const lines = stripped.split('\n');
|
|
107
|
+
const routesIdx = lines.findIndex((l) => /from\s+['"]toiljs\/routes['"]/.test(l));
|
|
108
|
+
let insertAt: number;
|
|
109
|
+
if (routesIdx !== -1) {
|
|
110
|
+
insertAt = routesIdx + 1;
|
|
111
|
+
} else {
|
|
112
|
+
const lastImport = lines.reduce((acc, l, i) => (/^\s*import\s/.test(l) ? i : acc), -1);
|
|
113
|
+
insertAt = lastImport + 1;
|
|
114
|
+
}
|
|
115
|
+
const head = lines.slice(0, insertAt).join('\n');
|
|
116
|
+
const tail = lines.slice(insertAt).join('\n');
|
|
117
|
+
return `${head}\n\n${block}\n${tail}`.replace(/\n{3,}/g, '\n\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A `toil.config` source containing `client: { images: <bool> }` (for scaffolding when none exists). */
|
|
121
|
+
export function defaultConfigSource(images: boolean): string {
|
|
122
|
+
return (
|
|
123
|
+
"import { defineConfig } from 'toiljs/compiler';\n\n" +
|
|
124
|
+
'export default defineConfig({\n' +
|
|
125
|
+
' client: {\n' +
|
|
126
|
+
' // Optimize images at build time (resize/compress imported images).\n' +
|
|
127
|
+
` images: ${String(images)},\n` +
|
|
128
|
+
' },\n' +
|
|
129
|
+
'});\n'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sets the `client.images` flag in a `toil.config` source, returning the updated source, or `null`
|
|
135
|
+
* if the file's shape isn't recognized (the caller should then fall back to a manual note). Handles
|
|
136
|
+
* an existing `images:` value, an existing `client: {` block, or a bare `defineConfig({ … })`.
|
|
137
|
+
*/
|
|
138
|
+
export function setConfigImages(source: string, enabled: boolean): string | null {
|
|
139
|
+
const value = String(enabled);
|
|
140
|
+
if (/\bimages\s*:\s*(?:true|false)/.test(source)) {
|
|
141
|
+
return source.replace(/\bimages\s*:\s*(?:true|false)/, `images: ${value}`);
|
|
142
|
+
}
|
|
143
|
+
if (/\bclient\s*:\s*\{/.test(source)) {
|
|
144
|
+
return source.replace(/\bclient\s*:\s*\{/, `client: {\n images: ${value},`);
|
|
145
|
+
}
|
|
146
|
+
if (/defineConfig\(\s*\{/.test(source)) {
|
|
147
|
+
return source.replace(
|
|
148
|
+
/defineConfig\(\s*\{/,
|
|
149
|
+
`defineConfig({\n client: { images: ${value} },`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Detects the active preprocessor from a project's combined dependency map. */
|
|
156
|
+
export function detectPreprocessor(deps: Record<string, string>): Preprocessor {
|
|
157
|
+
if ('sass' in deps) return 'sass';
|
|
158
|
+
if ('less' in deps) return 'less';
|
|
159
|
+
if ('stylus' in deps) return 'stylus';
|
|
160
|
+
return 'css';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Whether Tailwind is installed in a project's combined dependency map. */
|
|
164
|
+
export function detectTailwind(deps: Record<string, string>): boolean {
|
|
165
|
+
return '@tailwindcss/vite' in deps || 'tailwindcss' in deps;
|
|
166
|
+
}
|