@astryxdesign/cli 0.7.2-canary.ebcc404 → 0.7.2-canary.f8f7cf1
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 +9 -8
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +14 -0
- package/api/component/_adapter.mjs +48 -0
- package/api/component/component.mjs +10 -2
- package/api/component/integration-export-name.test.mjs +233 -8
- 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 +2 -1
- 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 +48 -3
- package/api/integration/pack-check.test.mjs +247 -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.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +20 -8
- 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/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 +10 -0
- package/assets/docs/tree/block-template.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +196 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
- package/assets/docs/tree/checks.doc.mjs +21 -13
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +1 -1
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/links.doc.mjs +2 -2
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- package/assets/docs/tree/single-component.doc.mjs +3 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/versioning.doc.mjs +1 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/theme.template.ts +1 -1
- package/authoring/doctypes/component/component.doc.mjs +1 -1
- 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 +1 -1
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +8 -1
- 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 +1 -0
- package/foundation/discovery/component-discovery.d.mts +108 -17
- package/foundation/discovery/component-discovery.export-names.test.mjs +469 -2
- package/foundation/discovery/component-discovery.mjs +380 -47
- package/foundation/discovery/component-replacement.test.mjs +20 -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 +6 -0
- package/foundation/discovery/module-exports.mjs +15 -2
- 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
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file integrationCheck(): each check counted the way its own subcommand
|
|
5
|
+
* exits, the reports a check carries, and what strict adds: the same data,
|
|
6
|
+
* reported again as errors.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
integrationComponentConflicts,
|
|
15
|
+
integrationDocConflicts,
|
|
16
|
+
integrationTemplateConflicts,
|
|
17
|
+
} from '../authoring-checks.mjs';
|
|
18
|
+
import {
|
|
19
|
+
CONFLICT_CODES,
|
|
20
|
+
NO_MANIFEST_CODE,
|
|
21
|
+
NO_MANIFEST_SKIP,
|
|
22
|
+
combineChecks,
|
|
23
|
+
integrationCheck,
|
|
24
|
+
isCheckReport,
|
|
25
|
+
skippedCode,
|
|
26
|
+
summarizeCheck,
|
|
27
|
+
} from './check.mjs';
|
|
28
|
+
import {validateIntegration} from '../validate-integration.mjs';
|
|
29
|
+
import {discoverCoreTemplates} from '../../../foundation/discovery/template-adapter.mjs';
|
|
30
|
+
|
|
31
|
+
const IDENTITY = {validated: true, name: '@acme/widgets', version: '1.2.3'};
|
|
32
|
+
|
|
33
|
+
describe('summarizeCheck', () => {
|
|
34
|
+
it('counts error and warning issues, and fails only on errors', () => {
|
|
35
|
+
expect(
|
|
36
|
+
summarizeCheck({
|
|
37
|
+
type: 'integration.validate',
|
|
38
|
+
data: {
|
|
39
|
+
...IDENTITY,
|
|
40
|
+
issues: [
|
|
41
|
+
{code: 'missing_root', severity: 'error', message: 'm'},
|
|
42
|
+
{code: 'unknown_manifest_key', severity: 'warning', message: 'w'},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
).toEqual({errors: 1, warnings: 1, skipped: [], failed: true});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('counts warning conflicts as warnings and ignores info', () => {
|
|
50
|
+
const conflict = (/** @type {'warning' | 'info'} */ severity) =>
|
|
51
|
+
/** @type {any} */ ({severity, id: 'x', message: 'm'});
|
|
52
|
+
expect(
|
|
53
|
+
summarizeCheck({
|
|
54
|
+
type: 'integration.template-conflicts',
|
|
55
|
+
data: {
|
|
56
|
+
...IDENTITY,
|
|
57
|
+
issues: [],
|
|
58
|
+
conflicts: [conflict('warning'), conflict('info')],
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
).toEqual({errors: 0, warnings: 1, skipped: [], failed: false});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('fails the docs check on an error finding, as its subcommand does', () => {
|
|
65
|
+
const finding = (/** @type {'error' | 'info'} */ severity) =>
|
|
66
|
+
/** @type {any} */ ({severity, topic: 't', message: 'm'});
|
|
67
|
+
expect(
|
|
68
|
+
summarizeCheck({
|
|
69
|
+
type: 'integration.doc-conflicts',
|
|
70
|
+
data: {
|
|
71
|
+
...IDENTITY,
|
|
72
|
+
issues: [],
|
|
73
|
+
findings: [finding('error'), finding('info')],
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
).toEqual({errors: 1, warnings: 0, skipped: [], failed: true});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('names a package with no manifest as skipped work, but not a missing package.json, which is an error', () => {
|
|
80
|
+
expect(
|
|
81
|
+
summarizeCheck({
|
|
82
|
+
type: 'integration.component-conflicts',
|
|
83
|
+
data: {
|
|
84
|
+
validated: false,
|
|
85
|
+
name: null,
|
|
86
|
+
version: null,
|
|
87
|
+
issues: [],
|
|
88
|
+
conflicts: [],
|
|
89
|
+
},
|
|
90
|
+
}).skipped,
|
|
91
|
+
).toEqual([NO_MANIFEST_SKIP]);
|
|
92
|
+
expect(
|
|
93
|
+
summarizeCheck({
|
|
94
|
+
type: 'integration.validate',
|
|
95
|
+
data: {
|
|
96
|
+
validated: false,
|
|
97
|
+
name: null,
|
|
98
|
+
version: null,
|
|
99
|
+
issues: [{code: 'no_package', severity: 'error', message: 'm'}],
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
).toEqual({errors: 1, warnings: 0, skipped: [], failed: true});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('counts the findings of a report a check carries as warnings, and a skipped report as skipped work', () => {
|
|
106
|
+
const data = /** @type {any} */ ({
|
|
107
|
+
...IDENTITY,
|
|
108
|
+
issues: [],
|
|
109
|
+
conflicts: [],
|
|
110
|
+
compared: {status: 'checked', findings: [{severity: 'warning'}, {}]},
|
|
111
|
+
typed: {
|
|
112
|
+
status: 'skipped',
|
|
113
|
+
reason: 'TypeScript is not installed',
|
|
114
|
+
findings: [],
|
|
115
|
+
},
|
|
116
|
+
notAReport: {status: 'checked'},
|
|
117
|
+
});
|
|
118
|
+
expect(
|
|
119
|
+
summarizeCheck({type: 'integration.component-conflicts', data}),
|
|
120
|
+
).toEqual({
|
|
121
|
+
errors: 0,
|
|
122
|
+
warnings: 2,
|
|
123
|
+
skipped: ['TypeScript is not installed'],
|
|
124
|
+
failed: false,
|
|
125
|
+
});
|
|
126
|
+
expect(isCheckReport(data.compared)).toBe(true);
|
|
127
|
+
expect(isCheckReport(data.notAReport)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('names prop types the comparison could not judge as skipped work, which strict fails and the default run shows', () => {
|
|
131
|
+
const reason =
|
|
132
|
+
'TypeScript 4.7.4 is older than 4.8, the oldest the type comparison reads';
|
|
133
|
+
/** @type {any[]} */
|
|
134
|
+
const responses = [
|
|
135
|
+
{
|
|
136
|
+
type: 'integration.validate',
|
|
137
|
+
data: {...IDENTITY, issues: []},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'integration.component-conflicts',
|
|
141
|
+
data: {
|
|
142
|
+
...IDENTITY,
|
|
143
|
+
issues: [],
|
|
144
|
+
conflicts: [],
|
|
145
|
+
propDrift: {status: 'checked', findings: [], typesSkipped: reason},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: 'integration.template-conflicts',
|
|
150
|
+
data: {...IDENTITY, issues: [], conflicts: []},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'integration.doc-conflicts',
|
|
154
|
+
data: {...IDENTITY, issues: [], findings: []},
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
const message = `prop types not judged: ${reason}`;
|
|
158
|
+
const loose = combineChecks(responses, false);
|
|
159
|
+
expect(loose.skipped).toEqual([`components: ${message}`]);
|
|
160
|
+
expect(loose.failures).toEqual([]);
|
|
161
|
+
expect(loose.failed).toBe(false);
|
|
162
|
+
const strict = combineChecks(responses, true);
|
|
163
|
+
expect(strict.failures).toEqual([
|
|
164
|
+
{
|
|
165
|
+
check: 'components',
|
|
166
|
+
severity: 'error',
|
|
167
|
+
code: 'prop_types_skipped',
|
|
168
|
+
message,
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
expect(strict.failed).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('names a skipped comparison by its key in snake case', () => {
|
|
175
|
+
expect(skippedCode('propDrift')).toBe('prop_drift_skipped');
|
|
176
|
+
expect(skippedCode('docTypes')).toBe('doc_types_skipped');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* What the default run reports as warnings and skipped work, read from each
|
|
182
|
+
* check's own envelope: what `--strict` must report again as errors. A later
|
|
183
|
+
* check's repeat of one of validate's items is validate's, counted once.
|
|
184
|
+
* @param {any} result an `integration.check` envelope
|
|
185
|
+
*/
|
|
186
|
+
function warningsAndSkips(result) {
|
|
187
|
+
/** @type {Map<string, number>} */
|
|
188
|
+
const fromValidate = new Map();
|
|
189
|
+
/** @type {Array<{check: string, code: string, message: string}>} */
|
|
190
|
+
const found = [];
|
|
191
|
+
result.data.checks.forEach((/** @type {any} */ {check, response}, i) => {
|
|
192
|
+
const {data} = response;
|
|
193
|
+
/** @type {any[]} */
|
|
194
|
+
const reports = Object.values(data).filter(isCheckReport);
|
|
195
|
+
const items = [
|
|
196
|
+
...data.issues
|
|
197
|
+
.filter((/** @type {any} */ x) => x.severity === 'warning')
|
|
198
|
+
.map((/** @type {any} */ x) => [x.code, x.message]),
|
|
199
|
+
...(data.conflicts ?? [])
|
|
200
|
+
.filter((/** @type {any} */ x) => x.severity === 'warning')
|
|
201
|
+
.map((/** @type {any} */ x) => [CONFLICT_CODES[check], x.message]),
|
|
202
|
+
...reports.flatMap(report =>
|
|
203
|
+
report.findings.map((/** @type {any} */ f) => [
|
|
204
|
+
f.code ?? f.problem ?? f.kind,
|
|
205
|
+
f.message,
|
|
206
|
+
]),
|
|
207
|
+
),
|
|
208
|
+
...(data.validated ? [] : [[NO_MANIFEST_CODE, NO_MANIFEST_SKIP]]),
|
|
209
|
+
...Object.entries(data)
|
|
210
|
+
.filter(([, v]) => isCheckReport(v) && v.status === 'skipped')
|
|
211
|
+
.map(([key, v]) => [skippedCode(key), v.reason]),
|
|
212
|
+
...reports
|
|
213
|
+
.filter(report => typeof report.typesSkipped === 'string')
|
|
214
|
+
.map(report => [
|
|
215
|
+
'prop_types_skipped',
|
|
216
|
+
`prop types not judged: ${report.typesSkipped}`,
|
|
217
|
+
]),
|
|
218
|
+
];
|
|
219
|
+
const repeats = new Map(fromValidate);
|
|
220
|
+
for (const [code, message] of items) {
|
|
221
|
+
const key = `${code}\0${message}`;
|
|
222
|
+
const left = repeats.get(key) ?? 0;
|
|
223
|
+
if (i === 0) fromValidate.set(key, left + 1);
|
|
224
|
+
else if (left > 0) {
|
|
225
|
+
repeats.set(key, left - 1);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
found.push({check, code, message});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return found;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
describe('combineChecks', () => {
|
|
235
|
+
const respond = (/** @type {string} */ type, /** @type {object} */ data) =>
|
|
236
|
+
/** @type {any} */ ({
|
|
237
|
+
type,
|
|
238
|
+
data: {...IDENTITY, issues: [], conflicts: [], findings: [], ...data},
|
|
239
|
+
});
|
|
240
|
+
const sameMessage = (/** @type {string} */ component) => ({
|
|
241
|
+
component,
|
|
242
|
+
prop: 'label',
|
|
243
|
+
severity: 'warning',
|
|
244
|
+
kind: 'required_mismatch',
|
|
245
|
+
message:
|
|
246
|
+
'"label" is required in the code, but the doc does not mark it required.',
|
|
247
|
+
doc: 'components/List.doc.mjs',
|
|
248
|
+
});
|
|
249
|
+
const responses = [
|
|
250
|
+
respond('integration.validate', {
|
|
251
|
+
issues: [
|
|
252
|
+
{
|
|
253
|
+
code: 'unknown_manifest_key',
|
|
254
|
+
severity: 'warning',
|
|
255
|
+
message: 'Unknown key.',
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
}),
|
|
259
|
+
respond('integration.component-conflicts', {
|
|
260
|
+
issues: [
|
|
261
|
+
{
|
|
262
|
+
code: 'unknown_manifest_key',
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
message: 'Unknown key.',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
report: {
|
|
268
|
+
status: 'checked',
|
|
269
|
+
findings: [sameMessage('List'), sameMessage('ListItem')],
|
|
270
|
+
unchecked: [
|
|
271
|
+
{component: 'Ghost', doc: 'components/Ghost.doc.mjs', reason: 'x'},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
respond('integration.template-conflicts', {}),
|
|
276
|
+
respond('integration.doc-conflicts', {}),
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
it('counts a package-level issue once, and two findings with one message twice, naming each component', () => {
|
|
280
|
+
const loose = combineChecks(responses, false);
|
|
281
|
+
const strict = combineChecks(responses, true);
|
|
282
|
+
|
|
283
|
+
expect(loose).toMatchObject({
|
|
284
|
+
errors: 0,
|
|
285
|
+
warnings: 3,
|
|
286
|
+
unchecked: 1,
|
|
287
|
+
failed: false,
|
|
288
|
+
});
|
|
289
|
+
expect(strict.warnings).toBe(3);
|
|
290
|
+
expect(
|
|
291
|
+
strict.failures.map(f => [f.check, f.code, f.component ?? null]),
|
|
292
|
+
).toEqual([
|
|
293
|
+
['validate', 'unknown_manifest_key', null],
|
|
294
|
+
['components', 'required_mismatch', 'List'],
|
|
295
|
+
['components', 'required_mismatch', 'ListItem'],
|
|
296
|
+
]);
|
|
297
|
+
expect(strict.failures[1]).toMatchObject({
|
|
298
|
+
prop: 'label',
|
|
299
|
+
file: 'components/List.doc.mjs',
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('counts each finding a check repeats within itself, and consumes only validate’s own issues as repeats', () => {
|
|
304
|
+
const issue = (/** @type {string} */ code, severity = 'warning') => ({
|
|
305
|
+
code,
|
|
306
|
+
severity,
|
|
307
|
+
message: code,
|
|
308
|
+
});
|
|
309
|
+
const root = issue('invalid_root', 'error');
|
|
310
|
+
const link = issue('invalid_doc_graph');
|
|
311
|
+
const broken = issue('bad_link', 'error');
|
|
312
|
+
const repeated = ['validate', 'component', 'template', 'doc'].map(kind =>
|
|
313
|
+
respond(
|
|
314
|
+
`integration.${kind === 'validate' ? kind : `${kind}-conflicts`}`,
|
|
315
|
+
{
|
|
316
|
+
issues: kind === 'doc' ? [root, link, link, broken, broken] : [root],
|
|
317
|
+
},
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const loose = combineChecks(repeated, false);
|
|
322
|
+
const strict = combineChecks(repeated, true);
|
|
323
|
+
|
|
324
|
+
expect(loose.checks.map(c => `${c.errors}/${c.warnings}`)).toEqual([
|
|
325
|
+
'1/0',
|
|
326
|
+
'1/0',
|
|
327
|
+
'1/0',
|
|
328
|
+
'3/2',
|
|
329
|
+
]);
|
|
330
|
+
expect(loose).toMatchObject({errors: 3, warnings: 2});
|
|
331
|
+
expect(strict.failures.map(f => [f.check, f.code])).toEqual([
|
|
332
|
+
['docs', 'invalid_doc_graph'],
|
|
333
|
+
['docs', 'invalid_doc_graph'],
|
|
334
|
+
]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('states each option’s default in its reference doc', async () => {
|
|
338
|
+
const {doc} = await import('../integrationCheck.doc.mjs');
|
|
339
|
+
const params = new Map(doc.params?.map(param => [param.name, param]));
|
|
340
|
+
expect(params.get('options.strict')?.default).toBe('false');
|
|
341
|
+
expect(params.get('options.cwd')?.default).toBe('process.cwd()');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('integrationCheck', () => {
|
|
346
|
+
/** @type {string} */
|
|
347
|
+
let pkg;
|
|
348
|
+
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
// Under the repository, so the components check resolves Core.
|
|
351
|
+
pkg = fs.mkdtempSync(
|
|
352
|
+
path.join(process.cwd(), '.astryx-integration-check-'),
|
|
353
|
+
);
|
|
354
|
+
fs.writeFileSync(
|
|
355
|
+
path.join(pkg, 'package.json'),
|
|
356
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
357
|
+
);
|
|
358
|
+
fs.writeFileSync(
|
|
359
|
+
path.join(pkg, 'astryx.integration.mjs'),
|
|
360
|
+
"export default {templates: './templates'};\n",
|
|
361
|
+
);
|
|
362
|
+
fs.mkdirSync(path.join(pkg, 'templates'));
|
|
363
|
+
fs.writeFileSync(
|
|
364
|
+
path.join(pkg, 'templates', 'acme-page.doc.mjs'),
|
|
365
|
+
"export default {type: 'page', name: 'Acme page', displayName: 'Acme page', description: 'fixture', replaces: 'missing-core-page'};\n",
|
|
366
|
+
);
|
|
367
|
+
fs.writeFileSync(
|
|
368
|
+
path.join(pkg, 'templates', 'acme-page.tsx'),
|
|
369
|
+
'export default function Fixture() { return null; }\n',
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
afterEach(() => {
|
|
374
|
+
fs.rmSync(pkg, {recursive: true, force: true});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('returns each check’s own envelope, unchanged, in the order the checks run', async () => {
|
|
378
|
+
const result = await integrationCheck({cwd: pkg});
|
|
379
|
+
|
|
380
|
+
expect(result.type).toBe('integration.check');
|
|
381
|
+
expect(result.data.checks.map(check => check.response)).toEqual([
|
|
382
|
+
await validateIntegration(undefined, {cwd: pkg}),
|
|
383
|
+
await integrationComponentConflicts(undefined, {cwd: pkg}),
|
|
384
|
+
await integrationTemplateConflicts(undefined, {cwd: pkg}),
|
|
385
|
+
await integrationDocConflicts(undefined, {cwd: pkg}),
|
|
386
|
+
]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('fails when a check fails on its own, with or without strict', async () => {
|
|
390
|
+
const result = await integrationCheck({cwd: pkg});
|
|
391
|
+
|
|
392
|
+
const templates = result.data.checks.find(
|
|
393
|
+
check => check.check === 'templates',
|
|
394
|
+
);
|
|
395
|
+
expect(templates?.failed).toBe(true);
|
|
396
|
+
expect(result.data).toMatchObject({
|
|
397
|
+
validated: true,
|
|
398
|
+
strict: false,
|
|
399
|
+
failures: [],
|
|
400
|
+
failed: true,
|
|
401
|
+
});
|
|
402
|
+
expect(result.data.errors).toBeGreaterThan(0);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('fails on warnings only with strict, which reports exactly the default run’s warnings and skipped work again, as errors', async () => {
|
|
406
|
+
const core = (await discoverCoreTemplates()).find(
|
|
407
|
+
template => template.type === 'page',
|
|
408
|
+
);
|
|
409
|
+
fs.writeFileSync(
|
|
410
|
+
path.join(pkg, 'astryx.integration.mjs'),
|
|
411
|
+
"export default {templates: './templates', unknownKey: true};\n",
|
|
412
|
+
);
|
|
413
|
+
fs.rmSync(path.join(pkg, 'templates', 'acme-page.doc.mjs'));
|
|
414
|
+
fs.renameSync(
|
|
415
|
+
path.join(pkg, 'templates', 'acme-page.tsx'),
|
|
416
|
+
path.join(pkg, 'templates', `${core?.dirName}.tsx`),
|
|
417
|
+
);
|
|
418
|
+
fs.writeFileSync(
|
|
419
|
+
path.join(pkg, 'templates', `${core?.dirName}.doc.mjs`),
|
|
420
|
+
"export default {type: 'page', name: 'Acme page', displayName: 'Acme page', description: 'fixture'};\n",
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const loose = await integrationCheck({cwd: pkg});
|
|
424
|
+
const strict = await integrationCheck({cwd: pkg, strict: true});
|
|
425
|
+
|
|
426
|
+
expect(loose.data).toMatchObject({errors: 0, failures: [], failed: false});
|
|
427
|
+
// The unknown key is validate's, and every check starts from validate's
|
|
428
|
+
// issues: it counts once.
|
|
429
|
+
expect(loose.data.warnings).toBe(2);
|
|
430
|
+
expect(strict.data).toMatchObject({strict: true, errors: 0, failed: true});
|
|
431
|
+
expect(strict.data.warnings).toBe(loose.data.warnings);
|
|
432
|
+
// Same data: the same checks, read once each, in both modes.
|
|
433
|
+
expect(strict.data.checks.map(check => check.response)).toEqual(
|
|
434
|
+
loose.data.checks.map(check => check.response),
|
|
435
|
+
);
|
|
436
|
+
// Only the severity changes.
|
|
437
|
+
expect(new Set(strict.data.failures.map(f => f.severity))).toEqual(
|
|
438
|
+
new Set(['error']),
|
|
439
|
+
);
|
|
440
|
+
expect(
|
|
441
|
+
strict.data.failures.map(({check, code, message}) => ({
|
|
442
|
+
check,
|
|
443
|
+
code,
|
|
444
|
+
message,
|
|
445
|
+
})),
|
|
446
|
+
).toEqual(warningsAndSkips(loose));
|
|
447
|
+
expect(strict.data.failures.map(f => [f.check, f.code])).toEqual([
|
|
448
|
+
['validate', 'unknown_manifest_key'],
|
|
449
|
+
['templates', 'template_id_conflict'],
|
|
450
|
+
]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('counts a missing root once, though every check reports it', async () => {
|
|
454
|
+
fs.rmSync(path.join(pkg, 'templates'), {recursive: true, force: true});
|
|
455
|
+
|
|
456
|
+
const result = await integrationCheck({cwd: pkg});
|
|
457
|
+
|
|
458
|
+
expect(result.data.checks.map(check => check.failed)).toEqual([
|
|
459
|
+
true,
|
|
460
|
+
true,
|
|
461
|
+
true,
|
|
462
|
+
true,
|
|
463
|
+
]);
|
|
464
|
+
expect(result.data.errors).toBe(1);
|
|
465
|
+
expect(result.data.failed).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('reads a missing package.json from validate alone', async () => {
|
|
469
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-pkg-'));
|
|
470
|
+
try {
|
|
471
|
+
const result = await integrationCheck({cwd: empty, strict: true});
|
|
472
|
+
|
|
473
|
+
expect(result.data).toMatchObject({
|
|
474
|
+
errors: 1,
|
|
475
|
+
skipped: [],
|
|
476
|
+
failures: [],
|
|
477
|
+
failed: true,
|
|
478
|
+
});
|
|
479
|
+
expect(result.data.checks.map(check => check.skipped)).toEqual([
|
|
480
|
+
[],
|
|
481
|
+
[],
|
|
482
|
+
[],
|
|
483
|
+
[],
|
|
484
|
+
]);
|
|
485
|
+
} finally {
|
|
486
|
+
fs.rmSync(empty, {recursive: true, force: true});
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Options for `integrationCheck()`.
|
|
6
|
+
*/
|
|
7
|
+
export type IntegrationCheckOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* The integration package folder to check; the
|
|
10
|
+
* current directory by default.
|
|
11
|
+
*/
|
|
12
|
+
cwd?: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Also fail on any warning and on any check that
|
|
15
|
+
* did not run, not only on errors.
|
|
16
|
+
*/
|
|
17
|
+
strict?: boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The checks `integrationCheck()` runs, in the order it runs them.
|
|
21
|
+
*/
|
|
22
|
+
export type IntegrationCheckName = "validate" | "components" | "templates" | "docs";
|
|
23
|
+
/**
|
|
24
|
+
* The envelope one check returns on its own, unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export type IntegrationCheckResponse = import("./validate-integration.type.mjs").ValidateIntegrationResponse | import("./authoring-checks.type.mjs").IntegrationComponentConflictResponse | import("./authoring-checks.type.mjs").IntegrationTemplateConflictResponse | import("./authoring-checks.type.mjs").IntegrationDocConflictResponse;
|
|
27
|
+
/**
|
|
28
|
+
* A comparison a check carries beside its issues, such as one that needs the
|
|
29
|
+
* project's own TypeScript. `skipped` means it did not run, and `reason` says
|
|
30
|
+
* why; each of its findings is a warning.
|
|
31
|
+
*/
|
|
32
|
+
export type IntegrationCheckReport = {
|
|
33
|
+
status: "checked" | "skipped";
|
|
34
|
+
reason?: string | undefined;
|
|
35
|
+
findings: Array<{
|
|
36
|
+
severity?: string;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* One check's result, counted the way its own subcommand exits.
|
|
41
|
+
*/
|
|
42
|
+
export type IntegrationCheckResult = {
|
|
43
|
+
check: IntegrationCheckName;
|
|
44
|
+
/**
|
|
45
|
+
* What makes the check's own subcommand exit 1.
|
|
46
|
+
*/
|
|
47
|
+
errors: number;
|
|
48
|
+
/**
|
|
49
|
+
* Warning issues, conflicts and findings, and the
|
|
50
|
+
* findings of every report the check carries.
|
|
51
|
+
*/
|
|
52
|
+
warnings: number;
|
|
53
|
+
/**
|
|
54
|
+
* Why work did not run: no integration manifest,
|
|
55
|
+
* or a report the check carries that was skipped.
|
|
56
|
+
*/
|
|
57
|
+
skipped: string[];
|
|
58
|
+
/**
|
|
59
|
+
* Whether the check's own subcommand exits 1.
|
|
60
|
+
*/
|
|
61
|
+
failed: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The check's own envelope.
|
|
64
|
+
*/
|
|
65
|
+
response: IntegrationCheckResponse;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A warning, or work a check skipped, as `--strict` reports it: an error in
|
|
69
|
+
* the checks' own issue format, with the finding's own code (a conflict's is
|
|
70
|
+
* `component_name_conflict` or `template_id_conflict`, a package with no
|
|
71
|
+
* manifest's is `missing_manifest`, and a skipped comparison's is its key in
|
|
72
|
+
* snake case with `_skipped`), message, and its fix, file, and line where it
|
|
73
|
+
* has them.
|
|
74
|
+
*/
|
|
75
|
+
export type IntegrationCheckFailure = {
|
|
76
|
+
check: IntegrationCheckName;
|
|
77
|
+
severity: "error";
|
|
78
|
+
code: string;
|
|
79
|
+
message: string;
|
|
80
|
+
/**
|
|
81
|
+
* the component a finding is about
|
|
82
|
+
*/
|
|
83
|
+
component?: string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* the prop a finding is about
|
|
86
|
+
*/
|
|
87
|
+
prop?: string | undefined;
|
|
88
|
+
fix?: string | undefined;
|
|
89
|
+
file?: string | undefined;
|
|
90
|
+
line?: number | undefined;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* `astryx --json integration check [--strict]`.
|
|
94
|
+
*
|
|
95
|
+
* `failed` decides the exit code: any check that fails on its own, and with
|
|
96
|
+
* `strict` also any warning or skipped work, which `failures` then lists as
|
|
97
|
+
* errors (it is empty without `strict`).
|
|
98
|
+
*/
|
|
99
|
+
export type IntegrationCheckCommandResponse = {
|
|
100
|
+
/**
|
|
101
|
+
* `unchecked` counts the items the checks list as not compared, which never
|
|
102
|
+
* fail it.
|
|
103
|
+
*/
|
|
104
|
+
type: "integration.check";
|
|
105
|
+
data: {
|
|
106
|
+
validated: boolean;
|
|
107
|
+
name: string | null;
|
|
108
|
+
version: string | null;
|
|
109
|
+
strict: boolean;
|
|
110
|
+
checks: IntegrationCheckResult[];
|
|
111
|
+
errors: number;
|
|
112
|
+
warnings: number;
|
|
113
|
+
skipped: string[];
|
|
114
|
+
unchecked: number;
|
|
115
|
+
failures: IntegrationCheckFailure[];
|
|
116
|
+
failed: boolean;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Typedefs for `integrationCheck()` / `astryx integration check`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for `integrationCheck()`.
|
|
9
|
+
* @typedef {object} IntegrationCheckOptions
|
|
10
|
+
* @property {string} [cwd] The integration package folder to check; the
|
|
11
|
+
* current directory by default.
|
|
12
|
+
* @property {boolean} [strict] Also fail on any warning and on any check that
|
|
13
|
+
* did not run, not only on errors.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The checks `integrationCheck()` runs, in the order it runs them.
|
|
18
|
+
* @typedef {'validate' | 'components' | 'templates' | 'docs'} IntegrationCheckName
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The envelope one check returns on its own, unchanged.
|
|
23
|
+
* @typedef {import('./validate-integration.type.mjs').ValidateIntegrationResponse
|
|
24
|
+
* | import('./authoring-checks.type.mjs').IntegrationComponentConflictResponse
|
|
25
|
+
* | import('./authoring-checks.type.mjs').IntegrationTemplateConflictResponse
|
|
26
|
+
* | import('./authoring-checks.type.mjs').IntegrationDocConflictResponse} IntegrationCheckResponse
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A comparison a check carries beside its issues, such as one that needs the
|
|
31
|
+
* project's own TypeScript. `skipped` means it did not run, and `reason` says
|
|
32
|
+
* why; each of its findings is a warning.
|
|
33
|
+
* @typedef {object} IntegrationCheckReport
|
|
34
|
+
* @property {'checked' | 'skipped'} status
|
|
35
|
+
* @property {string} [reason]
|
|
36
|
+
* @property {Array<{severity?: string}>} findings
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One check's result, counted the way its own subcommand exits.
|
|
41
|
+
* @typedef {object} IntegrationCheckResult
|
|
42
|
+
* @property {IntegrationCheckName} check
|
|
43
|
+
* @property {number} errors What makes the check's own subcommand exit 1.
|
|
44
|
+
* @property {number} warnings Warning issues, conflicts and findings, and the
|
|
45
|
+
* findings of every report the check carries.
|
|
46
|
+
* @property {string[]} skipped Why work did not run: no integration manifest,
|
|
47
|
+
* or a report the check carries that was skipped.
|
|
48
|
+
* @property {boolean} failed Whether the check's own subcommand exits 1.
|
|
49
|
+
* @property {IntegrationCheckResponse} response The check's own envelope.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A warning, or work a check skipped, as `--strict` reports it: an error in
|
|
54
|
+
* the checks' own issue format, with the finding's own code (a conflict's is
|
|
55
|
+
* `component_name_conflict` or `template_id_conflict`, a package with no
|
|
56
|
+
* manifest's is `missing_manifest`, and a skipped comparison's is its key in
|
|
57
|
+
* snake case with `_skipped`), message, and its fix, file, and line where it
|
|
58
|
+
* has them.
|
|
59
|
+
* @typedef {object} IntegrationCheckFailure
|
|
60
|
+
* @property {IntegrationCheckName} check
|
|
61
|
+
* @property {'error'} severity
|
|
62
|
+
* @property {string} code
|
|
63
|
+
* @property {string} message
|
|
64
|
+
* @property {string} [component] the component a finding is about
|
|
65
|
+
* @property {string} [prop] the prop a finding is about
|
|
66
|
+
* @property {string} [fix]
|
|
67
|
+
* @property {string} [file]
|
|
68
|
+
* @property {number} [line]
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `astryx --json integration check [--strict]`.
|
|
73
|
+
*
|
|
74
|
+
* `failed` decides the exit code: any check that fails on its own, and with
|
|
75
|
+
* `strict` also any warning or skipped work, which `failures` then lists as
|
|
76
|
+
* errors (it is empty without `strict`).
|
|
77
|
+
* @typedef {object} IntegrationCheckCommandResponse
|
|
78
|
+
* @property {'integration.check'} type
|
|
79
|
+
* `unchecked` counts the items the checks list as not compared, which never
|
|
80
|
+
* fail it.
|
|
81
|
+
* @property {{validated: boolean, name: string | null, version: string | null, strict: boolean, checks: IntegrationCheckResult[], errors: number, warnings: number, skipped: string[], unchecked: number, failures: IntegrationCheckFailure[], failed: boolean}} data
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* @file FunctionDoc for `integrationCheck()` / `astryx integration check`.
|
|
6
|
+
* Colocated with the API function it documents; the shape source of truth stays
|
|
7
|
+
* in `integration-check.type.mjs`.
|
|
8
|
+
*/
|
|
9
|
+
/** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
|
|
10
|
+
export const doc: import("@astryxdesign/cli/authoring").FunctionDoc;
|