@hydranium/cli 1.0.0-next.10
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/LICENSE +21 -0
- package/README.md +240 -0
- package/heap-analysis/analyze-heap.mjs +798 -0
- package/heap-analysis/heap/core/aggregate.mjs +152 -0
- package/heap-analysis/heap/core/bucket-label.mjs +23 -0
- package/heap-analysis/heap/core/cutpoints.mjs +51 -0
- package/heap-analysis/heap/core/diff.mjs +56 -0
- package/heap-analysis/heap/core/format.mjs +47 -0
- package/heap-analysis/heap/core/holders.mjs +80 -0
- package/heap-analysis/heap/core/load.mjs +46 -0
- package/heap-analysis/heap/core/report.mjs +68 -0
- package/heap-analysis/heap/core/strings.mjs +80 -0
- package/heap-analysis/heap/langium/concepts.mjs +230 -0
- package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
- package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
- package/heap-analysis/heap/renderer/renderer.mjs +122 -0
- package/lib/cli-version.d.ts +21 -0
- package/lib/cli-version.d.ts.map +1 -0
- package/lib/cli-version.js +30 -0
- package/lib/cli-version.js.map +1 -0
- package/lib/cli.d.ts +11 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +135 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/analyze-heap-args.d.ts +46 -0
- package/lib/commands/analyze-heap-args.d.ts.map +1 -0
- package/lib/commands/analyze-heap-args.js +174 -0
- package/lib/commands/analyze-heap-args.js.map +1 -0
- package/lib/commands/analyze-heap.d.ts +37 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -0
- package/lib/commands/analyze-heap.js +63 -0
- package/lib/commands/analyze-heap.js.map +1 -0
- package/lib/commands/ast-ground-truth-args.d.ts +23 -0
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-args.js +47 -0
- package/lib/commands/ast-ground-truth-args.js.map +1 -0
- package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
- package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-driver.js +72 -0
- package/lib/commands/ast-ground-truth-driver.js.map +1 -0
- package/lib/commands/ast-ground-truth.d.ts +31 -0
- package/lib/commands/ast-ground-truth.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth.js +36 -0
- package/lib/commands/ast-ground-truth.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts +38 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-args.js +183 -0
- package/lib/commands/generate-transfer-model-args.js.map +1 -0
- package/lib/commands/generate-transfer-model-config.d.ts +32 -0
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-config.js +122 -0
- package/lib/commands/generate-transfer-model-config.js.map +1 -0
- package/lib/commands/generate-transfer-model.d.ts +110 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model.js +587 -0
- package/lib/commands/generate-transfer-model.js.map +1 -0
- package/lib/commands/harness-args.d.ts +198 -0
- package/lib/commands/harness-args.d.ts.map +1 -0
- package/lib/commands/harness-args.js +339 -0
- package/lib/commands/harness-args.js.map +1 -0
- package/lib/commands/headless-harness.d.ts +97 -0
- package/lib/commands/headless-harness.d.ts.map +1 -0
- package/lib/commands/headless-harness.js +121 -0
- package/lib/commands/headless-harness.js.map +1 -0
- package/lib/commands/init-args.d.ts +81 -0
- package/lib/commands/init-args.d.ts.map +1 -0
- package/lib/commands/init-args.js +250 -0
- package/lib/commands/init-args.js.map +1 -0
- package/lib/commands/init-prompt.d.ts +66 -0
- package/lib/commands/init-prompt.d.ts.map +1 -0
- package/lib/commands/init-prompt.js +71 -0
- package/lib/commands/init-prompt.js.map +1 -0
- package/lib/commands/init-templates.d.ts +42 -0
- package/lib/commands/init-templates.d.ts.map +1 -0
- package/lib/commands/init-templates.js +1902 -0
- package/lib/commands/init-templates.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +65 -0
- package/lib/commands/init-wizard.d.ts.map +1 -0
- package/lib/commands/init-wizard.js +302 -0
- package/lib/commands/init-wizard.js.map +1 -0
- package/lib/commands/init-workspace.d.ts +168 -0
- package/lib/commands/init-workspace.d.ts.map +1 -0
- package/lib/commands/init-workspace.js +372 -0
- package/lib/commands/init-workspace.js.map +1 -0
- package/lib/commands/init.d.ts +273 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +303 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +23 -0
- package/lib/commands/lint-grammar-args.d.ts.map +1 -0
- package/lib/commands/lint-grammar-args.js +63 -0
- package/lib/commands/lint-grammar-args.js.map +1 -0
- package/lib/commands/lint-grammar-driver.d.ts +10 -0
- package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
- package/lib/commands/lint-grammar-driver.js +72 -0
- package/lib/commands/lint-grammar-driver.js.map +1 -0
- package/lib/commands/lint-grammar-report.d.ts +24 -0
- package/lib/commands/lint-grammar-report.d.ts.map +1 -0
- package/lib/commands/lint-grammar-report.js +65 -0
- package/lib/commands/lint-grammar-report.js.map +1 -0
- package/lib/commands/lint-grammar.d.ts +33 -0
- package/lib/commands/lint-grammar.d.ts.map +1 -0
- package/lib/commands/lint-grammar.js +42 -0
- package/lib/commands/lint-grammar.js.map +1 -0
- package/lib/commands/measure-memory-args.d.ts +23 -0
- package/lib/commands/measure-memory-args.d.ts.map +1 -0
- package/lib/commands/measure-memory-args.js +106 -0
- package/lib/commands/measure-memory-args.js.map +1 -0
- package/lib/commands/measure-memory-driver.d.ts +10 -0
- package/lib/commands/measure-memory-driver.d.ts.map +1 -0
- package/lib/commands/measure-memory-driver.js +110 -0
- package/lib/commands/measure-memory-driver.js.map +1 -0
- package/lib/commands/measure-memory.d.ts +68 -0
- package/lib/commands/measure-memory.d.ts.map +1 -0
- package/lib/commands/measure-memory.js +100 -0
- package/lib/commands/measure-memory.js.map +1 -0
- package/lib/commands/model-docs-args.d.ts +23 -0
- package/lib/commands/model-docs-args.d.ts.map +1 -0
- package/lib/commands/model-docs-args.js +53 -0
- package/lib/commands/model-docs-args.js.map +1 -0
- package/lib/commands/model-docs-driver.d.ts +10 -0
- package/lib/commands/model-docs-driver.d.ts.map +1 -0
- package/lib/commands/model-docs-driver.js +61 -0
- package/lib/commands/model-docs-driver.js.map +1 -0
- package/lib/commands/model-docs-report.d.ts +18 -0
- package/lib/commands/model-docs-report.d.ts.map +1 -0
- package/lib/commands/model-docs-report.js +123 -0
- package/lib/commands/model-docs-report.js.map +1 -0
- package/lib/commands/model-docs.d.ts +30 -0
- package/lib/commands/model-docs.d.ts.map +1 -0
- package/lib/commands/model-docs.js +37 -0
- package/lib/commands/model-docs.js.map +1 -0
- package/lib/commands/projects-args.d.ts +25 -0
- package/lib/commands/projects-args.d.ts.map +1 -0
- package/lib/commands/projects-args.js +49 -0
- package/lib/commands/projects-args.js.map +1 -0
- package/lib/commands/projects.d.ts +45 -0
- package/lib/commands/projects.d.ts.map +1 -0
- package/lib/commands/projects.js +39 -0
- package/lib/commands/projects.js.map +1 -0
- package/lib/commands/query-args.d.ts +25 -0
- package/lib/commands/query-args.d.ts.map +1 -0
- package/lib/commands/query-args.js +46 -0
- package/lib/commands/query-args.js.map +1 -0
- package/lib/commands/query.d.ts +32 -0
- package/lib/commands/query.d.ts.map +1 -0
- package/lib/commands/query.js +29 -0
- package/lib/commands/query.js.map +1 -0
- package/lib/commands/reflect-args.d.ts +23 -0
- package/lib/commands/reflect-args.d.ts.map +1 -0
- package/lib/commands/reflect-args.js +59 -0
- package/lib/commands/reflect-args.js.map +1 -0
- package/lib/commands/reflect-driver.d.ts +10 -0
- package/lib/commands/reflect-driver.d.ts.map +1 -0
- package/lib/commands/reflect-driver.js +66 -0
- package/lib/commands/reflect-driver.js.map +1 -0
- package/lib/commands/reflect-report.d.ts +20 -0
- package/lib/commands/reflect-report.d.ts.map +1 -0
- package/lib/commands/reflect-report.js +127 -0
- package/lib/commands/reflect-report.js.map +1 -0
- package/lib/commands/reflect.d.ts +30 -0
- package/lib/commands/reflect.d.ts.map +1 -0
- package/lib/commands/reflect.js +38 -0
- package/lib/commands/reflect.js.map +1 -0
- package/lib/commands/save-args.d.ts +26 -0
- package/lib/commands/save-args.d.ts.map +1 -0
- package/lib/commands/save-args.js +59 -0
- package/lib/commands/save-args.js.map +1 -0
- package/lib/commands/save.d.ts +38 -0
- package/lib/commands/save.d.ts.map +1 -0
- package/lib/commands/save.js +48 -0
- package/lib/commands/save.js.map +1 -0
- package/lib/commands/validate-args.d.ts +23 -0
- package/lib/commands/validate-args.d.ts.map +1 -0
- package/lib/commands/validate-args.js +60 -0
- package/lib/commands/validate-args.js.map +1 -0
- package/lib/commands/validate-driver.d.ts +10 -0
- package/lib/commands/validate-driver.d.ts.map +1 -0
- package/lib/commands/validate-driver.js +72 -0
- package/lib/commands/validate-driver.js.map +1 -0
- package/lib/commands/validate-report.d.ts +30 -0
- package/lib/commands/validate-report.d.ts.map +1 -0
- package/lib/commands/validate-report.js +87 -0
- package/lib/commands/validate-report.js.map +1 -0
- package/lib/commands/validate.d.ts +34 -0
- package/lib/commands/validate.d.ts.map +1 -0
- package/lib/commands/validate.js +41 -0
- package/lib/commands/validate.js.map +1 -0
- package/lib/commands/watch-args.d.ts +26 -0
- package/lib/commands/watch-args.d.ts.map +1 -0
- package/lib/commands/watch-args.js +59 -0
- package/lib/commands/watch-args.js.map +1 -0
- package/lib/commands/watch.d.ts +75 -0
- package/lib/commands/watch.d.ts.map +1 -0
- package/lib/commands/watch.js +112 -0
- package/lib/commands/watch.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/log-level.d.ts +39 -0
- package/lib/log-level.d.ts.map +1 -0
- package/lib/log-level.js +47 -0
- package/lib/log-level.js.map +1 -0
- package/lib/package-name.d.ts +33 -0
- package/lib/package-name.d.ts.map +1 -0
- package/lib/package-name.js +37 -0
- package/lib/package-name.js.map +1 -0
- package/lib/spawn-data-server.d.ts +102 -0
- package/lib/spawn-data-server.d.ts.map +1 -0
- package/lib/spawn-data-server.js +154 -0
- package/lib/spawn-data-server.js.map +1 -0
- package/lib/testing/echo-server.d.ts +11 -0
- package/lib/testing/echo-server.d.ts.map +1 -0
- package/lib/testing/echo-server.js +79 -0
- package/lib/testing/echo-server.js.map +1 -0
- package/package.json +105 -0
- package/src/cli-version.ts +32 -0
- package/src/cli.ts +139 -0
- package/src/commands/analyze-heap-args.ts +184 -0
- package/src/commands/analyze-heap.ts +76 -0
- package/src/commands/ast-ground-truth-args.ts +63 -0
- package/src/commands/ast-ground-truth-driver.ts +81 -0
- package/src/commands/ast-ground-truth.ts +49 -0
- package/src/commands/generate-transfer-model-args.ts +203 -0
- package/src/commands/generate-transfer-model-config.ts +128 -0
- package/src/commands/generate-transfer-model.ts +716 -0
- package/src/commands/harness-args.ts +429 -0
- package/src/commands/headless-harness.ts +165 -0
- package/src/commands/init-args.ts +273 -0
- package/src/commands/init-prompt.ts +149 -0
- package/src/commands/init-templates.ts +2037 -0
- package/src/commands/init-wizard.ts +378 -0
- package/src/commands/init-workspace.ts +442 -0
- package/src/commands/init.ts +554 -0
- package/src/commands/lint-grammar-args.ts +79 -0
- package/src/commands/lint-grammar-driver.ts +83 -0
- package/src/commands/lint-grammar-report.ts +74 -0
- package/src/commands/lint-grammar.ts +57 -0
- package/src/commands/measure-memory-args.ts +123 -0
- package/src/commands/measure-memory-driver.ts +128 -0
- package/src/commands/measure-memory.ts +143 -0
- package/src/commands/model-docs-args.ts +69 -0
- package/src/commands/model-docs-driver.ts +68 -0
- package/src/commands/model-docs-report.ts +133 -0
- package/src/commands/model-docs.ts +48 -0
- package/src/commands/projects-args.ts +62 -0
- package/src/commands/projects.ts +76 -0
- package/src/commands/query-args.ts +61 -0
- package/src/commands/query.ts +58 -0
- package/src/commands/reflect-args.ts +75 -0
- package/src/commands/reflect-driver.ts +76 -0
- package/src/commands/reflect-report.ts +138 -0
- package/src/commands/reflect.ts +51 -0
- package/src/commands/save-args.ts +74 -0
- package/src/commands/save.ts +84 -0
- package/src/commands/validate-args.ts +76 -0
- package/src/commands/validate-driver.ts +84 -0
- package/src/commands/validate-report.ts +95 -0
- package/src/commands/validate.ts +58 -0
- package/src/commands/watch-args.ts +74 -0
- package/src/commands/watch.ts +185 -0
- package/src/index.ts +16 -0
- package/src/log-level.ts +49 -0
- package/src/package-name.ts +39 -0
- package/src/spawn-data-server.ts +247 -0
- package/src/testing/echo-server.ts +93 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Workspace detection for `init --monorepo`.
|
|
12
|
+
*
|
|
13
|
+
* Split from `init.ts` because the rules here are the only part of `init` that
|
|
14
|
+
* reads files it does not write, and because the reliable/unreliable line runs
|
|
15
|
+
* straight through them: the root and the base tsconfig are *detected*, the
|
|
16
|
+
* package scope is *guessed from siblings* and the `private` flag is not
|
|
17
|
+
* derivable at all. Keeping them in one module makes that distinction reviewable
|
|
18
|
+
* rather than scattered across the scaffolder.
|
|
19
|
+
*
|
|
20
|
+
* Everything is expressed over {@link WorkspaceProbe} rather than `node:fs`, so
|
|
21
|
+
* the rules are testable against an in-memory tree instead of a temp directory.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
|
|
27
|
+
/** A JSON value as `JSON.parse` produces it. */
|
|
28
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
29
|
+
|
|
30
|
+
/** True for a JSON object — the shape `package.json` and `tsconfig.json` parse to. */
|
|
31
|
+
function isJsonObject(value: JsonValue | undefined): value is { [key: string]: JsonValue } {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The filesystem reads detection needs, injectable so the rules are unit-testable. */
|
|
36
|
+
export interface WorkspaceProbe {
|
|
37
|
+
/** Parse a JSON file, or `undefined` when it is missing or unparseable. */
|
|
38
|
+
readonly readJson: (absolutePath: string) => JsonValue | undefined;
|
|
39
|
+
/** Names of the subdirectories of `absolutePath`; empty when it is not a directory. */
|
|
40
|
+
readonly listDirectories: (absolutePath: string) => readonly string[];
|
|
41
|
+
/** Names of the files directly in `absolutePath`; empty when it is not a directory. */
|
|
42
|
+
readonly listFiles: (absolutePath: string) => readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Raw contents of a file, or `undefined` when it is missing or unreadable.
|
|
45
|
+
*
|
|
46
|
+
* Separate from {@link readJson} because the two configs read through it —
|
|
47
|
+
* prettier's and eslint's — are routinely JavaScript modules, which no JSON
|
|
48
|
+
* parser can reach. Required rather than optional so a test double cannot
|
|
49
|
+
* omit it and silently degrade every detection built on it to "not found".
|
|
50
|
+
*/
|
|
51
|
+
readonly readText: (absolutePath: string) => string | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Strip the two things `tsc` accepts in a config that `JSON.parse` rejects:
|
|
56
|
+
* comments, and a trailing comma before a closing brace or bracket.
|
|
57
|
+
*
|
|
58
|
+
* Written as a scanner rather than a regex because both constructs are legal
|
|
59
|
+
* INSIDE a string — `"https://example.com"` holds a `//`, and a description can
|
|
60
|
+
* end in a comma — so a pattern that ignores string state corrupts the very
|
|
61
|
+
* configs it is meant to rescue. Without this, a base config with a single
|
|
62
|
+
* comment reads as "no compilerOptions here" and the scaffold silently emits no
|
|
63
|
+
* `extends` at all, which is the worst failure shape available: quietly wrong
|
|
64
|
+
* rather than loudly broken.
|
|
65
|
+
*/
|
|
66
|
+
export function stripJsonComments(text: string): string {
|
|
67
|
+
const characters = [...text];
|
|
68
|
+
const output: string[] = [];
|
|
69
|
+
const commaIndices: number[] = [];
|
|
70
|
+
let inString = false;
|
|
71
|
+
let escaped = false;
|
|
72
|
+
for (let index = 0; index < characters.length; index += 1) {
|
|
73
|
+
const character = characters[index];
|
|
74
|
+
const next = characters[index + 1];
|
|
75
|
+
if (inString) {
|
|
76
|
+
output.push(character);
|
|
77
|
+
if (escaped) {
|
|
78
|
+
escaped = false;
|
|
79
|
+
} else if (character === '\\') {
|
|
80
|
+
escaped = true;
|
|
81
|
+
} else if (character === '"') {
|
|
82
|
+
inString = false;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (character === '"') {
|
|
87
|
+
inString = true;
|
|
88
|
+
output.push(character);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (character === '/' && next === '/') {
|
|
92
|
+
while (index < characters.length && characters[index] !== '\n') {
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
output.push('\n');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (character === '/' && next === '*') {
|
|
99
|
+
index += 2;
|
|
100
|
+
while (index < characters.length && !(characters[index] === '*' && characters[index + 1] === '/')) {
|
|
101
|
+
index += 1;
|
|
102
|
+
}
|
|
103
|
+
index += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (character === ',') {
|
|
107
|
+
commaIndices.push(output.length);
|
|
108
|
+
}
|
|
109
|
+
output.push(character);
|
|
110
|
+
}
|
|
111
|
+
// A comma is trailing only if the next thing that is not whitespace closes
|
|
112
|
+
// the object or array. Resolved after the scan, so the lookahead sees text
|
|
113
|
+
// with the comments already gone.
|
|
114
|
+
for (const commaIndex of commaIndices) {
|
|
115
|
+
let lookahead = commaIndex + 1;
|
|
116
|
+
while (lookahead < output.length && /\s/.test(output[lookahead])) {
|
|
117
|
+
lookahead += 1;
|
|
118
|
+
}
|
|
119
|
+
if (output[lookahead] === '}' || output[lookahead] === ']') {
|
|
120
|
+
output[commaIndex] = '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return output.join('');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The real-filesystem probe.
|
|
128
|
+
*
|
|
129
|
+
* Every read is best-effort: detection runs over directories the user may not
|
|
130
|
+
* own, so an unreadable or malformed file degrades to "not found" rather than
|
|
131
|
+
* aborting a scaffold.
|
|
132
|
+
*/
|
|
133
|
+
export function createNodeWorkspaceProbe(): WorkspaceProbe {
|
|
134
|
+
const listEntries = (absolutePath: string, wantDirectory: boolean): string[] => {
|
|
135
|
+
try {
|
|
136
|
+
return fs
|
|
137
|
+
.readdirSync(absolutePath, { withFileTypes: true })
|
|
138
|
+
.filter(entry => entry.isDirectory() === wantDirectory)
|
|
139
|
+
.map(entry => entry.name);
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
readJson: absolutePath => {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(stripJsonComments(fs.readFileSync(absolutePath, 'utf-8'))) as JsonValue;
|
|
148
|
+
} catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
listDirectories: absolutePath => listEntries(absolutePath, true),
|
|
153
|
+
listFiles: absolutePath => listEntries(absolutePath, false),
|
|
154
|
+
readText: absolutePath => {
|
|
155
|
+
try {
|
|
156
|
+
return fs.readFileSync(absolutePath, 'utf-8');
|
|
157
|
+
} catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What {@link detectWorkspace} found around a target directory. */
|
|
165
|
+
export interface WorkspaceDetection {
|
|
166
|
+
/** Absolute path of the directory whose `package.json` declares `workspaces`. */
|
|
167
|
+
readonly rootDir: string;
|
|
168
|
+
/** The root's `workspaces` globs, verbatim. */
|
|
169
|
+
readonly workspaces: readonly string[];
|
|
170
|
+
/** POSIX path of the target relative to {@link rootDir}, e.g. `packages/foo`. */
|
|
171
|
+
readonly targetPath: string;
|
|
172
|
+
/**
|
|
173
|
+
* The `workspaces` glob already covering {@link targetPath}, if any.
|
|
174
|
+
*
|
|
175
|
+
* Absent means the root manifest needs a new entry — which is why this is
|
|
176
|
+
* computed per target rather than assumed: a `packages/*` glob covers a new
|
|
177
|
+
* package for free, while a repo that lists its examples one by one does not.
|
|
178
|
+
*/
|
|
179
|
+
readonly coveredBy?: string;
|
|
180
|
+
/** The npm scope the sibling packages agree on (`@acme`), when they agree. */
|
|
181
|
+
readonly scope?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Relative specifier of the tsconfig carrying `compilerOptions`, e.g.
|
|
184
|
+
* `../../tsconfig.base.json`. Absent when the root has none.
|
|
185
|
+
*/
|
|
186
|
+
readonly baseTsconfig?: string;
|
|
187
|
+
/** That tsconfig's `compilerOptions`, so the emitted one can drop what it inherits. */
|
|
188
|
+
readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
|
|
189
|
+
/** The root prettier config's `printWidth`, so emitted sources wrap where the repo wraps. */
|
|
190
|
+
readonly printWidth?: number;
|
|
191
|
+
/** Filename of the root eslint config, when there is one. Absent means the repo does not lint. */
|
|
192
|
+
readonly eslintConfig?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The nearest ancestor of `targetDir` whose `package.json` declares
|
|
197
|
+
* `workspaces`.
|
|
198
|
+
*
|
|
199
|
+
* Nearest rather than outermost: nested workspaces are legal, and the inner one
|
|
200
|
+
* is the manifest that would actually claim the new package.
|
|
201
|
+
*/
|
|
202
|
+
export function findWorkspaceRoot(targetDir: string, probe: WorkspaceProbe): string | undefined {
|
|
203
|
+
let current = path.resolve(targetDir);
|
|
204
|
+
for (;;) {
|
|
205
|
+
const manifest = probe.readJson(path.join(current, 'package.json'));
|
|
206
|
+
if (isJsonObject(manifest) && Array.isArray(manifest.workspaces)) {
|
|
207
|
+
return current;
|
|
208
|
+
}
|
|
209
|
+
const parent = path.dirname(current);
|
|
210
|
+
if (parent === current) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
current = parent;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The `workspaces` globs of a detected root, as strings. */
|
|
218
|
+
function readWorkspaces(rootDir: string, probe: WorkspaceProbe): string[] {
|
|
219
|
+
const manifest = probe.readJson(path.join(rootDir, 'package.json'));
|
|
220
|
+
if (!isJsonObject(manifest) || !Array.isArray(manifest.workspaces)) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
return manifest.workspaces.filter((entry): entry is string => typeof entry === 'string');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Match one path segment against a glob segment, where `*` stands for "any run of non-separator". */
|
|
227
|
+
function segmentMatches(glob: string, segment: string): boolean {
|
|
228
|
+
const source = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
|
|
229
|
+
return new RegExp(`^${source}$`).test(segment);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* True when a `workspaces` glob covers a repo-relative path.
|
|
234
|
+
*
|
|
235
|
+
* `**` is treated as covering any run of segments. Deliberately no glob
|
|
236
|
+
* dependency: npm's own set is small, and a wrong answer here only costs a
|
|
237
|
+
* printed hint that turns out to be unnecessary.
|
|
238
|
+
*/
|
|
239
|
+
export function workspaceGlobCovers(glob: string, relativePath: string): boolean {
|
|
240
|
+
const globSegments = glob.split('/').filter(segment => segment.length > 0);
|
|
241
|
+
const pathSegments = relativePath.split('/').filter(segment => segment.length > 0);
|
|
242
|
+
const matches = (globIndex: number, pathIndex: number): boolean => {
|
|
243
|
+
if (globIndex === globSegments.length) {
|
|
244
|
+
return pathIndex === pathSegments.length;
|
|
245
|
+
}
|
|
246
|
+
if (globSegments[globIndex] === '**') {
|
|
247
|
+
for (let skip = pathIndex; skip <= pathSegments.length; skip += 1) {
|
|
248
|
+
if (matches(globIndex + 1, skip)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return (
|
|
255
|
+
pathIndex < pathSegments.length &&
|
|
256
|
+
segmentMatches(globSegments[globIndex], pathSegments[pathIndex]) &&
|
|
257
|
+
matches(globIndex + 1, pathIndex + 1)
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
return matches(0, 0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Expand a `workspaces` glob to the directories it currently names.
|
|
265
|
+
*
|
|
266
|
+
* `**` is not expanded — it returns nothing rather than walking an unbounded
|
|
267
|
+
* tree, because the only caller is scope inference, which a partial sample
|
|
268
|
+
* answers just as well.
|
|
269
|
+
*/
|
|
270
|
+
function expandWorkspaceGlob(rootDir: string, glob: string, probe: WorkspaceProbe): string[] {
|
|
271
|
+
const segments = glob.split('/').filter(segment => segment.length > 0);
|
|
272
|
+
let directories = [rootDir];
|
|
273
|
+
for (const segment of segments) {
|
|
274
|
+
if (segment === '**') {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
if (!segment.includes('*')) {
|
|
278
|
+
directories = directories.map(directory => path.join(directory, segment));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
directories = directories.flatMap(directory =>
|
|
282
|
+
probe
|
|
283
|
+
.listDirectories(directory)
|
|
284
|
+
.filter(name => segmentMatches(segment, name))
|
|
285
|
+
.map(name => path.join(directory, name))
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return directories;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The npm scope the existing workspace members agree on.
|
|
293
|
+
*
|
|
294
|
+
* Guessed from siblings and NOT from the root manifest, which in a framework
|
|
295
|
+
* repo is routinely named for the repo rather than the scope it publishes under
|
|
296
|
+
* — so reading it would confidently produce the wrong answer. Requires a
|
|
297
|
+
* unanimous scope: a split verdict is exactly the case where a prompt should
|
|
298
|
+
* ask rather than a heuristic should pick.
|
|
299
|
+
*/
|
|
300
|
+
export function inferPackageScope(rootDir: string, workspaces: readonly string[], probe: WorkspaceProbe): string | undefined {
|
|
301
|
+
const scopes = new Set<string>();
|
|
302
|
+
for (const glob of workspaces) {
|
|
303
|
+
for (const directory of expandWorkspaceGlob(rootDir, glob, probe)) {
|
|
304
|
+
const manifest = probe.readJson(path.join(directory, 'package.json'));
|
|
305
|
+
if (!isJsonObject(manifest) || typeof manifest.name !== 'string') {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const scope = manifest.name.startsWith('@') ? manifest.name.split('/')[0] : '';
|
|
309
|
+
scopes.add(scope);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
scopes.delete('');
|
|
313
|
+
return scopes.size === 1 ? [...scopes][0] : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The root tsconfig that actually carries `compilerOptions`.
|
|
318
|
+
*
|
|
319
|
+
* `tsconfig.json` at a monorepo root is very often a *solution* file — `files:
|
|
320
|
+
* []` plus `references` — so extending it inherits nothing and silently drops
|
|
321
|
+
* every compiler setting the package meant to pick up. Hence the search is by
|
|
322
|
+
* content, preferring the conventional `tsconfig.base.json` name only as a
|
|
323
|
+
* tie-break, never as the test.
|
|
324
|
+
*/
|
|
325
|
+
export function findBaseTsconfig(
|
|
326
|
+
rootDir: string,
|
|
327
|
+
probe: WorkspaceProbe
|
|
328
|
+
): { readonly file: string; readonly compilerOptions: Readonly<Record<string, JsonValue>> } | undefined {
|
|
329
|
+
const present = probe.listFiles(rootDir).filter(name => /^tsconfig(\..+)?\.json$/.test(name));
|
|
330
|
+
const preferred = ['tsconfig.base.json', 'tsconfig.json'];
|
|
331
|
+
const candidates = [...preferred.filter(name => present.includes(name)), ...present.filter(name => !preferred.includes(name)).sort()];
|
|
332
|
+
for (const file of candidates) {
|
|
333
|
+
const parsed = probe.readJson(path.join(rootDir, file));
|
|
334
|
+
if (isJsonObject(parsed) && isJsonObject(parsed.compilerOptions) && Object.keys(parsed.compilerOptions).length > 0) {
|
|
335
|
+
return { file, compilerOptions: parsed.compilerOptions };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The repo's prettier `printWidth`, so the emitted sources wrap where the repo
|
|
343
|
+
* wraps instead of at the scaffold's own default.
|
|
344
|
+
*
|
|
345
|
+
* **Why this is worth detecting at all.** There is no width that is stable for
|
|
346
|
+
* an unknown repo — 80 (prettier's own default), 100, 120 and 140 each produce
|
|
347
|
+
* different wrapping — so emitted code that "passes a formatter check as-is" is
|
|
348
|
+
* only ever true relative to a config. Matching the one we can see is the
|
|
349
|
+
* closest thing available.
|
|
350
|
+
*
|
|
351
|
+
* **A regex over the file, deliberately, and only for the module forms.** The
|
|
352
|
+
* JSON forms are parsed properly; `.prettierrc.js` and `prettier.config.mjs` are
|
|
353
|
+
* executable modules that no scaffold should evaluate, and skipping them would
|
|
354
|
+
* miss the configs most repos actually use. The pattern takes the FIRST
|
|
355
|
+
* `printWidth`, which is the top-level one in every config laid out
|
|
356
|
+
* conventionally — an `overrides` entry appearing before it would win, and that
|
|
357
|
+
* is the known limit.
|
|
358
|
+
*
|
|
359
|
+
* Getting it wrong costs exactly what getting it absent costs today: the
|
|
360
|
+
* adopter's formatter rewraps on first commit. So a heuristic that is usually
|
|
361
|
+
* right strictly improves on no detection, and can break nothing.
|
|
362
|
+
*/
|
|
363
|
+
export function findPrettierPrintWidth(rootDir: string, probe: WorkspaceProbe): number | undefined {
|
|
364
|
+
const manifest = probe.readJson(path.join(rootDir, 'package.json'));
|
|
365
|
+
if (isJsonObject(manifest) && isJsonObject(manifest.prettier) && typeof manifest.prettier.printWidth === 'number') {
|
|
366
|
+
return manifest.prettier.printWidth;
|
|
367
|
+
}
|
|
368
|
+
const present = probe.listFiles(rootDir);
|
|
369
|
+
for (const file of present.filter(name => name === '.prettierrc' || name === '.prettierrc.json')) {
|
|
370
|
+
const parsed = probe.readJson(path.join(rootDir, file));
|
|
371
|
+
if (isJsonObject(parsed) && typeof parsed.printWidth === 'number') {
|
|
372
|
+
return parsed.printWidth;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const file of present.filter(name => /^(\.prettierrc\.[cm]?[jt]s|prettier\.config\.[cm]?[jt]s)$/.test(name))) {
|
|
376
|
+
const width = /\bprintWidth\s*:\s*(\d+)/.exec(probe.readText(path.join(rootDir, file)) ?? '');
|
|
377
|
+
if (width) {
|
|
378
|
+
return Number(width[1]);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The root eslint config's filename, when the repo has one.
|
|
386
|
+
*
|
|
387
|
+
* Read to decide whether the scaffold emits a `lint` script. The failure it
|
|
388
|
+
* prevents is silent in the direction that matters: a task runner runs a script
|
|
389
|
+
* only where one is declared, so a package without `lint` is SKIPPED rather than
|
|
390
|
+
* reported, and the absence reads as a clean lint. Emitting the script into a
|
|
391
|
+
* repo whose lint invocation differs costs one visible line to edit.
|
|
392
|
+
*
|
|
393
|
+
* Both config generations, because detecting a repo that has not migrated off
|
|
394
|
+
* `.eslintrc` is free and the question asked here — does this repo lint? — is
|
|
395
|
+
* the same either way.
|
|
396
|
+
*/
|
|
397
|
+
export function findEslintConfig(rootDir: string, probe: WorkspaceProbe): string | undefined {
|
|
398
|
+
return probe.listFiles(rootDir).find(name => /^eslint\.config\.[cm]?[jt]s$/.test(name) || /^\.eslintrc(\..+)?$/.test(name));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** POSIX-style relative path, which is what both `extends` and `--prefix` want on every platform. */
|
|
402
|
+
function posixRelative(from: string, to: string): string {
|
|
403
|
+
return path.relative(from, to).split(path.sep).join('/');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Detect the workspace `targetDir` would join. `undefined` when no ancestor
|
|
408
|
+
* declares `workspaces`, which is the standalone case and not an error here —
|
|
409
|
+
* `runInit` decides whether that is fatal.
|
|
410
|
+
*
|
|
411
|
+
* The target is resolved against the process's working directory, so a relative
|
|
412
|
+
* one is relative to where the command was RUN, not to anything the scaffolder
|
|
413
|
+
* picks. That matters because the two are routinely different directories.
|
|
414
|
+
*/
|
|
415
|
+
export function detectWorkspace(targetDir: string, probe: WorkspaceProbe): WorkspaceDetection | undefined {
|
|
416
|
+
const absoluteTarget = path.resolve(targetDir);
|
|
417
|
+
const rootDir = findWorkspaceRoot(absoluteTarget, probe);
|
|
418
|
+
if (rootDir === undefined) {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
const workspaces = readWorkspaces(rootDir, probe);
|
|
422
|
+
const targetPath = posixRelative(rootDir, absoluteTarget);
|
|
423
|
+
// The target IS the workspace root — `init .` at the top of a monorepo, or a
|
|
424
|
+
// target that resolved to the cwd because it was empty. There is no member to
|
|
425
|
+
// place, so reporting a placement here describes a scaffold that cannot
|
|
426
|
+
// happen and names a directory the user never asked for.
|
|
427
|
+
if (targetPath === '') {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
const base = findBaseTsconfig(rootDir, probe);
|
|
431
|
+
return {
|
|
432
|
+
rootDir,
|
|
433
|
+
workspaces,
|
|
434
|
+
targetPath,
|
|
435
|
+
coveredBy: workspaces.find(glob => workspaceGlobCovers(glob, targetPath)),
|
|
436
|
+
scope: inferPackageScope(rootDir, workspaces, probe),
|
|
437
|
+
baseTsconfig: base && posixRelative(absoluteTarget, path.join(rootDir, base.file)),
|
|
438
|
+
baseCompilerOptions: base?.compilerOptions,
|
|
439
|
+
printWidth: findPrettierPrintWidth(rootDir, probe),
|
|
440
|
+
eslintConfig: findEslintConfig(rootDir, probe)
|
|
441
|
+
};
|
|
442
|
+
}
|