@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,202 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The words build's page ranker reads: a template's terms and families,
|
|
5
|
+
* an idea's modifiers, containers and phrase pairs, and search's synonyms.
|
|
6
|
+
*
|
|
7
|
+
* Family words come from the templates' own ids — a word in the ids of two or
|
|
8
|
+
* more templates of one family, plus the family's name when a template carries
|
|
9
|
+
* it — never from a list kept here or from synonyms, so an integration's
|
|
10
|
+
* templates join the vocabulary by being named like their family, and "a
|
|
11
|
+
* landing page" is not a hero because "landing" is a synonym of "hero".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {stem, STOPWORDS, SYNONYMS} from '../../search/search.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Words search drops that name layout: "side navigation", "side by side".
|
|
18
|
+
*/
|
|
19
|
+
const LAYOUT_WORDS = new Set(['side']);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Where an idea's head ends: its first clause, before the parts it lists.
|
|
23
|
+
*/
|
|
24
|
+
export const HEAD_END =
|
|
25
|
+
/:|,|;|\(|\s[-\u2013\u2014]\s|\b(?:with|showing|listing|for|that|where|plus|including|containing|featuring|which|to|from|of)\b/i;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Frames a page can live in, above whatever is underneath. A container phrase
|
|
29
|
+
* counts only when it names one of these.
|
|
30
|
+
*/
|
|
31
|
+
const OVERLAY_FRAMES = new Set([
|
|
32
|
+
'modal',
|
|
33
|
+
'dialog',
|
|
34
|
+
'drawer',
|
|
35
|
+
'sheet',
|
|
36
|
+
'popup',
|
|
37
|
+
'overlay',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The family whose templates are app chrome rather than page content: the
|
|
42
|
+
* explicit `Shell -` category (architecture:template-authoring/INV7).
|
|
43
|
+
*/
|
|
44
|
+
export const SHELL_FAMILY = 'Shell';
|
|
45
|
+
|
|
46
|
+
/** A container phrase: "in a modal", "inside the side panel". */
|
|
47
|
+
export const CONTAINER_PHRASE =
|
|
48
|
+
/\b(?:in|inside|within)\s+(?:a|an|the)\s+([a-z-]+)(?:\s+([a-z-]+))?/gi;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Where a phrase ends, for finding the word that heads it: punctuation and
|
|
52
|
+
* the words that join phrases.
|
|
53
|
+
*/
|
|
54
|
+
export const PHRASE_END =
|
|
55
|
+
/[^a-z0-9\s-]+|\s(?:and|or|with|for|of|to|in|on|at|by|from|the|a|an|plus)\s/;
|
|
56
|
+
|
|
57
|
+
/** @param {string} t */
|
|
58
|
+
const isContentWord = t =>
|
|
59
|
+
t.length >= 2 && (LAYOUT_WORDS.has(t) || !STOPWORDS.has(t));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Content terms of a text: lowercase alphanumeric words, stopwords removed,
|
|
63
|
+
* stemmed.
|
|
64
|
+
* @param {string} text
|
|
65
|
+
* @returns {string[]}
|
|
66
|
+
*/
|
|
67
|
+
export function terms(text) {
|
|
68
|
+
return (
|
|
69
|
+
String(text)
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.match(/[a-z0-9]+/g) ?? []
|
|
72
|
+
)
|
|
73
|
+
.filter(isContentWord)
|
|
74
|
+
.map(stem);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The terms of an idea that only ever modify another word, each with the words
|
|
79
|
+
* it modifies: in each phrase the last content word is its head, and the ones
|
|
80
|
+
* before it describe the next.
|
|
81
|
+
* @param {string} query
|
|
82
|
+
* @returns {Map<string, Set<string>>}
|
|
83
|
+
*/
|
|
84
|
+
export function modifiersOf(query) {
|
|
85
|
+
const heads = new Set();
|
|
86
|
+
/** @type {Map<string, Set<string>>} */
|
|
87
|
+
const modifiers = new Map();
|
|
88
|
+
for (const phrase of String(query).toLowerCase().split(PHRASE_END)) {
|
|
89
|
+
const words = terms(phrase);
|
|
90
|
+
words.forEach((w, i) => {
|
|
91
|
+
if (i === words.length - 1) heads.add(w);
|
|
92
|
+
else modifiers.set(w, (modifiers.get(w) ?? new Set()).add(words[i + 1]));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
for (const h of heads) modifiers.delete(h);
|
|
96
|
+
return modifiers;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Adjacent term pairs a text names, "a b", for telling a template that names
|
|
101
|
+
* a whole compound ("executive summary") from one that names only its parts.
|
|
102
|
+
* @param {string[]} texts
|
|
103
|
+
* @returns {Set<string>}
|
|
104
|
+
*/
|
|
105
|
+
export function pairsOf(texts) {
|
|
106
|
+
const pairs = new Set();
|
|
107
|
+
for (const text of texts) {
|
|
108
|
+
for (const phrase of String(text).toLowerCase().split(PHRASE_END)) {
|
|
109
|
+
const words = terms(phrase);
|
|
110
|
+
for (let i = 1; i < words.length; i++)
|
|
111
|
+
pairs.add(`${words[i - 1]} ${words[i]}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return pairs;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The terms of an idea that name its frame: "modal" in "in a modal". A card, a
|
|
119
|
+
* table or an existing page after "in a" is where a part goes, not a frame.
|
|
120
|
+
* @param {string} query
|
|
121
|
+
* @returns {Set<string>}
|
|
122
|
+
*/
|
|
123
|
+
export function containersOf(query) {
|
|
124
|
+
const found = new Set();
|
|
125
|
+
for (const m of String(query).matchAll(CONTAINER_PHRASE)) {
|
|
126
|
+
for (const t of terms(`${m[1]} ${m[2] ?? ''}`)) {
|
|
127
|
+
if (OVERLAY_FRAMES.has(t)) found.add(t);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @param {string} text */
|
|
134
|
+
export const normalized = text =>
|
|
135
|
+
(
|
|
136
|
+
String(text)
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.match(/[a-z0-9]+/g) ?? []
|
|
139
|
+
).join(' ');
|
|
140
|
+
|
|
141
|
+
/** Stemmed synonym lookup built from search's vocabulary, both directions. */
|
|
142
|
+
export const SYNONYMS_OF = (() => {
|
|
143
|
+
/** @type {Map<string, Set<string>>} */
|
|
144
|
+
const index = new Map();
|
|
145
|
+
/** @param {string} a @param {string} b */
|
|
146
|
+
const add = (a, b) => {
|
|
147
|
+
const set = index.get(a) ?? new Set();
|
|
148
|
+
set.add(b);
|
|
149
|
+
index.set(a, set);
|
|
150
|
+
};
|
|
151
|
+
for (const [key, values] of Object.entries(SYNONYMS)) {
|
|
152
|
+
const k = stem(key);
|
|
153
|
+
for (const value of values) {
|
|
154
|
+
const v = stem(value);
|
|
155
|
+
add(k, v);
|
|
156
|
+
add(v, k);
|
|
157
|
+
for (const other of values) if (other !== value) add(v, stem(other));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return index;
|
|
161
|
+
})();
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @typedef {import('../_adapter.mjs').PageTemplate} PageTemplate
|
|
165
|
+
* @typedef {import('../_adapter.mjs').ComponentWords} ComponentWords
|
|
166
|
+
* @typedef {{name: string, score: number, hits: number, familyNamed: boolean, containerMatched: boolean, family: string, base: boolean, matched: Set<string>}} RankedPage
|
|
167
|
+
* @typedef {'page' | 'part' | 'edit'} IdeaKind
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
/** @param {PageTemplate} page */
|
|
171
|
+
const familyOf = page => (page.category.split(' - ')[0] ?? '').trim();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Family words, from the templates' own ids: a word in the ids of two or more
|
|
175
|
+
* templates of one family, plus the family's name when a template carries it.
|
|
176
|
+
* Each word maps to its family.
|
|
177
|
+
* @param {PageTemplate[]} pages
|
|
178
|
+
* @returns {Map<string, string>}
|
|
179
|
+
*/
|
|
180
|
+
export function familyWordsOf(pages) {
|
|
181
|
+
/** @type {Map<string, Map<string, number>>} */
|
|
182
|
+
const idWords = new Map();
|
|
183
|
+
for (const page of pages) {
|
|
184
|
+
const family = familyOf(page);
|
|
185
|
+
const counts = idWords.get(family) ?? new Map();
|
|
186
|
+
for (const w of new Set(page.name.split('-'))) {
|
|
187
|
+
if (w.length >= 3) counts.set(stem(w), (counts.get(stem(w)) ?? 0) + 1);
|
|
188
|
+
}
|
|
189
|
+
idWords.set(family, counts);
|
|
190
|
+
}
|
|
191
|
+
/** @type {Map<string, string>} */
|
|
192
|
+
const familyOfWord = new Map();
|
|
193
|
+
for (const [family, counts] of idWords) {
|
|
194
|
+
const heads = [...counts].filter(([, c]) => c >= 2).map(([w]) => w);
|
|
195
|
+
const name = normalized(family).split(' ').pop() ?? '';
|
|
196
|
+
if (name.length >= 3 && counts.has(stem(name))) heads.push(stem(name));
|
|
197
|
+
for (const head of heads) {
|
|
198
|
+
if (!familyOfWord.has(head)) familyOfWord.set(head, family);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return familyOfWord;
|
|
202
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
export const BUILD_TREE: import("../../foundation/route/route.mjs").Tree<BuildRequest, BuildContext, import("./build.type.mjs").BuildKitResponse>;
|
|
5
|
+
export type BuildRequest = {
|
|
6
|
+
query: string;
|
|
7
|
+
options: NonNullable<Parameters<typeof kitSignals>[1]>;
|
|
8
|
+
};
|
|
9
|
+
export type BuildContext = {
|
|
10
|
+
/**
|
|
11
|
+
* the root's weights
|
|
12
|
+
*/
|
|
13
|
+
weights: import("../../foundation/route/route.mjs").ClassifierWeights;
|
|
14
|
+
/**
|
|
15
|
+
* the block picker's weights; null: build names its own blocks
|
|
16
|
+
*/
|
|
17
|
+
blocks: import("./blocks/blocks.mjs").BlockPickerWeights | null;
|
|
18
|
+
};
|
|
19
|
+
export type BuildAnswer = import("./build.type.mjs").BuildKitResponse;
|
|
20
|
+
import { kitSignals } from './kit/signals.mjs';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file How build answers: a root classifier sends each request to the
|
|
5
|
+
* searcher that should answer it. Its weights (tree.weights.json) have one
|
|
6
|
+
* class today, so every request goes to the main searcher: the kit's signals
|
|
7
|
+
* (kit/signals.mjs), the block picker's blocks (blocks/blocks.mjs), and the
|
|
8
|
+
* kit around them (kit/answer.mjs).
|
|
9
|
+
* @input {query, options} and the data build's adapter gathered: the root's
|
|
10
|
+
* weights and the block picker's
|
|
11
|
+
* @output the build.kit response
|
|
12
|
+
* @position api/build; build.mjs runs it with the shared route helper
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {routeKit} from '../../foundation/route/route.mjs';
|
|
16
|
+
import {kitAnswer} from './kit/answer.mjs';
|
|
17
|
+
import {kitSignals} from './kit/signals.mjs';
|
|
18
|
+
import {pickBlocks} from './blocks/blocks.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{query: string, options: NonNullable<Parameters<typeof kitSignals>[1]>}} BuildRequest
|
|
22
|
+
* @typedef {object} BuildContext
|
|
23
|
+
* @property {import('../../foundation/route/route.mjs').ClassifierWeights} weights the root's weights
|
|
24
|
+
* @property {import('./blocks/blocks.mjs').BlockPickerWeights | null} blocks the block picker's weights; null: build names its own blocks
|
|
25
|
+
* @typedef {import('./build.type.mjs').BuildKitResponse} BuildAnswer
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** @type {import('../../foundation/route/route.mjs').RouteKit<BuildRequest, BuildContext, BuildAnswer>} */
|
|
29
|
+
const {defineSearcher, defineClassifier, defineTree} = routeKit;
|
|
30
|
+
|
|
31
|
+
const main = defineSearcher({
|
|
32
|
+
id: 'main',
|
|
33
|
+
summary:
|
|
34
|
+
'Ranks page templates, blocks, and components by the words of the request, starts from the best page template, and lists its blocks in the order the block picker ranks them.',
|
|
35
|
+
run: async (request, ctx) => {
|
|
36
|
+
const signals = await kitSignals(request.query, request.options);
|
|
37
|
+
if (!ctx.blocks) return kitAnswer(signals);
|
|
38
|
+
return kitAnswer(signals, pickBlocks(request.query, signals, ctx.blocks));
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const root = defineClassifier({
|
|
43
|
+
id: 'root',
|
|
44
|
+
summary: 'Classifies the request and hands it to the searcher for its class.',
|
|
45
|
+
searchers: {},
|
|
46
|
+
fallback: 'main',
|
|
47
|
+
floor: 1,
|
|
48
|
+
features: () => ['#bias'],
|
|
49
|
+
weights: ctx => ctx.weights,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const BUILD_TREE = defineTree('root', [root, main]);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file build's tree end to end, offline: the network and subprocesses throw,
|
|
5
|
+
* and build still answers. The block picker changes only the kit's blocks:
|
|
6
|
+
* the start, its alternatives, and every other field stay build's own, the
|
|
7
|
+
* block count is build's own, and the blocks it names are pinned per phrase.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import {fileURLToPath} from 'node:url';
|
|
12
|
+
import {afterAll, beforeAll, describe, expect, it, vi} from 'vitest';
|
|
13
|
+
|
|
14
|
+
// vi.mock calls are hoisted above the imports; the stub maker is hoisted with them.
|
|
15
|
+
const {blocked} = vi.hoisted(() => ({
|
|
16
|
+
/** @param {string} name */
|
|
17
|
+
blocked: name => async (/** @type {() => Promise<any>} */ load) => {
|
|
18
|
+
const real = await load();
|
|
19
|
+
/** @type {Record<string, unknown>} */
|
|
20
|
+
const stub = {};
|
|
21
|
+
for (const [key, value] of Object.entries(real)) {
|
|
22
|
+
stub[key] =
|
|
23
|
+
typeof value === 'function'
|
|
24
|
+
? () => {
|
|
25
|
+
throw new Error(`build reached ${name}.${key}`);
|
|
26
|
+
}
|
|
27
|
+
: value;
|
|
28
|
+
}
|
|
29
|
+
return {...stub, default: stub};
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
vi.mock('node:http', blocked('http'));
|
|
33
|
+
vi.mock('node:https', blocked('https'));
|
|
34
|
+
vi.mock('node:net', blocked('net'));
|
|
35
|
+
vi.mock('node:dns', blocked('dns'));
|
|
36
|
+
vi.mock('node:child_process', blocked('child_process'));
|
|
37
|
+
vi.mock('http', blocked('http'));
|
|
38
|
+
vi.mock('https', blocked('https'));
|
|
39
|
+
vi.mock('net', blocked('net'));
|
|
40
|
+
vi.mock('dns', blocked('dns'));
|
|
41
|
+
vi.mock('child_process', blocked('child_process'));
|
|
42
|
+
|
|
43
|
+
const {build} = await import('./build.mjs');
|
|
44
|
+
const {buildKit} = await import('./kit/kit.mjs');
|
|
45
|
+
const {loadBlockPicker, withListingCache} = await import('./_adapter.mjs');
|
|
46
|
+
|
|
47
|
+
const REPO = path.resolve(
|
|
48
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
49
|
+
'../../../..',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hand-written phrases and the blocks the picker names for each. The rest of
|
|
54
|
+
* the answer carries search results, which change whenever the docs do, so it
|
|
55
|
+
* is compared with build's own kit rather than pinned.
|
|
56
|
+
* @type {[string, string[]][]}
|
|
57
|
+
*/
|
|
58
|
+
const PINNED = [
|
|
59
|
+
[
|
|
60
|
+
'analytics dashboard with a KPI row and a sortable table',
|
|
61
|
+
[
|
|
62
|
+
'PaginationPageSize',
|
|
63
|
+
'GridDashboardLayout',
|
|
64
|
+
'TableBulkActionsTable',
|
|
65
|
+
'TableGridDividersTable',
|
|
66
|
+
'TableRowExpansionTable',
|
|
67
|
+
],
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
'settings page with notification toggles and a save bar',
|
|
71
|
+
[
|
|
72
|
+
'ToggleButtonColor',
|
|
73
|
+
'ToggleButtonGroup',
|
|
74
|
+
'ToggleButtonGroupShowcase',
|
|
75
|
+
'ToggleButtonGroupVertical',
|
|
76
|
+
'ToggleButtonIconSwap',
|
|
77
|
+
],
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
'empty state for a question bank with an import button',
|
|
81
|
+
[
|
|
82
|
+
'EmptyStateActions',
|
|
83
|
+
'EmptyStateCompact',
|
|
84
|
+
'EmptyStateContainer',
|
|
85
|
+
'EmptyStateShowcase',
|
|
86
|
+
'ToolbarTableFilter',
|
|
87
|
+
],
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
'login form with single sign-on buttons',
|
|
91
|
+
[
|
|
92
|
+
'CardWithInnerLayout',
|
|
93
|
+
'BottomSheetMobileKeyboard',
|
|
94
|
+
'ButtonGroupBasic',
|
|
95
|
+
'CollapsibleControlledAccordion',
|
|
96
|
+
'BottomSheetSwitcherReviewFlow',
|
|
97
|
+
],
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
'calendar for booking appointments',
|
|
101
|
+
[
|
|
102
|
+
'CalendarRangeWithValue',
|
|
103
|
+
'CalendarSingle',
|
|
104
|
+
'CalendarTwoMonths',
|
|
105
|
+
'CalendarConstraints',
|
|
106
|
+
'CalendarShowcase',
|
|
107
|
+
],
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
'kanban board for sprint tasks with a filter bar',
|
|
111
|
+
['ToolbarTableFilter', 'PowerSearchShowcase'],
|
|
112
|
+
],
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** @type {typeof globalThis.fetch} */
|
|
116
|
+
let realFetch;
|
|
117
|
+
beforeAll(() => {
|
|
118
|
+
realFetch = globalThis.fetch;
|
|
119
|
+
globalThis.fetch = () => {
|
|
120
|
+
throw new Error('build reached fetch');
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
afterAll(() => {
|
|
124
|
+
globalThis.fetch = realFetch;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('build tree', () => {
|
|
128
|
+
it('keeps the blocks a request names in build’s slots', async () => {
|
|
129
|
+
const phrase = 'calendar for booking appointments';
|
|
130
|
+
const answer = await build(phrase, {cwd: REPO});
|
|
131
|
+
const own = await withListingCache(() => buildKit(phrase, {cwd: REPO}));
|
|
132
|
+
if (answer?.type !== 'build.kit' || own.type !== 'build.kit')
|
|
133
|
+
throw new Error('expected a kit');
|
|
134
|
+
own.data.blocks.forEach((b, k) => {
|
|
135
|
+
if (b.name.startsWith('Calendar'))
|
|
136
|
+
expect(answer.data.blocks[k].name).toBe(b.name);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('runs with the network and subprocesses blocked', async () => {
|
|
141
|
+
const cp = await import('node:child_process');
|
|
142
|
+
const https = await import('node:https');
|
|
143
|
+
expect(() => cp.execFileSync('true')).toThrow(/build reached/);
|
|
144
|
+
expect(() => https.request('https://example.com')).toThrow(/build reached/);
|
|
145
|
+
expect(() => globalThis.fetch('https://example.com')).toThrow(
|
|
146
|
+
/build reached/,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('loads the block picker weights as plain numbers', () => {
|
|
151
|
+
expect(loadBlockPicker()).not.toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
for (const [phrase, blocks] of PINNED) {
|
|
155
|
+
it(`answers "${phrase}" offline with the picker's blocks`, async () => {
|
|
156
|
+
const answer = await build(phrase, {cwd: REPO});
|
|
157
|
+
const own = await withListingCache(() => buildKit(phrase, {cwd: REPO}));
|
|
158
|
+
if (answer?.type !== 'build.kit' || own.type !== 'build.kit')
|
|
159
|
+
throw new Error('expected a kit');
|
|
160
|
+
// Only the blocks move: the start, its alternatives, and every other
|
|
161
|
+
// field are build's own.
|
|
162
|
+
expect({...answer.data, blocks: null}).toEqual({
|
|
163
|
+
...own.data,
|
|
164
|
+
blocks: null,
|
|
165
|
+
});
|
|
166
|
+
expect(answer.data.start?.alternatives).toEqual(
|
|
167
|
+
own.data.start?.alternatives,
|
|
168
|
+
);
|
|
169
|
+
// The picker only re-orders build's own blocks: each keeps build's
|
|
170
|
+
// entry (its real score and reason), none is added, none is dropped.
|
|
171
|
+
expect(answer.data.blocks.length).toBe(own.data.blocks.length);
|
|
172
|
+
for (const b of answer.data.blocks)
|
|
173
|
+
expect(own.data.blocks.find(o => o.name === b.name)).toEqual(b);
|
|
174
|
+
expect(answer.data.blocks.map(b => b.name)).toEqual(blocks);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"classes": ["main"], "tokens": {"#bias": [0]}}
|
|
@@ -461,8 +461,22 @@ async function componentRead(name, options = {}) {
|
|
|
461
461
|
return componentDetailBlocks(dirName, cwd, owner.package);
|
|
462
462
|
}
|
|
463
463
|
const loaded = await loadComponentDoc(owner.docPath, docOpts);
|
|
464
|
-
// A family member
|
|
465
|
-
// member
|
|
464
|
+
// A family member reads as that member, scoped the way the unscoped
|
|
465
|
+
// lookup scopes it: an integration member by its own entry, a Core
|
|
466
|
+
// sub-component (HStack in the Stack doc) by scopeSubComponent.
|
|
467
|
+
const sub =
|
|
468
|
+
scoped.kind === 'core'
|
|
469
|
+
? scopeSubComponent(loaded, dirName, coreDir)
|
|
470
|
+
: null;
|
|
471
|
+
if (sub) {
|
|
472
|
+
if (props) {
|
|
473
|
+
return componentDetailProps(
|
|
474
|
+
{props: sub.matchingComponent.props},
|
|
475
|
+
owner.package,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return componentDetail(sub.scoped, owner, dirName, coreDir);
|
|
479
|
+
}
|
|
466
480
|
const docs =
|
|
467
481
|
scoped.kind === 'integration'
|
|
468
482
|
? scopeIntegrationMember(loaded, owner, dirName)
|
|
@@ -135,6 +135,21 @@ describe('component list — components whose docs live in a family folder', ()
|
|
|
135
135
|
});
|
|
136
136
|
}, SLOW);
|
|
137
137
|
|
|
138
|
+
it('answers a Core-scoped lookup of a member with that member, as the unscoped lookup does', async () => {
|
|
139
|
+
for (const name of ['HStack', 'VStack']) {
|
|
140
|
+
const unscoped = await component(name, {cwd});
|
|
141
|
+
const scoped = await component(name, {
|
|
142
|
+
cwd,
|
|
143
|
+
package: '@astryxdesign/core',
|
|
144
|
+
});
|
|
145
|
+
expect(scoped.data.name).toBe(name);
|
|
146
|
+
expect(scoped.data.parentDoc).toBe('Stack');
|
|
147
|
+
expect(scoped.data.import).toBe(`@astryxdesign/core/${name}`);
|
|
148
|
+
expect(scoped.data.props).toEqual(unscoped.data.props);
|
|
149
|
+
expect(scoped.data.props.length).toBeGreaterThan(0);
|
|
150
|
+
}
|
|
151
|
+
}, SLOW);
|
|
152
|
+
|
|
138
153
|
it('prints each one\'s own import path in the list, the detail, and search', async () => {
|
|
139
154
|
// The path `component <Name>` prints is the one docs and examples teach:
|
|
140
155
|
// the component's own entry point, never its family's (/Stack, /Text) or
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
availableComputerIntegrations,
|
|
26
26
|
} from '../../foundation/config/project.mjs';
|
|
27
27
|
import {isOutsideProject} from '../../foundation/env/project-scope.mjs';
|
|
28
|
+
import {hostedRun} from '../../foundation/runtime/package-root.mjs';
|
|
28
29
|
import {
|
|
29
30
|
scanAllPackages,
|
|
30
31
|
findComponentInPackages,
|
|
@@ -242,6 +243,7 @@ export async function describeInstalled(project, scanned) {
|
|
|
242
243
|
* @returns {boolean}
|
|
243
244
|
*/
|
|
244
245
|
export function hasSources(project) {
|
|
246
|
+
if (doorSources().length > 0) return true;
|
|
245
247
|
if (project == null) return false;
|
|
246
248
|
if (typeof project.config?.discover === 'function') return true;
|
|
247
249
|
return project.loadedIntegrations.some(
|
|
@@ -249,6 +251,19 @@ export function hasSources(project) {
|
|
|
249
251
|
);
|
|
250
252
|
}
|
|
251
253
|
|
|
254
|
+
/** Where a discover source a host passed comes from (spec AST-081 FR1). */
|
|
255
|
+
const DOOR_SOURCE = 'the host';
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The discover sources the host that started this run passed, if any.
|
|
259
|
+
* @returns {readonly DiscoverSource[]}
|
|
260
|
+
*/
|
|
261
|
+
function doorSources() {
|
|
262
|
+
return /** @type {readonly DiscoverSource[]} */ (
|
|
263
|
+
hostedRun()?.discoverSources ?? []
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
252
267
|
/** The source name of a package installed for the computer. */
|
|
253
268
|
const COMPUTER_SOURCE = 'installed for this computer';
|
|
254
269
|
|
|
@@ -479,7 +494,8 @@ export function defaultCacheDir() {
|
|
|
479
494
|
|
|
480
495
|
/**
|
|
481
496
|
* @param {string} cacheDir
|
|
482
|
-
* @param {string} from
|
|
497
|
+
* @param {string} from the source's key: its `from` label, with the host for
|
|
498
|
+
* a host's source
|
|
483
499
|
* @param {{package?: string, version?: string}} request
|
|
484
500
|
*/
|
|
485
501
|
function savedCopyPath(cacheDir, from, request) {
|
|
@@ -557,8 +573,9 @@ async function callSource(source, request, label, timeoutMs) {
|
|
|
557
573
|
}
|
|
558
574
|
|
|
559
575
|
/**
|
|
560
|
-
* Call the
|
|
561
|
-
*
|
|
576
|
+
* Call the discover sources — the project's `discover` first, then each
|
|
577
|
+
* integration's in load order, then each a host passed, named as the host's
|
|
578
|
+
* (spec AST-081 FR1, AST-051 FR6) — once per distinct function. A source that
|
|
562
579
|
* throws, times out, or returns an invalid catalog falls back to its saved copy;
|
|
563
580
|
* with none, it is reported and adds nothing. The others always count.
|
|
564
581
|
*
|
|
@@ -572,20 +589,30 @@ async function callOwnSources(project, request = {}, options = {}) {
|
|
|
572
589
|
const {cacheDir = defaultCacheDir(), timeoutMs = SOURCE_TIMEOUT_MS} = options;
|
|
573
590
|
/** @type {CatalogResult} */
|
|
574
591
|
const result = {sources: [], packages: []};
|
|
575
|
-
|
|
592
|
+
const fromDoor = doorSources();
|
|
593
|
+
if (project == null && fromDoor.length === 0) return result;
|
|
576
594
|
|
|
577
|
-
/** @type {Array<{from: string, source?: DiscoverSource, error?: string}>} */
|
|
595
|
+
/** @type {Array<{from: string, key?: string, source?: DiscoverSource, error?: string}>} */
|
|
578
596
|
const origins = [];
|
|
579
|
-
if (typeof project
|
|
597
|
+
if (typeof project?.config?.discover === 'function') {
|
|
580
598
|
origins.push({from: 'astryx.config', source: project.config.discover});
|
|
581
599
|
}
|
|
582
|
-
for (const integration of project
|
|
600
|
+
for (const integration of project?.loadedIntegrations ?? []) {
|
|
583
601
|
if (integration.__discoverError) {
|
|
584
602
|
origins.push({from: integration.name, error: integration.__discoverError});
|
|
585
603
|
} else if (integration.__discover) {
|
|
586
604
|
origins.push({from: integration.name, source: integration.__discover});
|
|
587
605
|
}
|
|
588
606
|
}
|
|
607
|
+
// Every tool that carries the CLI reports its sources as the host's, so
|
|
608
|
+
// their saved copies are kept per host, by the root that holds its CLI
|
|
609
|
+
// (spec:AST-051 FR8).
|
|
610
|
+
const host = hostedRun()?.startingRoot ?? '';
|
|
611
|
+
fromDoor.forEach((source, index) => {
|
|
612
|
+
const from =
|
|
613
|
+
fromDoor.length === 1 ? DOOR_SOURCE : `${DOOR_SOURCE} (${index + 1})`;
|
|
614
|
+
origins.push({from, key: `${from}\n${host}`, source});
|
|
615
|
+
});
|
|
589
616
|
|
|
590
617
|
/** @type {Set<unknown>} */
|
|
591
618
|
const called = new Set();
|
|
@@ -594,7 +621,7 @@ async function callOwnSources(project, request = {}, options = {}) {
|
|
|
594
621
|
const ask = request.package
|
|
595
622
|
? {package: request.package, ...(request.version ? {version: request.version} : {})}
|
|
596
623
|
: {};
|
|
597
|
-
for (const {from, source, error} of origins) {
|
|
624
|
+
for (const {from, key = from, source, error} of origins) {
|
|
598
625
|
if (source == null) {
|
|
599
626
|
result.sources.push({name: from, from, status: 'failed', error});
|
|
600
627
|
continue;
|
|
@@ -603,7 +630,7 @@ async function callOwnSources(project, request = {}, options = {}) {
|
|
|
603
630
|
called.add(source);
|
|
604
631
|
|
|
605
632
|
const label = `the discover source from ${from}`;
|
|
606
|
-
const file = savedCopyPath(cacheDir,
|
|
633
|
+
const file = savedCopyPath(cacheDir, key, ask);
|
|
607
634
|
/** @type {import('../../authoring/discover/type').DiscoverCatalog} */
|
|
608
635
|
let catalog;
|
|
609
636
|
try {
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
withDeclaredOfficial,
|
|
22
22
|
} from './_adapter.mjs';
|
|
23
23
|
import {parseDiscoverCatalog} from '../../authoring/discover/parse.mjs';
|
|
24
|
+
import {
|
|
25
|
+
_resetHostedRun,
|
|
26
|
+
activateHostedRun,
|
|
27
|
+
} from '../../foundation/runtime/package-root.mjs';
|
|
24
28
|
|
|
25
29
|
/** No official packages, so a test sees only its own sources after it. */
|
|
26
30
|
const NO_OFFICIAL = {packages: []};
|
|
@@ -230,6 +234,44 @@ describe('callSources', () => {
|
|
|
230
234
|
expect(otherRequest.sources[0].status).toBe('failed');
|
|
231
235
|
});
|
|
232
236
|
|
|
237
|
+
it("keeps a host's saved copies per host, so one tool never answers with another's", async () => {
|
|
238
|
+
let online = true;
|
|
239
|
+
const source = async () => {
|
|
240
|
+
if (!online) throw new Error('offline');
|
|
241
|
+
return catalog('Acme catalog', [pkg('@acme/one')]);
|
|
242
|
+
};
|
|
243
|
+
const options = {cacheDir, official: NO_OFFICIAL};
|
|
244
|
+
try {
|
|
245
|
+
activateHostedRun({
|
|
246
|
+
discoverSources: [source],
|
|
247
|
+
startingRoot: '/tools/acme',
|
|
248
|
+
});
|
|
249
|
+
await callSources(undefined, {}, options);
|
|
250
|
+
online = false;
|
|
251
|
+
const sameHost = await callSources(undefined, {}, options);
|
|
252
|
+
expect(sameHost.sources[0]).toMatchObject({
|
|
253
|
+
name: 'Acme catalog',
|
|
254
|
+
from: 'the host',
|
|
255
|
+
status: 'saved',
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
_resetHostedRun();
|
|
259
|
+
activateHostedRun({
|
|
260
|
+
discoverSources: [source],
|
|
261
|
+
startingRoot: '/tools/other',
|
|
262
|
+
});
|
|
263
|
+
const otherHost = await callSources(undefined, {}, options);
|
|
264
|
+
expect(otherHost.sources[0]).toMatchObject({
|
|
265
|
+
from: 'the host',
|
|
266
|
+
status: 'failed',
|
|
267
|
+
error: 'offline',
|
|
268
|
+
});
|
|
269
|
+
expect(otherHost.packages).toEqual([]);
|
|
270
|
+
} finally {
|
|
271
|
+
_resetHostedRun();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
233
275
|
it('keeps the saved copy in the cache directory, not in the project', async () => {
|
|
234
276
|
await callSources(
|
|
235
277
|
project({
|