@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4
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 +99 -66
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +115 -3
- package/api/build/blocks/blocks.d.mts +42 -0
- package/api/build/blocks/blocks.mjs +169 -0
- package/api/build/blocks/blocks.test.mjs +108 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/kit/answer.d.mts +14 -0
- package/api/build/kit/answer.mjs +85 -0
- package/api/build/kit/answer.test.mjs +38 -0
- package/api/build/kit/kind.d.mts +25 -0
- package/api/build/kit/kind.mjs +170 -0
- package/api/build/kit/kit.d.mts +2 -4
- package/api/build/kit/kit.mjs +8 -398
- package/api/build/kit/rank.d.mts +3 -32
- package/api/build/kit/rank.mjs +25 -342
- package/api/build/kit/rank.test.mjs +2 -1
- package/api/build/kit/signals.d.mts +60 -0
- package/api/build/kit/signals.mjs +284 -0
- package/api/build/kit/start.d.mts +32 -0
- package/api/build/kit/start.mjs +163 -0
- package/api/build/kit/words.d.mts +72 -0
- package/api/build/kit/words.mjs +202 -0
- package/api/build/tree.d.mts +20 -0
- package/api/build/tree.mjs +52 -0
- package/api/build/tree.test.mjs +177 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/component.mjs +16 -2
- package/api/component/component.test.mjs +15 -0
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.mjs +17 -5
- package/api/docs/docs.test.mjs +7 -1
- package/api/doctor/doctor.mjs +86 -12
- package/api/doctor/doctor.test.mjs +28 -3
- package/api/init/init.doc.mjs +20 -6
- package/api/init/init.test.mjs +18 -2
- package/api/init/init.type.d.mts +3 -3
- package/api/init/init.type.mjs +3 -3
- package/api/init/remove/remove.mjs +17 -4
- package/api/init/remove/remove.test.mjs +114 -2
- package/api/init/run/run.mjs +69 -6
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/pack-check.mjs +123 -4
- package/api/integration/pack-check.test.mjs +87 -0
- package/api/integration/validate-integration-case-disk.test.mjs +111 -0
- package/api/integration/validate-integration-gaps.test.mjs +1964 -0
- package/api/integration/validate-integration.d.mts +11 -2
- package/api/integration/validate-integration.mjs +226 -32
- package/api/integration/validate-integration.type.d.mts +58 -1
- package/api/integration/validate-integration.type.mjs +45 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +66 -2
- package/api/search/_adapter.mjs +118 -10
- package/api/search/_candidate-score.d.mts +40 -0
- package/api/search/_candidate-score.mjs +238 -0
- package/api/search/_cards.json +6098 -0
- package/api/search/_compound.d.mts +50 -0
- package/api/search/_compound.mjs +170 -0
- package/api/search/_doc-searcher.d.mts +4 -0
- package/api/search/_doc-searcher.mjs +23 -0
- package/api/search/_doc-searcher.test.mjs +36 -0
- package/api/search/_doc-searcher.weights.json +1 -0
- package/api/search/_doc-text.d.mts +30 -0
- package/api/search/_doc-text.mjs +87 -0
- package/api/search/_entries.d.mts +52 -0
- package/api/search/_entries.mjs +158 -0
- package/api/search/_features.d.mts +35 -0
- package/api/search/_features.mjs +155 -0
- package/api/search/_gather-components.d.mts +26 -0
- package/api/search/_gather-components.mjs +284 -0
- package/api/search/_gather-docs.d.mts +20 -0
- package/api/search/_gather-docs.mjs +254 -0
- package/api/search/_gather-hooks.d.mts +12 -0
- package/api/search/_gather-hooks.mjs +52 -0
- package/api/search/_gather-templates.d.mts +25 -0
- package/api/search/_gather-templates.mjs +107 -0
- package/api/search/_lexical.d.mts +135 -0
- package/api/search/_lexical.mjs +351 -0
- package/api/search/_main-searcher.d.mts +19 -0
- package/api/search/_main-searcher.mjs +115 -0
- package/api/search/_main-searcher.test.mjs +106 -0
- package/api/search/_main-searcher.weights.json +1 -0
- package/api/search/_module-doc.d.mts +32 -0
- package/api/search/_module-doc.mjs +78 -0
- package/api/search/_order.d.mts +32 -0
- package/api/search/_order.mjs +162 -0
- package/api/search/_order.test.mjs +72 -0
- package/api/search/_priority.d.mts +50 -0
- package/api/search/_priority.mjs +139 -0
- package/api/search/_query-words.d.mts +25 -0
- package/api/search/_query-words.mjs +92 -0
- package/api/search/_query.d.mts +65 -0
- package/api/search/_query.mjs +176 -0
- package/api/search/_result.d.mts +97 -0
- package/api/search/_result.mjs +150 -0
- package/api/search/_score.d.mts +60 -0
- package/api/search/_score.mjs +283 -0
- package/api/search/_synonyms.d.mts +21 -0
- package/api/search/_synonyms.mjs +67 -0
- package/api/search/_template-searcher.d.mts +4 -0
- package/api/search/_template-searcher.mjs +23 -0
- package/api/search/_template-searcher.test.mjs +40 -0
- package/api/search/_template-searcher.weights.json +1 -0
- package/api/search/_titles.d.mts +33 -0
- package/api/search/_titles.mjs +65 -0
- package/api/search/_words.d.mts +64 -0
- package/api/search/_words.mjs +157 -0
- package/api/search/search.d.mts +22 -305
- package/api/search/search.doc.mjs +7 -5
- package/api/search/search.mjs +53 -2483
- package/api/search/search.type.d.mts +6 -1
- package/api/search/search.type.mjs +6 -1
- package/api/search/tree.d.mts +15 -0
- package/api/search/tree.mjs +40 -0
- package/api/search/tree.test.mjs +66 -0
- package/api/search/tree.weights.json +1 -0
- package/api/theme/build/build.packed-old-core.test.mjs +1 -0
- package/api/upgrade/_adapter.d.mts +57 -9
- package/api/upgrade/_adapter.mjs +198 -28
- package/api/upgrade/registry/registry.d.mts +10 -4
- package/api/upgrade/registry/registry.mjs +33 -16
- package/api/upgrade/registry/registry.test.mjs +85 -0
- package/api/upgrade/run/run.d.mts +4 -2
- package/api/upgrade/run/run.mjs +13 -2
- package/api/upgrade/run/run.test.mjs +417 -3
- package/api/upgrade/upgrade.doc.mjs +10 -5
- package/api/upgrade/upgrade.mjs +13 -2
- package/api/upgrade/upgrade.test.mjs +194 -0
- package/api/upgrade/upgrade.type.d.mts +5 -3
- package/api/upgrade/upgrade.type.mjs +8 -6
- package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
- package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +6 -2
- package/assets/templates/themes/butter/butterTheme.ts +4 -2
- package/assets/templates/themes/stone/stoneTheme.ts +4 -2
- package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
- package/authoring/debug/parse.d.mts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/init.behavior.test.mjs +230 -1
- package/clients/cli/commands/init.doc.mjs +28 -9
- package/clients/cli/commands/init.flag-help.test.mjs +1 -0
- package/clients/cli/commands/init.mjs +2 -2
- package/clients/cli/commands/outside-project.test.mjs +2 -1
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- package/clients/cli/commands/upgrade.doc.mjs +4 -2
- package/clients/cli/commands/upgrade.mjs +9 -2
- package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
- package/clients/cli/index.mjs +19 -1
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
- package/foundation/agent-docs/agent-doc-state.mjs +35 -8
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1010 -99
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +678 -35
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/component-discovery.d.mts +37 -0
- package/foundation/discovery/component-discovery.mjs +80 -5
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/docs-discovery.mjs +14 -1
- package/foundation/discovery/module-exports.d.mts +41 -0
- package/foundation/discovery/module-exports.mjs +253 -0
- package/foundation/discovery/template-adapter.mjs +18 -1
- package/foundation/discovery/template-needs.mjs +1 -44
- package/foundation/discovery/theme-discovery.mjs +147 -3
- package/foundation/doc-compiler/doc-loads.test.mjs +8 -0
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/fs/paths.d.mts +70 -8
- package/foundation/fs/paths.mjs +556 -38
- package/foundation/fs/paths.test.mjs +430 -54
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/computer-integrations.d.mts +6 -3
- package/foundation/integrations/computer-integrations.mjs +38 -9
- package/foundation/integrations/contribution-inventory.d.mts +15 -20
- package/foundation/integrations/contribution-inventory.mjs +41 -5
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/run-context.mjs +14 -0
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/route/route.d.mts +285 -0
- package/foundation/route/route.mjs +324 -0
- package/foundation/route/route.test.mjs +153 -0
- package/foundation/runtime/hosted-runtime.test.mjs +403 -0
- package/foundation/runtime/locked-versions.d.mts +32 -0
- package/foundation/runtime/locked-versions.mjs +65 -0
- package/foundation/runtime/package-root.d.mts +91 -0
- package/foundation/runtime/package-root.mjs +174 -0
- package/foundation/runtime/package-set.d.mts +62 -0
- package/foundation/runtime/package-set.mjs +130 -0
- package/foundation/runtime/package-set.test.mjs +432 -0
- package/host/door-conformance.test.mjs +274 -0
- package/host/index.d.ts +116 -0
- package/host/index.mjs +295 -0
- package/host/index.test.mjs +302 -0
- package/package.json +14 -9
package/foundation/fs/paths.mjs
CHANGED
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Path resolution utilities for Astryx CLI
|
|
5
5
|
*
|
|
6
|
-
* Finds
|
|
6
|
+
* Finds the Astryx project a folder belongs to and its workspace root, the
|
|
7
|
+
* project's @astryxdesign/core and the other packages it installed, and the
|
|
8
|
+
* CLI package root.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import * as fs from 'node:fs';
|
|
10
12
|
import {createRequire} from 'node:module';
|
|
13
|
+
import * as os from 'node:os';
|
|
11
14
|
import * as path from 'node:path';
|
|
12
15
|
import {fileURLToPath} from 'node:url';
|
|
13
16
|
import {listDir, pathExists} from './listing-cache.mjs';
|
|
14
17
|
import {ERROR_CODES} from '../response/error-codes.mjs';
|
|
15
18
|
import {carriedCore, runChoice} from '../release/run-context.mjs';
|
|
16
19
|
import {findDeclaringPackage} from '../release/project.mjs';
|
|
20
|
+
import {hostedRun, recordResolved} from '../runtime/package-root.mjs';
|
|
17
21
|
|
|
18
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
23
|
|
|
20
24
|
/** Root of the @astryxdesign/cli package */
|
|
21
25
|
export const CLI_ROOT = path.resolve(__dirname, '..', '..');
|
|
22
26
|
|
|
27
|
+
/** The package every Core lookup answers for. */
|
|
28
|
+
const CORE_PACKAGE = '@astryxdesign/core';
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* The Core a command answers from: the project's (see
|
|
25
32
|
* {@link findProjectCoreDir}), else, when a copy other than the project's own
|
|
@@ -35,7 +42,33 @@ export const CLI_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
35
42
|
* @returns {string | null}
|
|
36
43
|
*/
|
|
37
44
|
export function findCoreDir(startDir = process.cwd()) {
|
|
38
|
-
|
|
45
|
+
const own = findProjectCoreDir(startDir);
|
|
46
|
+
return own
|
|
47
|
+
? noteCore(own, 'project')
|
|
48
|
+
: noteCore(carriedCoreFor(startDir).dir);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Note the Core a hosted run reads, for the record its host entry returns
|
|
53
|
+
* (architecture:cli-surface INV30). Does nothing outside a hosted run.
|
|
54
|
+
* @template {string | null} T
|
|
55
|
+
* @param {T} dir
|
|
56
|
+
* @param {'project'} [source] where it came from, when the caller knows
|
|
57
|
+
* @returns {T}
|
|
58
|
+
*/
|
|
59
|
+
function noteCore(dir, source) {
|
|
60
|
+
if (dir && hostedRun()) {
|
|
61
|
+
let version;
|
|
62
|
+
try {
|
|
63
|
+
version = JSON.parse(
|
|
64
|
+
fs.readFileSync(path.join(dir, 'package.json'), 'utf8'),
|
|
65
|
+
).version;
|
|
66
|
+
} catch {
|
|
67
|
+
version = null;
|
|
68
|
+
}
|
|
69
|
+
recordResolved('@astryxdesign/core', version, dir, source);
|
|
70
|
+
}
|
|
71
|
+
return dir;
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
/**
|
|
@@ -67,14 +100,14 @@ export function unavailableCoreMessage(found, inProject) {
|
|
|
67
100
|
*/
|
|
68
101
|
export function requireCoreDir(startDir = process.cwd()) {
|
|
69
102
|
const found = findProjectCoreDir(startDir) ?? carriedCoreFor(startDir, true);
|
|
70
|
-
if (typeof found === 'string') return found;
|
|
103
|
+
if (typeof found === 'string') return noteCore(found, 'project');
|
|
71
104
|
if (found.unavailable) {
|
|
72
105
|
throw codedError(
|
|
73
106
|
unavailableCoreMessage(found, runChoice(startDir).inProject),
|
|
74
107
|
ERROR_CODES.ERR_VERSIONS_UNAVAILABLE,
|
|
75
108
|
);
|
|
76
109
|
}
|
|
77
|
-
if (found.dir) return found.dir;
|
|
110
|
+
if (found.dir) return noteCore(found.dir);
|
|
78
111
|
throw codedError(
|
|
79
112
|
'Could not find @astryxdesign/core package',
|
|
80
113
|
ERROR_CODES.ERR_CORE_NOT_FOUND,
|
|
@@ -121,12 +154,10 @@ function carriedCoreFor(startDir, fetch = false) {
|
|
|
121
154
|
}
|
|
122
155
|
|
|
123
156
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* released version used, else from the folder of the package.json that makes
|
|
129
|
-
* the folder part of a project, however deep the folder is.
|
|
157
|
+
* The project's own @astryxdesign/core, never a carried one: found from the
|
|
158
|
+
* folder (see {@link findNearbyCoreDir}), else from the folder of the
|
|
159
|
+
* package.json that makes the folder part of a project, however deep the
|
|
160
|
+
* folder is.
|
|
130
161
|
*
|
|
131
162
|
* @param {string} [startDir]
|
|
132
163
|
* @returns {string | null}
|
|
@@ -144,38 +175,464 @@ export function findProjectCoreDir(startDir = process.cwd()) {
|
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
/**
|
|
147
|
-
*
|
|
178
|
+
* How many folders, counting the start, the read lookups always search: the
|
|
179
|
+
* reach they had in released versions, so nothing they found before goes
|
|
180
|
+
* missing. Past this reach they continue only up to the workspace root of the
|
|
181
|
+
* Astryx project the start folder belongs to.
|
|
182
|
+
*/
|
|
183
|
+
const CORE_REACH = 5;
|
|
184
|
+
const INSTALLED_REACH = 6;
|
|
185
|
+
|
|
186
|
+
/** Package scopes whose declaration makes a package.json an Astryx project. */
|
|
187
|
+
const ASTRYX_SCOPES = ['@astryxdesign/', '@xds/'];
|
|
188
|
+
|
|
189
|
+
/** package.json fields in which a package declares what it depends on. */
|
|
190
|
+
const DEPENDENCY_FIELDS = [
|
|
191
|
+
'dependencies',
|
|
192
|
+
'devDependencies',
|
|
193
|
+
'optionalDependencies',
|
|
194
|
+
'peerDependencies',
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
/** Folders that mark a repository root; the project search stops there. */
|
|
198
|
+
const REPOSITORY_MARKERS = ['.git', '.hg', '.sl'];
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* `startDir` and every folder above it, nearest first, up to the filesystem
|
|
202
|
+
* root: the folders Node's resolver searches for `node_modules`.
|
|
203
|
+
*
|
|
148
204
|
* @param {string} startDir
|
|
149
|
-
* @returns {string
|
|
205
|
+
* @returns {Generator<string>}
|
|
150
206
|
*/
|
|
151
|
-
function
|
|
152
|
-
let dir = startDir;
|
|
207
|
+
function* foldersUpFrom(startDir) {
|
|
208
|
+
let dir = path.resolve(startDir);
|
|
209
|
+
for (;;) {
|
|
210
|
+
yield dir;
|
|
211
|
+
const parent = path.dirname(dir);
|
|
212
|
+
if (parent === dir) return;
|
|
213
|
+
dir = parent;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
153
216
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
217
|
+
/**
|
|
218
|
+
* `dir/node_modules/<name>` when the package is installed there. Its
|
|
219
|
+
* package.json decides, so a stale empty folder is not an install.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} dir
|
|
222
|
+
* @param {string} name
|
|
223
|
+
* @returns {string|null}
|
|
224
|
+
*/
|
|
225
|
+
function installedIn(dir, name) {
|
|
226
|
+
const candidate = path.join(dir, 'node_modules', ...name.split('/'));
|
|
227
|
+
return fs.existsSync(path.join(candidate, 'package.json')) ? candidate : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The package.json in `dir`, or null when it is missing or not a JSON object.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} dir
|
|
234
|
+
* @returns {Record<string, any>|null}
|
|
235
|
+
*/
|
|
236
|
+
function readPackageJson(dir) {
|
|
237
|
+
try {
|
|
238
|
+
const pkg = JSON.parse(
|
|
239
|
+
fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'),
|
|
240
|
+
);
|
|
241
|
+
return pkg && typeof pkg === 'object' ? pkg : null;
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Whether `dir` is @astryxdesign/core's own source, by the name in its
|
|
249
|
+
* package.json. The folder name says nothing: many monorepos keep an unrelated
|
|
250
|
+
* package in `packages/core`.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} dir
|
|
253
|
+
* @returns {boolean}
|
|
254
|
+
*/
|
|
255
|
+
function isCoreSource(dir) {
|
|
256
|
+
return readPackageJson(dir)?.name === CORE_PACKAGE;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `p` resolved through symlinks when it exists, so folders compare equal
|
|
261
|
+
* however they were reached.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} p
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
function realDir(p) {
|
|
267
|
+
try {
|
|
268
|
+
return fs.realpathSync(path.resolve(p));
|
|
269
|
+
} catch {
|
|
270
|
+
return path.resolve(p);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The home folder, resolved through symlinks, or null when there is none.
|
|
276
|
+
*
|
|
277
|
+
* @returns {string|null}
|
|
278
|
+
*/
|
|
279
|
+
function homeFolder() {
|
|
280
|
+
try {
|
|
281
|
+
const home = os.homedir();
|
|
282
|
+
return home ? realDir(home) : null;
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Whether `child` lies strictly inside `parent`.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} child
|
|
292
|
+
* @param {string} parent
|
|
293
|
+
* @returns {boolean}
|
|
294
|
+
*/
|
|
295
|
+
function isInside(child, parent) {
|
|
296
|
+
const rel = path.relative(parent, child);
|
|
297
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Whether the package.json in `dir` declares an Astryx package, in any
|
|
302
|
+
* dependency field.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} dir
|
|
305
|
+
* @returns {boolean}
|
|
306
|
+
*/
|
|
307
|
+
function declaresAstryx(dir) {
|
|
308
|
+
const pkg = readPackageJson(dir);
|
|
309
|
+
if (pkg == null) return false;
|
|
310
|
+
return DEPENDENCY_FIELDS.some(field => {
|
|
311
|
+
const deps = pkg[field];
|
|
312
|
+
return (
|
|
313
|
+
deps != null &&
|
|
314
|
+
typeof deps === 'object' &&
|
|
315
|
+
Object.keys(deps).some(name =>
|
|
316
|
+
ASTRYX_SCOPES.some(scope => name.startsWith(scope)),
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The project root: the directory of the nearest package.json at or above
|
|
324
|
+
* `startDir`, or null when there is none.
|
|
325
|
+
*
|
|
326
|
+
* @param {string} startDir
|
|
327
|
+
* @returns {string|null}
|
|
328
|
+
*/
|
|
329
|
+
export function findPackageRoot(startDir) {
|
|
330
|
+
for (const dir of foldersUpFrom(startDir)) {
|
|
331
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The Astryx project `startDir` belongs to: the nearest folder at or above it
|
|
338
|
+
* whose package.json declares an Astryx package (`@astryxdesign/*`, or the
|
|
339
|
+
* legacy `@xds/*`), or that a workspace whose root declares one lists as a
|
|
340
|
+
* member. The search stops at a repository root, and it ends at the home
|
|
341
|
+
* folder without counting it. Returns null when no folder qualifies.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} startDir
|
|
344
|
+
* @returns {string|null}
|
|
345
|
+
*/
|
|
346
|
+
export function findAstryxProject(startDir) {
|
|
347
|
+
const start = path.resolve(startDir);
|
|
348
|
+
const home = homeFolder();
|
|
349
|
+
for (const dir of foldersUpFrom(start)) {
|
|
350
|
+
if (home != null && realDir(dir) === home) return null;
|
|
351
|
+
if (declaresAstryx(dir) || isAstryxWorkspaceMember(dir)) return dir;
|
|
352
|
+
if (
|
|
353
|
+
REPOSITORY_MARKERS.some(marker => fs.existsSync(path.join(dir, marker)))
|
|
354
|
+
) {
|
|
355
|
+
return null;
|
|
158
356
|
}
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
159
360
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
361
|
+
/**
|
|
362
|
+
* The workspace root (`spec:AST-048` FR15): the nearest directory at or above
|
|
363
|
+
* `startDir` that declares a workspace, by a `pnpm-workspace.yaml` or a
|
|
364
|
+
* package.json `workspaces` field; outside a workspace, the directory of the
|
|
365
|
+
* nearest package.json; with no package.json, `startDir` itself.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} startDir
|
|
368
|
+
* @returns {string}
|
|
369
|
+
*/
|
|
370
|
+
export function findWorkspaceRoot(startDir) {
|
|
371
|
+
/** @type {string|null} */
|
|
372
|
+
let nearestPackage = null;
|
|
373
|
+
for (const dir of foldersUpFrom(startDir)) {
|
|
374
|
+
if (
|
|
375
|
+
fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')) ||
|
|
376
|
+
readPackageJson(dir)?.workspaces != null
|
|
377
|
+
) {
|
|
378
|
+
return dir;
|
|
379
|
+
}
|
|
380
|
+
if (
|
|
381
|
+
nearestPackage == null &&
|
|
382
|
+
fs.existsSync(path.join(dir, 'package.json'))
|
|
383
|
+
) {
|
|
384
|
+
nearestPackage = dir;
|
|
163
385
|
}
|
|
386
|
+
}
|
|
387
|
+
return nearestPackage ?? path.resolve(startDir);
|
|
388
|
+
}
|
|
164
389
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
390
|
+
/**
|
|
391
|
+
* The highest folder a lookup for `project`'s packages searches: its
|
|
392
|
+
* workspace root, where a workspace keeps the packages its members share. For
|
|
393
|
+
* a project inside the home folder, never the home folder or above.
|
|
394
|
+
*
|
|
395
|
+
* @param {string} project
|
|
396
|
+
* @returns {string}
|
|
397
|
+
*/
|
|
398
|
+
function workspaceBound(project) {
|
|
399
|
+
const workspace = findWorkspaceRoot(project);
|
|
400
|
+
const home = homeFolder();
|
|
401
|
+
if (
|
|
402
|
+
home != null &&
|
|
403
|
+
isInside(realDir(project), home) &&
|
|
404
|
+
!isInside(realDir(workspace), home)
|
|
405
|
+
) {
|
|
406
|
+
return project;
|
|
168
407
|
}
|
|
408
|
+
return workspace;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Escape `text` for use inside a regular expression.
|
|
413
|
+
* @param {string} text
|
|
414
|
+
* @returns {string}
|
|
415
|
+
*/
|
|
416
|
+
function escapeRegExp(text) {
|
|
417
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
418
|
+
}
|
|
169
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The alternatives of the brace list that opens at `glob[start]`, split at
|
|
422
|
+
* its own commas, and the index of its closing brace; null when it is never
|
|
423
|
+
* closed.
|
|
424
|
+
*
|
|
425
|
+
* @param {string} glob
|
|
426
|
+
* @param {number} start
|
|
427
|
+
* @returns {{alternatives: string[], end: number} | null}
|
|
428
|
+
*/
|
|
429
|
+
function braceList(glob, start) {
|
|
430
|
+
const alternatives = [];
|
|
431
|
+
let depth = 0;
|
|
432
|
+
let from = start + 1;
|
|
433
|
+
for (let i = start; i < glob.length; i++) {
|
|
434
|
+
if (glob[i] === '{') {
|
|
435
|
+
depth++;
|
|
436
|
+
} else if (glob[i] === '}' && --depth === 0) {
|
|
437
|
+
alternatives.push(glob.slice(from, i));
|
|
438
|
+
return {alternatives, end: i};
|
|
439
|
+
} else if (glob[i] === ',' && depth === 1) {
|
|
440
|
+
alternatives.push(glob.slice(from, i));
|
|
441
|
+
from = i + 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
170
444
|
return null;
|
|
171
445
|
}
|
|
172
446
|
|
|
447
|
+
/**
|
|
448
|
+
* The regular-expression source for a workspace glob: `*` and `?` stay
|
|
449
|
+
* within one folder name, `**` spans any number of folders, and `{a,b}`
|
|
450
|
+
* matches either alternative, each a glob of its own.
|
|
451
|
+
*
|
|
452
|
+
* @param {string} glob
|
|
453
|
+
* @returns {string}
|
|
454
|
+
*/
|
|
455
|
+
function globSource(glob) {
|
|
456
|
+
let source = '';
|
|
457
|
+
for (let i = 0; i < glob.length; i++) {
|
|
458
|
+
const char = glob[i];
|
|
459
|
+
const list = char === '{' ? braceList(glob, i) : null;
|
|
460
|
+
if (char === '*' && glob[i + 1] === '*') {
|
|
461
|
+
// `**/` also matches no folder at all.
|
|
462
|
+
source += glob[i + 2] === '/' ? '(?:.*/)?' : '.*';
|
|
463
|
+
i += glob[i + 2] === '/' ? 2 : 1;
|
|
464
|
+
} else if (char === '*') {
|
|
465
|
+
source += '[^/]*';
|
|
466
|
+
} else if (char === '?') {
|
|
467
|
+
source += '[^/]';
|
|
468
|
+
} else if (list) {
|
|
469
|
+
source += `(?:${list.alternatives.map(globSource).join('|')})`;
|
|
470
|
+
i = list.end;
|
|
471
|
+
} else {
|
|
472
|
+
source += escapeRegExp(char);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return source;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* A regular expression for a workspace pattern, tested against a member's
|
|
480
|
+
* path relative to the workspace root (see `globSource`).
|
|
481
|
+
*
|
|
482
|
+
* @param {string} pattern
|
|
483
|
+
* @returns {RegExp}
|
|
484
|
+
*/
|
|
485
|
+
function workspacePatternRegExp(pattern) {
|
|
486
|
+
const glob = pattern
|
|
487
|
+
.trim()
|
|
488
|
+
.replace(/^(?:\.\/)+/, '')
|
|
489
|
+
.replace(/\/+$/, '');
|
|
490
|
+
return new RegExp(`^${globSource(glob)}$`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The `packages` list in the pnpm-workspace.yaml in `dir`, as block or flow
|
|
495
|
+
* YAML; empty when there is none.
|
|
496
|
+
*
|
|
497
|
+
* @param {string} dir
|
|
498
|
+
* @returns {string[]}
|
|
499
|
+
*/
|
|
500
|
+
function pnpmWorkspacePatterns(dir) {
|
|
501
|
+
let text;
|
|
502
|
+
try {
|
|
503
|
+
text = fs.readFileSync(path.join(dir, 'pnpm-workspace.yaml'), 'utf-8');
|
|
504
|
+
} catch {
|
|
505
|
+
return [];
|
|
506
|
+
}
|
|
507
|
+
const lines = text.split(/\r?\n/);
|
|
508
|
+
const start = lines.findIndex(line => /^packages\s*:/.test(line));
|
|
509
|
+
if (start === -1) return [];
|
|
510
|
+
/** @param {string} item */
|
|
511
|
+
const unquote = item => item.trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
512
|
+
const inline = lines[start]
|
|
513
|
+
.replace(/^packages\s*:/, '')
|
|
514
|
+
.replace(/\s#.*$/, '')
|
|
515
|
+
.trim();
|
|
516
|
+
if (inline.startsWith('[')) {
|
|
517
|
+
return inline
|
|
518
|
+
.replace(/^\[|\]$/g, '')
|
|
519
|
+
.split(',')
|
|
520
|
+
.map(unquote)
|
|
521
|
+
.filter(Boolean);
|
|
522
|
+
}
|
|
523
|
+
/** @type {string[]} */
|
|
524
|
+
const patterns = [];
|
|
525
|
+
for (const line of lines.slice(start + 1)) {
|
|
526
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue;
|
|
527
|
+
const item = /^\s*-\s*(.+?)\s*(?:\s#.*)?$/.exec(line);
|
|
528
|
+
if (!item) break;
|
|
529
|
+
patterns.push(unquote(item[1]));
|
|
530
|
+
}
|
|
531
|
+
return patterns;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Whether the workspace rooted at `root` lists `dir` as a member: a pattern
|
|
536
|
+
* in its package.json `workspaces` (an array, or `{packages: [...]}`) or its
|
|
537
|
+
* pnpm-workspace.yaml `packages` matches `dir`, and no `!` pattern excludes
|
|
538
|
+
* it.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} dir
|
|
541
|
+
* @param {string} root
|
|
542
|
+
* @returns {boolean}
|
|
543
|
+
*/
|
|
544
|
+
function isWorkspaceMember(dir, root) {
|
|
545
|
+
const rel = path.relative(root, dir).split(path.sep).join('/');
|
|
546
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) return false;
|
|
547
|
+
const workspaces = readPackageJson(root)?.workspaces;
|
|
548
|
+
const patterns = [
|
|
549
|
+
...(Array.isArray(workspaces) ? workspaces : (workspaces?.packages ?? [])),
|
|
550
|
+
...pnpmWorkspacePatterns(root),
|
|
551
|
+
].filter(pattern => typeof pattern === 'string');
|
|
552
|
+
const matches = (/** @type {string} */ pattern) =>
|
|
553
|
+
workspacePatternRegExp(pattern).test(rel);
|
|
554
|
+
return (
|
|
555
|
+
patterns.some(pattern => !pattern.startsWith('!') && matches(pattern)) &&
|
|
556
|
+
!patterns.some(
|
|
557
|
+
pattern => pattern.startsWith('!') && matches(pattern.slice(1)),
|
|
558
|
+
)
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Whether `dir` is a member its workspace lists, where the workspace root
|
|
564
|
+
* declares an Astryx package. Such a member uses the Astryx its workspace
|
|
565
|
+
* installs, so it is an Astryx project of its own.
|
|
566
|
+
*
|
|
567
|
+
* @param {string} dir
|
|
568
|
+
* @returns {boolean}
|
|
569
|
+
*/
|
|
570
|
+
function isAstryxWorkspaceMember(dir) {
|
|
571
|
+
if (!fs.existsSync(path.join(dir, 'package.json'))) return false;
|
|
572
|
+
const root = workspaceBound(dir);
|
|
573
|
+
return root !== dir && declaresAstryx(root) && isWorkspaceMember(dir, root);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The folders a read lookup searches, nearest first: `startDir` and the
|
|
578
|
+
* `reach - 1` folders above it, as released versions did; then the root of
|
|
579
|
+
* the Astryx project `startDir` belongs to, if any, and the folders from it up
|
|
580
|
+
* to its workspace root. Past the reach the search starts at the project root,
|
|
581
|
+
* so a folder between the two that installs packages but declares no Astryx
|
|
582
|
+
* package (a tool inside the project) never answers for the project.
|
|
583
|
+
*
|
|
584
|
+
* @param {string} startDir
|
|
585
|
+
* @param {number} reach
|
|
586
|
+
* @returns {Generator<string>}
|
|
587
|
+
*/
|
|
588
|
+
function* readFolders(startDir, reach) {
|
|
589
|
+
/** @type {Set<string>} */
|
|
590
|
+
const searched = new Set();
|
|
591
|
+
for (const dir of foldersUpFrom(startDir)) {
|
|
592
|
+
if (searched.size >= reach) break;
|
|
593
|
+
searched.add(dir);
|
|
594
|
+
yield dir;
|
|
595
|
+
}
|
|
596
|
+
const project = findAstryxProject(startDir);
|
|
597
|
+
if (project == null) return;
|
|
598
|
+
const bound = workspaceBound(project);
|
|
599
|
+
for (const dir of foldersUpFrom(project)) {
|
|
600
|
+
if (!searched.has(dir)) yield dir;
|
|
601
|
+
if (dir === bound) return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* The @astryxdesign/core installed for `startDir`.
|
|
607
|
+
*
|
|
608
|
+
* Searches `startDir` and the four folders above it, as released versions
|
|
609
|
+
* did, then on up to the workspace root of the Astryx project it belongs to.
|
|
610
|
+
* In each folder a `packages/core` whose package.json names
|
|
611
|
+
* @astryxdesign/core (the Astryx repository's own source) comes first, then
|
|
612
|
+
* an installed `node_modules/@astryxdesign/core`. It takes the first it finds,
|
|
613
|
+
* whatever version the project declares: that is the copy Node loads. When no
|
|
614
|
+
* folder holds one, it asks Yarn Plug'n'Play, which keeps no node_modules.
|
|
615
|
+
*
|
|
616
|
+
* @param {string} startDir
|
|
617
|
+
* @returns {string|null} the Core package directory, or null when there is none
|
|
618
|
+
*/
|
|
619
|
+
function findNearbyCoreDir(startDir) {
|
|
620
|
+
for (const dir of readFolders(startDir, CORE_REACH)) {
|
|
621
|
+
const source = path.join(dir, 'packages', 'core');
|
|
622
|
+
if (isCoreSource(source)) return source;
|
|
623
|
+
const installed = installedIn(dir, CORE_PACKAGE);
|
|
624
|
+
if (installed) return installed;
|
|
625
|
+
}
|
|
626
|
+
return findPnpPackage(startDir, CORE_PACKAGE);
|
|
627
|
+
}
|
|
628
|
+
|
|
173
629
|
/**
|
|
174
630
|
* Where Yarn's Plug'n'Play runtime says a package lives.
|
|
175
631
|
*
|
|
176
|
-
* A PnP project has no node_modules for the
|
|
177
|
-
* from the project's own dependency graph and ignores NODE_PATH, so
|
|
178
|
-
* keeps the lookup project-local. Outside a PnP process there is
|
|
632
|
+
* A PnP project has no node_modules for the walks in this file to find. PnP
|
|
633
|
+
* answers from the project's own dependency graph and ignores NODE_PATH, so
|
|
634
|
+
* asking it keeps the lookup project-local. Outside a PnP process there is
|
|
635
|
+
* nothing to ask.
|
|
179
636
|
*
|
|
180
637
|
* @param {string} startDir
|
|
181
638
|
* @param {string} name
|
|
@@ -184,8 +641,9 @@ function findNearbyCoreDir(startDir) {
|
|
|
184
641
|
function findPnpPackage(startDir, name) {
|
|
185
642
|
if (!process.versions.pnp) return null;
|
|
186
643
|
try {
|
|
187
|
-
const
|
|
188
|
-
const
|
|
644
|
+
const from = path.resolve(startDir);
|
|
645
|
+
const api = createRequire(path.join(from, 'noop.js'))('pnpapi');
|
|
646
|
+
const dir = api.resolveToUnqualified(name, path.join(from, '/'));
|
|
189
647
|
return dir && fs.existsSync(path.join(dir, 'package.json')) ? dir : null;
|
|
190
648
|
} catch {
|
|
191
649
|
// The project does not declare it, or this is not a PnP process after all.
|
|
@@ -193,6 +651,24 @@ function findPnpPackage(startDir, name) {
|
|
|
193
651
|
}
|
|
194
652
|
}
|
|
195
653
|
|
|
654
|
+
/**
|
|
655
|
+
* The nearest `node_modules/<name>` at or above `startDir`, found the way
|
|
656
|
+
* Node's resolver finds a package, and the folder whose node_modules holds it.
|
|
657
|
+
* It searches every folder up to the filesystem root and does not ask
|
|
658
|
+
* Plug'n'Play.
|
|
659
|
+
*
|
|
660
|
+
* @param {string} startDir
|
|
661
|
+
* @param {string} name bare package name; callers reject path segments first
|
|
662
|
+
* @returns {{packageDir: string, hostDir: string}|null}
|
|
663
|
+
*/
|
|
664
|
+
export function findNodeModulesPackage(startDir, name) {
|
|
665
|
+
for (const dir of foldersUpFrom(startDir)) {
|
|
666
|
+
const packageDir = installedIn(dir, name);
|
|
667
|
+
if (packageDir) return {packageDir, hostDir: dir};
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
196
672
|
/**
|
|
197
673
|
* Locate a package the project at `startDir` actually installed.
|
|
198
674
|
*
|
|
@@ -204,25 +680,67 @@ function findPnpPackage(startDir, name) {
|
|
|
204
680
|
* Reading package.json off disk also sidesteps packages that don't export it,
|
|
205
681
|
* so the version is always available to range-check.
|
|
206
682
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
683
|
+
* Searches the node_modules of `startDir` and the five folders above it, as
|
|
684
|
+
* released versions did, then on up to the workspace root of the Astryx
|
|
685
|
+
* project it belongs to. Then it asks PnP, which covers the Yarn default
|
|
686
|
+
* layout where that chain does not exist.
|
|
209
687
|
*
|
|
210
688
|
* @param {string} startDir
|
|
211
689
|
* @param {string} name
|
|
212
690
|
* @returns {string|null} the package directory, or null when not installed
|
|
213
691
|
*/
|
|
214
692
|
export function findInstalledPackage(startDir, name) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (fs.existsSync(path.join(candidate, 'package.json'))) return candidate;
|
|
219
|
-
const parent = path.dirname(dir);
|
|
220
|
-
if (parent === dir) break;
|
|
221
|
-
dir = parent;
|
|
693
|
+
for (const dir of readFolders(startDir, INSTALLED_REACH)) {
|
|
694
|
+
const packageDir = installedIn(dir, name);
|
|
695
|
+
if (packageDir) return packageDir;
|
|
222
696
|
}
|
|
223
697
|
return findPnpPackage(startDir, name);
|
|
224
698
|
}
|
|
225
699
|
|
|
700
|
+
/**
|
|
701
|
+
* The first of `names` the Astryx project of `startDir` installed, for a
|
|
702
|
+
* command that writes to the project (`upgrade`).
|
|
703
|
+
*
|
|
704
|
+
* It searches the node_modules of the project root, nearest name first. For
|
|
705
|
+
* a member its workspace lists, it then searches each folder above the
|
|
706
|
+
* project up to the workspace root, where the workspace installs the packages
|
|
707
|
+
* its members share; otherwise the search stops at the project root. Then it
|
|
708
|
+
* asks Plug'n'Play, in name order. With no Astryx project it looks only in the
|
|
709
|
+
* node_modules of `startDir` itself, as released versions did.
|
|
710
|
+
*
|
|
711
|
+
* @param {string} startDir
|
|
712
|
+
* @param {readonly string[]} names in order of preference
|
|
713
|
+
* @returns {{name: string, packageDir: string}|null}
|
|
714
|
+
*/
|
|
715
|
+
export function findProjectPackageOf(startDir, names) {
|
|
716
|
+
const project = findAstryxProject(startDir);
|
|
717
|
+
if (project == null) {
|
|
718
|
+
const dir = path.resolve(startDir);
|
|
719
|
+
for (const name of names) {
|
|
720
|
+
const packageDir = installedIn(dir, name);
|
|
721
|
+
if (packageDir) return {name, packageDir};
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const workspace = workspaceBound(project);
|
|
726
|
+
const top =
|
|
727
|
+
workspace !== project && isWorkspaceMember(project, workspace)
|
|
728
|
+
? workspace
|
|
729
|
+
: project;
|
|
730
|
+
for (const dir of foldersUpFrom(project)) {
|
|
731
|
+
for (const name of names) {
|
|
732
|
+
const packageDir = installedIn(dir, name);
|
|
733
|
+
if (packageDir) return {name, packageDir};
|
|
734
|
+
}
|
|
735
|
+
if (dir === top) break;
|
|
736
|
+
}
|
|
737
|
+
for (const name of names) {
|
|
738
|
+
const packageDir = findPnpPackage(project, name);
|
|
739
|
+
if (packageDir) return {name, packageDir};
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
226
744
|
/**
|
|
227
745
|
* Find the monorepo root by looking for the root package.json
|
|
228
746
|
* that has workspaces defined.
|