@astryxdesign/cli 0.7.2-canary.fbc03f7 → 0.7.2-canary.fdd384f
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 +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 +9 -0
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +100 -13
- 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/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 +8 -0
- 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 +2 -2
- package/authoring/doctypes/component/type.ts +6 -3
- 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 +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
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Each doc against its published authoring type: unknown, missing, and
|
|
5
|
+
* wrongly typed fields, the root deciding the type, translations, the authors'
|
|
6
|
+
* own annotations, and the visible skip when the project has no TypeScript.
|
|
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 {createRequire} from 'node:module';
|
|
13
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
14
|
+
import {unusableTypeScriptReason} from './project-typescript.mjs';
|
|
15
|
+
import {
|
|
16
|
+
checkDocTypes,
|
|
17
|
+
DOC_TYPES_TYPESCRIPT_MISSING_REASON,
|
|
18
|
+
} from './doc-types.mjs';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const TYPESCRIPT_DIR = path.dirname(require.resolve('typescript/package.json'));
|
|
22
|
+
|
|
23
|
+
/** @type {string} */
|
|
24
|
+
let pkg;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
pkg = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-doc-types-'));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
fs.rmSync(pkg, {recursive: true, force: true});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function withTypeScript() {
|
|
35
|
+
fs.mkdirSync(path.join(pkg, 'node_modules'), {recursive: true});
|
|
36
|
+
fs.symlinkSync(
|
|
37
|
+
TYPESCRIPT_DIR,
|
|
38
|
+
path.join(pkg, 'node_modules', 'typescript'),
|
|
39
|
+
'dir',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} rel
|
|
45
|
+
* @param {string} text
|
|
46
|
+
*/
|
|
47
|
+
function write(rel, text) {
|
|
48
|
+
const file = path.join(pkg, rel);
|
|
49
|
+
fs.mkdirSync(path.dirname(file), {recursive: true});
|
|
50
|
+
fs.writeFileSync(file, text);
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {Array<[string, import('./doc-types.mjs').DocRoot]>} files
|
|
56
|
+
* @param {{core?: boolean}} [options]
|
|
57
|
+
*/
|
|
58
|
+
function check(files, options = {}) {
|
|
59
|
+
return checkDocTypes(
|
|
60
|
+
files.map(([rel, root]) => ({path: path.join(pkg, rel), root})),
|
|
61
|
+
{packageDir: pkg, resolveFrom: [pkg], ...options},
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {import('./doc-types.mjs').DocTypesReport} report */
|
|
66
|
+
const summary = report =>
|
|
67
|
+
report.findings.map(f => `${f.doc} ${f.export} ${f.field} ${f.problem}`);
|
|
68
|
+
|
|
69
|
+
const COMPONENT = {
|
|
70
|
+
type: 'component',
|
|
71
|
+
name: 'Badge',
|
|
72
|
+
displayName: 'Badge',
|
|
73
|
+
usage: {description: 'Shows a count.'},
|
|
74
|
+
props: [
|
|
75
|
+
{name: 'label', type: 'string', description: 'Text.', required: true},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe('checkDocTypes', () => {
|
|
80
|
+
it('skips with a reason, and checks nothing, when the project has no TypeScript', async () => {
|
|
81
|
+
write(
|
|
82
|
+
'components/Badge.doc.mjs',
|
|
83
|
+
`export default ${JSON.stringify(COMPONENT)};\n`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
87
|
+
|
|
88
|
+
expect(report).toEqual({
|
|
89
|
+
status: 'skipped',
|
|
90
|
+
reason: DOC_TYPES_TYPESCRIPT_MISSING_REASON,
|
|
91
|
+
docs: 0,
|
|
92
|
+
findings: [],
|
|
93
|
+
unchecked: [],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('names an installed TypeScript it cannot use, rather than asking to install one', async () => {
|
|
98
|
+
write(
|
|
99
|
+
'components/Badge.doc.mjs',
|
|
100
|
+
`export default ${JSON.stringify(COMPONENT)};\n`,
|
|
101
|
+
);
|
|
102
|
+
// TypeScript 7 has no JavaScript compiler API: one entry exports none,
|
|
103
|
+
// another cannot be loaded at all.
|
|
104
|
+
for (const stub of [
|
|
105
|
+
{main: 'index.js', code: "module.exports = {version: '7.0.0'};\n"},
|
|
106
|
+
{exports: {'./package.json': './package.json'}},
|
|
107
|
+
]) {
|
|
108
|
+
const dir = path.join(pkg, 'node_modules', 'typescript');
|
|
109
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
110
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
111
|
+
const {code, ...fields} = stub;
|
|
112
|
+
fs.writeFileSync(
|
|
113
|
+
path.join(dir, 'package.json'),
|
|
114
|
+
JSON.stringify({name: 'typescript', version: '7.0.0', ...fields}),
|
|
115
|
+
);
|
|
116
|
+
if (code) fs.writeFileSync(path.join(dir, 'index.js'), code);
|
|
117
|
+
|
|
118
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
119
|
+
|
|
120
|
+
expect(report).toEqual({
|
|
121
|
+
status: 'skipped',
|
|
122
|
+
reason: unusableTypeScriptReason('7.0.0', 'docs were not checked'),
|
|
123
|
+
docs: 0,
|
|
124
|
+
findings: [],
|
|
125
|
+
unchecked: [],
|
|
126
|
+
});
|
|
127
|
+
expect(report.reason).toContain('TypeScript 7.0.0 is installed');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('says there is nothing to check when the package has no doc files, with or without TypeScript', async () => {
|
|
132
|
+
const without = await check([]);
|
|
133
|
+
withTypeScript();
|
|
134
|
+
const withTs = await check([]);
|
|
135
|
+
|
|
136
|
+
for (const report of [without, withTs]) {
|
|
137
|
+
expect(report).toEqual({
|
|
138
|
+
status: 'checked',
|
|
139
|
+
docs: 0,
|
|
140
|
+
findings: [],
|
|
141
|
+
unchecked: [],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('reports what a value outside its type hid in the same run', async () => {
|
|
147
|
+
withTypeScript();
|
|
148
|
+
write(
|
|
149
|
+
'components/Badge.doc.mjs',
|
|
150
|
+
[
|
|
151
|
+
'export default {',
|
|
152
|
+
" type: 'component',",
|
|
153
|
+
" name: 'Badge',",
|
|
154
|
+
" keyword: ['count'],",
|
|
155
|
+
" usage: {description: 'Shows a count.', bestPractices: [{guidance: 'yes', description: 'Do.'}]},",
|
|
156
|
+
" props: [{name: 'label', type: 'string', description: 'Text.'}],",
|
|
157
|
+
'};',
|
|
158
|
+
'',
|
|
159
|
+
].join('\n'),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
163
|
+
|
|
164
|
+
expect(summary(report)).toEqual([
|
|
165
|
+
'components/Badge.doc.mjs default displayName missing_field',
|
|
166
|
+
'components/Badge.doc.mjs default keyword unknown_field',
|
|
167
|
+
'components/Badge.doc.mjs default usage.bestPractices[0].guidance wrong_type',
|
|
168
|
+
]);
|
|
169
|
+
expect(report.findings[0].fix).toBe(
|
|
170
|
+
'Add `displayName`: SingleComponentDoc requires it.',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('sets aside an object whose own `type` is wrong, and reports what it hid', async () => {
|
|
175
|
+
withTypeScript();
|
|
176
|
+
write(
|
|
177
|
+
'components/Badge.doc.mjs',
|
|
178
|
+
[
|
|
179
|
+
'export default {',
|
|
180
|
+
" type: 'component',",
|
|
181
|
+
" name: 'Badge',",
|
|
182
|
+
" keyword: ['count'],",
|
|
183
|
+
" usage: {description: 'Shows a count.'},",
|
|
184
|
+
" props: [{name: 'label', type: 42, description: 'Text.'}],",
|
|
185
|
+
'};',
|
|
186
|
+
'',
|
|
187
|
+
].join('\n'),
|
|
188
|
+
);
|
|
189
|
+
write(
|
|
190
|
+
'docs/setup.doc.mjs',
|
|
191
|
+
[
|
|
192
|
+
'export default {',
|
|
193
|
+
" type: 'generic',",
|
|
194
|
+
" name: 'setup',",
|
|
195
|
+
" title: 'Setup',",
|
|
196
|
+
" description: 'How.',",
|
|
197
|
+
' extra: true,',
|
|
198
|
+
" sections: [{title: 'One', content: [{type: 'para', text: 'x'}]}],",
|
|
199
|
+
'};',
|
|
200
|
+
'',
|
|
201
|
+
].join('\n'),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const report = await check([
|
|
205
|
+
['components/Badge.doc.mjs', 'components'],
|
|
206
|
+
['docs/setup.doc.mjs', 'docs'],
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
expect(summary(report)).toEqual([
|
|
210
|
+
'components/Badge.doc.mjs default displayName missing_field',
|
|
211
|
+
'components/Badge.doc.mjs default keyword unknown_field',
|
|
212
|
+
'components/Badge.doc.mjs default props[0].type wrong_type',
|
|
213
|
+
'docs/setup.doc.mjs default extra unknown_field',
|
|
214
|
+
'docs/setup.doc.mjs default sections[0].content[0].type wrong_type',
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('reports a missing top-level field that nested missing fields hid, and quotes no edited text', async () => {
|
|
219
|
+
withTypeScript();
|
|
220
|
+
write(
|
|
221
|
+
'components/Hide.doc.mjs',
|
|
222
|
+
[
|
|
223
|
+
'export default {',
|
|
224
|
+
" type: 'component',",
|
|
225
|
+
" name: 'Hide',",
|
|
226
|
+
' usage: {bestPractices: []},',
|
|
227
|
+
" props: [{name: 'label', type: 'string'}],",
|
|
228
|
+
'};',
|
|
229
|
+
'',
|
|
230
|
+
].join('\n'),
|
|
231
|
+
);
|
|
232
|
+
write(
|
|
233
|
+
'components/Bad.doc.mjs',
|
|
234
|
+
[
|
|
235
|
+
'export default {',
|
|
236
|
+
" type: 'component',",
|
|
237
|
+
" name: 'Bad',",
|
|
238
|
+
" displayName: 'Bad',",
|
|
239
|
+
" usage: {description: 'x', bestPractices: [{guidance: 42}]},",
|
|
240
|
+
' props: [],',
|
|
241
|
+
'};',
|
|
242
|
+
'',
|
|
243
|
+
].join('\n'),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const report = await check([
|
|
247
|
+
['components/Hide.doc.mjs', 'components'],
|
|
248
|
+
['components/Bad.doc.mjs', 'components'],
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
expect(summary(report)).toEqual([
|
|
252
|
+
'components/Bad.doc.mjs default usage.bestPractices[0].description missing_field',
|
|
253
|
+
'components/Bad.doc.mjs default usage.bestPractices[0].guidance wrong_type',
|
|
254
|
+
'components/Hide.doc.mjs default displayName missing_field',
|
|
255
|
+
'components/Hide.doc.mjs default usage.description missing_field',
|
|
256
|
+
'components/Hide.doc.mjs default props[0].description missing_field',
|
|
257
|
+
]);
|
|
258
|
+
for (const finding of report.findings) {
|
|
259
|
+
expect(finding.message).not.toMatch(/\bany\b/);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('checks a translation that carries props for its fields, not for what only the base doc needs', async () => {
|
|
264
|
+
withTypeScript();
|
|
265
|
+
write(
|
|
266
|
+
'components/Group.doc.mjs',
|
|
267
|
+
[
|
|
268
|
+
'export default {',
|
|
269
|
+
" type: 'component',",
|
|
270
|
+
" name: 'Group',",
|
|
271
|
+
" displayName: 'Group',",
|
|
272
|
+
" usage: {description: 'Rows.'},",
|
|
273
|
+
" props: [{name: 'title', type: 'string', description: 'Heading.'}],",
|
|
274
|
+
'};',
|
|
275
|
+
'export const docsZh = {',
|
|
276
|
+
" description: '一组行。',",
|
|
277
|
+
" props: [{name: 'title', description: '标题。'}],",
|
|
278
|
+
'};',
|
|
279
|
+
'export const docsDense = {',
|
|
280
|
+
" props: [{name: 'title', type: 42, description: 'Heading.'}, {description: 'No name.'}],",
|
|
281
|
+
'};',
|
|
282
|
+
'',
|
|
283
|
+
].join('\n'),
|
|
284
|
+
);
|
|
285
|
+
// The overlay merges props, and sub-component entries, by name and keeps
|
|
286
|
+
// the base doc's `type`: a translated entry needs only its `name`.
|
|
287
|
+
write(
|
|
288
|
+
'components/Family.doc.mjs',
|
|
289
|
+
[
|
|
290
|
+
'export default {',
|
|
291
|
+
" type: 'component',",
|
|
292
|
+
" name: 'Family',",
|
|
293
|
+
" displayName: 'Family',",
|
|
294
|
+
" usage: {description: 'Members.'},",
|
|
295
|
+
" components: [{name: 'Member', description: 'One.', props: [{name: 'size', type: 'number', description: 'Size.'}]}],",
|
|
296
|
+
'};',
|
|
297
|
+
'export const docsZh = {',
|
|
298
|
+
" components: [{name: 'Member', props: [{name: 'size', description: '大小。'}]}],",
|
|
299
|
+
'};',
|
|
300
|
+
'',
|
|
301
|
+
].join('\n'),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const report = await check([
|
|
305
|
+
['components/Group.doc.mjs', 'components'],
|
|
306
|
+
['components/Family.doc.mjs', 'components'],
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
expect(summary(report)).toEqual([
|
|
310
|
+
'components/Group.doc.mjs docsDense props[0].type wrong_type',
|
|
311
|
+
'components/Group.doc.mjs docsDense props[1].name missing_field',
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('says a doc that re-exports its value is not followed', async () => {
|
|
316
|
+
withTypeScript();
|
|
317
|
+
write(
|
|
318
|
+
'docs/shared.mjs',
|
|
319
|
+
`export default ${JSON.stringify({type: 'generic', name: 'shared', title: 'Shared', description: 'x', sections: []})};\n`,
|
|
320
|
+
);
|
|
321
|
+
write('docs/again.doc.mjs', "export {default} from './shared.mjs';\n");
|
|
322
|
+
|
|
323
|
+
const report = await check([['docs/again.doc.mjs', 'docs']]);
|
|
324
|
+
|
|
325
|
+
expect(report.unchecked).toEqual([
|
|
326
|
+
{
|
|
327
|
+
doc: 'docs/again.doc.mjs',
|
|
328
|
+
reason: expect.stringContaining('re-exports its doc from ./shared.mjs'),
|
|
329
|
+
},
|
|
330
|
+
]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('reports every unknown field, however many', async () => {
|
|
334
|
+
withTypeScript();
|
|
335
|
+
const extra = Array.from({length: 8}, (_, i) => ` extra${i + 1}: ${i},`);
|
|
336
|
+
write(
|
|
337
|
+
'components/Badge.doc.mjs',
|
|
338
|
+
[
|
|
339
|
+
`export default {...${JSON.stringify(COMPONENT)},`,
|
|
340
|
+
...extra,
|
|
341
|
+
'};',
|
|
342
|
+
'',
|
|
343
|
+
].join('\n'),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
347
|
+
|
|
348
|
+
expect(summary(report)).toEqual(
|
|
349
|
+
Array.from(
|
|
350
|
+
{length: 8},
|
|
351
|
+
(_, i) =>
|
|
352
|
+
`components/Badge.doc.mjs default extra${i + 1} unknown_field`,
|
|
353
|
+
),
|
|
354
|
+
);
|
|
355
|
+
expect(report.unchecked).toEqual([]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('passes a component doc that matches ComponentDoc, with a field another member declares', async () => {
|
|
359
|
+
withTypeScript();
|
|
360
|
+
write(
|
|
361
|
+
'components/Badge.doc.mjs',
|
|
362
|
+
`export default ${JSON.stringify({...COMPONENT, description: 'A count chip.'})};\n`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
366
|
+
|
|
367
|
+
expect(report.status).toBe('checked');
|
|
368
|
+
expect(report.typescript).toMatch(/^\d+\.\d+/);
|
|
369
|
+
expect(report.docs).toBe(1);
|
|
370
|
+
expect(report.findings).toEqual([]);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('names an unknown field, and the field TypeScript suggests instead', async () => {
|
|
374
|
+
withTypeScript();
|
|
375
|
+
write(
|
|
376
|
+
'components/Badge.doc.mjs',
|
|
377
|
+
[
|
|
378
|
+
`export default {...${JSON.stringify(COMPONENT)},`,
|
|
379
|
+
" keyword: ['count'],",
|
|
380
|
+
'};',
|
|
381
|
+
'',
|
|
382
|
+
].join('\n'),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
386
|
+
|
|
387
|
+
expect(report.findings).toEqual([
|
|
388
|
+
{
|
|
389
|
+
doc: 'components/Badge.doc.mjs',
|
|
390
|
+
line: 2,
|
|
391
|
+
export: 'default',
|
|
392
|
+
type: 'ComponentDoc',
|
|
393
|
+
field: 'keyword',
|
|
394
|
+
problem: 'unknown_field',
|
|
395
|
+
severity: 'warning',
|
|
396
|
+
message:
|
|
397
|
+
"Object literal may only specify known properties, but 'keyword' does not exist in type 'SingleComponentDoc'. Did you mean to write 'keywords'?",
|
|
398
|
+
fix: 'Rename `keyword` to `keywords`.',
|
|
399
|
+
},
|
|
400
|
+
]);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('names a value outside its type, deep in the doc, at its line', async () => {
|
|
404
|
+
withTypeScript();
|
|
405
|
+
write(
|
|
406
|
+
'components/Badge.doc.mjs',
|
|
407
|
+
[
|
|
408
|
+
'export default {',
|
|
409
|
+
" type: 'component',",
|
|
410
|
+
" name: 'Badge',",
|
|
411
|
+
" displayName: 'Badge',",
|
|
412
|
+
' usage: {',
|
|
413
|
+
" description: 'Shows a count.',",
|
|
414
|
+
" bestPractices: [{guidance: true, description: 'Do.'}, {guidance: 'yes', description: 'Dont.'}],",
|
|
415
|
+
' },',
|
|
416
|
+
" props: [{name: 'label', type: 'string', description: 'Text.'}],",
|
|
417
|
+
'};',
|
|
418
|
+
'',
|
|
419
|
+
].join('\n'),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
423
|
+
|
|
424
|
+
expect(report.findings).toEqual([
|
|
425
|
+
expect.objectContaining({
|
|
426
|
+
line: 7,
|
|
427
|
+
field: 'usage.bestPractices[1].guidance',
|
|
428
|
+
problem: 'wrong_type',
|
|
429
|
+
fix: 'Change `usage.bestPractices[1].guidance` to a value of type boolean.',
|
|
430
|
+
}),
|
|
431
|
+
]);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("checks a doc against its root's type, so a topic stamped beside a component source is flagged, with the rest of the doc", async () => {
|
|
435
|
+
withTypeScript();
|
|
436
|
+
const {displayName, ...rest} = COMPONENT;
|
|
437
|
+
write(
|
|
438
|
+
'components/Badge.doc.mjs',
|
|
439
|
+
`export default ${JSON.stringify({...rest, type: 'generic', colour: 'red'})};\n`,
|
|
440
|
+
);
|
|
441
|
+
// In the docs root the `type` picks the doc's type, so a wrong one leaves
|
|
442
|
+
// the rest of it unchecked.
|
|
443
|
+
write(
|
|
444
|
+
'docs/setup.doc.mjs',
|
|
445
|
+
"export default {type: 'generc', name: 'setup', title: 1, colour: 'red'};\n",
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const report = await check([
|
|
449
|
+
['components/Badge.doc.mjs', 'components'],
|
|
450
|
+
['docs/setup.doc.mjs', 'docs'],
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
expect(summary(report)).toEqual([
|
|
454
|
+
'components/Badge.doc.mjs default colour unknown_field',
|
|
455
|
+
'components/Badge.doc.mjs default displayName missing_field',
|
|
456
|
+
'components/Badge.doc.mjs default type wrong_type',
|
|
457
|
+
'docs/setup.doc.mjs default type wrong_type',
|
|
458
|
+
]);
|
|
459
|
+
expect(report.findings[2].fix).toBe(
|
|
460
|
+
"This doc is in the components root, so it is a component doc: set type: 'component', or move it to the docs root.",
|
|
461
|
+
);
|
|
462
|
+
expect(report.unchecked).toEqual([
|
|
463
|
+
{
|
|
464
|
+
doc: 'docs/setup.doc.mjs',
|
|
465
|
+
reason:
|
|
466
|
+
'its `type` names no type the docs root takes, so its other fields were not checked; fix the `type` and run the check again',
|
|
467
|
+
},
|
|
468
|
+
]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("ignores the author's own annotation and checks the value once, against the root's type", async () => {
|
|
472
|
+
withTypeScript();
|
|
473
|
+
write(
|
|
474
|
+
'components/Badge.doc.mjs',
|
|
475
|
+
[
|
|
476
|
+
"/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */",
|
|
477
|
+
`export const docs = ${JSON.stringify({...COMPONENT, keyword: []})};`,
|
|
478
|
+
'',
|
|
479
|
+
].join('\n'),
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
483
|
+
|
|
484
|
+
expect(summary(report)).toEqual([
|
|
485
|
+
'components/Badge.doc.mjs docs keyword unknown_field',
|
|
486
|
+
]);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("ignores a .doc.ts doc's own annotation and type assertion, as it does a JSDoc one", async () => {
|
|
490
|
+
withTypeScript();
|
|
491
|
+
const header =
|
|
492
|
+
"import type {ReferenceDoc} from '@astryxdesign/cli/authoring';";
|
|
493
|
+
const doc = JSON.stringify({...COMPONENT, keyword: []});
|
|
494
|
+
write(
|
|
495
|
+
'components/Badge.doc.ts',
|
|
496
|
+
`${header}\nexport const docs: ReferenceDoc = ${doc};\n`,
|
|
497
|
+
);
|
|
498
|
+
write(
|
|
499
|
+
'components/Chip.doc.ts',
|
|
500
|
+
`${header}\nexport default <ReferenceDoc>${doc};\n`,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const report = await check([
|
|
504
|
+
['components/Badge.doc.ts', 'components'],
|
|
505
|
+
['components/Chip.doc.ts', 'components'],
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
expect(summary(report)).toEqual([
|
|
509
|
+
'components/Badge.doc.ts docs keyword unknown_field',
|
|
510
|
+
'components/Chip.doc.ts default keyword unknown_field',
|
|
511
|
+
]);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('checks translations: a string overlay against ComponentTranslationDoc, a full one against ComponentDoc with no field required', async () => {
|
|
515
|
+
withTypeScript();
|
|
516
|
+
write(
|
|
517
|
+
'components/Badge.doc.mjs',
|
|
518
|
+
[
|
|
519
|
+
`export default ${JSON.stringify(COMPONENT)};`,
|
|
520
|
+
"export const docsDense = {name: 'Badge', description: 'count chip'};",
|
|
521
|
+
`export const docsZh = ${JSON.stringify({...COMPONENT, displayName: undefined, keyword: []})};`,
|
|
522
|
+
'',
|
|
523
|
+
].join('\n'),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const report = await check([['components/Badge.doc.mjs', 'components']]);
|
|
527
|
+
|
|
528
|
+
expect(report.docs).toBe(3);
|
|
529
|
+
expect(summary(report)).toEqual([
|
|
530
|
+
'components/Badge.doc.mjs docsDense name unknown_field',
|
|
531
|
+
'components/Badge.doc.mjs docsZh keyword unknown_field',
|
|
532
|
+
]);
|
|
533
|
+
expect(report.findings[0].type).toBe('ComponentTranslationDoc');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('checks a template, a docs topic by its stamp, a topic overlay, and a theme', async () => {
|
|
537
|
+
withTypeScript();
|
|
538
|
+
write(
|
|
539
|
+
'templates/Hero.doc.mjs',
|
|
540
|
+
"export default {type: 'block', name: 'Hero', description: 'A hero.', category: 'components/Hero'};\n",
|
|
541
|
+
);
|
|
542
|
+
write(
|
|
543
|
+
'docs/setup.doc.mjs',
|
|
544
|
+
"export default {type: 'generic', name: 'setup', title: 'Setup', description: 'How.', sections: [{title: 'One', content: [{type: 'prose', txt: 'x'}]}]};\n",
|
|
545
|
+
);
|
|
546
|
+
write(
|
|
547
|
+
'docs/setup.doc.zh.mjs',
|
|
548
|
+
"export const docsZh = {description: '如何', sections: [{section: 'One', title: '一'}]};\n",
|
|
549
|
+
);
|
|
550
|
+
write(
|
|
551
|
+
'themes/ocean/oceanTheme.doc.mjs',
|
|
552
|
+
"export default {type: 'theme', name: 'ocean', displayName: 'Ocean', description: 'Blue.', maintained: 'yes'};\n",
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
const report = await check([
|
|
556
|
+
['templates/Hero.doc.mjs', 'templates'],
|
|
557
|
+
['docs/setup.doc.mjs', 'docs'],
|
|
558
|
+
['docs/setup.doc.zh.mjs', 'docs-overlay'],
|
|
559
|
+
['themes/ocean/oceanTheme.doc.mjs', 'themes'],
|
|
560
|
+
]);
|
|
561
|
+
|
|
562
|
+
// An integration template's own category is allowed (IntegrationTemplateDoc);
|
|
563
|
+
// its missing displayName and aspectRatio are not.
|
|
564
|
+
// The topic's unknown `txt` hides its missing `text` from TypeScript;
|
|
565
|
+
// the check looks again without it, so both are reported.
|
|
566
|
+
expect(summary(report)).toEqual([
|
|
567
|
+
'docs/setup.doc.mjs default sections[0].content[0].text missing_field',
|
|
568
|
+
'docs/setup.doc.mjs default sections[0].content[0].txt unknown_field',
|
|
569
|
+
'docs/setup.doc.zh.mjs docsZh sections[0].content missing_field',
|
|
570
|
+
'templates/Hero.doc.mjs default aspectRatio missing_field',
|
|
571
|
+
'templates/Hero.doc.mjs default displayName missing_field',
|
|
572
|
+
'themes/ocean/oceanTheme.doc.mjs default maintained wrong_type',
|
|
573
|
+
]);
|
|
574
|
+
expect(new Set(report.findings.map(f => f.type))).toEqual(
|
|
575
|
+
new Set([
|
|
576
|
+
'ReferenceDoc',
|
|
577
|
+
'ReferenceTranslationDoc',
|
|
578
|
+
'IntegrationTemplateDoc',
|
|
579
|
+
'ThemeDoc',
|
|
580
|
+
]),
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
// Core's own templates keep TemplateDoc and its TemplateCategory values.
|
|
584
|
+
const core = await check([['templates/Hero.doc.mjs', 'templates']], {
|
|
585
|
+
core: true,
|
|
586
|
+
});
|
|
587
|
+
expect(summary(core)).toEqual([
|
|
588
|
+
'templates/Hero.doc.mjs default aspectRatio missing_field',
|
|
589
|
+
'templates/Hero.doc.mjs default category wrong_type',
|
|
590
|
+
'templates/Hero.doc.mjs default displayName missing_field',
|
|
591
|
+
]);
|
|
592
|
+
expect(new Set(core.findings.map(f => f.type))).toEqual(
|
|
593
|
+
new Set(['TemplateDoc']),
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('checks nothing with a TypeScript older than 4.9, even when every doc is TypeScript', async () => {
|
|
598
|
+
const fake = path.join(pkg, 'node_modules', 'typescript');
|
|
599
|
+
fs.mkdirSync(fake, {recursive: true});
|
|
600
|
+
fs.writeFileSync(
|
|
601
|
+
path.join(fake, 'package.json'),
|
|
602
|
+
JSON.stringify({name: 'typescript', version: '4.8.4', main: 'index.js'}),
|
|
603
|
+
);
|
|
604
|
+
fs.writeFileSync(
|
|
605
|
+
path.join(fake, 'index.js'),
|
|
606
|
+
`module.exports = {...require(${JSON.stringify(TYPESCRIPT_DIR)}), version: '4.8.4', versionMajorMinor: '4.8'};\n`,
|
|
607
|
+
);
|
|
608
|
+
write(
|
|
609
|
+
'components/Badge.doc.ts',
|
|
610
|
+
`export default ${JSON.stringify(COMPONENT)};\n`,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const report = await check([['components/Badge.doc.ts', 'components']]);
|
|
614
|
+
|
|
615
|
+
expect(report).toMatchObject({status: 'skipped', docs: 0, findings: []});
|
|
616
|
+
expect(report.reason).toContain('needs TypeScript 4.9 or later');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('names a doc the compiler cannot parse as not checked, never as a pass', async () => {
|
|
620
|
+
withTypeScript();
|
|
621
|
+
write(
|
|
622
|
+
'components/Badge.doc.ts',
|
|
623
|
+
"export default {type: 'component', name: 'Badge',, displayName: 'Badge'};\n",
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const report = await check([['components/Badge.doc.ts', 'components']]);
|
|
627
|
+
|
|
628
|
+
expect(report.docs).toBe(0);
|
|
629
|
+
expect(report.findings).toEqual([]);
|
|
630
|
+
expect(report.unchecked).toEqual([
|
|
631
|
+
expect.objectContaining({
|
|
632
|
+
doc: 'components/Badge.doc.ts',
|
|
633
|
+
reason: expect.stringContaining('cannot parse it'),
|
|
634
|
+
}),
|
|
635
|
+
]);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('names a file that exports no doc as not checked', async () => {
|
|
639
|
+
withTypeScript();
|
|
640
|
+
write('components/Empty.doc.mjs', 'export const other = {};\n');
|
|
641
|
+
|
|
642
|
+
const report = await check([['components/Empty.doc.mjs', 'components']]);
|
|
643
|
+
|
|
644
|
+
expect(report.findings).toEqual([]);
|
|
645
|
+
expect(report.unchecked).toEqual([
|
|
646
|
+
{
|
|
647
|
+
doc: 'components/Empty.doc.mjs',
|
|
648
|
+
reason: 'it exports no doc the CLI reads (default or docs)',
|
|
649
|
+
},
|
|
650
|
+
]);
|
|
651
|
+
});
|
|
652
|
+
});
|