@astryxdesign/cli 0.6.4-canary.ed2e54e → 0.6.4
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 +66 -64
- package/api/component/_adapter.d.mts +0 -25
- package/api/component/_adapter.mjs +5 -59
- package/api/component/component.d.mts +3 -6
- package/api/component/component.doc.mjs +10 -23
- package/api/component/component.mjs +9 -249
- package/api/component/component.type.d.mts +0 -25
- package/api/component/component.type.mjs +0 -44
- package/api/discover/_adapter.d.mts +6 -114
- package/api/discover/_adapter.mjs +17 -372
- package/api/discover/detail/detail.d.mts +6 -18
- package/api/discover/detail/detail.mjs +13 -67
- package/api/discover/detail/detail.test.mjs +0 -85
- package/api/discover/discover.d.mts +9 -3
- package/api/discover/discover.doc.mjs +18 -61
- package/api/discover/discover.mjs +36 -220
- package/api/discover/discover.test.mjs +2 -11
- package/api/discover/discover.type.d.mts +8 -147
- package/api/discover/discover.type.mjs +12 -102
- package/api/discover/list/list.d.mts +6 -20
- package/api/discover/list/list.mjs +12 -45
- package/api/discover/list/list.test.mjs +0 -46
- package/api/discover/search/search.d.mts +16 -18
- package/api/discover/search/search.mjs +56 -102
- package/api/discover/search/search.test.mjs +10 -144
- package/api/docs/docs.test.mjs +0 -2
- package/api/doctor/doctor.d.mts +3 -8
- package/api/doctor/doctor.mjs +9 -90
- package/api/doctor/doctor.test.mjs +10 -122
- package/api/index.d.mts +2 -1
- package/api/index.mjs +4 -4
- package/api/integration/add-helpers.d.mts +2 -5
- package/api/integration/add-helpers.mjs +9 -36
- package/api/integration/pack-check.mjs +3 -28
- package/api/json/index.ts +1 -0
- package/api/layout/_adapter.d.mts +34 -0
- package/api/layout/_adapter.mjs +148 -0
- package/api/layout/check/check.d.mts +16 -0
- package/api/layout/check/check.mjs +40 -0
- package/api/layout/expand/expand.d.mts +22 -0
- package/api/layout/expand/expand.mjs +155 -0
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.d.mts +13 -0
- package/api/layout/grammar/grammar.mjs +87 -0
- package/api/layout/layout.d.mts +6 -0
- package/api/layout/layout.mjs +17 -0
- package/api/layout/layout.test.mjs +297 -0
- package/api/layout/layout.type.d.mts +89 -0
- package/api/layout/layout.type.mjs +103 -0
- package/api/layout/layoutCheck.doc.d.mts +11 -0
- package/api/layout/layoutCheck.doc.mjs +85 -0
- package/api/layout/layoutExpand.doc.d.mts +11 -0
- package/api/layout/layoutExpand.doc.mjs +107 -0
- package/api/layout/layoutGrammar.doc.d.mts +11 -0
- package/api/layout/layoutGrammar.doc.mjs +57 -0
- package/api/search/search.test.mjs +0 -18
- package/api/template/template-integration.test.mjs +65 -1
- package/api/template/template.mjs +1 -1
- package/api/theme/add/add.mjs +25 -17
- package/api/theme/add/add.staging.test.mjs +23 -40
- package/api/theme/build/build.family.test.mjs +12 -7
- package/api/theme/build/build.mjs +18 -8
- package/api/upgrade/run/run.mjs +4 -6
- package/api/upgrade/upgrade.type.mjs +2 -2
- package/assets/codemods/__tests__/runner.test.mjs +1 -3
- package/assets/codemods/integration-runner.mjs +3 -3
- package/assets/codemods/runner.mjs +4 -5
- package/assets/docs/internationalization.doc.mjs +5 -7
- package/assets/docs/tree/integrations.doc.mjs +1 -20
- package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
- package/authoring/config/config.doc.mjs +1 -9
- package/authoring/config/parse.d.mts +0 -2
- package/authoring/config/parse.mjs +0 -19
- package/authoring/config/parse.test.mjs +0 -8
- package/authoring/config/type.ts +2 -13
- package/authoring/doctypes/command/command.doc.mjs +1 -1
- package/authoring/doctypes/command/type.ts +1 -1
- package/authoring/index.d.mts +0 -1
- package/authoring/index.d.ts +0 -10
- package/authoring/index.mjs +0 -1
- package/clients/cli/command-result-coverage.test.mjs +7 -7
- package/clients/cli/commands/component/index.mjs +55 -152
- package/clients/cli/commands/component-ownership.test.mjs +0 -89
- package/clients/cli/commands/component.doc.mjs +6 -23
- package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
- package/clients/cli/commands/discover.doc.mjs +9 -53
- package/clients/cli/commands/discover.mjs +118 -393
- package/clients/cli/commands/docs.test.mjs +0 -29
- package/clients/cli/commands/layout-check.doc.mjs +65 -0
- package/clients/cli/commands/layout-expand.doc.mjs +83 -0
- package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
- package/clients/cli/commands/layout.doc.mjs +34 -0
- package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
- package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
- package/clients/cli/commands/layout.mjs +275 -0
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/text-json-parity.test.mjs +17 -0
- package/clients/cli/index.mjs +4 -0
- package/clients/cli/lib/exit-codes.test.mjs +8 -1
- package/clients/cli/lib/json-shim.mjs +14 -24
- package/clients/cli/lib/json-shim.test.mjs +20 -6
- package/clients/cli/lib/manifest.mjs +8 -3
- package/clients/cli/lib/manifest.test.mjs +2 -5
- package/foundation/discovery/authoring-self-docs.mjs +0 -1
- package/foundation/discovery/template-adapter.mjs +1 -1
- package/foundation/doc-compiler/doc-loads.test.mjs +12 -0
- package/foundation/doc-compiler/tree.test.mjs +1 -9
- package/foundation/integrations/integrations.d.mts +1 -14
- package/foundation/integrations/integrations.mjs +1 -41
- package/foundation/integrations/integrations.test.mjs +0 -31
- package/foundation/response/response-types.doc.mjs +21 -15
- package/foundation/response/response-types.doc.test.mjs +0 -23
- package/foundation/xle/browser.d.mts +3 -3
- package/foundation/xle/browser.mjs +3 -3
- package/foundation/xle/expand.mjs +2 -2
- package/foundation/xle/parse.mjs +1 -1
- package/foundation/xle/print.mjs +2 -2
- package/foundation/xle/splice.mjs +1 -1
- package/package.json +9 -9
- package/api/discover/_adapter.test.mjs +0 -215
- package/api/discover/_catalog-view.d.mts +0 -115
- package/api/discover/_catalog-view.mjs +0 -203
- package/api/discover/_catalog-view.test.mjs +0 -128
- package/api/discover/detail/item/item.d.mts +0 -26
- package/api/discover/detail/item/item.mjs +0 -78
- package/api/discover/detail/item/item.test.mjs +0 -73
- package/api/integration/pack-check.lifecycle-output.test.mjs +0 -105
- package/api/theme/add/add.rollback.test.mjs +0 -158
- package/api/theme/build/build.rollback.test.mjs +0 -148
- package/api/upgrade/run/files-changed.test.mjs +0 -111
- package/assets/codemods/file-count.test.mjs +0 -163
- package/assets/docs/tree/component-lookups.doc.mjs +0 -149
- package/authoring/discover/discover.doc.d.mts +0 -13
- package/authoring/discover/discover.doc.mjs +0 -138
- package/authoring/discover/parse.d.mts +0 -24
- package/authoring/discover/parse.mjs +0 -128
- package/authoring/discover/parse.test.mjs +0 -124
- package/authoring/discover/type.ts +0 -87
- package/clients/cli/commands/component-batch.test.mjs +0 -341
- package/clients/cli/commands/discover.sources.test.mjs +0 -267
- package/clients/cli/lib/parse-error-format.test.mjs +0 -81
- package/foundation/response/batch.type.d.mts +0 -33
- package/foundation/response/batch.type.mjs +0 -34
|
@@ -1,81 +1,31 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file Shared adapter for the discover leaves
|
|
5
|
-
* the environment.
|
|
4
|
+
* @file Shared external-package adapter for the discover leaves.
|
|
6
5
|
*
|
|
7
6
|
* Every `discover.*` leaf projects already-resolved data into a typed response;
|
|
8
|
-
* none of them touch the integration system
|
|
9
|
-
* adapter does: it
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* none of them touch the integration system or the filesystem directly. This
|
|
8
|
+
* adapter is the single place that does: it discovers the configured external
|
|
9
|
+
* packages, locates a component's doc file within them, and loads + validates a
|
|
10
|
+
* component's docs. Keeping that I/O here (rather than duplicated across leaves)
|
|
11
|
+
* is what lets `list`, `detail`, `detail/doc`, and `search` stay pure
|
|
12
|
+
* projections.
|
|
14
13
|
*
|
|
15
|
-
* @position api/discover — orchestration over
|
|
16
|
-
*
|
|
14
|
+
* @position api/discover — orchestration over lib/project, lib/package-scanner,
|
|
15
|
+
* and lib/component-loader; the discover dispatcher + leaves consume it.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
|
-
import {createHash} from 'node:crypto';
|
|
20
|
-
import * as fs from 'node:fs';
|
|
21
|
-
import * as os from 'node:os';
|
|
22
|
-
import * as path from 'node:path';
|
|
23
18
|
import {Project} from '../../foundation/config/project.mjs';
|
|
24
19
|
import {
|
|
25
20
|
scanAllPackages,
|
|
26
21
|
findComponentInPackages,
|
|
27
22
|
} from './_package-scanner.mjs';
|
|
28
23
|
import {loadDocs} from '../../foundation/discovery/component-loader.mjs';
|
|
29
|
-
import {collectIdentities} from '../../foundation/integrations/contribution-inventory.mjs';
|
|
30
|
-
import {detectPackageManager} from '../../foundation/env/package-manager.mjs';
|
|
31
|
-
import {assertWithin} from '../../foundation/fs/path-safety.mjs';
|
|
32
|
-
import {parseDiscoverCatalog} from '../../authoring/discover/parse.mjs';
|
|
33
24
|
import {AstryxError} from '../error.mjs';
|
|
34
25
|
import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
|
|
35
26
|
|
|
36
|
-
const SOURCE_TIMEOUT_MS = 30_000;
|
|
37
|
-
|
|
38
|
-
const DEPENDENCY_FIELDS = [
|
|
39
|
-
'dependencies',
|
|
40
|
-
'devDependencies',
|
|
41
|
-
'optionalDependencies',
|
|
42
|
-
'peerDependencies',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
27
|
/**
|
|
46
28
|
* @typedef {import('./_package-scanner.mjs').ScannedPackage} ScannedPackage
|
|
47
|
-
* @typedef {import('../../authoring/discover/type').DiscoverPackage} DiscoverPackage
|
|
48
|
-
* @typedef {import('../../authoring/discover/type').DiscoverSource} DiscoverSource
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* An installed integration with what it adds, per kind. `components` is the
|
|
53
|
-
* scanned component list the discover leaves have always used.
|
|
54
|
-
* @typedef {ScannedPackage & {
|
|
55
|
-
* templates?: string[],
|
|
56
|
-
* docs?: string[],
|
|
57
|
-
* themes?: string[],
|
|
58
|
-
* codemods?: string[],
|
|
59
|
-
* }} InstalledPackage
|
|
60
|
-
*/
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* What one source did on this run.
|
|
64
|
-
* @typedef {object} SourceState
|
|
65
|
-
* @property {string} name the source's own name, or where it came from when it
|
|
66
|
-
* gave none
|
|
67
|
-
* @property {string} from `astryx.config`, or the integration that exports it
|
|
68
|
-
* @property {'fresh' | 'saved' | 'failed'} status
|
|
69
|
-
* @property {string} [generatedAt]
|
|
70
|
-
* @property {boolean} [complete]
|
|
71
|
-
* @property {string} [savedAt] when the saved copy in use was written
|
|
72
|
-
* @property {string} [error] why the live call failed
|
|
73
|
-
*/
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Everything every source returned, merged. When two sources list the same
|
|
77
|
-
* package, the earlier source's entry is kept.
|
|
78
|
-
* @typedef {{sources: SourceState[], packages: Array<DiscoverPackage & {source: string}>}} CatalogResult
|
|
79
29
|
*/
|
|
80
30
|
|
|
81
31
|
/**
|
|
@@ -118,7 +68,7 @@ function validateDocs(docs) {
|
|
|
118
68
|
* configured" (`false`) from "configured but nothing discovered" (`true`),
|
|
119
69
|
* which the list leaf surfaces as `meta`.
|
|
120
70
|
*
|
|
121
|
-
* @returns {Promise<{packages: ScannedPackage[], configured: boolean
|
|
71
|
+
* @returns {Promise<{packages: ScannedPackage[], configured: boolean}>}
|
|
122
72
|
*/
|
|
123
73
|
export async function discoverPackages() {
|
|
124
74
|
const project = await Project.load();
|
|
@@ -136,323 +86,25 @@ export async function discoverPackages() {
|
|
|
136
86
|
docsDir: integration.components,
|
|
137
87
|
}));
|
|
138
88
|
if (explicitPackages.length === 0) {
|
|
139
|
-
return {packages: [], configured: loadedIntegrations.length > 0
|
|
89
|
+
return {packages: [], configured: loadedIntegrations.length > 0};
|
|
140
90
|
}
|
|
141
91
|
|
|
142
92
|
const packages = scanAllPackages(
|
|
143
93
|
[],
|
|
144
94
|
/** @type {ScannedPackage[]} */ (/** @type {unknown} */ (explicitPackages)),
|
|
145
95
|
);
|
|
146
|
-
return {packages, configured: true
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Every integration the project loads, with what it adds. An integration with
|
|
151
|
-
* no components root is included with `components: []`; one that failed to
|
|
152
|
-
* load is left out (the integration-issues nudge and Doctor report it).
|
|
153
|
-
*
|
|
154
|
-
* @param {Project | undefined} project
|
|
155
|
-
* @param {ScannedPackage[]} scanned the packages {@link discoverPackages} found
|
|
156
|
-
* @returns {Promise<InstalledPackage[]>}
|
|
157
|
-
*/
|
|
158
|
-
export async function describeInstalled(project, scanned) {
|
|
159
|
-
if (project == null) return scanned;
|
|
160
|
-
const byName = new Map(scanned.map(pkg => [pkg.name, pkg]));
|
|
161
|
-
/** @type {InstalledPackage[]} */
|
|
162
|
-
const installed = [];
|
|
163
|
-
for (const integration of project.loadedIntegrations) {
|
|
164
|
-
if (integration.__loadError) continue;
|
|
165
|
-
const base =
|
|
166
|
-
byName.get(integration.name) ??
|
|
167
|
-
/** @type {ScannedPackage} */ (
|
|
168
|
-
/** @type {unknown} */ ({
|
|
169
|
-
name: integration.name,
|
|
170
|
-
category: integration.name,
|
|
171
|
-
version: integration.version,
|
|
172
|
-
components: [],
|
|
173
|
-
})
|
|
174
|
-
);
|
|
175
|
-
// Components come from the scan above; loading every component doc again
|
|
176
|
-
// here would only slow the list down.
|
|
177
|
-
const {identities} = await collectIdentities({
|
|
178
|
-
...integration,
|
|
179
|
-
components: undefined,
|
|
180
|
-
});
|
|
181
|
-
installed.push({
|
|
182
|
-
...base,
|
|
183
|
-
templates: identities.templates.map(t => t.id),
|
|
184
|
-
docs: identities.docs,
|
|
185
|
-
themes: identities.themes.map(t => t.slug),
|
|
186
|
-
codemods: identities.codemods.map(c => `${c.version}/${c.id}`),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
return installed;
|
|
96
|
+
return {packages, configured: true};
|
|
190
97
|
}
|
|
191
98
|
|
|
192
99
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* @
|
|
197
|
-
*/
|
|
198
|
-
export function hasSources(project) {
|
|
199
|
-
if (project == null) return false;
|
|
200
|
-
if (typeof project.config?.discover === 'function') return true;
|
|
201
|
-
return project.loadedIntegrations.some(
|
|
202
|
-
i => i.__discover != null || i.__discoverError != null,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Package names the project's package.json declares in any dependency field.
|
|
208
|
-
* @param {Project | undefined} project
|
|
209
|
-
* @returns {Set<string>}
|
|
210
|
-
*/
|
|
211
|
-
export function declaredDependencies(project) {
|
|
212
|
-
/** @type {Set<string>} */
|
|
213
|
-
const names = new Set();
|
|
214
|
-
if (project == null) return names;
|
|
215
|
-
try {
|
|
216
|
-
const pkg = JSON.parse(
|
|
217
|
-
fs.readFileSync(path.join(project.cwd, 'package.json'), 'utf8'),
|
|
218
|
-
);
|
|
219
|
-
for (const field of DEPENDENCY_FIELDS) {
|
|
220
|
-
for (const name of Object.keys(pkg?.[field] ?? {})) names.add(name);
|
|
221
|
-
}
|
|
222
|
-
} catch {
|
|
223
|
-
// No readable package.json: nothing is declared.
|
|
224
|
-
}
|
|
225
|
-
return names;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* The command that adds a package with the project's package manager. Discover
|
|
230
|
-
* only prints it; it never runs it.
|
|
231
|
-
* @param {Project | undefined} project
|
|
232
|
-
* @returns {(name: string, version?: string) => string}
|
|
233
|
-
*/
|
|
234
|
-
export function addCommand(project) {
|
|
235
|
-
const pm = detectPackageManager(project?.cwd ?? process.cwd());
|
|
236
|
-
const verb =
|
|
237
|
-
pm === 'pnpm'
|
|
238
|
-
? 'pnpm add'
|
|
239
|
-
: pm === 'yarn'
|
|
240
|
-
? 'yarn add'
|
|
241
|
-
: pm === 'bun'
|
|
242
|
-
? 'bun add'
|
|
243
|
-
: 'npm install';
|
|
244
|
-
return (name, version) => `${verb} ${version ? `${name}@${version}` : name}`;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* The per-user cache directory the saved copies live in, never inside a
|
|
249
|
-
* project. Standard platform locations only.
|
|
250
|
-
* @returns {string}
|
|
251
|
-
*/
|
|
252
|
-
export function defaultCacheDir() {
|
|
253
|
-
const home = os.homedir();
|
|
254
|
-
if (process.platform === 'win32') {
|
|
255
|
-
return path.join(
|
|
256
|
-
process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'),
|
|
257
|
-
'astryx',
|
|
258
|
-
'Cache',
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
if (process.platform === 'darwin') {
|
|
262
|
-
return path.join(home, 'Library', 'Caches', 'astryx');
|
|
263
|
-
}
|
|
264
|
-
return path.join(
|
|
265
|
-
process.env.XDG_CACHE_HOME || path.join(home, '.cache'),
|
|
266
|
-
'astryx',
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* @param {string} cacheDir
|
|
272
|
-
* @param {string} from
|
|
273
|
-
* @param {{package?: string, version?: string}} request
|
|
274
|
-
*/
|
|
275
|
-
function savedCopyPath(cacheDir, from, request) {
|
|
276
|
-
const key = createHash('sha256')
|
|
277
|
-
.update(`${from}\n${request.package ?? ''}\n${request.version ?? ''}`)
|
|
278
|
-
.digest('hex')
|
|
279
|
-
.slice(0, 32);
|
|
280
|
-
const dir = path.join(cacheDir, 'discover');
|
|
281
|
-
return assertWithin(`${key}.json`, dir, {label: 'discover saved copy'});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* @param {string} file
|
|
286
|
-
* @param {string} label
|
|
287
|
-
*/
|
|
288
|
-
function readSavedCopy(file, label) {
|
|
289
|
-
try {
|
|
290
|
-
const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
291
|
-
return {
|
|
292
|
-
savedAt: String(saved.savedAt),
|
|
293
|
-
catalog: parseDiscoverCatalog(saved.catalog, label),
|
|
294
|
-
};
|
|
295
|
-
} catch {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* @param {string} file
|
|
302
|
-
* @param {import('../../authoring/discover/type').DiscoverCatalog} catalog
|
|
303
|
-
*/
|
|
304
|
-
function writeSavedCopy(file, catalog) {
|
|
305
|
-
try {
|
|
306
|
-
fs.mkdirSync(path.dirname(file), {recursive: true});
|
|
307
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
308
|
-
fs.writeFileSync(
|
|
309
|
-
tmp,
|
|
310
|
-
JSON.stringify({savedAt: new Date().toISOString(), catalog}),
|
|
311
|
-
);
|
|
312
|
-
fs.renameSync(tmp, file);
|
|
313
|
-
} catch {
|
|
314
|
-
// An unwritable cache only costs the offline copy.
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* @param {DiscoverSource} source
|
|
320
|
-
* @param {{package?: string, version?: string}} request
|
|
321
|
-
* @param {string} label
|
|
322
|
-
* @param {number} timeoutMs
|
|
323
|
-
*/
|
|
324
|
-
async function callSource(source, request, label, timeoutMs) {
|
|
325
|
-
const controller = new AbortController();
|
|
326
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
327
|
-
let timer;
|
|
328
|
-
const timedOut = new Promise((_, reject) => {
|
|
329
|
-
timer = setTimeout(() => {
|
|
330
|
-
controller.abort();
|
|
331
|
-
reject(
|
|
332
|
-
new Error(`${label} did not answer within ${timeoutMs / 1000} seconds`),
|
|
333
|
-
);
|
|
334
|
-
}, timeoutMs);
|
|
335
|
-
});
|
|
336
|
-
try {
|
|
337
|
-
const answer = await Promise.race([
|
|
338
|
-
Promise.resolve().then(() =>
|
|
339
|
-
source({signal: controller.signal, ...request}),
|
|
340
|
-
),
|
|
341
|
-
timedOut,
|
|
342
|
-
]);
|
|
343
|
-
return parseDiscoverCatalog(answer, label);
|
|
344
|
-
} finally {
|
|
345
|
-
clearTimeout(timer);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Call every discover source — the project's `discover` first, then each
|
|
351
|
-
* integration's in load order — once per distinct function. A source that
|
|
352
|
-
* throws, times out, or returns an invalid catalog falls back to its saved copy;
|
|
353
|
-
* with none, it is reported and adds nothing. The others always count.
|
|
354
|
-
*
|
|
355
|
-
* @param {Project | undefined} project
|
|
356
|
-
* @param {{package?: string, version?: string}} [request] ask for one package,
|
|
357
|
-
* and optionally one version's contributions
|
|
358
|
-
* @param {{cacheDir?: string, timeoutMs?: number}} [options]
|
|
359
|
-
* @returns {Promise<CatalogResult>}
|
|
360
|
-
*/
|
|
361
|
-
export async function callSources(project, request = {}, options = {}) {
|
|
362
|
-
const {cacheDir = defaultCacheDir(), timeoutMs = SOURCE_TIMEOUT_MS} = options;
|
|
363
|
-
/** @type {CatalogResult} */
|
|
364
|
-
const result = {sources: [], packages: []};
|
|
365
|
-
if (project == null) return result;
|
|
366
|
-
|
|
367
|
-
/** @type {Array<{from: string, source?: DiscoverSource, error?: string}>} */
|
|
368
|
-
const origins = [];
|
|
369
|
-
if (typeof project.config?.discover === 'function') {
|
|
370
|
-
origins.push({from: 'astryx.config', source: project.config.discover});
|
|
371
|
-
}
|
|
372
|
-
for (const integration of project.loadedIntegrations) {
|
|
373
|
-
if (integration.__discoverError) {
|
|
374
|
-
origins.push({from: integration.name, error: integration.__discoverError});
|
|
375
|
-
} else if (integration.__discover) {
|
|
376
|
-
origins.push({from: integration.name, source: integration.__discover});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** @type {Set<unknown>} */
|
|
381
|
-
const called = new Set();
|
|
382
|
-
/** @type {Set<string>} */
|
|
383
|
-
const seen = new Set();
|
|
384
|
-
const ask = request.package
|
|
385
|
-
? {package: request.package, ...(request.version ? {version: request.version} : {})}
|
|
386
|
-
: {};
|
|
387
|
-
for (const {from, source, error} of origins) {
|
|
388
|
-
if (source == null) {
|
|
389
|
-
result.sources.push({name: from, from, status: 'failed', error});
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
if (called.has(source)) continue;
|
|
393
|
-
called.add(source);
|
|
394
|
-
|
|
395
|
-
const label = `the discover source from ${from}`;
|
|
396
|
-
const file = savedCopyPath(cacheDir, from, ask);
|
|
397
|
-
/** @type {import('../../authoring/discover/type').DiscoverCatalog} */
|
|
398
|
-
let catalog;
|
|
399
|
-
try {
|
|
400
|
-
catalog = await callSource(source, ask, label, timeoutMs);
|
|
401
|
-
writeSavedCopy(file, catalog);
|
|
402
|
-
result.sources.push({
|
|
403
|
-
name: catalog.source.name,
|
|
404
|
-
from,
|
|
405
|
-
status: 'fresh',
|
|
406
|
-
generatedAt: catalog.source.generatedAt,
|
|
407
|
-
complete: catalog.source.complete,
|
|
408
|
-
});
|
|
409
|
-
} catch (err) {
|
|
410
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
411
|
-
const saved = readSavedCopy(file, label);
|
|
412
|
-
if (saved == null) {
|
|
413
|
-
result.sources.push({name: from, from, status: 'failed', error: message});
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
catalog = saved.catalog;
|
|
417
|
-
result.sources.push({
|
|
418
|
-
name: catalog.source.name,
|
|
419
|
-
from,
|
|
420
|
-
status: 'saved',
|
|
421
|
-
generatedAt: catalog.source.generatedAt,
|
|
422
|
-
complete: catalog.source.complete,
|
|
423
|
-
savedAt: saved.savedAt,
|
|
424
|
-
error: message,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
for (const pkg of catalog.packages) {
|
|
428
|
-
if (seen.has(pkg.package)) continue;
|
|
429
|
-
seen.add(pkg.package);
|
|
430
|
-
result.packages.push({...pkg, source: catalog.source.name});
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return result;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** The list and detail entry fields for each item kind, in display order. */
|
|
437
|
-
export const KIND_FIELDS = /** @type {const} */ ([
|
|
438
|
-
['component', 'components'],
|
|
439
|
-
['template', 'templates'],
|
|
440
|
-
['doc', 'docs'],
|
|
441
|
-
['theme', 'themes'],
|
|
442
|
-
['codemod', 'codemods'],
|
|
443
|
-
['agent-doc', 'agentDocs'],
|
|
444
|
-
]);
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Project a scanned or installed package into the discover list/detail entry
|
|
448
|
-
* shape. Shared by the list and detail leaves so the entry keys (and their
|
|
449
|
-
* order) stay in one place. A kind list appears only when it is not empty.
|
|
450
|
-
* @param {InstalledPackage & {latest?: string}} pkg
|
|
100
|
+
* Project a scanned package into the discover list/detail entry shape. Shared
|
|
101
|
+
* by the list and detail leaves so the entry keys (and their order) stay in one
|
|
102
|
+
* place.
|
|
103
|
+
* @param {ScannedPackage} pkg
|
|
451
104
|
* @returns {import('./discover.type.mjs').DiscoverListEntry}
|
|
452
105
|
*/
|
|
453
106
|
export function toEntry(pkg) {
|
|
454
|
-
|
|
455
|
-
const entry = {
|
|
107
|
+
return {
|
|
456
108
|
name: pkg.name,
|
|
457
109
|
category: pkg.category,
|
|
458
110
|
components: pkg.components,
|
|
@@ -460,13 +112,6 @@ export function toEntry(pkg) {
|
|
|
460
112
|
description: pkg.description,
|
|
461
113
|
displayName: pkg.displayName,
|
|
462
114
|
};
|
|
463
|
-
for (const [, field] of KIND_FIELDS) {
|
|
464
|
-
if (field === 'components') continue;
|
|
465
|
-
const values = /** @type {Record<string, unknown>} */ (pkg)[field];
|
|
466
|
-
if (Array.isArray(values) && values.length > 0) entry[field] = values;
|
|
467
|
-
}
|
|
468
|
-
if (pkg.latest) entry.latest = pkg.latest;
|
|
469
|
-
return /** @type {import('./discover.type.mjs').DiscoverListEntry} */ (entry);
|
|
470
115
|
}
|
|
471
116
|
|
|
472
117
|
/**
|
|
@@ -2,24 +2,12 @@
|
|
|
2
2
|
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Build the discover.detail response for
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* that adds it — printed, never run.
|
|
5
|
+
* Build the discover.detail response for a scoped package name. Throws
|
|
6
|
+
* AstryxError (ERR_UNKNOWN_PACKAGE) — with the available packages as
|
|
7
|
+
* suggestions — when no package matches.
|
|
9
8
|
*
|
|
10
|
-
* @param {import('../
|
|
11
|
-
* @param {string}
|
|
12
|
-
* @param {{
|
|
13
|
-
* catalog?: import('../_catalog-view.mjs').CatalogPackage,
|
|
14
|
-
* version?: string,
|
|
15
|
-
* add?: (name: string, version?: string) => string,
|
|
16
|
-
* installedAs?: string,
|
|
17
|
-
* }} [options]
|
|
9
|
+
* @param {import('../_package-scanner.mjs').ScannedPackage[]} packages
|
|
10
|
+
* @param {string} query scoped package name, e.g. `@scope/name`
|
|
18
11
|
* @returns {import('../discover.type.mjs').DiscoverDetailResponse}
|
|
19
12
|
*/
|
|
20
|
-
export function detail(packages: import("../
|
|
21
|
-
catalog?: import("../_catalog-view.mjs").CatalogPackage;
|
|
22
|
-
version?: string;
|
|
23
|
-
add?: (name: string, version?: string) => string;
|
|
24
|
-
installedAs?: string;
|
|
25
|
-
}): import("../discover.type.mjs").DiscoverDetailResponse;
|
|
13
|
+
export function detail(packages: import("../_package-scanner.mjs").ScannedPackage[], query: string): import("../discover.type.mjs").DiscoverDetailResponse;
|
|
@@ -1,87 +1,33 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file discover.detail leaf — one package
|
|
5
|
-
* source knows it, every version and the latest one.
|
|
4
|
+
* @file discover.detail leaf — browse one external package (@scope/name).
|
|
6
5
|
*
|
|
7
|
-
* @position api/discover/detail — pure projection over the packages
|
|
8
|
-
*
|
|
9
|
-
* a package neither the project nor any source knows.
|
|
6
|
+
* @position api/discover/detail — pure projection over the packages resolved by
|
|
7
|
+
* ../_adapter; throws AstryxError (ERR_UNKNOWN_PACKAGE) for an unknown scope.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import {toEntry} from '../_adapter.mjs';
|
|
13
|
-
import {catalogEntry, defaultVersion} from '../_catalog-view.mjs';
|
|
14
11
|
import {AstryxError} from '../../error.mjs';
|
|
15
12
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
|
-
* Build the discover.detail response for
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* that adds it — printed, never run.
|
|
15
|
+
* Build the discover.detail response for a scoped package name. Throws
|
|
16
|
+
* AstryxError (ERR_UNKNOWN_PACKAGE) — with the available packages as
|
|
17
|
+
* suggestions — when no package matches.
|
|
22
18
|
*
|
|
23
|
-
* @param {import('../
|
|
24
|
-
* @param {string}
|
|
25
|
-
* @param {{
|
|
26
|
-
* catalog?: import('../_catalog-view.mjs').CatalogPackage,
|
|
27
|
-
* version?: string,
|
|
28
|
-
* add?: (name: string, version?: string) => string,
|
|
29
|
-
* installedAs?: string,
|
|
30
|
-
* }} [options]
|
|
19
|
+
* @param {import('../_package-scanner.mjs').ScannedPackage[]} packages
|
|
20
|
+
* @param {string} query scoped package name, e.g. `@scope/name`
|
|
31
21
|
* @returns {import('../discover.type.mjs').DiscoverDetailResponse}
|
|
32
22
|
*/
|
|
33
|
-
export function detail(packages,
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
if (!pkg && !catalog) {
|
|
23
|
+
export function detail(packages, query) {
|
|
24
|
+
const pkg = packages.find(p => p.name === query);
|
|
25
|
+
if (!pkg) {
|
|
37
26
|
throw new AstryxError(
|
|
38
|
-
`Package "${
|
|
27
|
+
`Package "${query}" not found`,
|
|
39
28
|
packages.map(p => ({name: p.name, reason: 'available package'})),
|
|
40
29
|
ERROR_CODES.ERR_UNKNOWN_PACKAGE,
|
|
41
30
|
);
|
|
42
31
|
}
|
|
43
|
-
|
|
44
|
-
const known = new Set([
|
|
45
|
-
...(catalog?.versions.map(v => v.version) ?? []),
|
|
46
|
-
...(pkg?.version ? [pkg.version] : []),
|
|
47
|
-
]);
|
|
48
|
-
if (version && !known.has(version)) {
|
|
49
|
-
const releases = catalog?.versions.filter(v => !v.prerelease) ?? [];
|
|
50
|
-
throw new AstryxError(
|
|
51
|
-
`Version "${version}" of ${name} not found`,
|
|
52
|
-
(releases.length > 0 ? releases.map(v => v.version) : [...known])
|
|
53
|
-
.slice(0, 5)
|
|
54
|
-
.map(v => ({name: `${name}@${v}`, reason: 'published version'})),
|
|
55
|
-
ERROR_CODES.ERR_NOT_FOUND,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const local = pkg != null && (!version || version === pkg.version);
|
|
60
|
-
/** @type {Record<string, unknown>} */
|
|
61
|
-
const data = local
|
|
62
|
-
? {...toEntry(pkg), installed: true}
|
|
63
|
-
: {
|
|
64
|
-
...catalogEntry(
|
|
65
|
-
/** @type {NonNullable<typeof catalog>} */ (catalog),
|
|
66
|
-
version ?? defaultVersion(/** @type {NonNullable<typeof catalog>} */ (catalog)),
|
|
67
|
-
),
|
|
68
|
-
installed: pkg != null,
|
|
69
|
-
...(pkg?.version ? {installedVersion: pkg.version} : {}),
|
|
70
|
-
};
|
|
71
|
-
if (catalog) {
|
|
72
|
-
if (catalog.latest) data.latest = catalog.latest;
|
|
73
|
-
if (catalog.aliases.length > 0) data.aliases = catalog.aliases;
|
|
74
|
-
data.source = catalog.source;
|
|
75
|
-
}
|
|
76
|
-
if (!pkg) {
|
|
77
|
-
if (installedAs) data.installedAs = installedAs;
|
|
78
|
-
else if (add) data.install = add(name, version);
|
|
79
|
-
}
|
|
80
|
-
if (catalog) data.versions = catalog.versions;
|
|
81
|
-
return {
|
|
82
|
-
type: 'discover.detail',
|
|
83
|
-
data: /** @type {import('../discover.type.mjs').DiscoverDetailResponse['data']} */ (
|
|
84
|
-
data
|
|
85
|
-
),
|
|
86
|
-
};
|
|
32
|
+
return {type: 'discover.detail', data: toEntry(pkg)};
|
|
87
33
|
}
|
|
@@ -36,7 +36,6 @@ describe('discover.detail leaf', () => {
|
|
|
36
36
|
category: '@acme/widgets',
|
|
37
37
|
components: ['Alpha', 'Beta'],
|
|
38
38
|
version: '1.2.3',
|
|
39
|
-
installed: true,
|
|
40
39
|
});
|
|
41
40
|
});
|
|
42
41
|
|
|
@@ -56,87 +55,3 @@ describe('discover.detail leaf', () => {
|
|
|
56
55
|
]);
|
|
57
56
|
});
|
|
58
57
|
});
|
|
59
|
-
|
|
60
|
-
describe('discover.detail leaf with a discover source', () => {
|
|
61
|
-
/** @type {any} */
|
|
62
|
-
const catalog = {
|
|
63
|
-
package: '@acme/charts',
|
|
64
|
-
integration: 'acme-charts',
|
|
65
|
-
aliases: ['@acme/charts-legacy'],
|
|
66
|
-
latest: '2.0.0',
|
|
67
|
-
source: 'Acme',
|
|
68
|
-
versions: [
|
|
69
|
-
{version: '2.1.0-beta.1', publishedAt: '2026-09-30T00:00:00.000Z', prerelease: true, status: 'ok'},
|
|
70
|
-
{version: '2.0.0', publishedAt: '2026-09-20T00:00:00.000Z', prerelease: false, status: 'ok'},
|
|
71
|
-
{version: '1.0.0', publishedAt: null, prerelease: false, status: 'manifest_load_error'},
|
|
72
|
-
],
|
|
73
|
-
contributions: [
|
|
74
|
-
{kind: 'component', name: 'Chart'},
|
|
75
|
-
{kind: 'template', name: 'pages/Report'},
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
/** @param {string} name @param {string} [version] */
|
|
79
|
-
const add = (name, version) => `pnpm add ${version ? `${name}@${version}` : name}`;
|
|
80
|
-
|
|
81
|
-
it('describes a package the project does not have, with the command that adds it', () => {
|
|
82
|
-
expect(detail(PACKAGES, '@acme/charts', {catalog, add}).data).toEqual({
|
|
83
|
-
name: '@acme/charts',
|
|
84
|
-
category: '@acme/charts',
|
|
85
|
-
components: ['Chart'],
|
|
86
|
-
version: '2.0.0',
|
|
87
|
-
templates: ['pages/Report'],
|
|
88
|
-
installed: false,
|
|
89
|
-
latest: '2.0.0',
|
|
90
|
-
aliases: ['@acme/charts-legacy'],
|
|
91
|
-
source: 'Acme',
|
|
92
|
-
install: 'pnpm add @acme/charts',
|
|
93
|
-
versions: catalog.versions,
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('describes any version, prereleases included', () => {
|
|
98
|
-
const {data} = detail(PACKAGES, '@acme/charts', {catalog, add, version: '2.1.0-beta.1'});
|
|
99
|
-
expect(data.version).toBe('2.1.0-beta.1');
|
|
100
|
-
expect(data.install).toBe('pnpm add @acme/charts@2.1.0-beta.1');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('refuses a version that was never published, and suggests releases', () => {
|
|
104
|
-
let err;
|
|
105
|
-
try {
|
|
106
|
-
detail(PACKAGES, '@acme/charts', {catalog, add, version: '3.0.0'});
|
|
107
|
-
} catch (e) {
|
|
108
|
-
err = e;
|
|
109
|
-
}
|
|
110
|
-
expect(err).toBeInstanceOf(AstryxError);
|
|
111
|
-
expect(err.code).toBe('ERR_NOT_FOUND');
|
|
112
|
-
expect(err.suggestions.map(s => s.name)).toEqual([
|
|
113
|
-
'@acme/charts@2.0.0',
|
|
114
|
-
'@acme/charts@1.0.0',
|
|
115
|
-
]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('names the package the project has instead of offering an alias', () => {
|
|
119
|
-
const {data} = detail(PACKAGES, '@acme/charts', {
|
|
120
|
-
catalog,
|
|
121
|
-
add,
|
|
122
|
-
installedAs: '@acme/charts-legacy',
|
|
123
|
-
});
|
|
124
|
-
expect(data.installedAs).toBe('@acme/charts-legacy');
|
|
125
|
-
expect(data).not.toHaveProperty('install');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("keeps describing the installed copy from the project, with the source's versions", () => {
|
|
129
|
-
const {data} = detail(PACKAGES, '@acme/widgets', {
|
|
130
|
-
catalog: {...catalog, package: '@acme/widgets', latest: '1.3.0'},
|
|
131
|
-
add,
|
|
132
|
-
});
|
|
133
|
-
expect(data).toMatchObject({
|
|
134
|
-
name: '@acme/widgets',
|
|
135
|
-
components: ['Alpha', 'Beta'],
|
|
136
|
-
version: '1.2.3',
|
|
137
|
-
installed: true,
|
|
138
|
-
latest: '1.3.0',
|
|
139
|
-
});
|
|
140
|
-
expect(data).not.toHaveProperty('install');
|
|
141
|
-
});
|
|
142
|
-
});
|