toiljs 0.0.15 → 0.0.19
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/.babelrc +13 -13
- package/.gitattributes +2 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
- package/.github/changelog-config.json +45 -45
- package/.github/dependabot.yml +27 -27
- package/.github/workflows/ci.yml +191 -191
- package/.prettierrc.json +11 -11
- package/.vscode/settings.json +9 -9
- package/CHANGELOG.md +116 -5
- package/LICENSE +187 -187
- package/README.md +524 -315
- package/as-pect.asconfig.json +34 -34
- package/as-pect.config.js +65 -65
- package/assets/logo.svg +36 -36
- package/build/backend/.tsbuildinfo +1 -1
- package/build/backend/index.d.ts +1 -0
- package/build/backend/index.js +20 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +1320 -696
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.d.ts +6 -0
- package/build/client/dev/devtools.js +479 -0
- package/build/client/dev/error-overlay.d.ts +9 -0
- package/build/client/dev/error-overlay.js +19 -4
- package/build/client/errors.d.ts +1 -0
- package/build/client/errors.js +3 -0
- package/build/client/index.d.ts +2 -0
- package/build/client/index.js +2 -0
- package/build/client/navigation/prefetch.d.ts +1 -0
- package/build/client/navigation/prefetch.js +35 -0
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/hooks.js +6 -2
- package/build/client/routing/loader.d.ts +23 -0
- package/build/client/routing/loader.js +53 -7
- package/build/client/routing/mount.js +4 -3
- package/build/client/rpc.d.ts +1 -0
- package/build/client/rpc.js +37 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +16 -0
- package/build/compiler/config.js +9 -0
- package/build/compiler/docs.js +78 -21
- package/build/compiler/generate.js +5 -4
- package/build/compiler/index.d.ts +3 -2
- package/build/compiler/index.js +2 -2
- package/build/compiler/plugin.js +228 -0
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +1 -1
- package/build/compiler/seo.d.ts +1 -1
- package/build/compiler/seo.js +20 -5
- package/build/compiler/ssg.js +39 -2
- package/build/compiler/vite.js +25 -0
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +54 -0
- package/build/io/codec.js +143 -0
- package/build/io/index.d.ts +1 -2
- package/build/io/index.js +1 -2
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/eslint.config.js +48 -48
- package/examples/basic/client/404.tsx +11 -11
- package/examples/basic/client/components/.gitkeep +1 -1
- package/examples/basic/client/global-error.tsx +13 -13
- package/examples/basic/client/layout.tsx +25 -25
- package/examples/basic/client/public/images/.gitkeep +1 -1
- package/examples/basic/client/public/images/logo.svg +36 -36
- package/examples/basic/client/public/robots.txt +2 -2
- package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
- package/examples/basic/client/routes/features/error/error.tsx +16 -16
- package/examples/basic/client/routes/features/index.tsx +1 -1
- package/examples/basic/client/routes/features/template/b.tsx +14 -14
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
- package/examples/basic/client/routes/gallery/layout.tsx +13 -13
- package/examples/basic/client/routes/io.tsx +23 -24
- package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
- package/examples/basic/client/routes/rest.tsx +74 -0
- package/examples/basic/client/routes/rpc.tsx +43 -0
- package/examples/basic/client/routes/search.tsx +61 -61
- package/examples/basic/client/toil.tsx +5 -5
- package/package.json +167 -148
- package/presets/eslint.js +88 -88
- package/presets/no-uint8array-tostring.js +200 -200
- package/presets/prettier-plugin.js +51 -0
- package/presets/prettier.json +19 -18
- package/presets/tsconfig.json +37 -37
- package/server/runtime/README.md +97 -0
- package/server/runtime/abort/abort.ts +27 -0
- package/server/runtime/env/Server.ts +61 -0
- package/server/runtime/envelope.ts +191 -0
- package/server/runtime/exports/index.ts +52 -0
- package/server/runtime/handlers/ToilHandler.ts +34 -0
- package/server/runtime/index.ts +26 -0
- package/server/runtime/lang/Potential.ts +5 -0
- package/server/runtime/memory.ts +81 -0
- package/server/runtime/request.ts +55 -0
- package/server/runtime/response.ts +86 -0
- package/server/runtime/rest/Rest.ts +39 -0
- package/server/runtime/rest/RestHandler.ts +20 -0
- package/server/runtime/rest/RouteContext.ts +82 -0
- package/server/runtime/rest/match.ts +48 -0
- package/server/runtime/tsconfig.json +7 -0
- package/src/backend/index.ts +202 -160
- package/src/cli/create.ts +15 -5
- package/src/cli/diagnostics.ts +81 -0
- package/src/cli/doctor.ts +384 -7
- package/src/cli/index.ts +11 -2
- package/src/cli/proc.ts +50 -50
- package/src/cli/updates.ts +69 -69
- package/src/cli/validate.ts +31 -31
- package/src/client/channel/channel.ts +146 -146
- package/src/client/components/Form.tsx +65 -65
- package/src/client/components/Script.tsx +113 -113
- package/src/client/components/Slot.tsx +21 -21
- package/src/client/dev/devtools.tsx +1018 -0
- package/src/client/dev/error-overlay.tsx +30 -4
- package/src/client/errors.ts +11 -0
- package/src/client/head/head.ts +167 -167
- package/src/client/head/metadata.ts +112 -112
- package/src/client/index.ts +91 -89
- package/src/client/navigation/NavLink.tsx +86 -86
- package/src/client/navigation/navigation.ts +235 -235
- package/src/client/navigation/prefetch.ts +169 -130
- package/src/client/navigation/scroll.ts +53 -53
- package/src/client/routing/Router.tsx +8 -2
- package/src/client/routing/action.ts +122 -122
- package/src/client/routing/error-boundary.tsx +43 -43
- package/src/client/routing/hooks.ts +21 -6
- package/src/client/routing/loader.ts +325 -235
- package/src/client/routing/match.ts +47 -47
- package/src/client/routing/mount.tsx +54 -52
- package/src/client/routing/params-context.ts +10 -10
- package/src/client/routing/slot-context.ts +7 -7
- package/src/client/rpc.ts +64 -0
- package/src/client/search/search.ts +189 -189
- package/src/client/search/use-page-search.ts +73 -73
- package/src/client/types.ts +73 -73
- package/src/compiler/config.ts +221 -182
- package/src/compiler/docs.ts +285 -228
- package/src/compiler/generate.ts +395 -394
- package/src/compiler/index.ts +66 -57
- package/src/compiler/pages.ts +70 -70
- package/src/compiler/plugin.ts +258 -2
- package/src/compiler/prerender.ts +156 -156
- package/src/compiler/seo.ts +417 -390
- package/src/compiler/ssg.ts +171 -126
- package/src/compiler/vite.ts +34 -0
- package/src/io/FastMap.ts +151 -127
- package/src/io/FastSet.ts +15 -1
- package/src/io/codec.ts +217 -0
- package/src/io/index.ts +10 -11
- package/src/io/lengths.ts +14 -14
- package/src/io/types.ts +19 -18
- package/src/logger/index.ts +22 -22
- package/src/shared/index.ts +10 -10
- package/std/client/index.d.ts +15 -15
- package/std/client/package.json +3 -3
- package/test/assembly/example.spec.ts +17 -7
- package/test/channel.test.ts +21 -21
- package/test/doctor.test.ts +65 -0
- package/test/dom/Link.test.tsx +47 -47
- package/test/dom/NavLink.test.tsx +37 -37
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +121 -121
- package/test/dom/navigation.test.ts +59 -59
- package/test/dom/revalidate.test.tsx +38 -38
- package/test/dom/route-head.test.tsx +78 -78
- package/test/dom/router-loading.test.tsx +44 -44
- package/test/dom/scroll.test.ts +56 -56
- package/test/dom/use-metadata.test.tsx +58 -58
- package/test/errors.test.ts +21 -0
- package/test/io.test.ts +117 -93
- package/test/navlink.test.ts +28 -28
- package/test/placeholder.test.ts +9 -9
- package/test/prettier-plugin.test.ts +46 -0
- package/test/routes.test.ts +76 -76
- package/test/rpc.test.ts +50 -0
- package/test/seo.test.ts +175 -164
- package/test/slot-layouts.test.ts +69 -69
- package/test/ssg.test.ts +36 -36
- package/test/update.test.ts +44 -44
- package/test/validate.test.ts +42 -42
- package/tests/data-parity/generated-parity.ts +99 -0
- package/tests/data-parity/parity.ts +80 -0
- package/tests/data-parity/spec.ts +46 -0
- package/toil-routes.d.ts +7 -0
- package/tsconfig.backend.json +13 -13
- package/tsconfig.base.json +35 -35
- package/tsconfig.cli.json +13 -13
- package/tsconfig.client.json +14 -14
- package/tsconfig.compiler.json +13 -13
- package/tsconfig.io.json +12 -12
- package/tsconfig.json +22 -22
- package/tsconfig.logger.json +12 -12
- package/tsconfig.server.json +10 -10
- package/tsconfig.shared.json +12 -12
- package/vitest.config.ts +26 -26
- package/.idea/codeStyles/Project.xml +0 -54
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -7
- package/.idea/toiljs.iml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.toil/entry.tsx +0 -9
- package/.toil/index.html +0 -12
- package/.toil/routes.ts +0 -9
- package/build/cli/configure.d.ts +0 -16
- package/build/cli/configure.js +0 -272
- package/build/cli/create.d.ts +0 -16
- package/build/cli/create.js +0 -420
- package/build/cli/diagnostics.d.ts +0 -55
- package/build/cli/diagnostics.js +0 -333
- package/build/cli/doctor.d.ts +0 -6
- package/build/cli/doctor.js +0 -249
- package/build/cli/features.d.ts +0 -25
- package/build/cli/features.js +0 -107
- package/build/cli/index.d.ts +0 -2
- package/build/cli/proc.d.ts +0 -6
- package/build/cli/proc.js +0 -31
- package/build/cli/ui.d.ts +0 -9
- package/build/cli/ui.js +0 -75
- package/build/cli/update.d.ts +0 -7
- package/build/cli/update.js +0 -117
- package/build/cli/updates.d.ts +0 -10
- package/build/cli/updates.js +0 -45
- package/build/cli/validate.d.ts +0 -4
- package/build/cli/validate.js +0 -19
- package/build/client/Link.d.ts +0 -8
- package/build/client/Link.js +0 -44
- package/build/client/NavLink.d.ts +0 -14
- package/build/client/NavLink.js +0 -37
- package/build/client/Router.d.ts +0 -7
- package/build/client/Router.js +0 -55
- package/build/client/channel.d.ts +0 -23
- package/build/client/channel.js +0 -94
- package/build/client/error-boundary.d.ts +0 -16
- package/build/client/error-boundary.js +0 -19
- package/build/client/head.d.ts +0 -26
- package/build/client/head.js +0 -87
- package/build/client/hooks.d.ts +0 -17
- package/build/client/hooks.js +0 -48
- package/build/client/lazy.d.ts +0 -16
- package/build/client/lazy.js +0 -53
- package/build/client/match.d.ts +0 -2
- package/build/client/match.js +0 -32
- package/build/client/mount.d.ts +0 -2
- package/build/client/mount.js +0 -13
- package/build/client/navigation.d.ts +0 -13
- package/build/client/navigation.js +0 -97
- package/build/client/params-context.d.ts +0 -2
- package/build/client/params-context.js +0 -2
- package/build/client/prefetch.d.ts +0 -11
- package/build/client/prefetch.js +0 -100
- package/build/client/runtime.d.ts +0 -31
- package/build/client/runtime.js +0 -112
- package/build/client/scroll.d.ts +0 -8
- package/build/client/scroll.js +0 -36
- package/build/io/BinaryReader.d.ts +0 -44
- package/build/io/BinaryReader.js +0 -244
- package/build/io/BinaryWriter.d.ts +0 -44
- package/build/io/BinaryWriter.js +0 -297
- package/build/server/release.wasm +0 -0
- package/build/server/release.wat +0 -9
- package/src/io/BinaryReader.ts +0 -340
- package/src/io/BinaryWriter.ts +0 -385
- package/src/server/index.ts +0 -10
- package/src/server/main.ts +0 -13
- package/src/server/tsconfig.json +0 -4
- package/toil-env.d.ts +0 -16
- package/toilconfig.json +0 -30
package/src/cli/doctor.ts
CHANGED
|
@@ -23,9 +23,12 @@ import {
|
|
|
23
23
|
checkNode,
|
|
24
24
|
checkPackageManager,
|
|
25
25
|
checkPeer,
|
|
26
|
+
checkPrettierPlugin,
|
|
26
27
|
checkRelativeAssets,
|
|
28
|
+
checkRestDispatch,
|
|
27
29
|
checkRootElement,
|
|
28
30
|
checkRoutesPresent,
|
|
31
|
+
checkRpcWiring,
|
|
29
32
|
checkSeoUrl,
|
|
30
33
|
checkServerEntry,
|
|
31
34
|
type CheckStatus,
|
|
@@ -36,6 +39,10 @@ import {
|
|
|
36
39
|
checkWasmBuilt,
|
|
37
40
|
findRelativeAssets,
|
|
38
41
|
hasFailures,
|
|
42
|
+
type RestFacts,
|
|
43
|
+
type RpcFacts,
|
|
44
|
+
RPC_TOILSCRIPT_MIN,
|
|
45
|
+
satisfiesMin,
|
|
39
46
|
type SourceFile,
|
|
40
47
|
summarize,
|
|
41
48
|
} from './diagnostics.js';
|
|
@@ -53,6 +60,8 @@ export interface DoctorOptions {
|
|
|
53
60
|
readonly cwd: string;
|
|
54
61
|
/** Emit machine-readable JSON instead of the human report. */
|
|
55
62
|
readonly json?: boolean;
|
|
63
|
+
/** Auto-fix what can be fixed in place (currently the typed-RPC wiring). */
|
|
64
|
+
readonly fix?: boolean;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/** Parses a JSON file into a plain object, or null on any error / non-object. */
|
|
@@ -83,6 +92,336 @@ function readFile(file: string): string | null {
|
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
function writeFile(file: string, content: string): void {
|
|
96
|
+
fs.writeFileSync(file, content);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether `name` is installed for the project at `root`. Tries Node resolution first (handles
|
|
101
|
+
* hoisting), then falls back to walking `node_modules`, since a strict `exports` map can make
|
|
102
|
+
* `require.resolve('<pkg>')` throw even when the package is present (the toilscript false positive).
|
|
103
|
+
*/
|
|
104
|
+
function isPackageInstalled(root: string, name: string): boolean {
|
|
105
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
106
|
+
for (const id of [`${name}/package.json`, name]) {
|
|
107
|
+
try {
|
|
108
|
+
require.resolve(id);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
// try the next resolution strategy
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (let dir = root; ; ) {
|
|
115
|
+
if (fs.existsSync(path.join(dir, 'node_modules', name, 'package.json'))) return true;
|
|
116
|
+
const parent = path.dirname(dir);
|
|
117
|
+
if (parent === dir) return false;
|
|
118
|
+
dir = parent;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Narrows a value to a plain (non-array) object, or null. */
|
|
123
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
124
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
125
|
+
? (value as Record<string, unknown>)
|
|
126
|
+
: null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const RPC_MODULE_FLAG = '--rpcModule shared/server.ts';
|
|
130
|
+
const RPC_GITIGNORE_LINE = 'shared/server.ts';
|
|
131
|
+
const RPC_GITIGNORE_RE = /(^|\n)\s*shared\/server\.ts\s*(\r?\n|$)/;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether a dependency range is an ordinary registry semver range (so a deliberate
|
|
135
|
+
* `latest`/`*`/`file:`/`github:`/`workspace:`/`npm:` pin is left untouched and treated
|
|
136
|
+
* as already-OK rather than clobbered to a version).
|
|
137
|
+
*/
|
|
138
|
+
function looksLikeSemverRange(range: string): boolean {
|
|
139
|
+
return /^\s*[v^~>=<]*\s*\d+\.\d+/.test(range);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Reads the project and reports which parts of the typed-RPC wiring are present. */
|
|
143
|
+
function gatherRpcFacts(root: string): RpcFacts {
|
|
144
|
+
const pkg = readJsonObject(path.join(root, 'package.json'));
|
|
145
|
+
const scripts = pkg ? stringRecord(pkg.scripts) : {};
|
|
146
|
+
const deps = {
|
|
147
|
+
...(pkg ? stringRecord(pkg.dependencies) : {}),
|
|
148
|
+
...(pkg ? stringRecord(pkg.devDependencies) : {}),
|
|
149
|
+
};
|
|
150
|
+
const tsconfig = readJsonObject(path.join(root, 'tsconfig.json'));
|
|
151
|
+
const gitignore = readFile(path.join(root, '.gitignore'));
|
|
152
|
+
|
|
153
|
+
// Either the combined `build` or `build:server` carrying --rpcModule counts (the fixer writes both).
|
|
154
|
+
const buildServerWired = [scripts['build:server'], scripts['build']].some(
|
|
155
|
+
(s) => typeof s === 'string' && s.includes('--rpcModule'),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
let tsconfigWired = false;
|
|
159
|
+
if (tsconfig) {
|
|
160
|
+
// An absent `include` compiles all files, so `shared` is covered implicitly.
|
|
161
|
+
const include = tsconfig.include;
|
|
162
|
+
const hasShared = !Array.isArray(include) || include.includes('shared');
|
|
163
|
+
const paths = asRecord(asRecord(tsconfig.compilerOptions)?.paths);
|
|
164
|
+
tsconfigWired = hasShared && paths !== null && 'shared/*' in paths;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const gitignoreWired = gitignore !== null && RPC_GITIGNORE_RE.test(gitignore);
|
|
168
|
+
const range = deps.toilscript;
|
|
169
|
+
// A non-semver range (file:/github:/latest/*) can't be assessed; don't flag it.
|
|
170
|
+
const toilscriptOk =
|
|
171
|
+
range == null
|
|
172
|
+
? false
|
|
173
|
+
: looksLikeSemverRange(range)
|
|
174
|
+
? satisfiesMin(range, RPC_TOILSCRIPT_MIN)
|
|
175
|
+
: true;
|
|
176
|
+
|
|
177
|
+
return { buildServerWired, tsconfigWired, gitignoreWired, toilscriptOk };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The server `.ts` sources, read from the directories of the toilconfig entries (capped). */
|
|
181
|
+
function serverSources(root: string, toilconfig: Record<string, unknown> | null): string[] {
|
|
182
|
+
const dirs = new Set<string>();
|
|
183
|
+
const entries = Array.isArray(toilconfig?.entries)
|
|
184
|
+
? (toilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
|
|
185
|
+
: [];
|
|
186
|
+
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
187
|
+
|
|
188
|
+
const out: string[] = [];
|
|
189
|
+
const cap = 200;
|
|
190
|
+
const maxDepth = 16; // bounds the walk so a symlink cycle in a hostile project can't hang doctor
|
|
191
|
+
const visit = (current: string, depth: number): void => {
|
|
192
|
+
if (out.length >= cap || depth > maxDepth) return;
|
|
193
|
+
let listing: fs.Dirent[];
|
|
194
|
+
try {
|
|
195
|
+
listing = fs.readdirSync(current, { withFileTypes: true });
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const entry of listing) {
|
|
200
|
+
if (out.length >= cap) break;
|
|
201
|
+
const full = path.join(current, entry.name);
|
|
202
|
+
// isDirectory() follows symlinks; the depth cap keeps a symlink cycle bounded.
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
if (entry.name !== 'node_modules') visit(full, depth + 1);
|
|
205
|
+
} else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
206
|
+
const src = readFile(full);
|
|
207
|
+
if (src !== null) out.push(src);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
for (const dir of dirs) visit(dir, 0);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Scans the server sources for `@rest` controllers and whether anything dispatches them. */
|
|
216
|
+
function gatherRestFacts(root: string, toilconfig: Record<string, unknown> | null): RestFacts {
|
|
217
|
+
let hasControllers = false;
|
|
218
|
+
let dispatched = false;
|
|
219
|
+
for (const src of serverSources(root, toilconfig)) {
|
|
220
|
+
if (/@rest\b/.test(src)) hasControllers = true;
|
|
221
|
+
if (/\bRest\s*\.\s*dispatch\s*\(/.test(src) || /\bRestHandler\b/.test(src))
|
|
222
|
+
dispatched = true;
|
|
223
|
+
if (hasControllers && dispatched) break;
|
|
224
|
+
}
|
|
225
|
+
return { hasControllers, dispatched };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface RpcFixResult {
|
|
229
|
+
/** Files written. */
|
|
230
|
+
readonly changed: string[];
|
|
231
|
+
/** Files that need a manual edit (e.g. tsconfig with comments). */
|
|
232
|
+
readonly skipped: string[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Applies the typed-RPC wiring in place: appends `--rpcModule` to the toilscript build scripts,
|
|
237
|
+
* adds `shared` (+ the `shared/*` alias) to tsconfig, ignores the generated module, and lifts the
|
|
238
|
+
* toilscript floor. Idempotent; only writes files it actually changes.
|
|
239
|
+
*/
|
|
240
|
+
function applyRpcFix(root: string): RpcFixResult {
|
|
241
|
+
const changed: string[] = [];
|
|
242
|
+
const skipped: string[] = [];
|
|
243
|
+
|
|
244
|
+
const pkgPath = path.join(root, 'package.json');
|
|
245
|
+
const pkgRaw = readFile(pkgPath);
|
|
246
|
+
const pkg = pkgRaw !== null ? readJsonObject(pkgPath) : null;
|
|
247
|
+
if (pkg !== null) {
|
|
248
|
+
let touched = false;
|
|
249
|
+
const scripts = asRecord(pkg.scripts) ?? {};
|
|
250
|
+
for (const key of ['build', 'build:server']) {
|
|
251
|
+
const value = scripts[key];
|
|
252
|
+
if (
|
|
253
|
+
typeof value === 'string' &&
|
|
254
|
+
value.includes('toilscript') &&
|
|
255
|
+
!value.includes('--rpcModule')
|
|
256
|
+
) {
|
|
257
|
+
scripts[key] = `${value} ${RPC_MODULE_FLAG}`;
|
|
258
|
+
pkg.scripts = scripts;
|
|
259
|
+
touched = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let toilscriptDeclared = false;
|
|
263
|
+
for (const field of ['devDependencies', 'dependencies']) {
|
|
264
|
+
const bag = asRecord(pkg[field]);
|
|
265
|
+
const current = bag?.toilscript;
|
|
266
|
+
if (bag && typeof current === 'string') {
|
|
267
|
+
toilscriptDeclared = true;
|
|
268
|
+
// Only lift a real semver range that floors below the minimum; leave file:/latest/* pins alone.
|
|
269
|
+
if (looksLikeSemverRange(current) && !satisfiesMin(current, RPC_TOILSCRIPT_MIN)) {
|
|
270
|
+
bag.toilscript = `^${RPC_TOILSCRIPT_MIN}`;
|
|
271
|
+
touched = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!toilscriptDeclared) {
|
|
276
|
+
// A server project needs toilscript; add it so the wiring actually resolves.
|
|
277
|
+
const dd = asRecord(pkg.devDependencies) ?? {};
|
|
278
|
+
dd.toilscript = `^${RPC_TOILSCRIPT_MIN}`;
|
|
279
|
+
pkg.devDependencies = dd;
|
|
280
|
+
touched = true;
|
|
281
|
+
}
|
|
282
|
+
if (touched) {
|
|
283
|
+
writeFile(pkgPath, JSON.stringify(pkg, null, 4) + '\n');
|
|
284
|
+
changed.push('package.json');
|
|
285
|
+
}
|
|
286
|
+
} else if (pkgRaw !== null) {
|
|
287
|
+
skipped.push('package.json (unparseable)');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const tsPath = path.join(root, 'tsconfig.json');
|
|
291
|
+
const tsRaw = readFile(tsPath);
|
|
292
|
+
const tsconfig = tsRaw !== null ? readJsonObject(tsPath) : null;
|
|
293
|
+
if (tsconfig !== null) {
|
|
294
|
+
let touched = false;
|
|
295
|
+
// Only touch `include` if it already exists; an absent `include` compiles all files,
|
|
296
|
+
// and synthesizing one would narrow what TypeScript sees.
|
|
297
|
+
if (Array.isArray(tsconfig.include)) {
|
|
298
|
+
const include = [...(tsconfig.include as unknown[])];
|
|
299
|
+
if (!include.includes('shared')) {
|
|
300
|
+
const at = include.indexOf('client');
|
|
301
|
+
include.splice(at >= 0 ? at + 1 : include.length, 0, 'shared');
|
|
302
|
+
tsconfig.include = include;
|
|
303
|
+
touched = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const co = asRecord(tsconfig.compilerOptions) ?? {};
|
|
307
|
+
const paths = asRecord(co.paths) ?? {};
|
|
308
|
+
if (!('shared/*' in paths)) {
|
|
309
|
+
paths['shared/*'] = ['./shared/*'];
|
|
310
|
+
co.paths = paths;
|
|
311
|
+
tsconfig.compilerOptions = co;
|
|
312
|
+
touched = true;
|
|
313
|
+
}
|
|
314
|
+
if (touched) {
|
|
315
|
+
writeFile(tsPath, JSON.stringify(tsconfig, null, 4) + '\n');
|
|
316
|
+
changed.push('tsconfig.json');
|
|
317
|
+
}
|
|
318
|
+
} else if (tsRaw !== null) {
|
|
319
|
+
skipped.push('tsconfig.json (JSON with comments, add "shared" + paths by hand)');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const giPath = path.join(root, '.gitignore');
|
|
323
|
+
const giRaw = readFile(giPath);
|
|
324
|
+
if (giRaw === null) {
|
|
325
|
+
writeFile(giPath, `${RPC_GITIGNORE_LINE}\n`);
|
|
326
|
+
changed.push('.gitignore');
|
|
327
|
+
} else if (!RPC_GITIGNORE_RE.test(giRaw)) {
|
|
328
|
+
const sep = giRaw.length === 0 || giRaw.endsWith('\n') ? '' : '\n';
|
|
329
|
+
writeFile(giPath, `${giRaw}${sep}${RPC_GITIGNORE_LINE}\n`);
|
|
330
|
+
changed.push('.gitignore');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { changed, skipped };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const PRETTIER_PLUGIN = 'toiljs/prettier-plugin';
|
|
337
|
+
const PRETTIER_MENTION = /toiljs\/prettier(-plugin)?/;
|
|
338
|
+
const PRETTIER_CONFIG_FILES = [
|
|
339
|
+
'.prettierrc',
|
|
340
|
+
'.prettierrc.json',
|
|
341
|
+
'.prettierrc.json5',
|
|
342
|
+
'.prettierrc.yaml',
|
|
343
|
+
'.prettierrc.yml',
|
|
344
|
+
'.prettierrc.js',
|
|
345
|
+
'.prettierrc.cjs',
|
|
346
|
+
'.prettierrc.mjs',
|
|
347
|
+
'.prettierrc.ts',
|
|
348
|
+
'prettier.config.js',
|
|
349
|
+
'prettier.config.cjs',
|
|
350
|
+
'prettier.config.mjs',
|
|
351
|
+
'prettier.config.ts',
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
/** Whether any prettier config (file or package.json field) pulls in the toilscript plugin. */
|
|
355
|
+
function prettierPluginPresent(root: string, pkg: Record<string, unknown> | null): boolean {
|
|
356
|
+
if (pkg && pkg.prettier !== undefined && PRETTIER_MENTION.test(JSON.stringify(pkg.prettier))) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
for (const name of PRETTIER_CONFIG_FILES) {
|
|
360
|
+
const raw = readFile(path.join(root, name));
|
|
361
|
+
if (raw !== null && PRETTIER_MENTION.test(raw)) return true;
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Adds `toiljs/prettier-plugin` to the project's prettier config so prettier can format the
|
|
368
|
+
* toilscript server. Handles the common cases (package.json `prettier`, a JSON `.prettierrc`,
|
|
369
|
+
* or no config at all); warns for shapes it can't safely edit (a JS config, or a string preset).
|
|
370
|
+
*/
|
|
371
|
+
function applyPrettierFix(root: string, pkg: Record<string, unknown> | null): RpcFixResult {
|
|
372
|
+
const changed: string[] = [];
|
|
373
|
+
const skipped: string[] = [];
|
|
374
|
+
if (prettierPluginPresent(root, pkg)) return { changed, skipped };
|
|
375
|
+
|
|
376
|
+
// package.json "prettier" object.
|
|
377
|
+
const pkgPath = path.join(root, 'package.json');
|
|
378
|
+
const pkgConfig = pkg ? asRecord(pkg.prettier) : null;
|
|
379
|
+
if (pkgConfig !== null) {
|
|
380
|
+
const full = readJsonObject(pkgPath);
|
|
381
|
+
const target = full ? asRecord(full.prettier) : null;
|
|
382
|
+
if (full && target) {
|
|
383
|
+
target.plugins = [
|
|
384
|
+
...(Array.isArray(target.plugins) ? target.plugins : []),
|
|
385
|
+
PRETTIER_PLUGIN,
|
|
386
|
+
];
|
|
387
|
+
writeFile(pkgPath, JSON.stringify(full, null, 4) + '\n');
|
|
388
|
+
changed.push('package.json');
|
|
389
|
+
return { changed, skipped };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// A JSON .prettierrc / .prettierrc.json object.
|
|
394
|
+
for (const name of ['.prettierrc', '.prettierrc.json']) {
|
|
395
|
+
const filePath = path.join(root, name);
|
|
396
|
+
const raw = readFile(filePath);
|
|
397
|
+
if (raw === null) continue;
|
|
398
|
+
const obj = readJsonObject(filePath);
|
|
399
|
+
if (obj === null) {
|
|
400
|
+
skipped.push(`${name} (add "${PRETTIER_PLUGIN}" to plugins by hand)`);
|
|
401
|
+
return { changed, skipped };
|
|
402
|
+
}
|
|
403
|
+
obj.plugins = [...(Array.isArray(obj.plugins) ? obj.plugins : []), PRETTIER_PLUGIN];
|
|
404
|
+
writeFile(filePath, JSON.stringify(obj, null, 4) + '\n');
|
|
405
|
+
changed.push(name);
|
|
406
|
+
return { changed, skipped };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// A JS/TS config we can't safely edit.
|
|
410
|
+
const jsConfig = PRETTIER_CONFIG_FILES.find((name) => readFile(path.join(root, name)) !== null);
|
|
411
|
+
if (jsConfig) {
|
|
412
|
+
skipped.push(`${jsConfig} (add "${PRETTIER_PLUGIN}" to its plugins by hand)`);
|
|
413
|
+
return { changed, skipped };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// No config at all: create one.
|
|
417
|
+
writeFile(
|
|
418
|
+
path.join(root, '.prettierrc.json'),
|
|
419
|
+
JSON.stringify({ plugins: [PRETTIER_PLUGIN] }, null, 4) + '\n',
|
|
420
|
+
);
|
|
421
|
+
changed.push('.prettierrc.json');
|
|
422
|
+
return { changed, skipped };
|
|
423
|
+
}
|
|
424
|
+
|
|
86
425
|
/** Reads the framework's own package.json (engines + peerDependencies) for the requirements. */
|
|
87
426
|
function frameworkMeta(): { node: string; peers: Record<string, string> } {
|
|
88
427
|
const pkgPath = path.resolve(
|
|
@@ -216,12 +555,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
216
555
|
? toilconfig.entries.filter((e): e is string => typeof e === 'string')
|
|
217
556
|
: [];
|
|
218
557
|
missingEntries = entries.filter((e) => !fs.existsSync(path.join(root, e)));
|
|
219
|
-
|
|
220
|
-
createRequire(path.join(root, 'package.json')).resolve('toilscript');
|
|
221
|
-
toilscriptInstalled = true;
|
|
222
|
-
} catch {
|
|
223
|
-
toilscriptInstalled = false;
|
|
224
|
-
}
|
|
558
|
+
toilscriptInstalled = isPackageInstalled(root, 'toilscript');
|
|
225
559
|
const targets =
|
|
226
560
|
typeof toilconfig.targets === 'object' && toilconfig.targets !== null
|
|
227
561
|
? (toilconfig.targets as Record<string, unknown>)
|
|
@@ -248,6 +582,23 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
248
582
|
const peerName = (n: string): Check => checkPeer(n, deps[n] ?? null, meta.peers[n] ?? '*');
|
|
249
583
|
const peerChecks = Object.keys(meta.peers).map(peerName);
|
|
250
584
|
|
|
585
|
+
// Server tooling (RPC wiring + the prettier plugin): optionally fix in place, then re-read.
|
|
586
|
+
const rpcFix = serverPresent && opts.fix ? applyRpcFix(root) : null;
|
|
587
|
+
const prettierFix = serverPresent && opts.fix ? applyPrettierFix(root, projectPkg) : null;
|
|
588
|
+
const rpcFacts = gatherRpcFacts(root);
|
|
589
|
+
const restFacts = gatherRestFacts(root, toilconfig);
|
|
590
|
+
const prettierPresent = prettierPluginPresent(
|
|
591
|
+
root,
|
|
592
|
+
readJsonObject(path.join(root, 'package.json')),
|
|
593
|
+
);
|
|
594
|
+
const serverFix =
|
|
595
|
+
rpcFix || prettierFix
|
|
596
|
+
? {
|
|
597
|
+
changed: [...(rpcFix?.changed ?? []), ...(prettierFix?.changed ?? [])],
|
|
598
|
+
skipped: [...(rpcFix?.skipped ?? []), ...(prettierFix?.skipped ?? [])],
|
|
599
|
+
}
|
|
600
|
+
: null;
|
|
601
|
+
|
|
251
602
|
const groups: CheckGroup[] = [
|
|
252
603
|
{
|
|
253
604
|
title: 'Environment',
|
|
@@ -302,6 +653,9 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
302
653
|
checkServerEntry(missingEntries),
|
|
303
654
|
checkToilscriptInstalled(toilscriptInstalled),
|
|
304
655
|
checkWasmBuilt(wasmExists),
|
|
656
|
+
checkRpcWiring(rpcFacts),
|
|
657
|
+
checkRestDispatch(restFacts),
|
|
658
|
+
checkPrettierPlugin(prettierPresent),
|
|
305
659
|
]
|
|
306
660
|
: [checkToilconfig(false)],
|
|
307
661
|
},
|
|
@@ -309,10 +663,33 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
309
663
|
|
|
310
664
|
const summary = summarize(groups);
|
|
311
665
|
if (opts.json) {
|
|
312
|
-
process.stdout.write(JSON.stringify({ groups, summary }, null, 2) + '\n');
|
|
666
|
+
process.stdout.write(JSON.stringify({ groups, summary, fixed: serverFix }, null, 2) + '\n');
|
|
313
667
|
} else {
|
|
314
668
|
process.stdout.write('\n' + accent(' Doctor') + dim(` ${root}`) + '\n\n');
|
|
315
669
|
renderHuman(groups);
|
|
670
|
+
if (serverFix) renderRpcFix(serverFix);
|
|
671
|
+
else if (opts.fix && !serverPresent) {
|
|
672
|
+
process.stdout.write(
|
|
673
|
+
' ' + dim('--fix: no server (toilconfig.json) found, nothing to wire.') + '\n\n',
|
|
674
|
+
);
|
|
675
|
+
}
|
|
316
676
|
}
|
|
317
677
|
if (hasFailures(summary)) process.exitCode = 1;
|
|
318
678
|
}
|
|
679
|
+
|
|
680
|
+
/** Prints the result of `--fix`, and whether a reinstall is needed (toilscript bump). */
|
|
681
|
+
function renderRpcFix(result: RpcFixResult): void {
|
|
682
|
+
const out: string[] = [];
|
|
683
|
+
if (result.changed.length > 0) {
|
|
684
|
+
out.push(' ' + success('fixed RPC wiring') + dim(` ${result.changed.join(', ')}`));
|
|
685
|
+
if (result.changed.includes('package.json')) {
|
|
686
|
+
out.push(
|
|
687
|
+
' ' + dim('run your installer (npm/pnpm/yarn) if the toilscript version changed.'),
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
out.push(' ' + dim('RPC wiring already in place, nothing to fix.'));
|
|
692
|
+
}
|
|
693
|
+
for (const item of result.skipped) out.push(' ' + warn('skipped') + dim(` ${item}`));
|
|
694
|
+
process.stdout.write(out.join('\n') + '\n\n');
|
|
695
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { accent, banner, bold, danger, dim, success, version } from './ui.js';
|
|
|
16
16
|
interface Flags {
|
|
17
17
|
root?: string;
|
|
18
18
|
port?: number;
|
|
19
|
+
host?: string;
|
|
19
20
|
name?: string;
|
|
20
21
|
template?: Template;
|
|
21
22
|
preprocessor?: Preprocessor;
|
|
@@ -27,6 +28,7 @@ interface Flags {
|
|
|
27
28
|
pm?: string;
|
|
28
29
|
yes?: boolean;
|
|
29
30
|
json?: boolean;
|
|
31
|
+
fix?: boolean;
|
|
30
32
|
target?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -43,6 +45,9 @@ function parseArgs(argv: string[]): Flags {
|
|
|
43
45
|
if (!Number.isNaN(port)) flags.port = port;
|
|
44
46
|
break;
|
|
45
47
|
}
|
|
48
|
+
case '--host':
|
|
49
|
+
flags.host = argv[++i];
|
|
50
|
+
break;
|
|
46
51
|
case '--template':
|
|
47
52
|
case '-t': {
|
|
48
53
|
const t = argv[++i];
|
|
@@ -95,6 +100,9 @@ function parseArgs(argv: string[]): Flags {
|
|
|
95
100
|
case '--json':
|
|
96
101
|
flags.json = true;
|
|
97
102
|
break;
|
|
103
|
+
case '--fix':
|
|
104
|
+
flags.fix = true;
|
|
105
|
+
break;
|
|
98
106
|
case '--target':
|
|
99
107
|
flags.target = argv[++i];
|
|
100
108
|
break;
|
|
@@ -130,6 +138,7 @@ function printHelp(): void {
|
|
|
130
138
|
cmd('-y, --yes', 'create: accept defaults (non-interactive)'),
|
|
131
139
|
cmd('--no-install', "create: don't install dependencies"),
|
|
132
140
|
cmd('--json', 'doctor: machine-readable output'),
|
|
141
|
+
cmd('--fix', 'doctor: auto-fix what it can (typed-RPC wiring)'),
|
|
133
142
|
cmd('--target <t>', 'update: latest | minor | patch | newest | greatest'),
|
|
134
143
|
cmd('-v, --version', 'print the toiljs version'),
|
|
135
144
|
'',
|
|
@@ -193,7 +202,7 @@ async function main(): Promise<void> {
|
|
|
193
202
|
case 'start': {
|
|
194
203
|
banner();
|
|
195
204
|
process.stdout.write(dim(' self-hosting the built app…') + '\n\n');
|
|
196
|
-
const server = await start({ root: flags.root, port: flags.port });
|
|
205
|
+
const server = await start({ root: flags.root, port: flags.port, host: flags.host });
|
|
197
206
|
process.stdout.write(
|
|
198
207
|
accent(' ➜ ') +
|
|
199
208
|
bold(`http://localhost:${String(server.port)}`) +
|
|
@@ -206,7 +215,7 @@ async function main(): Promise<void> {
|
|
|
206
215
|
case 'doctor':
|
|
207
216
|
// Skip the banner for --json so stdout stays valid JSON.
|
|
208
217
|
if (!flags.json) banner();
|
|
209
|
-
await runDoctor({ root: flags.root, cwd: process.cwd(), json: flags.json });
|
|
218
|
+
await runDoctor({ root: flags.root, cwd: process.cwd(), json: flags.json, fix: flags.fix });
|
|
210
219
|
break;
|
|
211
220
|
|
|
212
221
|
case 'update':
|
package/src/cli/proc.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Spawns `cmd args` in `cwd`, resolving on a 0 exit code and rejecting otherwise. On Windows the
|
|
5
|
-
* `npm`/`pnpm`/`yarn` shims are `.cmd` files that need a shell; passing an args array with
|
|
6
|
-
* `shell: true` is deprecated (DEP0190), so the whole command is passed as one string there
|
|
7
|
-
* (args are fixed/allowlisted, never raw user input). POSIX spawns directly.
|
|
8
|
-
*/
|
|
9
|
-
export function run(cmd: string, args: string[], cwd: string): Promise<void> {
|
|
10
|
-
return new Promise((resolve, reject) => {
|
|
11
|
-
const onWindows = process.platform === 'win32';
|
|
12
|
-
const child = onWindows
|
|
13
|
-
? spawn([cmd, ...args].join(' '), { cwd, stdio: 'ignore', shell: true })
|
|
14
|
-
: spawn(cmd, args, { cwd, stdio: 'ignore' });
|
|
15
|
-
child.on('error', reject);
|
|
16
|
-
child.on('close', (code) =>
|
|
17
|
-
code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${String(code)}`)),
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Like {@link run}, but captures stdout/stderr and resolves with them plus the exit code (it never
|
|
24
|
-
* rejects on a non-zero exit, the caller decides). Used to read JSON from tools like
|
|
25
|
-
* `npm-check-updates`. Same Windows shell handling as {@link run}; args are fixed/allowlisted.
|
|
26
|
-
*/
|
|
27
|
-
export function capture(
|
|
28
|
-
cmd: string,
|
|
29
|
-
args: string[],
|
|
30
|
-
cwd: string,
|
|
31
|
-
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
const onWindows = process.platform === 'win32';
|
|
34
|
-
const child = onWindows
|
|
35
|
-
? spawn([cmd, ...args].join(' '), { cwd, shell: true })
|
|
36
|
-
: spawn(cmd, args, { cwd });
|
|
37
|
-
let stdout = '';
|
|
38
|
-
let stderr = '';
|
|
39
|
-
child.stdout?.on('data', (d: Buffer) => {
|
|
40
|
-
stdout += d.toString();
|
|
41
|
-
});
|
|
42
|
-
child.stderr?.on('data', (d: Buffer) => {
|
|
43
|
-
stderr += d.toString();
|
|
44
|
-
});
|
|
45
|
-
child.on('error', reject);
|
|
46
|
-
child.on('close', (code) => {
|
|
47
|
-
resolve({ stdout, stderr, code: code ?? 1 });
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spawns `cmd args` in `cwd`, resolving on a 0 exit code and rejecting otherwise. On Windows the
|
|
5
|
+
* `npm`/`pnpm`/`yarn` shims are `.cmd` files that need a shell; passing an args array with
|
|
6
|
+
* `shell: true` is deprecated (DEP0190), so the whole command is passed as one string there
|
|
7
|
+
* (args are fixed/allowlisted, never raw user input). POSIX spawns directly.
|
|
8
|
+
*/
|
|
9
|
+
export function run(cmd: string, args: string[], cwd: string): Promise<void> {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const onWindows = process.platform === 'win32';
|
|
12
|
+
const child = onWindows
|
|
13
|
+
? spawn([cmd, ...args].join(' '), { cwd, stdio: 'ignore', shell: true })
|
|
14
|
+
: spawn(cmd, args, { cwd, stdio: 'ignore' });
|
|
15
|
+
child.on('error', reject);
|
|
16
|
+
child.on('close', (code) =>
|
|
17
|
+
code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${String(code)}`)),
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Like {@link run}, but captures stdout/stderr and resolves with them plus the exit code (it never
|
|
24
|
+
* rejects on a non-zero exit, the caller decides). Used to read JSON from tools like
|
|
25
|
+
* `npm-check-updates`. Same Windows shell handling as {@link run}; args are fixed/allowlisted.
|
|
26
|
+
*/
|
|
27
|
+
export function capture(
|
|
28
|
+
cmd: string,
|
|
29
|
+
args: string[],
|
|
30
|
+
cwd: string,
|
|
31
|
+
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const onWindows = process.platform === 'win32';
|
|
34
|
+
const child = onWindows
|
|
35
|
+
? spawn([cmd, ...args].join(' '), { cwd, shell: true })
|
|
36
|
+
: spawn(cmd, args, { cwd });
|
|
37
|
+
let stdout = '';
|
|
38
|
+
let stderr = '';
|
|
39
|
+
child.stdout?.on('data', (d: Buffer) => {
|
|
40
|
+
stdout += d.toString();
|
|
41
|
+
});
|
|
42
|
+
child.stderr?.on('data', (d: Buffer) => {
|
|
43
|
+
stderr += d.toString();
|
|
44
|
+
});
|
|
45
|
+
child.on('error', reject);
|
|
46
|
+
child.on('close', (code) => {
|
|
47
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|