@astryxdesign/cli 0.7.2-canary.e26723d → 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 +9 -8
- package/api/docs/_adapter.d.mts +11 -0
- package/api/docs/_adapter.mjs +37 -0
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/integration/add-contribution.mjs +1 -1
- package/api/integration/add-contribution.test.mjs +4 -2
- 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 +19 -4
- package/api/integration/validate-integration.mjs +8 -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 +13 -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 +33 -9
- package/api/theme/build/build.family.test.mjs +54 -6
- package/api/theme/build/build.mjs +110 -13
- package/api/theme/themeBuild.doc.mjs +4 -4
- package/assets/docs/styling-overview.doc.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +1 -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 +51 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
- package/assets/docs/tree/checks.doc.mjs +21 -13
- package/assets/docs/tree/codemods.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +3 -5
- package/assets/docs/tree/component-imports.doc.mjs +2 -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 +1 -1
- 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 +5 -4
- 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 +12 -4
- 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 +4 -4
- 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 +6 -6
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -2
- 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 +28 -25
- package/authoring/doctypes/component/type.ts +7 -5
- 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.mjs +1 -1
- package/authoring/doctypes/template/template.doc.mjs +3 -3
- 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/integration/integration.doc.mjs +4 -3
- package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
- package/clients/cli/commands/doctor-integration-components.doc.mjs +8 -3
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +447 -2
- package/clients/cli/commands/doctor.mjs +116 -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/discovery/authoring-self-docs.mjs +1 -1
- package/foundation/discovery/doc-types.mjs +2 -2
- 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/prop-drift.d.mts +61 -3
- package/foundation/discovery/prop-drift.mjs +559 -61
- package/foundation/discovery/prop-drift.test.mjs +1032 -10
- package/foundation/doc-compiler/doc-loads.test.mjs +2 -0
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
|
@@ -210,6 +210,425 @@ describe('doctor integration — command', () => {
|
|
|
210
210
|
expect(process.exitCode).toBeUndefined();
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
it('validate warns about each doc field that does not match its authoring type, and still exits 0', async () => {
|
|
214
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
215
|
+
process.chdir(tmpDir);
|
|
216
|
+
|
|
217
|
+
await createProgram().parseAsync([
|
|
218
|
+
'node',
|
|
219
|
+
'astryx',
|
|
220
|
+
'doctor',
|
|
221
|
+
'integration',
|
|
222
|
+
'validate',
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const output = logCalls.join('\n');
|
|
226
|
+
expect(output).toContain('[ok] No integration issues found.');
|
|
227
|
+
expect(output).toMatch(
|
|
228
|
+
/Checking 1 doc\(s\) against the authoring types \(TypeScript \d+\.\d+/,
|
|
229
|
+
);
|
|
230
|
+
expect(output).toContain('templates/hero.doc.mjs');
|
|
231
|
+
expect(output).toMatch(/export:\s+default/);
|
|
232
|
+
expect(output).toMatch(/type:\s+IntegrationTemplateDoc/);
|
|
233
|
+
expect(output).toContain(
|
|
234
|
+
'Add `aspectRatio`: IntegrationBlockTemplateDoc requires it.',
|
|
235
|
+
);
|
|
236
|
+
expect(output).toContain(
|
|
237
|
+
'Add `displayName`: IntegrationBlockTemplateDoc requires it.',
|
|
238
|
+
);
|
|
239
|
+
expect(output).toContain(
|
|
240
|
+
'2 doc field(s) in 1 file(s) do not match the authoring types.',
|
|
241
|
+
);
|
|
242
|
+
expect(process.exitCode).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('validate says there is no doc to type-check, rather than asking for TypeScript', async () => {
|
|
246
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-docs-'));
|
|
247
|
+
try {
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(outside, 'package.json'),
|
|
250
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
251
|
+
);
|
|
252
|
+
fs.writeFileSync(
|
|
253
|
+
path.join(outside, 'astryx.integration.mjs'),
|
|
254
|
+
"export default {components: './components'};\n",
|
|
255
|
+
);
|
|
256
|
+
fs.mkdirSync(path.join(outside, 'components'));
|
|
257
|
+
fs.writeFileSync(
|
|
258
|
+
path.join(outside, 'components', 'Widget.tsx'),
|
|
259
|
+
'export function Widget() { return null; }\n',
|
|
260
|
+
);
|
|
261
|
+
process.chdir(outside);
|
|
262
|
+
|
|
263
|
+
await createProgram().parseAsync([
|
|
264
|
+
'node',
|
|
265
|
+
'astryx',
|
|
266
|
+
'doctor',
|
|
267
|
+
'integration',
|
|
268
|
+
'validate',
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
const output = logCalls.join('\n');
|
|
272
|
+
expect(output).toContain('[info] No doc files to type-check.');
|
|
273
|
+
expect(output).not.toContain('Docs not type-checked');
|
|
274
|
+
expect(output).not.toContain('against the authoring types');
|
|
275
|
+
} finally {
|
|
276
|
+
process.chdir(previousCwd);
|
|
277
|
+
fs.rmSync(outside, {recursive: true, force: true});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('validate says the docs matched even when another check failed', async () => {
|
|
282
|
+
writeIntegration({id: 'hero', missingRoot: true});
|
|
283
|
+
fs.writeFileSync(
|
|
284
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
285
|
+
"export default {templates: './missing', components: './components'};\n",
|
|
286
|
+
);
|
|
287
|
+
fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
|
|
288
|
+
fs.writeFileSync(
|
|
289
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
290
|
+
"export default {type: 'component', name: 'AcmeBadge', displayName: 'Badge', usage: {description: 'A badge.'}, props: []};\n",
|
|
291
|
+
);
|
|
292
|
+
fs.writeFileSync(
|
|
293
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
294
|
+
'export function AcmeBadge() { return null; }\n',
|
|
295
|
+
);
|
|
296
|
+
process.chdir(tmpDir);
|
|
297
|
+
|
|
298
|
+
await createProgram().parseAsync([
|
|
299
|
+
'node',
|
|
300
|
+
'astryx',
|
|
301
|
+
'doctor',
|
|
302
|
+
'integration',
|
|
303
|
+
'validate',
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const output = logCalls.join('\n');
|
|
307
|
+
expect(process.exitCode).toBe(1);
|
|
308
|
+
expect(output).toContain(
|
|
309
|
+
'[info] No doc field differs from its authoring type (1 checked).',
|
|
310
|
+
);
|
|
311
|
+
expect(output).not.toContain('[ok] Every doc');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('validate prints every field of the doc type check that JSON carries', async () => {
|
|
315
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
316
|
+
fs.writeFileSync(
|
|
317
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
318
|
+
"export default {templates: './templates', components: './components'};\n",
|
|
319
|
+
);
|
|
320
|
+
fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
323
|
+
"export default {type: 'component', name: 'AcmeBadge', displayName: 5, usage: {description: 'A badge.'}, props: [], keyword: ['x']};\n",
|
|
324
|
+
);
|
|
325
|
+
fs.writeFileSync(
|
|
326
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
327
|
+
'export function AcmeBadge() { return null; }\n',
|
|
328
|
+
);
|
|
329
|
+
fs.writeFileSync(
|
|
330
|
+
path.join(tmpDir, 'components', 'shared.mjs'),
|
|
331
|
+
`export default {type: 'component', name: 'AcmeAgain', displayName: 'Again', usage: {description: 'x'}, props: []};\n`,
|
|
332
|
+
);
|
|
333
|
+
fs.writeFileSync(
|
|
334
|
+
path.join(tmpDir, 'components', 'AcmeAgain.doc.mjs'),
|
|
335
|
+
"export {default} from './shared.mjs';\n",
|
|
336
|
+
);
|
|
337
|
+
fs.writeFileSync(
|
|
338
|
+
path.join(tmpDir, 'components', 'AcmeAgain.tsx'),
|
|
339
|
+
'export function AcmeAgain() { return null; }\n',
|
|
340
|
+
);
|
|
341
|
+
process.chdir(tmpDir);
|
|
342
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
343
|
+
logCalls.length = 0;
|
|
344
|
+
await createProgram().parseAsync([
|
|
345
|
+
'node',
|
|
346
|
+
'astryx',
|
|
347
|
+
...flags,
|
|
348
|
+
'doctor',
|
|
349
|
+
'integration',
|
|
350
|
+
'validate',
|
|
351
|
+
]);
|
|
352
|
+
return logCalls.join('\n');
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const report = JSON.parse(await run(['--json'])).data.docTypes;
|
|
356
|
+
const output = await run([]);
|
|
357
|
+
|
|
358
|
+
expect([...new Set(report.findings.map(f => f.problem))].sort()).toEqual([
|
|
359
|
+
'missing_field',
|
|
360
|
+
'unknown_field',
|
|
361
|
+
'wrong_type',
|
|
362
|
+
]);
|
|
363
|
+
expect(report.unchecked).toHaveLength(1);
|
|
364
|
+
expectRecordsCarry(output, report.findings, ['doc', 'field', 'problem']);
|
|
365
|
+
expectRecordsCarry(output, report.unchecked, ['doc', 'reason']);
|
|
366
|
+
expect(output).toContain(
|
|
367
|
+
`Checking ${report.docs} doc(s) against the authoring types (TypeScript ${report.typescript})`,
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('validate type-checks each doc component discovery serves, in __fixtures__, in a dot-folder or behind a symlink', async () => {
|
|
372
|
+
fs.writeFileSync(
|
|
373
|
+
path.join(tmpDir, 'package.json'),
|
|
374
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
375
|
+
);
|
|
376
|
+
fs.writeFileSync(
|
|
377
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
378
|
+
`export default {components: './components'};\n`,
|
|
379
|
+
);
|
|
380
|
+
for (const [rel, name] of [
|
|
381
|
+
['components/__fixtures__/AcmeFixture.doc.mjs', 'AcmeFixture'],
|
|
382
|
+
['components/.drafts/AcmeDraft.doc.mjs', 'AcmeDraft'],
|
|
383
|
+
['shared/AcmeLinked.doc.mjs', 'AcmeLinked'],
|
|
384
|
+
]) {
|
|
385
|
+
fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
|
|
386
|
+
fs.writeFileSync(
|
|
387
|
+
path.join(tmpDir, rel),
|
|
388
|
+
`export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: [], keyword: []};\n`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
fs.symlinkSync(
|
|
392
|
+
path.join(tmpDir, 'shared', 'AcmeLinked.doc.mjs'),
|
|
393
|
+
path.join(tmpDir, 'components', 'AcmeLinked.doc.mjs'),
|
|
394
|
+
);
|
|
395
|
+
process.chdir(tmpDir);
|
|
396
|
+
|
|
397
|
+
await createProgram().parseAsync([
|
|
398
|
+
'node',
|
|
399
|
+
'astryx',
|
|
400
|
+
'--json',
|
|
401
|
+
'doctor',
|
|
402
|
+
'integration',
|
|
403
|
+
'validate',
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
const {docTypes} = JSON.parse(logCalls.join('\n')).data;
|
|
407
|
+
expect(docTypes).toMatchObject({status: 'checked', docs: 3, unchecked: []});
|
|
408
|
+
expect(
|
|
409
|
+
docTypes.findings.map(
|
|
410
|
+
(/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
|
|
411
|
+
),
|
|
412
|
+
).toEqual([
|
|
413
|
+
'components/__fixtures__/AcmeFixture.doc.mjs keyword unknown_field',
|
|
414
|
+
'components/.drafts/AcmeDraft.doc.mjs keyword unknown_field',
|
|
415
|
+
'components/AcmeLinked.doc.mjs keyword unknown_field',
|
|
416
|
+
]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('validate type-checks a hidden doc and a second doc for one name, and counts a test fixture', async () => {
|
|
420
|
+
fs.writeFileSync(
|
|
421
|
+
path.join(tmpDir, 'package.json'),
|
|
422
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
423
|
+
);
|
|
424
|
+
fs.writeFileSync(
|
|
425
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
426
|
+
`export default {components: './components'};\n`,
|
|
427
|
+
);
|
|
428
|
+
for (const [rel, name, extra] of [
|
|
429
|
+
['components/AcmeBadge.doc.mjs', 'AcmeBadge', ''],
|
|
430
|
+
['components/again/AcmeBadge.doc.mjs', 'AcmeBadge', ', keyword: []'],
|
|
431
|
+
[
|
|
432
|
+
'components/AcmeHidden.doc.mjs',
|
|
433
|
+
'AcmeHidden',
|
|
434
|
+
', hidden: true, keyword: []',
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
'components/__tests__/AcmeTested.doc.mjs',
|
|
438
|
+
'AcmeTested',
|
|
439
|
+
', keyword: []',
|
|
440
|
+
],
|
|
441
|
+
]) {
|
|
442
|
+
fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
|
|
443
|
+
fs.writeFileSync(
|
|
444
|
+
path.join(tmpDir, rel),
|
|
445
|
+
`export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: []${extra}};\n`,
|
|
446
|
+
);
|
|
447
|
+
fs.writeFileSync(
|
|
448
|
+
path.join(tmpDir, rel.replace('.doc.mjs', '.tsx')),
|
|
449
|
+
'export default function Fixture() { return null; }\n',
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
process.chdir(tmpDir);
|
|
453
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
454
|
+
logCalls.length = 0;
|
|
455
|
+
await createProgram().parseAsync([
|
|
456
|
+
'node',
|
|
457
|
+
'astryx',
|
|
458
|
+
...flags,
|
|
459
|
+
'doctor',
|
|
460
|
+
'integration',
|
|
461
|
+
'validate',
|
|
462
|
+
]);
|
|
463
|
+
return logCalls.join('\n');
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const {docTypes} = JSON.parse(await run(['--json'])).data;
|
|
467
|
+
const output = await run([]);
|
|
468
|
+
|
|
469
|
+
expect(docTypes).toMatchObject({docs: 3, fixtures: 1, unchecked: []});
|
|
470
|
+
expect(
|
|
471
|
+
docTypes.findings.map(
|
|
472
|
+
(/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
|
|
473
|
+
),
|
|
474
|
+
).toEqual([
|
|
475
|
+
'components/AcmeHidden.doc.mjs keyword unknown_field',
|
|
476
|
+
'components/again/AcmeBadge.doc.mjs keyword unknown_field',
|
|
477
|
+
]);
|
|
478
|
+
expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
|
|
479
|
+
expect(output).not.toContain('[ok] Every doc matches');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('validate keeps the fixture count when test fixtures are the only docs', async () => {
|
|
483
|
+
fs.writeFileSync(
|
|
484
|
+
path.join(tmpDir, 'package.json'),
|
|
485
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
486
|
+
);
|
|
487
|
+
fs.writeFileSync(
|
|
488
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
489
|
+
`export default {components: './components'};\n`,
|
|
490
|
+
);
|
|
491
|
+
const rel = 'components/__tests__/AcmeTested.doc.mjs';
|
|
492
|
+
fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
|
|
493
|
+
fs.writeFileSync(
|
|
494
|
+
path.join(tmpDir, rel),
|
|
495
|
+
`export default {type: 'component', name: 'AcmeTested', displayName: 'AcmeTested', usage: {description: 'Fixture.'}, props: []};\n`,
|
|
496
|
+
);
|
|
497
|
+
process.chdir(tmpDir);
|
|
498
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
499
|
+
logCalls.length = 0;
|
|
500
|
+
await createProgram().parseAsync([
|
|
501
|
+
'node',
|
|
502
|
+
'astryx',
|
|
503
|
+
...flags,
|
|
504
|
+
'doctor',
|
|
505
|
+
'integration',
|
|
506
|
+
'validate',
|
|
507
|
+
]);
|
|
508
|
+
return logCalls.join('\n');
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const {docTypes} = JSON.parse(await run(['--json'])).data;
|
|
512
|
+
const output = await run([]);
|
|
513
|
+
|
|
514
|
+
expect(docTypes).toMatchObject({docs: 0, fixtures: 1, findings: []});
|
|
515
|
+
expect(output).toContain('[info] No doc files to type-check.');
|
|
516
|
+
expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("validate keeps main's issue for a root it cannot read, and lists a root with a folder it cannot read", async () => {
|
|
520
|
+
fs.writeFileSync(
|
|
521
|
+
path.join(tmpDir, 'package.json'),
|
|
522
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
523
|
+
);
|
|
524
|
+
fs.writeFileSync(
|
|
525
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
526
|
+
`export default {components: './components', docs: './docs'};\n`,
|
|
527
|
+
);
|
|
528
|
+
fs.mkdirSync(path.join(tmpDir, 'components', 'locked'), {recursive: true});
|
|
529
|
+
fs.mkdirSync(path.join(tmpDir, 'docs'));
|
|
530
|
+
const locked = [
|
|
531
|
+
path.join(tmpDir, 'components', 'locked'),
|
|
532
|
+
path.join(tmpDir, 'docs'),
|
|
533
|
+
];
|
|
534
|
+
for (const dir of locked) fs.chmodSync(dir, 0o000);
|
|
535
|
+
process.chdir(tmpDir);
|
|
536
|
+
try {
|
|
537
|
+
await createProgram().parseAsync([
|
|
538
|
+
'node',
|
|
539
|
+
'astryx',
|
|
540
|
+
'--json',
|
|
541
|
+
'doctor',
|
|
542
|
+
'integration',
|
|
543
|
+
'validate',
|
|
544
|
+
]);
|
|
545
|
+
} finally {
|
|
546
|
+
for (const dir of locked) fs.chmodSync(dir, 0o755);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const {data} = JSON.parse(logCalls.join('\n'));
|
|
550
|
+
expect(data.issues.map((/** @type {any} */ i) => i.code).sort()).toEqual([
|
|
551
|
+
'invalid_component',
|
|
552
|
+
'unreadable_root',
|
|
553
|
+
]);
|
|
554
|
+
expect(data.docTypes.unchecked).toEqual([
|
|
555
|
+
{
|
|
556
|
+
doc: 'components',
|
|
557
|
+
reason: expect.stringMatching(
|
|
558
|
+
/^the components root does not load, so no doc in it was checked: .*EACCES/,
|
|
559
|
+
),
|
|
560
|
+
},
|
|
561
|
+
]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('validate reports the doc type check as additive JSON', async () => {
|
|
565
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
566
|
+
process.chdir(tmpDir);
|
|
567
|
+
|
|
568
|
+
await createProgram().parseAsync([
|
|
569
|
+
'node',
|
|
570
|
+
'astryx',
|
|
571
|
+
'--json',
|
|
572
|
+
'doctor',
|
|
573
|
+
'integration',
|
|
574
|
+
'validate',
|
|
575
|
+
]);
|
|
576
|
+
|
|
577
|
+
const parsed = JSON.parse(logCalls.join('\n'));
|
|
578
|
+
expect(parsed.data.issues).toEqual([]);
|
|
579
|
+
expect(parsed.data.docTypes).toMatchObject({
|
|
580
|
+
status: 'checked',
|
|
581
|
+
docs: 1,
|
|
582
|
+
unchecked: [],
|
|
583
|
+
});
|
|
584
|
+
expect(parsed.data.docTypes.typescript).toMatch(/^\d+\.\d+/);
|
|
585
|
+
expect(parsed.data.docTypes.findings).toEqual([
|
|
586
|
+
expect.objectContaining({
|
|
587
|
+
doc: 'templates/hero.doc.mjs',
|
|
588
|
+
export: 'default',
|
|
589
|
+
type: 'IntegrationTemplateDoc',
|
|
590
|
+
field: 'aspectRatio',
|
|
591
|
+
problem: 'missing_field',
|
|
592
|
+
severity: 'warning',
|
|
593
|
+
}),
|
|
594
|
+
expect.objectContaining({field: 'displayName', problem: 'missing_field'}),
|
|
595
|
+
]);
|
|
596
|
+
expect(process.exitCode).toBeUndefined();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('validate says, in one visible line, that docs were not type-checked when the project has no TypeScript', async () => {
|
|
600
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-ts-'));
|
|
601
|
+
const inside = tmpDir;
|
|
602
|
+
try {
|
|
603
|
+
// The fixture helpers write to tmpDir; point it outside the repository,
|
|
604
|
+
// where no TypeScript resolves, and restore it for afterEach.
|
|
605
|
+
tmpDir = outside;
|
|
606
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
607
|
+
tmpDir = inside;
|
|
608
|
+
process.chdir(outside);
|
|
609
|
+
|
|
610
|
+
await createProgram().parseAsync([
|
|
611
|
+
'node',
|
|
612
|
+
'astryx',
|
|
613
|
+
'doctor',
|
|
614
|
+
'integration',
|
|
615
|
+
'validate',
|
|
616
|
+
]);
|
|
617
|
+
|
|
618
|
+
const output = logCalls.join('\n');
|
|
619
|
+
expect(output).toContain(
|
|
620
|
+
'[skip] Docs not type-checked: TypeScript is not installed in this project',
|
|
621
|
+
);
|
|
622
|
+
expect(output).not.toMatch(
|
|
623
|
+
/Checking \d+ doc\(s\) against the authoring types/,
|
|
624
|
+
);
|
|
625
|
+
expect(process.exitCode).toBeUndefined();
|
|
626
|
+
} finally {
|
|
627
|
+
process.chdir(previousCwd);
|
|
628
|
+
fs.rmSync(outside, {recursive: true, force: true});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
213
632
|
it('templates warns on a Core id and writes the exact --package command', async () => {
|
|
214
633
|
const [core] = await discoverCoreTemplates();
|
|
215
634
|
expect(core).toBeDefined();
|
|
@@ -639,12 +1058,14 @@ describe('doctor integration — command', () => {
|
|
|
639
1058
|
`export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [${[
|
|
640
1059
|
"{name: 'label', type: 'string', description: 'Text.'}",
|
|
641
1060
|
"{name: 'elevation', type: 'number', description: 'Shadow.'}",
|
|
1061
|
+
"{name: 'size', type: 'string', description: 'Size.'}",
|
|
1062
|
+
"{name: 'meta', type: '{id: string; extra?: number}', description: 'Data.'}",
|
|
642
1063
|
].join(', ')}]};\n`,
|
|
643
1064
|
);
|
|
644
1065
|
// `key` is a prop a doc never needs, though the component declares it.
|
|
645
1066
|
fs.writeFileSync(
|
|
646
1067
|
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
647
|
-
"export function AcmeBadge(props: {label: string; tone?: 'neutral' | 'info'; key?: string}) { return null; }\n",
|
|
1068
|
+
"export function AcmeBadge(props: {label: string; tone?: 'neutral' | 'info'; size?: number; meta?: {id: string}; key?: string}) { return null; }\n",
|
|
648
1069
|
);
|
|
649
1070
|
fs.writeFileSync(
|
|
650
1071
|
path.join(tmpDir, 'components', 'AcmeLonely.doc.mjs'),
|
|
@@ -674,10 +1095,30 @@ describe('doctor integration — command', () => {
|
|
|
674
1095
|
expect(report.findings.map(f => `${f.prop} ${f.kind}`)).toEqual([
|
|
675
1096
|
'elevation not_in_code',
|
|
676
1097
|
'label required_mismatch',
|
|
1098
|
+
'size type_mismatch',
|
|
677
1099
|
'tone not_in_doc',
|
|
678
1100
|
]);
|
|
679
1101
|
expectRecordsCarry(output, report.findings, ['component', 'prop', 'kind']);
|
|
680
1102
|
expectRecordsCarry(output, report.unchecked, ['component', 'reason']);
|
|
1103
|
+
expectRecordsCarry(output, report.typesNotCompared, [
|
|
1104
|
+
'component',
|
|
1105
|
+
'reason',
|
|
1106
|
+
'props',
|
|
1107
|
+
]);
|
|
1108
|
+
expect(report.typesNotCompared).toHaveLength(1);
|
|
1109
|
+
expect(output).toContain(
|
|
1110
|
+
`Documented prop types judged: ${report.typesCompared}; not compared: 1.`,
|
|
1111
|
+
);
|
|
1112
|
+
// The response-type reference, which the README's JSON table comes from,
|
|
1113
|
+
// names every field too.
|
|
1114
|
+
const {doc: responseTypes} =
|
|
1115
|
+
await import('../../../foundation/response/response-types.doc.mjs');
|
|
1116
|
+
const member = responseTypes.members.find(
|
|
1117
|
+
m => m.value === 'integration.component-conflicts',
|
|
1118
|
+
);
|
|
1119
|
+
for (const field of Object.keys(report)) {
|
|
1120
|
+
expect(member?.description).toMatch(new RegExp(`\\b${field}\\b`));
|
|
1121
|
+
}
|
|
681
1122
|
expect(output).toContain(
|
|
682
1123
|
`in 1 of ${report.components} component(s) compared (TypeScript ${report.typescript}).`,
|
|
683
1124
|
);
|
|
@@ -695,9 +1136,13 @@ describe('doctor integration — command', () => {
|
|
|
695
1136
|
'components',
|
|
696
1137
|
]);
|
|
697
1138
|
|
|
698
|
-
|
|
1139
|
+
const output = logCalls.join('\n');
|
|
1140
|
+
expect(output).toMatch(
|
|
699
1141
|
/\[ok\] Documented prop names and required flags match the code in 1 component\(s\) \(TypeScript \d/,
|
|
700
1142
|
);
|
|
1143
|
+
expect(output).toMatch(
|
|
1144
|
+
/\[info\] Documented prop types judged: \d+; not compared: 0\./,
|
|
1145
|
+
);
|
|
701
1146
|
});
|
|
702
1147
|
|
|
703
1148
|
it('components prints no [ok] for props it could not compare', async () => {
|
|
@@ -98,6 +98,85 @@ function issueBlocks(issues) {
|
|
|
98
98
|
];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The docs checked against the authoring types: a visible line when it was
|
|
103
|
+
* skipped, the findings (all warnings), the files it could not check, or an
|
|
104
|
+
* [ok] (never after a failed check).
|
|
105
|
+
* @param {import('../../../api/integration/validate-integration.type.mjs').DocTypesReport | undefined} report
|
|
106
|
+
* @param {boolean} failed
|
|
107
|
+
*/
|
|
108
|
+
function docTypesBlocks(report, failed) {
|
|
109
|
+
if (!report) return [];
|
|
110
|
+
if (
|
|
111
|
+
report.status === 'checked' &&
|
|
112
|
+
report.docs === 0 &&
|
|
113
|
+
report.findings.length === 0 &&
|
|
114
|
+
report.unchecked.length === 0
|
|
115
|
+
) {
|
|
116
|
+
return [
|
|
117
|
+
text('[info] No doc files to type-check.'),
|
|
118
|
+
...(report.fixtures
|
|
119
|
+
? [text(`[info] ${report.fixtures} test fixture doc(s) not checked.`)]
|
|
120
|
+
: []),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
if (report.status === 'skipped') {
|
|
124
|
+
return [text(`[skip] Docs not type-checked: ${report.reason}`)];
|
|
125
|
+
}
|
|
126
|
+
const blocks = [
|
|
127
|
+
section(
|
|
128
|
+
`Checking ${report.docs} doc(s) against the authoring types${report.typescript ? ` (TypeScript ${report.typescript})` : ''}`,
|
|
129
|
+
),
|
|
130
|
+
];
|
|
131
|
+
if (report.findings.length > 0) {
|
|
132
|
+
const files = new Set(report.findings.map(f => f.doc)).size;
|
|
133
|
+
blocks.push(
|
|
134
|
+
records(report.findings, {
|
|
135
|
+
fields: [
|
|
136
|
+
'severity',
|
|
137
|
+
'doc',
|
|
138
|
+
'line',
|
|
139
|
+
'export',
|
|
140
|
+
'type',
|
|
141
|
+
'field',
|
|
142
|
+
'problem',
|
|
143
|
+
'message',
|
|
144
|
+
'fix',
|
|
145
|
+
],
|
|
146
|
+
format: {severity: statusToken},
|
|
147
|
+
}),
|
|
148
|
+
text(
|
|
149
|
+
`${report.findings.length} doc field(s) in ${files} file(s) do not match the authoring types. ` +
|
|
150
|
+
'These are warnings; `astryx docs authoring` lists every field.',
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (report.unchecked.length > 0) {
|
|
155
|
+
blocks.push(
|
|
156
|
+
text(`[info] Not type-checked: ${report.unchecked.length} doc file(s):`),
|
|
157
|
+
records(report.unchecked, {fields: ['doc', 'reason']}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (report.fixtures) {
|
|
161
|
+
blocks.push(
|
|
162
|
+
text(`[info] ${report.fixtures} test fixture doc(s) not checked.`),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (report.findings.length === 0 && report.docs > 0) {
|
|
166
|
+
// After a failed check an [ok] reads as a pass, but the result still shows.
|
|
167
|
+
blocks.push(
|
|
168
|
+
text(
|
|
169
|
+
failed
|
|
170
|
+
? `[info] No doc field differs from its authoring type (${report.docs} checked).`
|
|
171
|
+
: report.unchecked.length > 0
|
|
172
|
+
? `[ok] Every doc that was checked matches its authoring type (${report.docs} checked).`
|
|
173
|
+
: `[ok] Every doc matches its authoring type (${report.docs} checked).`,
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return blocks;
|
|
178
|
+
}
|
|
179
|
+
|
|
101
180
|
/**
|
|
102
181
|
* @param {import('../../../api/integration/validate-integration.type.mjs').ValidateIntegrationResponse['data']} data
|
|
103
182
|
*/
|
|
@@ -119,6 +198,7 @@ function printIntegrationValidation(data) {
|
|
|
119
198
|
`Summary: ${errors} error${errors === 1 ? '' : 's'}, ` +
|
|
120
199
|
`${warnings} warning${warnings === 1 ? '' : 's'}`,
|
|
121
200
|
),
|
|
201
|
+
...docTypesBlocks(data.docTypes, errors > 0),
|
|
122
202
|
);
|
|
123
203
|
}
|
|
124
204
|
|
|
@@ -178,6 +258,7 @@ function printTemplateConflicts(data) {
|
|
|
178
258
|
* visible line when it was skipped, the findings (all warnings), the
|
|
179
259
|
* components it could not read, and how many were compared with which
|
|
180
260
|
* TypeScript, as an [ok] when nothing differs (never after a failed check).
|
|
261
|
+
* Then how many documented prop types were judged and which were not.
|
|
181
262
|
* @param {import('../../../api/integration/authoring-checks.type.mjs').PropDriftReport | undefined} report
|
|
182
263
|
* @param {boolean} failed
|
|
183
264
|
*/
|
|
@@ -243,6 +324,24 @@ function propDriftBlocks(report, failed) {
|
|
|
243
324
|
),
|
|
244
325
|
);
|
|
245
326
|
}
|
|
327
|
+
if (report.typesSkipped) {
|
|
328
|
+
blocks.push(text(`[skip] Prop types not judged: ${report.typesSkipped}.`));
|
|
329
|
+
return blocks;
|
|
330
|
+
}
|
|
331
|
+
const notCompared = report.typesNotCompared.flatMap(entry => entry.props);
|
|
332
|
+
blocks.push(
|
|
333
|
+
text(
|
|
334
|
+
`[info] Documented prop types judged: ${report.typesCompared}; not compared: ${notCompared.length}. ` +
|
|
335
|
+
'A type is judged when the doc writes it the same as the source (exactly, if it contains `readonly`), or when both are primitives, literals, arrays and unions of those, or names for them.',
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
if (notCompared.length > 0) {
|
|
339
|
+
blocks.push(
|
|
340
|
+
records(report.typesNotCompared, {
|
|
341
|
+
fields: ['component', 'doc', 'reason', 'props'],
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
246
345
|
return blocks;
|
|
247
346
|
}
|
|
248
347
|
|
|
@@ -316,6 +415,23 @@ function printDocConflicts(data) {
|
|
|
316
415
|
emit(...output);
|
|
317
416
|
}
|
|
318
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Print one `doctor integration` check's result as its own subcommand prints
|
|
420
|
+
* it. `astryx integration check` prints each check it runs through this.
|
|
421
|
+
* @param {import('../../../api/integration/integration-check.type.mjs').IntegrationCheckResponse} response
|
|
422
|
+
*/
|
|
423
|
+
export function printIntegrationCheckResponse(response) {
|
|
424
|
+
if (response.type === 'integration.validate') {
|
|
425
|
+
printIntegrationValidation(response.data);
|
|
426
|
+
} else if (response.type === 'integration.component-conflicts') {
|
|
427
|
+
printComponentConflicts(response.data);
|
|
428
|
+
} else if (response.type === 'integration.template-conflicts') {
|
|
429
|
+
printTemplateConflicts(response.data);
|
|
430
|
+
} else {
|
|
431
|
+
printDocConflicts(response.data);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
319
435
|
/** @param {import('commander').Command} program */
|
|
320
436
|
async function runProjectDoctor(program) {
|
|
321
437
|
const report = await runChecks();
|