@astryxdesign/cli 0.7.2-canary.e2909cf → 0.7.2-canary.e4bdd70
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 +15 -14
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/docs/_adapter.d.mts +11 -0
- package/api/docs/_adapter.mjs +244 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +8 -4
- package/api/integration/add-contribution.test.mjs +4 -2
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/check/check.d.mts +96 -0
- package/api/integration/check/check.mjs +357 -0
- package/api/integration/check/check.test.mjs +489 -0
- package/api/integration/integration-check.type.d.mts +118 -0
- package/api/integration/integration-check.type.mjs +84 -0
- package/api/integration/integrationCheck.doc.d.mts +10 -0
- package/api/integration/integrationCheck.doc.mjs +73 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
- package/api/integration/pack-check.mjs +51 -97
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.mjs +8 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/integration/validate-integration.type.d.mts +8 -0
- package/api/integration/validate-integration.type.mjs +12 -1
- package/api/integration/validateIntegration.doc.mjs +17 -3
- package/api/search/search.d.mts +22 -6
- package/api/search/search.domain-priority.test.mjs +93 -18
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +133 -22
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/theme/build/build.family.test.mjs +54 -6
- package/api/theme/build/build.mjs +114 -17
- package/api/theme/themeBuild.doc.mjs +4 -4
- package/assets/docs/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/styling-overview.doc.mjs +1 -1
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +11 -1
- package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
- package/assets/docs/tree/add-a-topic.doc.mjs +10 -7
- package/assets/docs/tree/agent-guidance.doc.mjs +2 -2
- package/assets/docs/tree/block-template.doc.mjs +8 -20
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/check-the-props.doc.mjs +196 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
- package/assets/docs/tree/checks.doc.mjs +24 -16
- package/assets/docs/tree/codemods.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +8 -6
- package/assets/docs/tree/component-imports.doc.mjs +10 -2
- package/assets/docs/tree/components.doc.mjs +1 -1
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +7 -18
- package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
- package/assets/docs/tree/describe-the-component.doc.mjs +2 -2
- package/assets/docs/tree/docs.doc.mjs +3 -3
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +6 -6
- package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
- package/assets/docs/tree/generate-a-palette.doc.mjs +5 -3
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +1 -1
- package/assets/docs/tree/integrations.doc.mjs +8 -0
- package/assets/docs/tree/links.doc.mjs +6 -5
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +4 -8
- package/assets/docs/tree/publishing.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +37 -21
- package/assets/docs/tree/replace-a-core-component.doc.mjs +4 -6
- package/assets/docs/tree/replace-a-core-template.doc.mjs +6 -8
- package/assets/docs/tree/sections-and-placement.doc.mjs +4 -6
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +4 -2
- package/assets/docs/tree/short-and-findable.doc.mjs +6 -5
- package/assets/docs/tree/single-component.doc.mjs +15 -6
- package/assets/docs/tree/start-a-template.doc.mjs +3 -3
- package/assets/docs/tree/subcomponent.doc.mjs +6 -6
- package/assets/docs/tree/template-assets.doc.mjs +2 -2
- package/assets/docs/tree/template-doc-overview.doc.mjs +4 -15
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- package/assets/docs/tree/template-icons.doc.mjs +1 -1
- package/assets/docs/tree/template-images-media.doc.mjs +1 -1
- package/assets/docs/tree/template-styles.doc.mjs +2 -2
- package/assets/docs/tree/templates.doc.mjs +1 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +3 -3
- package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
- package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
- package/assets/docs/tree/troubleshooting.doc.mjs +13 -5
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/upgrading.doc.mjs +3 -3
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +3 -1
- package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
- package/assets/docs/tree/versioning.doc.mjs +7 -6
- package/assets/docs/tree/write-the-template-file.doc.mjs +4 -3
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/theme.template.ts +1 -1
- package/authoring/codemod/codemod.doc.mjs +7 -3
- package/authoring/config/config.doc.mjs +9 -6
- package/authoring/config/type.ts +4 -2
- package/authoring/discover/discover.doc.mjs +1 -1
- package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
- package/authoring/doctypes/command/command.doc.mjs +14 -12
- package/authoring/doctypes/component/component.doc.mjs +29 -26
- package/authoring/doctypes/component/type.ts +13 -8
- package/authoring/doctypes/enum/enum.doc.mjs +7 -4
- package/authoring/doctypes/function/function.doc.mjs +9 -8
- package/authoring/doctypes/hook/hook.doc.mjs +5 -4
- package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
- package/authoring/doctypes/reference/reference.doc.mjs +11 -10
- package/authoring/doctypes/schema/schema.doc.mjs +9 -9
- package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/template.doc.mjs +3 -3
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/doctypes/theme/theme.doc.mjs +2 -2
- package/authoring/gap-report/gap-report.doc.mjs +2 -2
- package/authoring/identity/identity.doc.mjs +3 -3
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +12 -4
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
- package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +837 -0
- package/clients/cli/commands/doctor.mjs +190 -0
- package/clients/cli/commands/integration-check.doc.mjs +85 -0
- package/clients/cli/commands/integration-check.test.mjs +267 -0
- package/clients/cli/commands/integration.doc.mjs +6 -2
- package/clients/cli/commands/integration.mjs +107 -1
- package/clients/cli/commands/text-json-parity.test.mjs +5 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/index.mjs +1 -0
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +2 -1
- package/foundation/discovery/component-discovery.d.mts +178 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +659 -62
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1224 -0
- package/foundation/discovery/doc-types.test.mjs +652 -0
- package/foundation/discovery/docs-discovery.d.mts +81 -0
- package/foundation/discovery/docs-discovery.mjs +57 -0
- package/foundation/discovery/integration-doc-files.d.mts +22 -0
- package/foundation/discovery/integration-doc-files.mjs +231 -0
- package/foundation/discovery/integration-doc-files.test.mjs +246 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +278 -0
- package/foundation/discovery/prop-drift.mjs +2263 -0
- package/foundation/discovery/prop-drift.test.mjs +2219 -0
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
- package/foundation/doc-compiler/links.d.mts +9 -5
- package/foundation/doc-compiler/links.mjs +4 -2
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
|
@@ -75,14 +75,75 @@ async function placedGuides(route, seen = new Set()) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
describe('search domain priority — the rule', () => {
|
|
78
|
-
it('gives a doc no priority for a
|
|
78
|
+
it('gives a doc no priority for a heading match or a one-word keyword', () => {
|
|
79
79
|
const topic = {
|
|
80
80
|
domain: 'doc',
|
|
81
81
|
name: 'typography/font-setup',
|
|
82
|
-
keywords: ['
|
|
82
|
+
keywords: ['size'],
|
|
83
83
|
titles: ['Font Setup', 'Font Sizes'],
|
|
84
84
|
};
|
|
85
85
|
expect(priority('font size', topic)).toBe(0);
|
|
86
|
+
expect(priority('size', topic)).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('gives a doc priority when the query is one of its declared keywords', () => {
|
|
90
|
+
const guide = {
|
|
91
|
+
domain: 'doc',
|
|
92
|
+
name: 'guides/quality',
|
|
93
|
+
titles: ['Is my code good?'],
|
|
94
|
+
keywords: ['code review', 'review my code'],
|
|
95
|
+
_declared: ['code review', 'review my code'],
|
|
96
|
+
};
|
|
97
|
+
expect(priority('code review', guide)).toBe(1);
|
|
98
|
+
expect(priority('Code Reviews', guide)).toBe(1);
|
|
99
|
+
expect(priority('review my code', guide)).toBe(1);
|
|
100
|
+
// A section's keywords are its headings and code terms.
|
|
101
|
+
const section = {
|
|
102
|
+
...guide,
|
|
103
|
+
name: 'before-you-ship',
|
|
104
|
+
_topic: 'guides/quality',
|
|
105
|
+
_section: 'before-you-ship',
|
|
106
|
+
};
|
|
107
|
+
expect(priority('code review', section)).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("gives a doc no priority for a code term or table cell it didn't declare", () => {
|
|
111
|
+
// A typed doc's keywords hold the first column of its tables and the code
|
|
112
|
+
// terms its text mentions; `missing_field` reads as two words.
|
|
113
|
+
const reference = {
|
|
114
|
+
domain: 'doc',
|
|
115
|
+
name: 'cli/api/functions/validate-integration',
|
|
116
|
+
titles: ['validateIntegration'],
|
|
117
|
+
keywords: ['validate-integration', 'missing_field', 'file required'],
|
|
118
|
+
_declared: [],
|
|
119
|
+
_topic: 'cli/api/functions/validate-integration',
|
|
120
|
+
};
|
|
121
|
+
expect(priority('missing field', reference)).toBe(0);
|
|
122
|
+
expect(priority('file required', reference)).toBe(0);
|
|
123
|
+
expect(
|
|
124
|
+
priority('missing field', {...reference, _declared: ['missing field']}),
|
|
125
|
+
).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('keeps a component the query names exactly above a doc that declares it', async () => {
|
|
129
|
+
const tokens = tokenizeQuery('empty state');
|
|
130
|
+
const component = {domain: 'component', name: 'EmptyState'};
|
|
131
|
+
const doc = {
|
|
132
|
+
domain: 'doc',
|
|
133
|
+
name: 'illustrations',
|
|
134
|
+
keywords: ['empty state'],
|
|
135
|
+
_declared: ['empty state'],
|
|
136
|
+
};
|
|
137
|
+
const c = scoreQuery('empty state', tokens, component);
|
|
138
|
+
const d = scoreQuery('empty state', tokens, doc);
|
|
139
|
+
expect(
|
|
140
|
+
domainPriority('empty state', tokens, /** @type {any} */ (component), c),
|
|
141
|
+
).toBe(1);
|
|
142
|
+
expect(
|
|
143
|
+
domainPriority('empty state', tokens, /** @type {any} */ (doc), d),
|
|
144
|
+
).toBe(1);
|
|
145
|
+
// Both have priority, so the stronger match wins: the exact name.
|
|
146
|
+
expect(c?.score).toBeGreaterThan(d?.score ?? 0);
|
|
86
147
|
});
|
|
87
148
|
|
|
88
149
|
it('gives a doc priority when the query names it', () => {
|
|
@@ -190,32 +251,46 @@ describe('search domain priority — the rule', () => {
|
|
|
190
251
|
|
|
191
252
|
describe('search domain priority — real docs and components', () => {
|
|
192
253
|
it(
|
|
193
|
-
'finds
|
|
254
|
+
'finds a guide that declares the whole phrase above a component that matches one word',
|
|
194
255
|
async () => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
r => r.domain === '
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
256
|
+
const results = (await search('font size', {cwd})).data.results;
|
|
257
|
+
expect(results[0]).toMatchObject({domain: 'doc'});
|
|
258
|
+
expect(results[0].name).toMatch(/^typography(?:\/|$)/);
|
|
259
|
+
// The component the phrase points to stays near the top.
|
|
260
|
+
expect(
|
|
261
|
+
results
|
|
262
|
+
.slice(0, 3)
|
|
263
|
+
.some(r => r.domain === 'component' && r.name === 'Text'),
|
|
264
|
+
).toBe(true);
|
|
265
|
+
},
|
|
266
|
+
SLOW,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
it(
|
|
270
|
+
'keeps a component above a reference doc whose table or code terms hold the phrase',
|
|
271
|
+
async () => {
|
|
272
|
+
for (const q of ['missing field', 'file required']) {
|
|
273
|
+
const [top] = (await search(q, {cwd})).data.results;
|
|
274
|
+
expect(top, q).toMatchObject({domain: 'component', name: 'Field'});
|
|
206
275
|
}
|
|
207
276
|
},
|
|
208
277
|
SLOW,
|
|
209
278
|
);
|
|
210
279
|
|
|
211
280
|
it(
|
|
212
|
-
'
|
|
281
|
+
'keeps a component above docs that match only by a heading, with scores unchanged',
|
|
213
282
|
async () => {
|
|
214
283
|
const results = (await search('font size', {cwd})).data.results;
|
|
215
|
-
const
|
|
216
|
-
r => r.domain === '
|
|
284
|
+
const text = results.findIndex(
|
|
285
|
+
r => r.domain === 'component' && r.name === 'Text',
|
|
217
286
|
);
|
|
218
|
-
expect(
|
|
287
|
+
expect(text).toBeGreaterThanOrEqual(0);
|
|
288
|
+
// A doc below Text outscores it on text alone: priority moved it, not the score.
|
|
289
|
+
expect(
|
|
290
|
+
results
|
|
291
|
+
.slice(text + 1)
|
|
292
|
+
.some(r => r.domain === 'doc' && r.score > results[text].score),
|
|
293
|
+
).toBe(true);
|
|
219
294
|
},
|
|
220
295
|
SLOW,
|
|
221
296
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A reader looking for the check a build runs finds
|
|
5
|
+
* `astryx integration check` first.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {describe, expect, it} from 'vitest';
|
|
9
|
+
import {search} from './search.mjs';
|
|
10
|
+
|
|
11
|
+
describe('search finds integration check for CI', () => {
|
|
12
|
+
it.each([
|
|
13
|
+
'ci',
|
|
14
|
+
'continuous integration',
|
|
15
|
+
'typecheck integration',
|
|
16
|
+
'integration check',
|
|
17
|
+
])('`%s` ranks it first', async query => {
|
|
18
|
+
const {results} = (await search(query)).data;
|
|
19
|
+
expect(results[0], JSON.stringify(results.slice(0, 3))).toMatchObject({
|
|
20
|
+
domain: 'doc',
|
|
21
|
+
name: 'cli/commands/integration-check',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
package/api/search/search.mjs
CHANGED
|
@@ -57,9 +57,10 @@
|
|
|
57
57
|
* Between domains, ranking adds one rule ahead of the score: domain priority
|
|
58
58
|
* (see {@link domainPriority}). A component, hook, template, or theme whose
|
|
59
59
|
* name or keyword a query word hits ranks ahead of every doc the reader did not
|
|
60
|
-
* ask for by name or
|
|
61
|
-
*
|
|
62
|
-
* query holds,
|
|
60
|
+
* ask for by name, title, or a declared phrase, so `font size` finds `Text`
|
|
61
|
+
* before a token page whose heading holds the phrase. A doc the query names,
|
|
62
|
+
* whose title the query holds, or that declares the whole query as a keyword
|
|
63
|
+
* keeps its place.
|
|
63
64
|
*
|
|
64
65
|
* Description and guidance are separate tiers on purpose. A component's own
|
|
65
66
|
* one-line description saying "notification" is a claim about what it IS; the
|
|
@@ -81,12 +82,20 @@
|
|
|
81
82
|
|
|
82
83
|
import {withListingCache} from '../../foundation/fs/listing-cache.mjs';
|
|
83
84
|
import {readDocView} from '../../foundation/doc-compiler/read.mjs';
|
|
84
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
discoverExternalPackages,
|
|
87
|
+
findCoreDir,
|
|
88
|
+
} from '../../foundation/fs/paths.mjs';
|
|
85
89
|
import {
|
|
86
90
|
discoverComponents,
|
|
87
91
|
discoverValidIntegrationComponents,
|
|
92
|
+
docStem,
|
|
88
93
|
familyMember,
|
|
89
94
|
findComponentReadme,
|
|
95
|
+
forEachIntegrationDoc,
|
|
96
|
+
integrationMemberEntry,
|
|
97
|
+
integrationRecordHold,
|
|
98
|
+
isIntegrationMember,
|
|
90
99
|
resolveImportPath,
|
|
91
100
|
resolveIntegrationImportPath,
|
|
92
101
|
CORE_PACKAGE,
|
|
@@ -98,6 +107,7 @@ import {
|
|
|
98
107
|
import {
|
|
99
108
|
loadComponentReplacements,
|
|
100
109
|
loadIntegrationsSafely,
|
|
110
|
+
resolveLegacyExternalDoc,
|
|
101
111
|
} from '../component/_adapter.mjs';
|
|
102
112
|
import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
|
|
103
113
|
import {discoverTemplates, extractComponents} from '../template/template.mjs';
|
|
@@ -129,6 +139,9 @@ import {setResultCoverage} from './coverage.mjs';
|
|
|
129
139
|
* @property {string[]} [aliases] - Other names the candidate answers to, scored
|
|
130
140
|
* with the same name signals: a theme's display name.
|
|
131
141
|
* @property {string[]} [keywords]
|
|
142
|
+
* @property {string[]} [_declared] - A doc's own declared keywords: what its
|
|
143
|
+
* author lists in `keywords`, without the code terms and table cells its
|
|
144
|
+
* other keywords add.
|
|
132
145
|
* @property {string[]} [weakKeywords]
|
|
133
146
|
* @property {string} [description]
|
|
134
147
|
* @property {string[]} [prose]
|
|
@@ -150,6 +163,9 @@ import {setResultCoverage} from './coverage.mjs';
|
|
|
150
163
|
* @property {'page'|'block'} [_kind]
|
|
151
164
|
* @property {string} [_resultName]
|
|
152
165
|
* @property {string} [_commandName]
|
|
166
|
+
* @property {'file'|'doc'} [_hold] - How an integration component holds its
|
|
167
|
+
* name, for the owner rule.
|
|
168
|
+
* @property {string} [_docPath] - An integration component's doc.
|
|
153
169
|
*/
|
|
154
170
|
|
|
155
171
|
/**
|
|
@@ -501,19 +517,20 @@ const STRONG_TOKEN_SCORE = 70;
|
|
|
501
517
|
* typography/font-setup, `side panel` is "Side panels");
|
|
502
518
|
* - 1: a word of the query is the topic's name (`illustration` in a longer
|
|
503
519
|
* question is the illustrations guide);
|
|
520
|
+
* - 1: a query of two or more words is one of the topic's own declared
|
|
521
|
+
* keywords, whatever the plural (`code review` on a guide that declares it);
|
|
504
522
|
* - 1: the query holds one of the doc's titles whole (`resizable side panels`
|
|
505
523
|
* names "Side panels"; `light dark mode button` names "Light/Dark Mode");
|
|
506
524
|
* - 1: the doc matches every word of the query and one of them is a word of
|
|
507
525
|
* its title (`switch to a dark theme` and "Use a theme").
|
|
508
526
|
* A section answers to its topic's name.
|
|
509
527
|
*
|
|
510
|
-
* A doc that matched only by
|
|
528
|
+
* A doc that matched only by a heading holding the query, by one word, or by
|
|
511
529
|
* words spread through its text has no priority. Docs are split into many
|
|
512
|
-
* small topics,
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
* above the component. Broad reference pages match every word of many queries
|
|
530
|
+
* small topics, each with its own headings, so a common phrase such as
|
|
531
|
+
* `font size` hits a heading exactly (170) while the component the reader is
|
|
532
|
+
* after matches one word by keyword (`Text`, 98). Ranked on text alone, every
|
|
533
|
+
* split adds another doc above the component. Broad reference pages match every word of many queries
|
|
517
534
|
* in their text the same way. The score stays the text-match strength the
|
|
518
535
|
* result reports, so callers that gate on it (`build`) see the same numbers.
|
|
519
536
|
*
|
|
@@ -539,6 +556,20 @@ export function domainPriority(term, tokens, candidate, hit) {
|
|
|
539
556
|
if (named.some(n => samePhrase(term, n))) {
|
|
540
557
|
return 2;
|
|
541
558
|
}
|
|
559
|
+
// A query of two or more words that is one of the doc's own declared
|
|
560
|
+
// keywords, read as words whatever the plural, is the author naming the
|
|
561
|
+
// doc for that phrase: `code review` on a guide that declares it. Only
|
|
562
|
+
// what the author declared counts: the code terms and table cells a
|
|
563
|
+
// doc's other keywords add (`missing_field`) name what it mentions, not
|
|
564
|
+
// what it is for. One word is left out too, and a section declares
|
|
565
|
+
// nothing.
|
|
566
|
+
if (
|
|
567
|
+
phraseWords(term).length >= 2 &&
|
|
568
|
+
candidate._section == null &&
|
|
569
|
+
(candidate._declared ?? []).some(k => samePhrase(term, String(k)))
|
|
570
|
+
) {
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
542
573
|
if (
|
|
543
574
|
words.some(
|
|
544
575
|
w =>
|
|
@@ -1110,20 +1141,57 @@ async function gatherCoreComponents(coreDir) {
|
|
|
1110
1141
|
* usage/description from each component's .doc.mjs, same as core. Without
|
|
1111
1142
|
* this, an integration component is invisible to `search`/`build` even
|
|
1112
1143
|
* though `component --list`/`component <Name>` already resolve it — the two
|
|
1113
|
-
* discovery paths silently disagreed.
|
|
1144
|
+
* discovery paths silently disagreed. Also returns, per name, the docs that
|
|
1145
|
+
* hold it by their file name and the loadable docs that only record it.
|
|
1114
1146
|
* @param {import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]} loadedIntegrations
|
|
1115
|
-
* @returns {Promise<Candidate[]>}
|
|
1147
|
+
* @returns {Promise<{candidates: Candidate[], fileNamed: Map<string, Set<string>>, documented: Map<string, Set<string>>}>}
|
|
1116
1148
|
*/
|
|
1117
1149
|
async function gatherIntegrationComponents(loadedIntegrations) {
|
|
1118
1150
|
/** @type {Candidate[]} */
|
|
1119
1151
|
const candidates = [];
|
|
1152
|
+
// What the owner rule reads: the docs that hold a name by their file name,
|
|
1153
|
+
// loadable or not and hidden ones too, and the loadable docs that only
|
|
1154
|
+
// record it.
|
|
1155
|
+
/** @type {Map<string, Set<string>>} */
|
|
1156
|
+
const fileNamed = new Map();
|
|
1157
|
+
/** @type {Map<string, Set<string>>} */
|
|
1158
|
+
const documented = new Map();
|
|
1159
|
+
/** @param {Map<string, Set<string>>} map @param {string} name @param {string} docPath */
|
|
1160
|
+
const hold = (map, name, docPath) =>
|
|
1161
|
+
map.set(name, (map.get(name) ?? new Set()).add(docPath));
|
|
1120
1162
|
for (const integration of loadedIntegrations) {
|
|
1121
|
-
const {components} =
|
|
1163
|
+
const {components, discovered} =
|
|
1164
|
+
await discoverValidIntegrationComponents(integration);
|
|
1165
|
+
const renamed = new Set(
|
|
1166
|
+
discovered
|
|
1167
|
+
.filter(rec => integrationRecordHold(rec) === 'doc')
|
|
1168
|
+
.filter(rec => !isIntegrationMember(rec))
|
|
1169
|
+
.map(rec => rec.docPath),
|
|
1170
|
+
);
|
|
1171
|
+
forEachIntegrationDoc(integration.components, (docPath, stem) => {
|
|
1172
|
+
if (!renamed.has(docPath)) hold(fileNamed, stem, docPath);
|
|
1173
|
+
});
|
|
1122
1174
|
for (const rec of components) {
|
|
1123
|
-
const
|
|
1175
|
+
const loaded = await loadModuleDoc(rec.docPath);
|
|
1176
|
+
// A family member is indexed from its own entry: the family's keywords
|
|
1177
|
+
// and guidance describe the family.
|
|
1178
|
+
const member = isIntegrationMember(rec);
|
|
1179
|
+
const doc = member ? integrationMemberEntry(loaded, rec) : loaded;
|
|
1180
|
+
// A doc that names another export of its file still answers to the
|
|
1181
|
+
// file's name, as `component <file name>` does.
|
|
1182
|
+
const fileName = docStem(rec.docPath);
|
|
1183
|
+
if (integrationRecordHold(rec) === 'doc') {
|
|
1184
|
+
hold(documented, rec.name, rec.docPath);
|
|
1185
|
+
}
|
|
1186
|
+
if (renamed.has(rec.docPath) && !member) {
|
|
1187
|
+
hold(documented, fileName, rec.docPath);
|
|
1188
|
+
}
|
|
1124
1189
|
candidates.push({
|
|
1125
1190
|
domain: 'component',
|
|
1126
1191
|
name: rec.name,
|
|
1192
|
+
aliases: !member && fileName !== rec.name ? [fileName] : [],
|
|
1193
|
+
_hold: integrationRecordHold(rec),
|
|
1194
|
+
_docPath: rec.docPath,
|
|
1127
1195
|
keywords: doc && Array.isArray(doc.keywords) ? doc.keywords : [],
|
|
1128
1196
|
description: doc ? doc.usage?.description || doc.description || '' : '',
|
|
1129
1197
|
guidance: guidanceFrom(doc),
|
|
@@ -1143,12 +1211,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
|
|
|
1143
1211
|
packageName: rec.package,
|
|
1144
1212
|
},
|
|
1145
1213
|
rec.name,
|
|
1146
|
-
|
|
1214
|
+
loaded?.import,
|
|
1147
1215
|
),
|
|
1148
1216
|
});
|
|
1149
1217
|
}
|
|
1150
1218
|
}
|
|
1151
|
-
return candidates;
|
|
1219
|
+
return {candidates, fileNamed, documented};
|
|
1152
1220
|
}
|
|
1153
1221
|
|
|
1154
1222
|
/**
|
|
@@ -1160,11 +1228,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
|
|
|
1160
1228
|
*/
|
|
1161
1229
|
async function gatherComponents(coreDir, cwd) {
|
|
1162
1230
|
const loadedIntegrations = await loadIntegrationsSafely(cwd);
|
|
1163
|
-
const [core,
|
|
1231
|
+
const [core, gathered, replacements] = await Promise.all([
|
|
1164
1232
|
gatherCoreComponents(coreDir),
|
|
1165
1233
|
gatherIntegrationComponents(loadedIntegrations),
|
|
1166
1234
|
loadComponentReplacements(coreDir, loadedIntegrations),
|
|
1167
1235
|
]);
|
|
1236
|
+
const {candidates: integrations, fileNamed, documented} = gathered;
|
|
1168
1237
|
// An active replacement answers to the Core name it replaces, so a search
|
|
1169
1238
|
// for that name finds the replacement and not the Core original
|
|
1170
1239
|
// (spec:AST-035 FR11).
|
|
@@ -1175,9 +1244,43 @@ async function gatherComponents(coreDir, cwd) {
|
|
|
1175
1244
|
active.target,
|
|
1176
1245
|
]),
|
|
1177
1246
|
);
|
|
1247
|
+
// The owner rule, as component detail applies it: a candidate `component
|
|
1248
|
+
// <name>` does not open names its package and follows the owner, and a
|
|
1249
|
+
// file-name alias that opens another owner is dropped.
|
|
1250
|
+
const coreNames = new Set(core.map(candidate => candidate.name));
|
|
1251
|
+
/** @param {string} name @param {Candidate} candidate */
|
|
1252
|
+
const opens = (name, candidate) =>
|
|
1253
|
+
!coreNames.has(name) &&
|
|
1254
|
+
findComponentReadme(coreDir, name) == null &&
|
|
1255
|
+
[...(fileNamed.get(name) ?? [])].every(doc => doc === candidate._docPath) &&
|
|
1256
|
+
[...(documented.get(name) ?? [])].every(doc => doc === candidate._docPath);
|
|
1257
|
+
// A legacy docs package keeps an exact name over an export name a doc records.
|
|
1258
|
+
/** @type {ReturnType<typeof discoverExternalPackages> | undefined} */
|
|
1259
|
+
let externals;
|
|
1260
|
+
/** @param {string} name */
|
|
1261
|
+
const legacyDocuments = name =>
|
|
1262
|
+
(externals ??= discoverExternalPackages(cwd)).some(
|
|
1263
|
+
ext => resolveLegacyExternalDoc(ext, name) != null,
|
|
1264
|
+
);
|
|
1265
|
+
const ruled = integrations.map(candidate => {
|
|
1266
|
+
const aliases = (candidate.aliases ?? []).filter(alias =>
|
|
1267
|
+
opens(alias, candidate),
|
|
1268
|
+
);
|
|
1269
|
+
return candidate._hold === 'doc' &&
|
|
1270
|
+
(!opens(candidate.name, candidate) || legacyDocuments(candidate.name))
|
|
1271
|
+
? {
|
|
1272
|
+
...candidate,
|
|
1273
|
+
aliases,
|
|
1274
|
+
_command: `astryx component ${candidate.name} --package ${candidate._package}`,
|
|
1275
|
+
}
|
|
1276
|
+
: {...candidate, aliases};
|
|
1277
|
+
});
|
|
1178
1278
|
return [
|
|
1179
1279
|
...core.filter(candidate => !replacements.forTarget(candidate.name)),
|
|
1180
|
-
...
|
|
1280
|
+
...[
|
|
1281
|
+
...ruled.filter(candidate => !candidate._command),
|
|
1282
|
+
...ruled.filter(candidate => candidate._command),
|
|
1283
|
+
].map(candidate => {
|
|
1181
1284
|
const target = targetOf.get(`${candidate._package}\0${candidate.name}`);
|
|
1182
1285
|
if (target != null) {
|
|
1183
1286
|
return {...candidate, aliases: [...(candidate.aliases ?? []), target]};
|
|
@@ -1244,10 +1347,13 @@ export async function componentKeywords(coreDir, cwd) {
|
|
|
1244
1347
|
const integrations = loadedIntegrations.map(async integration => {
|
|
1245
1348
|
const {components} = await discoverValidIntegrationComponents(integration);
|
|
1246
1349
|
return Promise.all(
|
|
1247
|
-
components.map(async rec =>
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1350
|
+
components.map(async rec => {
|
|
1351
|
+
const loaded = await loadModuleDoc(rec.docPath);
|
|
1352
|
+
return {
|
|
1353
|
+
name: rec.name,
|
|
1354
|
+
keywords: keywordsOf(integrationMemberEntry(loaded, rec) ?? loaded),
|
|
1355
|
+
};
|
|
1356
|
+
}),
|
|
1251
1357
|
);
|
|
1252
1358
|
});
|
|
1253
1359
|
return [
|
|
@@ -1421,6 +1527,10 @@ async function gatherDocs(cwd) {
|
|
|
1421
1527
|
...defined,
|
|
1422
1528
|
...codeTerms({content}),
|
|
1423
1529
|
],
|
|
1530
|
+
_declared: [
|
|
1531
|
+
...(Array.isArray(selfDoc?.keywords) ? selfDoc.keywords : []),
|
|
1532
|
+
...(Array.isArray(node.keywords) ? node.keywords : []),
|
|
1533
|
+
],
|
|
1424
1534
|
description: node.summary || '',
|
|
1425
1535
|
prose: sectionProse({title: node.title, content}),
|
|
1426
1536
|
titles: [node.title],
|
|
@@ -1557,6 +1667,7 @@ function topicCandidates(
|
|
|
1557
1667
|
...(doc?.title || title ? [doc?.title || title] : []),
|
|
1558
1668
|
...(Array.isArray(doc?.keywords) ? doc.keywords : []),
|
|
1559
1669
|
],
|
|
1670
|
+
_declared: Array.isArray(doc?.keywords) ? doc.keywords : [],
|
|
1560
1671
|
description: doc?.description || summary,
|
|
1561
1672
|
prose: split
|
|
1562
1673
|
? sections.map(section => section.title).filter(Boolean)
|
|
@@ -21,8 +21,10 @@ import {checkGhCli} from '../_github.mjs';
|
|
|
21
21
|
import {Project} from '../../../foundation/config/project.mjs';
|
|
22
22
|
import {
|
|
23
23
|
CORE_PACKAGE,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
docStem,
|
|
25
|
+
findComponentReadme,
|
|
26
|
+
findIntegrationComponent,
|
|
27
|
+
unscopedComponentOwners,
|
|
26
28
|
} from '../../../foundation/discovery/component-discovery.mjs';
|
|
27
29
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
28
30
|
import {AstryxError, writeFailed} from '../../error.mjs';
|
|
@@ -126,33 +128,38 @@ async function loadConfigSafely(cwd) {
|
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* Build the set of OWNER packages that provide a component named `name` across
|
|
129
|
-
* core + every loaded integration.
|
|
131
|
+
* core + every loaded integration. Core holds every name it documents, but has
|
|
132
|
+
* source to copy (`sourceDir`) only for a name with its own folder.
|
|
130
133
|
* @param {string} coreDir
|
|
131
134
|
* @param {Array<{name: string, components?: string, issuesUrl?: string}>} loadedIntegrations
|
|
132
135
|
* @param {string} name
|
|
133
136
|
* @param {string|undefined} coreIssuesUrl
|
|
134
|
-
* @returns {Array<{package: string, sourceDir: string|null, ownerPackage: string, issuesUrl: string|undefined}>}
|
|
137
|
+
* @returns {Array<{package: string, sourceDir: string|null, docPath: string|null, ownerPackage: string, issuesUrl: string|undefined, hold: string}>}
|
|
135
138
|
*/
|
|
136
139
|
function resolveOwners(coreDir, loadedIntegrations, name, coreIssuesUrl) {
|
|
137
140
|
const owners = [];
|
|
138
141
|
const coreComponentDir = path.join(coreDir, 'src', name);
|
|
139
|
-
|
|
142
|
+
const coreFolder = fs.existsSync(coreComponentDir);
|
|
143
|
+
if (coreFolder || findComponentReadme(coreDir, name)) {
|
|
140
144
|
owners.push({
|
|
141
145
|
package: CORE_PACKAGE,
|
|
142
|
-
sourceDir: coreComponentDir,
|
|
146
|
+
sourceDir: coreFolder ? coreComponentDir : null,
|
|
147
|
+
docPath: null,
|
|
143
148
|
ownerPackage: CORE_PACKAGE,
|
|
144
149
|
issuesUrl: coreIssuesUrl || DEFAULT_ISSUES_URL,
|
|
150
|
+
hold: 'core',
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
153
|
for (const integration of loadedIntegrations) {
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
150
|
-
const sourcePath = findIntegrationComponentSource(integration, name);
|
|
154
|
+
const found = findIntegrationComponent(integration, name);
|
|
155
|
+
if (!found) continue;
|
|
151
156
|
owners.push({
|
|
152
157
|
package: integration.name,
|
|
153
|
-
sourceDir: sourcePath ? path.dirname(sourcePath) : null,
|
|
158
|
+
sourceDir: found.sourcePath ? path.dirname(found.sourcePath) : null,
|
|
159
|
+
docPath: found.docPath,
|
|
154
160
|
ownerPackage: integration.name,
|
|
155
161
|
issuesUrl: integration.issuesUrl,
|
|
162
|
+
hold: found.hold,
|
|
156
163
|
});
|
|
157
164
|
}
|
|
158
165
|
return owners;
|
|
@@ -222,25 +229,32 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
222
229
|
: undefined;
|
|
223
230
|
// An active integration replacement answers to the Core name it replaces,
|
|
224
231
|
// bare or qualified by its own package (spec:AST-035 FR11): copy it under its
|
|
225
|
-
//
|
|
232
|
+
// doc's file name, the folder it has always gone to. `--package
|
|
233
|
+
// @astryxdesign/core` still copies the Core original.
|
|
226
234
|
if (pkg !== CORE_PACKAGE) {
|
|
227
235
|
const replacement = await resolveComponentReplacement(
|
|
228
236
|
coreDir,
|
|
229
237
|
loadedIntegrations,
|
|
230
238
|
dirName,
|
|
231
239
|
);
|
|
240
|
+
const copyName = replacement ? docStem(replacement.docPath) : null;
|
|
232
241
|
if (
|
|
233
242
|
replacement &&
|
|
234
|
-
|
|
243
|
+
copyName &&
|
|
244
|
+
(!pkg || (pkg === replacement.package && copyName !== dirName))
|
|
235
245
|
) {
|
|
236
|
-
return swizzleCopy(
|
|
246
|
+
return swizzleCopy(copyName, {
|
|
237
247
|
...options,
|
|
238
248
|
package: replacement.package,
|
|
239
249
|
});
|
|
240
250
|
}
|
|
241
251
|
}
|
|
242
252
|
|
|
243
|
-
const
|
|
253
|
+
const found = resolveOwners(coreDir, loadedIntegrations, dirName, coreIssuesUrl);
|
|
254
|
+
// The owner rule, then a Core name without its own folder drops out.
|
|
255
|
+
const allOwners = (pkg ? found : unscopedComponentOwners(found)).filter(
|
|
256
|
+
owner => owner.package !== CORE_PACKAGE || owner.sourceDir != null,
|
|
257
|
+
);
|
|
244
258
|
|
|
245
259
|
if (allOwners.length === 0) {
|
|
246
260
|
throw new AstryxError(
|
|
@@ -13,6 +13,7 @@ import {themeBuild, themeBuildFamily} from './build.mjs';
|
|
|
13
13
|
import {interceptCore} from './core-interception.mjs';
|
|
14
14
|
import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
|
|
15
15
|
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
16
17
|
vi.setConfig({testTimeout: 120_000});
|
|
17
18
|
const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
|
|
18
19
|
const FAMILY_KEY = 'ocean-family';
|
|
@@ -252,22 +253,69 @@ describe('themeBuildFamily()', () => {
|
|
|
252
253
|
expect(fs.existsSync(dtsPath)).toBe(false);
|
|
253
254
|
});
|
|
254
255
|
|
|
255
|
-
it('refuses family outputs outside cwd
|
|
256
|
+
it('refuses to write family outputs outside cwd, and checks them read-only', async () => {
|
|
256
257
|
const dir = makeDir();
|
|
257
258
|
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
258
259
|
const app = path.join(dir, 'app');
|
|
259
260
|
fs.mkdirSync(app);
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
await expect(
|
|
263
|
+
themeBuildFamily(files, {familyKey: FAMILY_KEY}, {cwd: app}),
|
|
264
|
+
).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
|
|
265
|
+
const checked = await themeBuildFamily(
|
|
266
|
+
files,
|
|
267
|
+
{familyKey: FAMILY_KEY, check: true},
|
|
268
|
+
{cwd: app},
|
|
269
|
+
);
|
|
270
|
+
expect(checked?.type).toBe('theme.build.check');
|
|
271
|
+
expect(checked?.data.upToDate).toBe(false);
|
|
272
|
+
// The `.css.d.ts` import stub may be absent in check mode.
|
|
273
|
+
expect(checked?.data.stale).toEqual(
|
|
274
|
+
OUTPUTS.filter(file => !file.endsWith('.css.d.ts')).map(file => ({
|
|
275
|
+
path: `../themes/${file}`,
|
|
276
|
+
reason: 'missing',
|
|
277
|
+
})),
|
|
278
|
+
);
|
|
266
279
|
for (const output of OUTPUTS) {
|
|
267
280
|
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(false);
|
|
268
281
|
}
|
|
269
282
|
});
|
|
270
283
|
|
|
284
|
+
it('ends a stale family report for outputs outside cwd with a remedy that works', async () => {
|
|
285
|
+
const dir = makeDir();
|
|
286
|
+
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
287
|
+
const app = path.join(dir, 'app');
|
|
288
|
+
fs.mkdirSync(app);
|
|
289
|
+
const family = ['--family', ...files, '--family-key', FAMILY_KEY];
|
|
290
|
+
|
|
291
|
+
const stale = await runCli(['theme', 'build', ...family, '--check'], app);
|
|
292
|
+
expect(stale.code).toBe(1);
|
|
293
|
+
// The old remedy ran the build from cwd, which refuses these outputs.
|
|
294
|
+
expect(stale.stderr).not.toContain('Rebuild with:');
|
|
295
|
+
|
|
296
|
+
const lines = stale.stderr.split('\n').map(line => line.trim());
|
|
297
|
+
const runDir = lines.find(line => line.startsWith('cd '))?.slice(3);
|
|
298
|
+
expect(runDir).toBe(fs.realpathSync(path.join(dir, 'themes')));
|
|
299
|
+
const stem = `${getCliInvocation(runDir)} theme build `;
|
|
300
|
+
const [buildArgs, checkArgs] = lines
|
|
301
|
+
.filter(line => line.startsWith(stem))
|
|
302
|
+
.map(line => line.slice(stem.length).split(' '));
|
|
303
|
+
expect(checkArgs).toEqual([...buildArgs, '--check']);
|
|
304
|
+
|
|
305
|
+
// Following the remedy builds, and the check from there passes.
|
|
306
|
+
const built = await runCli(['theme', 'build', ...buildArgs], runDir);
|
|
307
|
+
expect(built.code).toBe(0);
|
|
308
|
+
for (const output of OUTPUTS) {
|
|
309
|
+
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(true);
|
|
310
|
+
}
|
|
311
|
+
const checked = await runCli(
|
|
312
|
+
['--json', 'theme', 'build', ...checkArgs],
|
|
313
|
+
runDir,
|
|
314
|
+
);
|
|
315
|
+
expect(checked.code).toBe(0);
|
|
316
|
+
expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
271
319
|
it('fails without publishing a mixed trio when staging a write fails', async () => {
|
|
272
320
|
const dir = makeDir();
|
|
273
321
|
const files = writeFamily(dir);
|