@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.e26723d
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 +7 -7
- 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/component.test.mjs +420 -167
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/component/list/list.mjs +42 -4
- package/api/docs/_adapter.mjs +207 -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/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 +7 -3
- 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/integrationComponentConflicts.doc.mjs +21 -5
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +108 -16
- package/api/search/search.test.mjs +36 -0
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/template/template.test.mjs +120 -3
- package/api/theme/build/build.family.test.mjs +16 -0
- package/api/theme/build/build.mjs +81 -22
- package/api/theme/themeBuild.doc.mjs +3 -3
- package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
- 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/check-the-props.doc.mjs +145 -0
- package/assets/docs/tree/checks.doc.mjs +4 -4
- 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/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/docs/working-with-ai.doc.mjs +43 -4
- package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
- package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
- package/assets/templates/pages/detail-page/page.tsx +3 -3
- package/assets/templates/pages/table-inbox/page.tsx +10 -8
- package/assets/theme.template.ts +1 -1
- package/authoring/doctypes/base/type.ts +6 -0
- package/authoring/doctypes/component/component.doc.mjs +4 -4
- 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/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 +84 -0
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/doctor-integration-components.doc.mjs +9 -3
- package/clients/cli/commands/doctor-integration.test.mjs +392 -0
- package/clients/cli/commands/doctor.mjs +74 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/component-discovery.d.mts +190 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +864 -56
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- 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/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 +220 -0
- package/foundation/discovery/prop-drift.mjs +1765 -0
- package/foundation/discovery/prop-drift.test.mjs +1197 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- 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 +9 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- 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 +1 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -21,6 +21,7 @@ import {discoverOwnedComponents} from '../../../foundation/discovery/component-d
|
|
|
21
21
|
import {discoverBuiltinTopics} from '../../../foundation/discovery/docs-discovery.mjs';
|
|
22
22
|
import {findCoreDir} from '../../../foundation/fs/paths.mjs';
|
|
23
23
|
import {registerDoctor} from './doctor.mjs';
|
|
24
|
+
import {expectRecordsCarry} from '../../../test-utils/text-projection.mjs';
|
|
24
25
|
|
|
25
26
|
let tmpDir;
|
|
26
27
|
let previousCwd;
|
|
@@ -366,6 +367,10 @@ describe('doctor integration — command', () => {
|
|
|
366
367
|
const printed = logCalls.join('\n');
|
|
367
368
|
expect(printed).toContain('core_not_found');
|
|
368
369
|
expect(printed).not.toContain('[ok]');
|
|
370
|
+
// No TypeScript out here either: the props comparison says it skipped.
|
|
371
|
+
expect(printed).toContain(
|
|
372
|
+
'[skip] Props not compared: TypeScript is not installed in this project',
|
|
373
|
+
);
|
|
369
374
|
expect(process.exitCode).toBe(1);
|
|
370
375
|
} finally {
|
|
371
376
|
tmpDir = previousTmp;
|
|
@@ -396,6 +401,328 @@ describe('doctor integration — command', () => {
|
|
|
396
401
|
expect(process.exitCode).toBeUndefined();
|
|
397
402
|
});
|
|
398
403
|
|
|
404
|
+
it('components prints each documented prop that differs from the code, and still exits 0', async () => {
|
|
405
|
+
writeComponentIntegration('AcmeBadge');
|
|
406
|
+
fs.writeFileSync(
|
|
407
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
408
|
+
`export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [{name: 'label', type: 'string', description: 'Text.'}]};\n`,
|
|
409
|
+
);
|
|
410
|
+
fs.writeFileSync(
|
|
411
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
412
|
+
'export function AcmeBadge(props: {label: string}) { return null; }\n',
|
|
413
|
+
);
|
|
414
|
+
process.chdir(tmpDir);
|
|
415
|
+
|
|
416
|
+
await createProgram().parseAsync([
|
|
417
|
+
'node',
|
|
418
|
+
'astryx',
|
|
419
|
+
'doctor',
|
|
420
|
+
'integration',
|
|
421
|
+
'components',
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
const output = logCalls.join('\n');
|
|
425
|
+
expect(output).toMatch(/^kind:\s+required_mismatch$/m);
|
|
426
|
+
expect(output).toContain(
|
|
427
|
+
'"label" is required in the code, but the doc does not mark it required.',
|
|
428
|
+
);
|
|
429
|
+
expect(output).toContain(
|
|
430
|
+
'1 prop(s) differ between the docs and the code in 1 of 1 component(s) compared (TypeScript',
|
|
431
|
+
);
|
|
432
|
+
expect(output).not.toContain('[ok] Documented prop');
|
|
433
|
+
expect(process.exitCode).toBeUndefined();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('components compares a hidden doc and a family entry with no record, so their drift is never an [ok]', async () => {
|
|
437
|
+
writeComponentIntegration('AcmeMenu');
|
|
438
|
+
const dir = path.join(tmpDir, 'components');
|
|
439
|
+
fs.writeFileSync(
|
|
440
|
+
path.join(dir, 'AcmeMenu.doc.mjs'),
|
|
441
|
+
`export default {type: 'component', name: 'AcmeMenuRoot', displayName: 'AcmeMenuRoot', description: 'Fixture component.', hiddenComponents: ['AcmeMenuItem'], components: [{name: 'AcmeMenuItem', displayName: 'AcmeMenuItem', description: 'Fixture item.', props: [{name: 'label', type: 'string', description: 'Text.'}]}]};\n`,
|
|
442
|
+
);
|
|
443
|
+
fs.writeFileSync(
|
|
444
|
+
path.join(dir, 'AcmeMenu.tsx'),
|
|
445
|
+
'export function AcmeMenuRoot() { return null; }\nexport function AcmeMenuItem(props: {value?: string}) { return null; }\n',
|
|
446
|
+
);
|
|
447
|
+
fs.writeFileSync(
|
|
448
|
+
path.join(dir, 'AcmeSecret.doc.mjs'),
|
|
449
|
+
`export default {type: 'component', name: 'AcmeSecret', displayName: 'AcmeSecret', description: 'Fixture component.', hidden: true, props: [{name: 'tone', type: 'string', description: 'Tone.'}]};\n`,
|
|
450
|
+
);
|
|
451
|
+
fs.writeFileSync(
|
|
452
|
+
path.join(dir, 'AcmeSecret.tsx'),
|
|
453
|
+
'export function AcmeSecret(props: {size?: number}) { return null; }\n',
|
|
454
|
+
);
|
|
455
|
+
process.chdir(tmpDir);
|
|
456
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
457
|
+
logCalls.length = 0;
|
|
458
|
+
await createProgram().parseAsync([
|
|
459
|
+
'node',
|
|
460
|
+
'astryx',
|
|
461
|
+
...flags,
|
|
462
|
+
'doctor',
|
|
463
|
+
'integration',
|
|
464
|
+
'components',
|
|
465
|
+
]);
|
|
466
|
+
return logCalls.join('\n');
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const report = JSON.parse(await run(['--json'])).data.propDrift;
|
|
470
|
+
const output = await run([]);
|
|
471
|
+
|
|
472
|
+
expect(
|
|
473
|
+
report.findings.map(f => `${f.component}.${f.prop} ${f.kind}`),
|
|
474
|
+
).toEqual([
|
|
475
|
+
'AcmeMenuItem.label not_in_code',
|
|
476
|
+
'AcmeMenuItem.value not_in_doc',
|
|
477
|
+
'AcmeSecret.size not_in_doc',
|
|
478
|
+
'AcmeSecret.tone not_in_code',
|
|
479
|
+
]);
|
|
480
|
+
expect(output).not.toContain('[ok] Documented prop');
|
|
481
|
+
expect(process.exitCode).toBeUndefined();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('components lists props a mapped type builds from keys it cannot trace, and says the [ok] leaves them out', async () => {
|
|
485
|
+
writeComponentIntegration('AcmeChip');
|
|
486
|
+
const dir = path.join(tmpDir, 'components');
|
|
487
|
+
fs.writeFileSync(
|
|
488
|
+
path.join(dir, 'AcmeChip.doc.mjs'),
|
|
489
|
+
`export default {type: 'component', name: 'AcmeChip', displayName: 'AcmeChip', description: 'Fixture.', props: [{name: 'label', type: 'string', description: 'Text.'}]};\n`,
|
|
490
|
+
);
|
|
491
|
+
fs.writeFileSync(
|
|
492
|
+
path.join(dir, 'AcmeChip.tsx'),
|
|
493
|
+
"export function AcmeChip(props: {label?: string} & {[K in `on${Capitalize<'open'>}`]?: () => void}) { return null; }\n",
|
|
494
|
+
);
|
|
495
|
+
process.chdir(tmpDir);
|
|
496
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
497
|
+
logCalls.length = 0;
|
|
498
|
+
await createProgram().parseAsync([
|
|
499
|
+
'node',
|
|
500
|
+
'astryx',
|
|
501
|
+
...flags,
|
|
502
|
+
'doctor',
|
|
503
|
+
'integration',
|
|
504
|
+
'components',
|
|
505
|
+
]);
|
|
506
|
+
return logCalls.join('\n');
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const {propsNotChecked} = JSON.parse(await run(['--json'])).data.propDrift;
|
|
510
|
+
const output = await run([]);
|
|
511
|
+
|
|
512
|
+
expect(propsNotChecked).toEqual([
|
|
513
|
+
expect.objectContaining({component: 'AcmeChip', props: ['onOpen']}),
|
|
514
|
+
]);
|
|
515
|
+
expect(output).toContain(
|
|
516
|
+
'[info] Props not checked for a doc entry in 1 component(s):',
|
|
517
|
+
);
|
|
518
|
+
expect(output).toContain(
|
|
519
|
+
'in 1 component(s), apart from the props not checked (TypeScript',
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('components compares the entries of a family doc whose own source is missing', async () => {
|
|
524
|
+
writeComponentIntegration('AcmeTabs');
|
|
525
|
+
const dir = path.join(tmpDir, 'components');
|
|
526
|
+
const entry = (/** @type {string} */ name, /** @type {string} */ prop) =>
|
|
527
|
+
`{name: '${name}', displayName: '${name}', description: 'Fixture.', props: [{name: '${prop}', type: 'string', description: 'Text.'}]}`;
|
|
528
|
+
fs.writeFileSync(
|
|
529
|
+
path.join(dir, 'AcmeTabs.doc.mjs'),
|
|
530
|
+
`export default {type: 'component', name: 'AcmeTabs', displayName: 'AcmeTabs', description: 'Fixture.', props: [], hiddenComponents: ['AcmeTabsPanel'], components: [${entry('AcmeTabsItem', 'label')}, ${entry('AcmeTabsPanel', 'ghost')}]};\n`,
|
|
531
|
+
);
|
|
532
|
+
fs.rmSync(path.join(dir, 'AcmeTabs.tsx'), {force: true});
|
|
533
|
+
// The package's entry exports the members, so discovery records the
|
|
534
|
+
// listed one, though the family's own file is missing.
|
|
535
|
+
for (const name of ['AcmeTabsItem', 'AcmeTabsPanel']) {
|
|
536
|
+
fs.writeFileSync(
|
|
537
|
+
path.join(dir, `${name}.tsx`),
|
|
538
|
+
`export function ${name}(props: {label?: string; open?: boolean}) { return null; }\n`,
|
|
539
|
+
);
|
|
540
|
+
fs.appendFileSync(
|
|
541
|
+
path.join(tmpDir, 'index.ts'),
|
|
542
|
+
`export {${name}} from './components/${name}';\n`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
process.chdir(tmpDir);
|
|
546
|
+
|
|
547
|
+
await createProgram().parseAsync([
|
|
548
|
+
'node',
|
|
549
|
+
'astryx',
|
|
550
|
+
'--json',
|
|
551
|
+
'doctor',
|
|
552
|
+
'integration',
|
|
553
|
+
'components',
|
|
554
|
+
]);
|
|
555
|
+
const report = JSON.parse(logCalls.join('\n')).data.propDrift;
|
|
556
|
+
|
|
557
|
+
expect(
|
|
558
|
+
report.findings.map(
|
|
559
|
+
(/** @type {any} */ f) => `${f.component}.${f.prop} ${f.kind}`,
|
|
560
|
+
),
|
|
561
|
+
).toEqual([
|
|
562
|
+
'AcmeTabsItem.open not_in_doc',
|
|
563
|
+
'AcmeTabsPanel.ghost not_in_code',
|
|
564
|
+
'AcmeTabsPanel.label not_in_doc',
|
|
565
|
+
'AcmeTabsPanel.open not_in_doc',
|
|
566
|
+
]);
|
|
567
|
+
expect(report.unchecked).toEqual([
|
|
568
|
+
{
|
|
569
|
+
component: 'AcmeTabs',
|
|
570
|
+
doc: 'components/AcmeTabs.doc.mjs',
|
|
571
|
+
reason: 'no source file for AcmeTabs',
|
|
572
|
+
},
|
|
573
|
+
]);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('components counts two docs of one name as two components, and compares a .doc.ts beside a .doc.mjs', async () => {
|
|
577
|
+
writeComponentIntegration('AcmeTwin');
|
|
578
|
+
const dir = path.join(tmpDir, 'components');
|
|
579
|
+
const doc = (/** @type {string} */ name, /** @type {string} */ prop) =>
|
|
580
|
+
`export default {type: 'component', name: '${name}', displayName: '${name}', description: 'Fixture.', props: [{name: '${prop}', type: 'string', description: 'Text.'}]};\n`;
|
|
581
|
+
fs.writeFileSync(
|
|
582
|
+
path.join(dir, 'AcmeTwin.doc.mjs'),
|
|
583
|
+
doc('AcmeTwin', 'tone'),
|
|
584
|
+
);
|
|
585
|
+
fs.writeFileSync(
|
|
586
|
+
path.join(dir, 'AcmeTwin.doc.ts'),
|
|
587
|
+
doc('AcmeTwin', 'size'),
|
|
588
|
+
);
|
|
589
|
+
fs.writeFileSync(
|
|
590
|
+
path.join(dir, 'AcmeTwin.tsx'),
|
|
591
|
+
'export function AcmeTwin(props: {label?: string}) { return null; }\n',
|
|
592
|
+
);
|
|
593
|
+
for (const folder of ['a', 'b']) {
|
|
594
|
+
fs.mkdirSync(path.join(dir, folder));
|
|
595
|
+
fs.writeFileSync(
|
|
596
|
+
path.join(dir, folder, 'AcmeX.doc.mjs'),
|
|
597
|
+
doc('AcmeX', folder),
|
|
598
|
+
);
|
|
599
|
+
fs.writeFileSync(
|
|
600
|
+
path.join(dir, folder, 'AcmeX.tsx'),
|
|
601
|
+
'export function AcmeX() { return null; }\n',
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
process.chdir(tmpDir);
|
|
605
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
606
|
+
logCalls.length = 0;
|
|
607
|
+
await createProgram().parseAsync([
|
|
608
|
+
'node',
|
|
609
|
+
'astryx',
|
|
610
|
+
...flags,
|
|
611
|
+
'doctor',
|
|
612
|
+
'integration',
|
|
613
|
+
'components',
|
|
614
|
+
]);
|
|
615
|
+
return logCalls.join('\n');
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const report = JSON.parse(await run(['--json'])).data.propDrift;
|
|
619
|
+
const output = await run([]);
|
|
620
|
+
|
|
621
|
+
expect(
|
|
622
|
+
report.findings
|
|
623
|
+
.filter(f => f.kind === 'not_in_code')
|
|
624
|
+
.map(f => `${f.doc} ${f.prop}`)
|
|
625
|
+
.sort(),
|
|
626
|
+
).toEqual([
|
|
627
|
+
'components/AcmeTwin.doc.mjs tone',
|
|
628
|
+
'components/AcmeTwin.doc.ts size',
|
|
629
|
+
'components/a/AcmeX.doc.mjs a',
|
|
630
|
+
'components/b/AcmeX.doc.mjs b',
|
|
631
|
+
]);
|
|
632
|
+
expect(output).toMatch(/ in 4 of 4 component\(s\) compared /);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('components prints every field of the props comparison that JSON carries', async () => {
|
|
636
|
+
writeComponentIntegration('AcmeBadge');
|
|
637
|
+
fs.writeFileSync(
|
|
638
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
639
|
+
`export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [${[
|
|
640
|
+
"{name: 'label', type: 'string', description: 'Text.'}",
|
|
641
|
+
"{name: 'elevation', type: 'number', description: 'Shadow.'}",
|
|
642
|
+
].join(', ')}]};\n`,
|
|
643
|
+
);
|
|
644
|
+
// `key` is a prop a doc never needs, though the component declares it.
|
|
645
|
+
fs.writeFileSync(
|
|
646
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
647
|
+
"export function AcmeBadge(props: {label: string; tone?: 'neutral' | 'info'; key?: string}) { return null; }\n",
|
|
648
|
+
);
|
|
649
|
+
fs.writeFileSync(
|
|
650
|
+
path.join(tmpDir, 'components', 'AcmeLonely.doc.mjs'),
|
|
651
|
+
`export default {type: 'component', name: 'AcmeLonely', displayName: 'AcmeLonely', description: 'Fixture component.', props: []};\n`,
|
|
652
|
+
);
|
|
653
|
+
fs.writeFileSync(
|
|
654
|
+
path.join(tmpDir, 'components', 'AcmeLonely.tsx'),
|
|
655
|
+
'export function Other() { return null; }\n',
|
|
656
|
+
);
|
|
657
|
+
process.chdir(tmpDir);
|
|
658
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
659
|
+
logCalls.length = 0;
|
|
660
|
+
await createProgram().parseAsync([
|
|
661
|
+
'node',
|
|
662
|
+
'astryx',
|
|
663
|
+
...flags,
|
|
664
|
+
'doctor',
|
|
665
|
+
'integration',
|
|
666
|
+
'components',
|
|
667
|
+
]);
|
|
668
|
+
return logCalls.join('\n');
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const report = JSON.parse(await run(['--json'])).data.propDrift;
|
|
672
|
+
const output = await run([]);
|
|
673
|
+
|
|
674
|
+
expect(report.findings.map(f => `${f.prop} ${f.kind}`)).toEqual([
|
|
675
|
+
'elevation not_in_code',
|
|
676
|
+
'label required_mismatch',
|
|
677
|
+
'tone not_in_doc',
|
|
678
|
+
]);
|
|
679
|
+
expectRecordsCarry(output, report.findings, ['component', 'prop', 'kind']);
|
|
680
|
+
expectRecordsCarry(output, report.unchecked, ['component', 'reason']);
|
|
681
|
+
expect(output).toContain(
|
|
682
|
+
`in 1 of ${report.components} component(s) compared (TypeScript ${report.typescript}).`,
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('components says documented props match when they do', async () => {
|
|
687
|
+
writeComponentIntegration('AcmeOnlyWidget');
|
|
688
|
+
process.chdir(tmpDir);
|
|
689
|
+
|
|
690
|
+
await createProgram().parseAsync([
|
|
691
|
+
'node',
|
|
692
|
+
'astryx',
|
|
693
|
+
'doctor',
|
|
694
|
+
'integration',
|
|
695
|
+
'components',
|
|
696
|
+
]);
|
|
697
|
+
|
|
698
|
+
expect(logCalls.join('\n')).toMatch(
|
|
699
|
+
/\[ok\] Documented prop names and required flags match the code in 1 component\(s\) \(TypeScript \d/,
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('components prints no [ok] for props it could not compare', async () => {
|
|
704
|
+
writeComponentIntegration('AcmeLonely');
|
|
705
|
+
fs.writeFileSync(
|
|
706
|
+
path.join(tmpDir, 'components', 'AcmeLonely.tsx'),
|
|
707
|
+
'export function Other() { return null; }\n',
|
|
708
|
+
);
|
|
709
|
+
process.chdir(tmpDir);
|
|
710
|
+
|
|
711
|
+
await createProgram().parseAsync([
|
|
712
|
+
'node',
|
|
713
|
+
'astryx',
|
|
714
|
+
'doctor',
|
|
715
|
+
'integration',
|
|
716
|
+
'components',
|
|
717
|
+
]);
|
|
718
|
+
|
|
719
|
+
const output = logCalls.join('\n');
|
|
720
|
+
expect(output).toContain(
|
|
721
|
+
'[info] Props not compared for 1 documented component(s):',
|
|
722
|
+
);
|
|
723
|
+
expect(output).not.toContain('[ok] Documented prop');
|
|
724
|
+
});
|
|
725
|
+
|
|
399
726
|
it('components exposes conflicts as structured JSON warnings', async () => {
|
|
400
727
|
const coreDir = findCoreDir(tmpDir);
|
|
401
728
|
expect(coreDir).not.toBeNull();
|
|
@@ -425,6 +752,71 @@ describe('doctor integration — command', () => {
|
|
|
425
752
|
expect(process.exitCode).toBeUndefined();
|
|
426
753
|
});
|
|
427
754
|
|
|
755
|
+
it('docs resolves a link to a component or a template, its own or Core’s, and still flags one that names no doc', async () => {
|
|
756
|
+
const page = (await discoverCoreTemplates()).find(
|
|
757
|
+
template => template.type === 'page',
|
|
758
|
+
);
|
|
759
|
+
expect(page).toBeDefined();
|
|
760
|
+
fs.writeFileSync(
|
|
761
|
+
path.join(tmpDir, 'package.json'),
|
|
762
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
763
|
+
);
|
|
764
|
+
fs.writeFileSync(
|
|
765
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
766
|
+
"export default {components: './components', templates: './templates', docs: './docs'};\n",
|
|
767
|
+
);
|
|
768
|
+
for (const dir of ['components', 'templates', 'docs']) {
|
|
769
|
+
fs.mkdirSync(path.join(tmpDir, dir), {recursive: true});
|
|
770
|
+
}
|
|
771
|
+
fs.writeFileSync(
|
|
772
|
+
path.join(tmpDir, 'components', 'AcmeButton.doc.mjs'),
|
|
773
|
+
"export default {type: 'component', name: 'AcmeButton', displayName: 'Acme Button', description: 'A button.', props: []};\n",
|
|
774
|
+
);
|
|
775
|
+
fs.writeFileSync(
|
|
776
|
+
path.join(tmpDir, 'components', 'AcmeButton.tsx'),
|
|
777
|
+
'export function AcmeButton() { return null; }\n',
|
|
778
|
+
);
|
|
779
|
+
fs.writeFileSync(
|
|
780
|
+
path.join(tmpDir, 'templates', 'acme-card.doc.mjs'),
|
|
781
|
+
"export default {type: 'block', name: 'Acme card', description: 'A card.'};\n",
|
|
782
|
+
);
|
|
783
|
+
fs.writeFileSync(
|
|
784
|
+
path.join(tmpDir, 'templates', 'acme-card.tsx'),
|
|
785
|
+
'export default function Fixture() { return null; }\n',
|
|
786
|
+
);
|
|
787
|
+
const text =
|
|
788
|
+
'Start with {@link component:AcmeButton} or {@link block:acme-card}, ' +
|
|
789
|
+
'built on {@link @astryxdesign/core:component:Button} and ' +
|
|
790
|
+
`{@link @astryxdesign/core:page:${page?.dirName}}. Not {@link component:AcmeMissing}.`;
|
|
791
|
+
fs.writeFileSync(
|
|
792
|
+
path.join(tmpDir, 'docs', 'using-widgets.doc.mjs'),
|
|
793
|
+
`export default ${JSON.stringify({
|
|
794
|
+
type: 'generic',
|
|
795
|
+
name: 'using-widgets',
|
|
796
|
+
title: 'Using widgets',
|
|
797
|
+
description: 'Fixture topic.',
|
|
798
|
+
sections: [{title: 'Overview', content: [{type: 'prose', text}]}],
|
|
799
|
+
})};\n`,
|
|
800
|
+
);
|
|
801
|
+
process.chdir(tmpDir);
|
|
802
|
+
|
|
803
|
+
await createProgram().parseAsync([
|
|
804
|
+
'node',
|
|
805
|
+
'astryx',
|
|
806
|
+
'--json',
|
|
807
|
+
'doctor',
|
|
808
|
+
'integration',
|
|
809
|
+
'docs',
|
|
810
|
+
]);
|
|
811
|
+
|
|
812
|
+
const parsed = JSON.parse(logCalls.join('\n'));
|
|
813
|
+
const unresolved = parsed.data.issues
|
|
814
|
+
.filter(issue => issue.code === 'invalid_doc_graph')
|
|
815
|
+
.map(issue => issue.message);
|
|
816
|
+
expect(unresolved).toHaveLength(1);
|
|
817
|
+
expect(unresolved[0]).toContain('"component:AcmeMissing" names no doc');
|
|
818
|
+
});
|
|
819
|
+
|
|
428
820
|
it('docs reports an intentional replacement as info', async () => {
|
|
429
821
|
const [coreTopic] = Object.keys(discoverBuiltinTopics());
|
|
430
822
|
writeDocIntegration({name: 'acme-setup', replaces: coreTopic});
|
|
@@ -173,6 +173,79 @@ function printTemplateConflicts(data) {
|
|
|
173
173
|
emit(...output);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* The documented-props comparison, with every field its JSON carries: a
|
|
178
|
+
* visible line when it was skipped, the findings (all warnings), the
|
|
179
|
+
* components it could not read, and how many were compared with which
|
|
180
|
+
* TypeScript, as an [ok] when nothing differs (never after a failed check).
|
|
181
|
+
* @param {import('../../../api/integration/authoring-checks.type.mjs').PropDriftReport | undefined} report
|
|
182
|
+
* @param {boolean} failed
|
|
183
|
+
*/
|
|
184
|
+
function propDriftBlocks(report, failed) {
|
|
185
|
+
if (!report) return [];
|
|
186
|
+
if (report.status === 'skipped') {
|
|
187
|
+
return [text(`[skip] Props not compared: ${report.reason}`)];
|
|
188
|
+
}
|
|
189
|
+
const blocks = [];
|
|
190
|
+
if (report.findings.length > 0) {
|
|
191
|
+
// Two docs of one name are two components.
|
|
192
|
+
const components = new Set(
|
|
193
|
+
report.findings.map(f => `${f.doc}\0${f.component}`),
|
|
194
|
+
).size;
|
|
195
|
+
blocks.push(
|
|
196
|
+
records(report.findings, {
|
|
197
|
+
fields: [
|
|
198
|
+
'severity',
|
|
199
|
+
'component',
|
|
200
|
+
'prop',
|
|
201
|
+
'kind',
|
|
202
|
+
'message',
|
|
203
|
+
'doc',
|
|
204
|
+
'source',
|
|
205
|
+
'docType',
|
|
206
|
+
'codeType',
|
|
207
|
+
'docRequired',
|
|
208
|
+
'codeRequired',
|
|
209
|
+
],
|
|
210
|
+
format: {severity: statusToken},
|
|
211
|
+
}),
|
|
212
|
+
text(
|
|
213
|
+
`${report.findings.length} prop(s) differ between the docs and the code in ${components} of ${report.components} component(s) compared (TypeScript ${report.typescript}). ` +
|
|
214
|
+
'These are warnings: update the doc, or the code if the doc is right.',
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (report.unchecked.length > 0) {
|
|
219
|
+
blocks.push(
|
|
220
|
+
text(
|
|
221
|
+
`[info] Props not compared for ${report.unchecked.length} documented component(s):`,
|
|
222
|
+
),
|
|
223
|
+
records(report.unchecked, {fields: ['component', 'doc', 'reason']}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const notChecked = report.propsNotChecked ?? [];
|
|
227
|
+
if (notChecked.length > 0) {
|
|
228
|
+
blocks.push(
|
|
229
|
+
text(
|
|
230
|
+
`[info] Props not checked for a doc entry in ${notChecked.length} component(s):`,
|
|
231
|
+
),
|
|
232
|
+
records(notChecked, {fields: ['component', 'doc', 'reason', 'props']}),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (report.findings.length === 0) {
|
|
236
|
+
const apart =
|
|
237
|
+
notChecked.length > 0 ? ', apart from the props not checked' : '';
|
|
238
|
+
blocks.push(
|
|
239
|
+
text(
|
|
240
|
+
!failed && report.components > 0
|
|
241
|
+
? `[ok] Documented prop names and required flags match the code in ${report.components} component(s)${apart} (TypeScript ${report.typescript}).`
|
|
242
|
+
: `[info] Props compared in ${report.components} component(s) (TypeScript ${report.typescript}).`,
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return blocks;
|
|
247
|
+
}
|
|
248
|
+
|
|
176
249
|
/**
|
|
177
250
|
* @param {import('../../../api/integration/authoring-checks.type.mjs').IntegrationComponentConflictResponse['data']} data
|
|
178
251
|
*/
|
|
@@ -205,6 +278,7 @@ function printComponentConflicts(data) {
|
|
|
205
278
|
),
|
|
206
279
|
);
|
|
207
280
|
}
|
|
281
|
+
output.push(...propDriftBlocks(data.propDrift, failed));
|
|
208
282
|
emit(...output);
|
|
209
283
|
}
|
|
210
284
|
|
|
@@ -30,7 +30,7 @@ export const doc = {
|
|
|
30
30
|
{
|
|
31
31
|
flag: '--family',
|
|
32
32
|
description:
|
|
33
|
-
'Build the selected extension family into one keyed CSS, JS, and declaration set. Requires --family-key; refused with --out or --watch (ERR_THEME_INVALID)',
|
|
33
|
+
'Build the selected extension family into one keyed CSS, JS, and declaration set, written beside the family root theme file, which must be inside the working directory (ERR_PATH_TRAVERSAL). Requires --family-key; refused with --out or --watch (ERR_THEME_INVALID)',
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
flag: '--family-key <key>',
|
|
@@ -41,7 +41,7 @@ export const doc = {
|
|
|
41
41
|
flag: '-o, --out <path>',
|
|
42
42
|
param: 'options.out',
|
|
43
43
|
description:
|
|
44
|
-
'Output CSS file path; the .js and .d.ts are written beside it (default: <theme name>.css next to the source).
|
|
44
|
+
'Output CSS file path, relative or absolute; the .js and .d.ts are written beside it (default: <theme name>.css next to the source). Every output must resolve inside the working directory, the defaults beside the source too (ERR_PATH_TRAVERSAL). Single theme only: refused with more than one file or with --family (ERR_THEME_INVALID)',
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
flag: '--icons-specifier <specifier>',
|
|
@@ -68,8 +68,12 @@ function formatPropsTable(props) {
|
|
|
68
68
|
for (const p of props) {
|
|
69
69
|
const def = p.default ? `\`${mdCell(p.default)}\`` : '-';
|
|
70
70
|
const req = p.required ? ' **(required)**' : '';
|
|
71
|
+
// The text projection of the prop doc's `stability` field: an
|
|
72
|
+
// experimental prop is outside the stable compatibility promise.
|
|
73
|
+
const stability =
|
|
74
|
+
p.stability === 'experimental' ? ' **(experimental)**' : '';
|
|
71
75
|
lines.push(
|
|
72
|
-
`| \`${mdCell(p.name)}\` | \`${mdCell(p.type)}\` | ${def} | ${mdCell(p.description)}${req} |`,
|
|
76
|
+
`| \`${mdCell(p.name)}\` | \`${mdCell(p.type)}\` | ${def} | ${mdCell(p.description)}${req}${stability} |`,
|
|
73
77
|
);
|
|
74
78
|
}
|
|
75
79
|
return lines.join('\n');
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
applyTemplateReplacements,
|
|
61
61
|
effectiveTemplateDiscovery,
|
|
62
62
|
discoverAllUnresolved as discoverTemplates,
|
|
63
|
+
discoverCoreTemplates,
|
|
63
64
|
discoverIntegrationTemplatesForOne,
|
|
64
65
|
} from '../discovery/template-adapter.mjs';
|
|
65
66
|
import {
|
|
@@ -821,6 +822,85 @@ export class Project {
|
|
|
821
822
|
});
|
|
822
823
|
}
|
|
823
824
|
|
|
825
|
+
/**
|
|
826
|
+
* The components and templates this project reads, as links name them: by
|
|
827
|
+
* owner provider id, kind, and name (a template by its id).
|
|
828
|
+
* @returns {Promise<import('../discovery/docs-discovery.mjs').LinkableContributions>}
|
|
829
|
+
*/
|
|
830
|
+
async #linkableContributions() {
|
|
831
|
+
/** @type {Map<string, string>} */
|
|
832
|
+
const providers = new Map([[CORE_PACKAGE, CORE_PACKAGE]]);
|
|
833
|
+
for (const integration of this.#loadedIntegrations) {
|
|
834
|
+
if (integration?.name) {
|
|
835
|
+
providers.set(
|
|
836
|
+
integration.name,
|
|
837
|
+
integration.providerId ?? integration.name,
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const providerOf = (/** @type {string | undefined} */ pkg) =>
|
|
842
|
+
providers.get(pkg ?? CORE_PACKAGE) ?? pkg ?? CORE_PACKAGE;
|
|
843
|
+
// An active replacement answers to the Core name it replaces, so a link
|
|
844
|
+
// to that Core component has to select Core (spec:AST-035 FR11).
|
|
845
|
+
/** @type {Map<string, string>} */
|
|
846
|
+
const replacing = new Map();
|
|
847
|
+
if (this.#loadedIntegrations.length > 0) {
|
|
848
|
+
const {active} = await resolveComponentReplacements(
|
|
849
|
+
findCoreDir(this.#cwd),
|
|
850
|
+
this.#loadedIntegrations,
|
|
851
|
+
);
|
|
852
|
+
for (const each of active) {
|
|
853
|
+
replacing.set(`${each.package}\0${each.name}`, each.target);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const components = (await this.components()).map(record => {
|
|
857
|
+
const replaces = replacing.get(`${record.package}\0${record.name}`);
|
|
858
|
+
return {
|
|
859
|
+
providerId: providerOf(record.package),
|
|
860
|
+
package: record.package,
|
|
861
|
+
name: record.name,
|
|
862
|
+
docPath: record.docPath,
|
|
863
|
+
...(replaces == null ? {} : {replaces}),
|
|
864
|
+
};
|
|
865
|
+
});
|
|
866
|
+
/** @param {any} template @param {string} pkg */
|
|
867
|
+
const linkable = (template, pkg) => ({
|
|
868
|
+
providerId: providerOf(pkg),
|
|
869
|
+
package: pkg,
|
|
870
|
+
type: template.type,
|
|
871
|
+
id: template.dirName,
|
|
872
|
+
name: template.name,
|
|
873
|
+
...(template.displayName == null
|
|
874
|
+
? {}
|
|
875
|
+
: {displayName: template.displayName}),
|
|
876
|
+
...(template.description == null
|
|
877
|
+
? {}
|
|
878
|
+
: {description: template.description}),
|
|
879
|
+
...(typeof template.replaces === 'string'
|
|
880
|
+
? {replaces: template.replaces}
|
|
881
|
+
: {}),
|
|
882
|
+
});
|
|
883
|
+
/** @param {any} template */
|
|
884
|
+
const isPageOrBlock = template =>
|
|
885
|
+
template.type === 'page' || template.type === 'block';
|
|
886
|
+
// Every Core template, a replaced one too, since `--package
|
|
887
|
+
// @astryxdesign/core` still opens it; everyone else's as unqualified
|
|
888
|
+
// lookups see them, an active replacement with the id it takes over.
|
|
889
|
+
const templates = [
|
|
890
|
+
...(await discoverCoreTemplates())
|
|
891
|
+
.filter(isPageOrBlock)
|
|
892
|
+
.map(template => linkable(template, CORE_PACKAGE)),
|
|
893
|
+
.../** @type {any[]} */ (await this.templates())
|
|
894
|
+
.filter(
|
|
895
|
+
template =>
|
|
896
|
+
isPageOrBlock(template) &&
|
|
897
|
+
(template.package ?? CORE_PACKAGE) !== CORE_PACKAGE,
|
|
898
|
+
)
|
|
899
|
+
.map(template => linkable(template, template.package)),
|
|
900
|
+
];
|
|
901
|
+
return {components, templates};
|
|
902
|
+
}
|
|
903
|
+
|
|
824
904
|
/**
|
|
825
905
|
* The CLI's own doc topics plus the ones the configured integrations
|
|
826
906
|
* contribute, resolved into one catalog: additions, replacements (with the
|
|
@@ -838,6 +918,8 @@ export class Project {
|
|
|
838
918
|
async docs() {
|
|
839
919
|
return this.#memo('docs', async () => {
|
|
840
920
|
const catalog = DocsCatalog.fromBuiltins();
|
|
921
|
+
// Links name component, page, and block docs too (spec:AST-047 FR9).
|
|
922
|
+
catalog.setContributions(() => this.#linkableContributions());
|
|
841
923
|
let rank = 0;
|
|
842
924
|
|
|
843
925
|
for (const integration of this.#loadedIntegrations) {
|