@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
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A hosted run end to end, through `@astryxdesign/cli/host` in a host
|
|
5
|
+
* process (architecture:cli-surface INV30, spec AST-081 FR1, FR2, FR9): a
|
|
6
|
+
* release other than this copy comes from the first package set that
|
|
7
|
+
* qualifies (another release's host entry runs in this process with the same
|
|
8
|
+
* inputs, discover sources included, and the call settles with its result), a
|
|
9
|
+
* hosted run installs nothing, a declined project CLI never runs, the host's
|
|
10
|
+
* discover sources are called and named as the host's, and the result names
|
|
11
|
+
* each package the run resolved and where it came from.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {describe, it, expect, afterEach} from 'vitest';
|
|
15
|
+
import {spawnSync} from 'node:child_process';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import {fileURLToPath} from 'node:url';
|
|
19
|
+
import {
|
|
20
|
+
cleanupFixtures,
|
|
21
|
+
installCore,
|
|
22
|
+
installStandInCli,
|
|
23
|
+
project,
|
|
24
|
+
tempDir,
|
|
25
|
+
writeJson,
|
|
26
|
+
} from '../../test-utils/release-fixtures.mjs';
|
|
27
|
+
|
|
28
|
+
const HOST = fileURLToPath(new URL('../../host/index.mjs', import.meta.url));
|
|
29
|
+
const SELF_VERSION = JSON.parse(
|
|
30
|
+
fs.readFileSync(
|
|
31
|
+
fileURLToPath(new URL('../../package.json', import.meta.url)),
|
|
32
|
+
'utf-8',
|
|
33
|
+
),
|
|
34
|
+
).version;
|
|
35
|
+
|
|
36
|
+
afterEach(cleanupFixtures);
|
|
37
|
+
|
|
38
|
+
/** JavaScript for a host's `discoverSources`: one source with one package. */
|
|
39
|
+
const ACME_SOURCE = `[async () => ({
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
source: {name: 'Acme catalog', generatedAt: '2026-10-20T00:00:00.000Z', complete: true},
|
|
42
|
+
packages: [{
|
|
43
|
+
package: 'acme-widgets',
|
|
44
|
+
integration: 'acme-widgets',
|
|
45
|
+
aliases: [],
|
|
46
|
+
latest: '1.0.0',
|
|
47
|
+
versions: [{version: '1.0.0', publishedAt: null, prerelease: false, status: 'ok'}],
|
|
48
|
+
contributions: [{kind: 'component', name: 'AcmeWidget'}],
|
|
49
|
+
}],
|
|
50
|
+
})]`;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Start a host process that calls `runHostedCli` once and writes what it
|
|
54
|
+
* returned to a file.
|
|
55
|
+
* @param {Record<string, unknown>} options JSON-safe options
|
|
56
|
+
* @param {{sources?: string}} [extra] JavaScript for a `discoverSources` array
|
|
57
|
+
*/
|
|
58
|
+
function host(options, extra = {}) {
|
|
59
|
+
const home = tempDir('home');
|
|
60
|
+
const cache = tempDir('cache');
|
|
61
|
+
const out = path.join(tempDir('out'), 'result.json');
|
|
62
|
+
const script = `
|
|
63
|
+
const {runHostedCli} = await import(${JSON.stringify(HOST)});
|
|
64
|
+
const options = ${JSON.stringify(options)};
|
|
65
|
+
${extra.sources ? `options.discoverSources = ${extra.sources};` : ''}
|
|
66
|
+
const result = await runHostedCli(options);
|
|
67
|
+
(await import('node:fs')).writeFileSync(${JSON.stringify(out)}, JSON.stringify(result));
|
|
68
|
+
process.exitCode = result.status;
|
|
69
|
+
`;
|
|
70
|
+
const run = spawnSync(
|
|
71
|
+
process.execPath,
|
|
72
|
+
['--input-type=module', '-e', script],
|
|
73
|
+
{
|
|
74
|
+
cwd: tempDir('host-cwd'),
|
|
75
|
+
encoding: 'utf-8',
|
|
76
|
+
env: {
|
|
77
|
+
...Object.fromEntries(
|
|
78
|
+
Object.entries(process.env).filter(
|
|
79
|
+
([key]) => !/^(?:npm_|CI$|NO_UPDATE_NOTIFIER$)/iu.test(key),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
HOME: home,
|
|
83
|
+
USERPROFILE: home,
|
|
84
|
+
XDG_CACHE_HOME: cache,
|
|
85
|
+
LOCALAPPDATA: cache,
|
|
86
|
+
XDG_CONFIG_HOME: path.join(home, '.config'),
|
|
87
|
+
APPDATA: path.join(home, 'AppData', 'Roaming'),
|
|
88
|
+
NO_COLOR: '1',
|
|
89
|
+
npm_config_registry: 'http://127.0.0.1:9',
|
|
90
|
+
},
|
|
91
|
+
timeout: 60_000,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
return {
|
|
95
|
+
status: run.status,
|
|
96
|
+
stdout: run.stdout,
|
|
97
|
+
stderr: run.stderr,
|
|
98
|
+
cache,
|
|
99
|
+
/** @returns {any} what runHostedCli returned, when it returned */
|
|
100
|
+
result: () =>
|
|
101
|
+
fs.existsSync(out) ? JSON.parse(fs.readFileSync(out, 'utf-8')) : null,
|
|
102
|
+
/** @returns {any} the last stdout line, parsed */
|
|
103
|
+
report: () => JSON.parse(run.stdout.trim().split('\n').pop() ?? 'null'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A package set holding a stand-in CLI and a Core of `version`; with
|
|
109
|
+
* `hostEntry`, the CLI ships a stand-in host entry that reports its options,
|
|
110
|
+
* with the name of the catalog each discover source returns, and settles with
|
|
111
|
+
* a record of its own.
|
|
112
|
+
* @param {string} version
|
|
113
|
+
* @param {{hostEntry?: boolean}} [options]
|
|
114
|
+
*/
|
|
115
|
+
function packageSet(version, options = {}) {
|
|
116
|
+
const root = tempDir('set');
|
|
117
|
+
const cli = installStandInCli(root, version, {bin: options.bin ?? true});
|
|
118
|
+
installCore(root, version);
|
|
119
|
+
if (options.hostEntry === 'broken') {
|
|
120
|
+
// A host entry that loads but offers no runHostedCli.
|
|
121
|
+
fs.mkdirSync(path.join(cli, 'host'), {recursive: true});
|
|
122
|
+
fs.writeFileSync(path.join(cli, 'host', 'index.mjs'), 'export {};\n');
|
|
123
|
+
} else if (options.hostEntry) {
|
|
124
|
+
fs.mkdirSync(path.join(cli, 'host'), {recursive: true});
|
|
125
|
+
fs.writeFileSync(
|
|
126
|
+
path.join(cli, 'host', 'index.mjs'),
|
|
127
|
+
`export async function runHostedCli({discoverSources = [], ...options}) {
|
|
128
|
+
const catalogs = await Promise.all(discoverSources.map(source => source({})));
|
|
129
|
+
console.log(JSON.stringify({
|
|
130
|
+
hostEntry: ${JSON.stringify(version)},
|
|
131
|
+
options,
|
|
132
|
+
discoverSources: catalogs.map(catalog => catalog.source.name),
|
|
133
|
+
}));
|
|
134
|
+
return {
|
|
135
|
+
status: 0,
|
|
136
|
+
packages: [{name: '@astryxdesign/cli', version: ${JSON.stringify(version)}, source: 'package-set'}],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return root;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Whether the run installed any release into the per-user cache. */
|
|
146
|
+
function installedARelease(cache) {
|
|
147
|
+
return fs.existsSync(path.join(cache, 'astryx', 'releases'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
describe('a project whose own CLI cannot start, in a hosted run', () => {
|
|
151
|
+
/** A project on Core 0.7.0 whose CLI 0.6.8 has no command. */
|
|
152
|
+
function brokenProject() {
|
|
153
|
+
const root = project(tempDir(), {'@astryxdesign/core': '0.7.0'});
|
|
154
|
+
installCore(root, '0.7.0');
|
|
155
|
+
installStandInCli(root, '0.6.8', {bin: false});
|
|
156
|
+
return root;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
it("is answered from the first package set whose CLI and Core match the project's Core", () => {
|
|
160
|
+
const root = brokenProject();
|
|
161
|
+
const run = host({
|
|
162
|
+
argv: ['docs', '--json'],
|
|
163
|
+
cwd: root,
|
|
164
|
+
roots: [packageSet('0.6.8'), packageSet('0.7.0'), packageSet('0.7.0')],
|
|
165
|
+
});
|
|
166
|
+
expect(run.status).toBe(0);
|
|
167
|
+
expect(run.report()).toMatchObject({
|
|
168
|
+
version: '0.7.0',
|
|
169
|
+
args: ['docs', '--json'],
|
|
170
|
+
});
|
|
171
|
+
expect(installedARelease(run.cache)).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("runs a matching package set's own host entry with the same inputs, and settles with its result", () => {
|
|
175
|
+
const root = brokenProject();
|
|
176
|
+
const set = packageSet('0.7.0', {hostEntry: true});
|
|
177
|
+
const run = host(
|
|
178
|
+
{
|
|
179
|
+
argv: ['component', 'Button'],
|
|
180
|
+
cwd: root,
|
|
181
|
+
roots: [set],
|
|
182
|
+
cliCommand: 'acme astryx',
|
|
183
|
+
integrations: ['acme-widgets'],
|
|
184
|
+
},
|
|
185
|
+
{sources: ACME_SOURCE},
|
|
186
|
+
);
|
|
187
|
+
expect(run.status).toBe(0);
|
|
188
|
+
expect(run.report()).toEqual({
|
|
189
|
+
hostEntry: '0.7.0',
|
|
190
|
+
options: {
|
|
191
|
+
argv: ['component', 'Button'],
|
|
192
|
+
cwd: root,
|
|
193
|
+
roots: [set],
|
|
194
|
+
integrations: ['acme-widgets'],
|
|
195
|
+
cliCommand: 'acme astryx',
|
|
196
|
+
},
|
|
197
|
+
discoverSources: ['Acme catalog'],
|
|
198
|
+
});
|
|
199
|
+
expect(run.result()).toEqual({
|
|
200
|
+
status: 0,
|
|
201
|
+
packages: [
|
|
202
|
+
{name: '@astryxdesign/cli', version: '0.7.0', source: 'package-set'},
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('installs nothing when no package set matches: this copy answers, announced', () => {
|
|
208
|
+
const root = brokenProject();
|
|
209
|
+
const roots = [packageSet('0.6.8')];
|
|
210
|
+
const text = host({argv: ['--version'], cwd: root, roots});
|
|
211
|
+
expect(text.status).toBe(0);
|
|
212
|
+
expect(text.stdout.trim()).toBe(SELF_VERSION);
|
|
213
|
+
expect(text.stderr).toContain(
|
|
214
|
+
"astryx: could not get Astryx 0.7.0 to match this project's Core (no package set the host passed holds CLI 0.7.0 with this project's packages); answering with CLI",
|
|
215
|
+
);
|
|
216
|
+
expect(installedARelease(text.cache)).toBe(false);
|
|
217
|
+
const json = host({argv: ['docs', '--json'], cwd: root, roots});
|
|
218
|
+
expect(JSON.parse(json.stdout).meta.answeredBy.copy).toBe('fallback');
|
|
219
|
+
expect(installedARelease(json.cache)).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("installs nothing when the matching set's CLI starts through neither its host entry nor a bin: this copy answers, announced", () => {
|
|
223
|
+
const root = brokenProject();
|
|
224
|
+
const set = packageSet('0.7.0', {hostEntry: 'broken', bin: false});
|
|
225
|
+
const run = host({argv: ['--version'], cwd: root, roots: [set]});
|
|
226
|
+
expect(run.status).toBe(0);
|
|
227
|
+
expect(run.stdout.trim()).toBe(SELF_VERSION);
|
|
228
|
+
expect(run.stderr).toMatch(
|
|
229
|
+
/astryx: could not get Astryx 0\.7\.0 to match this project's Core \(the CLI 0\.7\.0 in the package set at .+ could not be started\); answering with CLI/,
|
|
230
|
+
);
|
|
231
|
+
expect(run.stderr).not.toContain('installing CLI release');
|
|
232
|
+
expect(installedARelease(run.cache)).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('a project CLI the host declined, in a hosted run', () => {
|
|
237
|
+
/** A project on Core and CLI 0.7.0, the CLI a stand-in that reports when it runs. */
|
|
238
|
+
function projectWithCli() {
|
|
239
|
+
const root = project(tempDir(), {
|
|
240
|
+
'@astryxdesign/core': '0.7.0',
|
|
241
|
+
'@astryxdesign/cli': '0.7.0',
|
|
242
|
+
});
|
|
243
|
+
installCore(root, '0.7.0');
|
|
244
|
+
installStandInCli(root, '0.7.0');
|
|
245
|
+
return root;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
it("is answered by a package set of that CLI's release, through its host entry, with the reason", () => {
|
|
249
|
+
const root = projectWithCli();
|
|
250
|
+
const set = packageSet('0.7.0', {hostEntry: true});
|
|
251
|
+
const run = host(
|
|
252
|
+
{
|
|
253
|
+
argv: ['docs'],
|
|
254
|
+
cwd: root,
|
|
255
|
+
roots: [packageSet('0.7.0'), set],
|
|
256
|
+
declinedProjectCli: 'untrusted',
|
|
257
|
+
},
|
|
258
|
+
{sources: ACME_SOURCE},
|
|
259
|
+
);
|
|
260
|
+
expect(run.status).toBe(0);
|
|
261
|
+
expect(run.stderr).not.toContain('stand-in');
|
|
262
|
+
expect(run.report()).toEqual({
|
|
263
|
+
hostEntry: '0.7.0',
|
|
264
|
+
options: {
|
|
265
|
+
argv: ['docs'],
|
|
266
|
+
cwd: root,
|
|
267
|
+
roots: [set],
|
|
268
|
+
integrations: [],
|
|
269
|
+
declinedProjectCli: 'untrusted',
|
|
270
|
+
},
|
|
271
|
+
discoverSources: ['Acme catalog'],
|
|
272
|
+
});
|
|
273
|
+
expect(run.result().packages).toEqual([
|
|
274
|
+
{name: '@astryxdesign/cli', version: '0.7.0', source: 'package-set'},
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('never runs it through a package set without a host entry: this copy answers, announced', () => {
|
|
279
|
+
const root = projectWithCli();
|
|
280
|
+
const run = host({
|
|
281
|
+
argv: ['--version'],
|
|
282
|
+
cwd: root,
|
|
283
|
+
roots: [packageSet('0.7.0')],
|
|
284
|
+
declinedProjectCli: 'untrusted',
|
|
285
|
+
});
|
|
286
|
+
expect(run.status).toBe(0);
|
|
287
|
+
expect(run.stdout.trim()).toBe(SELF_VERSION);
|
|
288
|
+
expect(run.stderr).not.toContain('stand-in');
|
|
289
|
+
expect(run.stderr).toContain('(project CLI skipped: untrusted)');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("the host's discover sources", () => {
|
|
294
|
+
it("are called after every other source, and reported as the host's", () => {
|
|
295
|
+
const empty = tempDir('empty');
|
|
296
|
+
const run = host(
|
|
297
|
+
{argv: ['discover', '--json'], cwd: empty},
|
|
298
|
+
{sources: ACME_SOURCE},
|
|
299
|
+
);
|
|
300
|
+
expect(run.status).toBe(0);
|
|
301
|
+
const out = JSON.parse(run.stdout);
|
|
302
|
+
// The host's source comes after every other source and before the
|
|
303
|
+
// official list, which is always last.
|
|
304
|
+
expect(out.meta.sources).toEqual([
|
|
305
|
+
expect.objectContaining({
|
|
306
|
+
name: 'Acme catalog',
|
|
307
|
+
from: 'the host',
|
|
308
|
+
status: 'fresh',
|
|
309
|
+
}),
|
|
310
|
+
expect.objectContaining({name: 'Astryx'}),
|
|
311
|
+
]);
|
|
312
|
+
expect(JSON.stringify(out.meta.available)).toContain('acme-widgets');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("the host's discover sources in a project", () => {
|
|
317
|
+
/**
|
|
318
|
+
* JavaScript for a discover source whose catalog is named `name`.
|
|
319
|
+
* @param {string} name
|
|
320
|
+
*/
|
|
321
|
+
const source = name =>
|
|
322
|
+
`async () => ({schemaVersion: 1, source: {name: ${JSON.stringify(name)}, generatedAt: '2026-10-20T00:00:00.000Z', complete: true}, packages: []})`;
|
|
323
|
+
|
|
324
|
+
it("come after the project's own source and its integrations' sources, and before the official list", () => {
|
|
325
|
+
const root = project(tempDir(), {
|
|
326
|
+
'@astryxdesign/core': SELF_VERSION,
|
|
327
|
+
'acme-widgets': '1.0.0',
|
|
328
|
+
});
|
|
329
|
+
fs.writeFileSync(
|
|
330
|
+
path.join(root, 'astryx.config.mjs'),
|
|
331
|
+
`export default {discover: ${source('Project catalog')}};\n`,
|
|
332
|
+
);
|
|
333
|
+
const pkgDir = path.join(root, 'node_modules', 'acme-widgets');
|
|
334
|
+
writeJson(path.join(pkgDir, 'package.json'), {
|
|
335
|
+
name: 'acme-widgets',
|
|
336
|
+
version: '1.0.0',
|
|
337
|
+
});
|
|
338
|
+
fs.writeFileSync(
|
|
339
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
340
|
+
`export const discover = ${source('Widgets catalog')};\nexport default {};\n`,
|
|
341
|
+
);
|
|
342
|
+
const run = host(
|
|
343
|
+
{argv: ['discover', '--json'], cwd: root},
|
|
344
|
+
{sources: ACME_SOURCE},
|
|
345
|
+
);
|
|
346
|
+
expect(run.status).toBe(0);
|
|
347
|
+
expect(
|
|
348
|
+
JSON.parse(run.stdout).meta.sources.map(s => [s.name, s.from]),
|
|
349
|
+
).toEqual([
|
|
350
|
+
['Project catalog', 'astryx.config'],
|
|
351
|
+
['Widgets catalog', 'acme-widgets'],
|
|
352
|
+
['Acme catalog', 'the host'],
|
|
353
|
+
['Astryx', '@astryxdesign/cli'],
|
|
354
|
+
]);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('what a hosted run resolved', () => {
|
|
359
|
+
it('names the CLI and its Core from the package set outside a project', () => {
|
|
360
|
+
const run = host({
|
|
361
|
+
argv: ['component', '--list', '--json'],
|
|
362
|
+
cwd: tempDir('empty'),
|
|
363
|
+
});
|
|
364
|
+
expect(run.status).toBe(0);
|
|
365
|
+
expect(run.result().packages).toEqual(
|
|
366
|
+
expect.arrayContaining([
|
|
367
|
+
{
|
|
368
|
+
name: '@astryxdesign/cli',
|
|
369
|
+
version: SELF_VERSION,
|
|
370
|
+
source: 'package-set',
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: '@astryxdesign/core',
|
|
374
|
+
version: SELF_VERSION,
|
|
375
|
+
source: 'package-set',
|
|
376
|
+
},
|
|
377
|
+
]),
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("names a project's own integration as the project's", () => {
|
|
382
|
+
const root = project(tempDir(), {
|
|
383
|
+
'@astryxdesign/core': SELF_VERSION,
|
|
384
|
+
'acme-widgets': '1.0.0',
|
|
385
|
+
});
|
|
386
|
+
const pkgDir = path.join(root, 'node_modules', 'acme-widgets');
|
|
387
|
+
writeJson(path.join(pkgDir, 'package.json'), {
|
|
388
|
+
name: 'acme-widgets',
|
|
389
|
+
version: '1.0.0',
|
|
390
|
+
});
|
|
391
|
+
fs.writeFileSync(
|
|
392
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
393
|
+
'export default {};\n',
|
|
394
|
+
);
|
|
395
|
+
const run = host({argv: ['discover', '--json'], cwd: root});
|
|
396
|
+
expect(run.status).toBe(0);
|
|
397
|
+
expect(run.result().packages).toContainEqual({
|
|
398
|
+
name: 'acme-widgets',
|
|
399
|
+
version: '1.0.0',
|
|
400
|
+
source: 'project',
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} AstryxPin
|
|
6
|
+
* @property {string} name
|
|
7
|
+
* @property {string} declared the value the project's package.json declares
|
|
8
|
+
* @property {string | null} locked the version the project's lockfile records
|
|
9
|
+
* @property {string | null} installed the version installed for the project
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The project's `@astryxdesign/*` dependencies, in field then declaration
|
|
13
|
+
* order; a name declared twice keeps its first field.
|
|
14
|
+
* @param {string} projectDir the folder whose package.json declares them
|
|
15
|
+
* @returns {AstryxPin[]}
|
|
16
|
+
*/
|
|
17
|
+
export function projectAstryxPins(projectDir: string): AstryxPin[];
|
|
18
|
+
export type AstryxPin = {
|
|
19
|
+
name: string;
|
|
20
|
+
/**
|
|
21
|
+
* the value the project's package.json declares
|
|
22
|
+
*/
|
|
23
|
+
declared: string;
|
|
24
|
+
/**
|
|
25
|
+
* the version the project's lockfile records
|
|
26
|
+
*/
|
|
27
|
+
locked: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* the version installed for the project
|
|
30
|
+
*/
|
|
31
|
+
installed: string | null;
|
|
32
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The versions a project would install for the Astryx packages it
|
|
5
|
+
* declares (architecture:cli-surface INV30): for each `@astryxdesign/*`
|
|
6
|
+
* dependency, the value its package.json declares, the version its lockfile
|
|
7
|
+
* records, and the version installed for it.
|
|
8
|
+
*
|
|
9
|
+
* @input A project folder.
|
|
10
|
+
* @output One pin per declared `@astryxdesign/*` package.
|
|
11
|
+
* @position foundation/runtime — read by package-set.mjs to match a project to
|
|
12
|
+
* a host's package set.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import {findLockfile, lockedVersion, readJson} from '../release/project.mjs';
|
|
17
|
+
import {resolveInstalledPackageDir} from '../integrations/autolink.mjs';
|
|
18
|
+
|
|
19
|
+
const SCOPE = '@astryxdesign/';
|
|
20
|
+
const FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} AstryxPin
|
|
24
|
+
* @property {string} name
|
|
25
|
+
* @property {string} declared the value the project's package.json declares
|
|
26
|
+
* @property {string | null} locked the version the project's lockfile records
|
|
27
|
+
* @property {string | null} installed the version installed for the project
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The project's `@astryxdesign/*` dependencies, in field then declaration
|
|
32
|
+
* order; a name declared twice keeps its first field.
|
|
33
|
+
* @param {string} projectDir the folder whose package.json declares them
|
|
34
|
+
* @returns {AstryxPin[]}
|
|
35
|
+
*/
|
|
36
|
+
export function projectAstryxPins(projectDir) {
|
|
37
|
+
const pkg = readJson(path.join(projectDir, 'package.json'));
|
|
38
|
+
if (!pkg || typeof pkg !== 'object') return [];
|
|
39
|
+
const lockfile = findLockfile(projectDir);
|
|
40
|
+
/** @type {AstryxPin[]} */
|
|
41
|
+
const pins = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const field of FIELDS) {
|
|
44
|
+
const deps = pkg[field];
|
|
45
|
+
if (!deps || typeof deps !== 'object' || Array.isArray(deps)) continue;
|
|
46
|
+
for (const [name, declared] of Object.entries(deps)) {
|
|
47
|
+
if (!name.startsWith(SCOPE) || seen.has(name)) continue;
|
|
48
|
+
if (typeof declared !== 'string') continue;
|
|
49
|
+
seen.add(name);
|
|
50
|
+
const found = resolveInstalledPackageDir(name, projectDir);
|
|
51
|
+
const version = found
|
|
52
|
+
? readJson(path.join(found.packageDir, 'package.json'))?.version
|
|
53
|
+
: null;
|
|
54
|
+
pins.push({
|
|
55
|
+
name,
|
|
56
|
+
declared,
|
|
57
|
+
locked: lockfile
|
|
58
|
+
? lockedVersion(lockfile, projectDir, name, declared)
|
|
59
|
+
: null,
|
|
60
|
+
installed: typeof version === 'string' ? version : null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return pins;
|
|
65
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Start the hosted run of this process.
|
|
6
|
+
* @param {{roots?: readonly string[], integrations?: readonly string[], discoverSources?: readonly Function[], startingRoot?: string | null, projectRoot?: string | null, loadHostEntry?: HostedRun['loadHostEntry']}} inputs
|
|
7
|
+
* @returns {HostedRun}
|
|
8
|
+
*/
|
|
9
|
+
export function activateHostedRun({ roots, integrations, discoverSources, startingRoot, projectRoot, loadHostEntry, }: {
|
|
10
|
+
roots?: readonly string[];
|
|
11
|
+
integrations?: readonly string[];
|
|
12
|
+
discoverSources?: readonly Function[];
|
|
13
|
+
startingRoot?: string | null;
|
|
14
|
+
projectRoot?: string | null;
|
|
15
|
+
loadHostEntry?: HostedRun["loadHostEntry"];
|
|
16
|
+
}): HostedRun;
|
|
17
|
+
/**
|
|
18
|
+
* The hosted run of this process, or null when no host started it.
|
|
19
|
+
* @returns {HostedRun | null}
|
|
20
|
+
*/
|
|
21
|
+
export function hostedRun(): HostedRun | null;
|
|
22
|
+
/**
|
|
23
|
+
* Note one package the run resolved. The first record of a name stands. Does
|
|
24
|
+
* nothing outside a hosted run.
|
|
25
|
+
* @param {string} name
|
|
26
|
+
* @param {unknown} version
|
|
27
|
+
* @param {string | null | undefined} dir
|
|
28
|
+
* @param {'project' | 'package-set'} [source] where it came from, when the
|
|
29
|
+
* caller knows; otherwise the folder that holds `dir` says
|
|
30
|
+
*/
|
|
31
|
+
export function recordResolved(name: string, version: unknown, dir: string | null | undefined, source?: "project" | "package-set"): void;
|
|
32
|
+
/**
|
|
33
|
+
* Note every integration a project load resolved: one installed for the
|
|
34
|
+
* computer, an approved one included, comes from a package set; any other,
|
|
35
|
+
* from the project.
|
|
36
|
+
* @param {ReadonlyArray<{name: string, version?: string, __packageDir?: string, __computer?: boolean}>} integrations
|
|
37
|
+
*/
|
|
38
|
+
export function recordResolvedIntegrations(integrations: ReadonlyArray<{
|
|
39
|
+
name: string;
|
|
40
|
+
version?: string;
|
|
41
|
+
__packageDir?: string;
|
|
42
|
+
__computer?: boolean;
|
|
43
|
+
}>): void;
|
|
44
|
+
/**
|
|
45
|
+
* Each package the run resolved: its name, version, and whether it came from
|
|
46
|
+
* the project or a package set (INV30).
|
|
47
|
+
* @returns {Array<{name: string, version: string | null, source: 'project' | 'package-set'}>}
|
|
48
|
+
*/
|
|
49
|
+
export function resolvedPackages(): Array<{
|
|
50
|
+
name: string;
|
|
51
|
+
version: string | null;
|
|
52
|
+
source: "project" | "package-set";
|
|
53
|
+
}>;
|
|
54
|
+
/** Forget the hosted run. Tests only. */
|
|
55
|
+
export function _resetHostedRun(): void;
|
|
56
|
+
export type HostedRun = {
|
|
57
|
+
/**
|
|
58
|
+
* the host's package sets, in its order
|
|
59
|
+
*/
|
|
60
|
+
roots: readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* the integrations it approves for
|
|
63
|
+
* use outside a project
|
|
64
|
+
*/
|
|
65
|
+
integrations: readonly string[];
|
|
66
|
+
/**
|
|
67
|
+
* its own discover sources
|
|
68
|
+
*/
|
|
69
|
+
discoverSources: readonly Function[];
|
|
70
|
+
/**
|
|
71
|
+
* the root that holds the copy that
|
|
72
|
+
* started the run
|
|
73
|
+
*/
|
|
74
|
+
startingRoot: string | null;
|
|
75
|
+
/**
|
|
76
|
+
* the project the run works in, if any
|
|
77
|
+
*/
|
|
78
|
+
projectRoot: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* loads another release's host entry, from one of the package sets, to hand
|
|
81
|
+
* this run over to in this process
|
|
82
|
+
*/
|
|
83
|
+
loadHostEntry: ((hostEntry: string) => Promise<(options: object) => Promise<{
|
|
84
|
+
status: number;
|
|
85
|
+
packages: Array<{
|
|
86
|
+
name: string;
|
|
87
|
+
version: string | null;
|
|
88
|
+
source: "project" | "package-set";
|
|
89
|
+
}>;
|
|
90
|
+
}>>) | null;
|
|
91
|
+
};
|