@astryxdesign/cli 0.7.2-canary.c9f2659 → 0.7.2-canary.cbb7fa3
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/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/gap-report/gap-report.mjs +67 -4
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +7 -3
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -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 +77 -18
- 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/tree/add-a-component.doc.mjs +10 -0
- package/assets/docs/tree/checks.doc.mjs +1 -1
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/tree/single-component.doc.mjs +1 -1
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/versioning.doc.mjs +1 -0
- 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/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/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/theme-build.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/discovery/component-discovery.d.mts +172 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +863 -55
- 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/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/template-adapter.mjs +74 -5
- package/foundation/doc-compiler/doc-loads.test.mjs +5 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -754,6 +754,285 @@ describe('integrationPackCheck', () => {
|
|
|
754
754
|
);
|
|
755
755
|
});
|
|
756
756
|
|
|
757
|
+
it('checks the export a component doc names when its file is named otherwise', async () => {
|
|
758
|
+
fs.writeFileSync(
|
|
759
|
+
path.join(tmpDir, 'package.json'),
|
|
760
|
+
`${JSON.stringify({
|
|
761
|
+
name: '@acme/widgets',
|
|
762
|
+
version: '1.0.0',
|
|
763
|
+
files: ['astryx.integration.mjs', 'components'],
|
|
764
|
+
exports: {'./components/Slides': './components/Slides.tsx'},
|
|
765
|
+
})}\n`,
|
|
766
|
+
);
|
|
767
|
+
fs.writeFileSync(
|
|
768
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
769
|
+
"export default {components: './components'};\n",
|
|
770
|
+
);
|
|
771
|
+
fs.mkdirSync(path.join(tmpDir, 'components'));
|
|
772
|
+
// Slides.tsx exports AcmeCarousel and nothing named Slides.
|
|
773
|
+
fs.writeFileSync(
|
|
774
|
+
path.join(tmpDir, 'components', 'Slides.tsx'),
|
|
775
|
+
'export function AcmeCarousel() { return null; }\n',
|
|
776
|
+
);
|
|
777
|
+
fs.writeFileSync(
|
|
778
|
+
path.join(tmpDir, 'components', 'Slides.doc.mjs'),
|
|
779
|
+
"export default {type: 'component', name: 'AcmeCarousel', import: '@acme/widgets/components/Slides', description: 'x', props: []};\n",
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
783
|
+
expect(result.data.packable).toBe(true);
|
|
784
|
+
expect(result.data.contributions.packed.components).toEqual([
|
|
785
|
+
'AcmeCarousel',
|
|
786
|
+
]);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('checks each family member against the module its family doc imports', async () => {
|
|
790
|
+
fs.writeFileSync(
|
|
791
|
+
path.join(tmpDir, 'package.json'),
|
|
792
|
+
`${JSON.stringify({
|
|
793
|
+
name: '@acme/widgets',
|
|
794
|
+
version: '1.0.0',
|
|
795
|
+
files: ['astryx.integration.mjs', 'components'],
|
|
796
|
+
exports: {'./tabs': './components/tabs/index.ts'},
|
|
797
|
+
})}\n`,
|
|
798
|
+
);
|
|
799
|
+
fs.writeFileSync(
|
|
800
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
801
|
+
"export default {components: './components'};\n",
|
|
802
|
+
);
|
|
803
|
+
const dir = path.join(tmpDir, 'components', 'tabs');
|
|
804
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
805
|
+
fs.writeFileSync(
|
|
806
|
+
path.join(dir, 'AcmeTabs.tsx'),
|
|
807
|
+
'export function AcmeTabs() { return null; }\nexport function AcmeTabsLegacy() { return null; }\n',
|
|
808
|
+
);
|
|
809
|
+
fs.writeFileSync(
|
|
810
|
+
path.join(dir, 'tab.tsx'),
|
|
811
|
+
'export function AcmeTab() { return null; }\n',
|
|
812
|
+
);
|
|
813
|
+
// The module apps import exports AcmeTabs and AcmeTab, not AcmeTabsLegacy.
|
|
814
|
+
fs.writeFileSync(
|
|
815
|
+
path.join(dir, 'index.ts'),
|
|
816
|
+
"export {AcmeTabs} from './AcmeTabs';\nexport {AcmeTab} from './tab';\n",
|
|
817
|
+
);
|
|
818
|
+
fs.writeFileSync(
|
|
819
|
+
path.join(dir, 'AcmeTabs.doc.mjs'),
|
|
820
|
+
"export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTabs', description: 'x', props: []}, {name: 'AcmeTab', description: 'x', props: []}, {name: 'AcmeTabsLegacy', description: 'x', props: []}]};\n",
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
824
|
+
|
|
825
|
+
expect(result.data.issues).toEqual([]);
|
|
826
|
+
expect(result.data.packable).toBe(true);
|
|
827
|
+
expect(result.data.contributions.packed.components).toEqual([
|
|
828
|
+
'AcmeTab',
|
|
829
|
+
'AcmeTabs',
|
|
830
|
+
]);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it.each([
|
|
834
|
+
'a clean checkout',
|
|
835
|
+
'a stale build',
|
|
836
|
+
'a stale build that exports more',
|
|
837
|
+
])(
|
|
838
|
+
'reads members a family imports from output its pack lifecycle builds, from %s',
|
|
839
|
+
async start => {
|
|
840
|
+
fs.writeFileSync(
|
|
841
|
+
path.join(tmpDir, 'package.json'),
|
|
842
|
+
`${JSON.stringify({
|
|
843
|
+
name: '@acme/widgets',
|
|
844
|
+
version: '1.0.0',
|
|
845
|
+
files: ['astryx.integration.mjs', 'components', 'dist'],
|
|
846
|
+
exports: {'./tabs': './dist/tabs/index.mjs'},
|
|
847
|
+
scripts: {prepack: 'node build.mjs'},
|
|
848
|
+
})}\n`,
|
|
849
|
+
);
|
|
850
|
+
fs.writeFileSync(
|
|
851
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
852
|
+
"export default {components: './components'};\n",
|
|
853
|
+
);
|
|
854
|
+
// The build writes the module the family imports; a clean checkout has none.
|
|
855
|
+
fs.writeFileSync(
|
|
856
|
+
path.join(tmpDir, 'build.mjs'),
|
|
857
|
+
"import fs from 'node:fs';\nfs.mkdirSync('dist/tabs', {recursive: true});\nfs.writeFileSync('dist/tabs/index.mjs', \"export {AcmeTabs, AcmeTab} from '../../components/tabs/AcmeTabs.tsx';\\n\");\n",
|
|
858
|
+
);
|
|
859
|
+
const dir = path.join(tmpDir, 'components', 'tabs');
|
|
860
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
861
|
+
fs.writeFileSync(
|
|
862
|
+
path.join(dir, 'AcmeTabs.tsx'),
|
|
863
|
+
'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\nexport function AcmeTabLegacy() { return null; }\n',
|
|
864
|
+
);
|
|
865
|
+
fs.writeFileSync(
|
|
866
|
+
path.join(dir, 'AcmeTabs.doc.mjs'),
|
|
867
|
+
"export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTabs', description: 'x', props: []}, {name: 'AcmeTab', description: 'x', props: []}, {name: 'AcmeTabLegacy', description: 'x', props: []}]};\n",
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
if (start !== 'a clean checkout') {
|
|
871
|
+
// An earlier build: before AcmeTab was exported, or while AcmeTabLegacy
|
|
872
|
+
// still was.
|
|
873
|
+
const names =
|
|
874
|
+
start === 'a stale build'
|
|
875
|
+
? 'AcmeTabs'
|
|
876
|
+
: 'AcmeTabs, AcmeTab, AcmeTabLegacy';
|
|
877
|
+
fs.mkdirSync(path.join(tmpDir, 'dist', 'tabs'), {recursive: true});
|
|
878
|
+
fs.writeFileSync(
|
|
879
|
+
path.join(tmpDir, 'dist', 'tabs', 'index.mjs'),
|
|
880
|
+
`export {${names}} from '../../components/tabs/AcmeTabs.tsx';\n`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
885
|
+
|
|
886
|
+
expect(result.data.issues).toEqual([]);
|
|
887
|
+
expect(result.data.packable).toBe(true);
|
|
888
|
+
expect(result.data.contributions.local.components).toEqual(
|
|
889
|
+
result.data.contributions.packed.components,
|
|
890
|
+
);
|
|
891
|
+
},
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
it.each(['a clean checkout', 'an incremental build'])(
|
|
895
|
+
'reads members built output re-exports from compiled files, from %s',
|
|
896
|
+
async start => {
|
|
897
|
+
fs.writeFileSync(
|
|
898
|
+
path.join(tmpDir, 'package.json'),
|
|
899
|
+
`${JSON.stringify({
|
|
900
|
+
name: '@acme/widgets',
|
|
901
|
+
version: '1.0.0',
|
|
902
|
+
files: ['astryx.integration.mjs', 'components', 'dist'],
|
|
903
|
+
exports: {'./tabs': './dist/tabs/index.mjs'},
|
|
904
|
+
scripts: {prepack: 'node build.mjs'},
|
|
905
|
+
})}\n`,
|
|
906
|
+
);
|
|
907
|
+
fs.writeFileSync(
|
|
908
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
909
|
+
"export default {components: './components'};\n",
|
|
910
|
+
);
|
|
911
|
+
// The build compiles each source to its own file and, like an
|
|
912
|
+
// incremental build, rewrites only the files whose output changed.
|
|
913
|
+
const output = {
|
|
914
|
+
'index.mjs':
|
|
915
|
+
"export {AcmeTabs} from './tabs.mjs';\nexport * from './tab.mjs';\n",
|
|
916
|
+
'tabs.mjs': 'export function AcmeTabs() { return null; }\n',
|
|
917
|
+
'tab.mjs': 'export function AcmeTab() { return null; }\n',
|
|
918
|
+
};
|
|
919
|
+
fs.writeFileSync(
|
|
920
|
+
path.join(tmpDir, 'build.mjs'),
|
|
921
|
+
`import fs from 'node:fs';\nconst output = ${JSON.stringify(output)};\nfs.mkdirSync('dist/tabs', {recursive: true});\nfor (const [file, text] of Object.entries(output)) {\n const target = 'dist/tabs/' + file;\n if (!fs.existsSync(target) || fs.readFileSync(target, 'utf-8') !== text) fs.writeFileSync(target, text);\n}\n`,
|
|
922
|
+
);
|
|
923
|
+
const dir = path.join(tmpDir, 'components', 'tabs');
|
|
924
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
925
|
+
fs.writeFileSync(
|
|
926
|
+
path.join(dir, 'AcmeTabs.tsx'),
|
|
927
|
+
'export function AcmeTabs() { return null; }\n',
|
|
928
|
+
);
|
|
929
|
+
fs.writeFileSync(
|
|
930
|
+
path.join(dir, 'AcmeTab.tsx'),
|
|
931
|
+
'export function AcmeTab() { return null; }\n',
|
|
932
|
+
);
|
|
933
|
+
fs.writeFileSync(
|
|
934
|
+
path.join(dir, 'AcmeTabs.doc.mjs'),
|
|
935
|
+
"export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTabs', description: 'x', props: []}, {name: 'AcmeTab', description: 'x', props: []}]};\n",
|
|
936
|
+
);
|
|
937
|
+
if (start === 'an incremental build') {
|
|
938
|
+
// An earlier build from before AcmeTab existed: the file the import
|
|
939
|
+
// names is already current, so the build leaves it untouched.
|
|
940
|
+
fs.mkdirSync(path.join(tmpDir, 'dist', 'tabs'), {recursive: true});
|
|
941
|
+
for (const [file, text] of Object.entries({...output, 'tab.mjs': ''})) {
|
|
942
|
+
fs.writeFileSync(path.join(tmpDir, 'dist', 'tabs', file), text);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
947
|
+
|
|
948
|
+
expect(result.data.issues).toEqual([]);
|
|
949
|
+
expect(result.data.packable).toBe(true);
|
|
950
|
+
expect(result.data.contributions.packed.components).toEqual([
|
|
951
|
+
'AcmeTab',
|
|
952
|
+
'AcmeTabs',
|
|
953
|
+
]);
|
|
954
|
+
expect(result.data.contributions.local.components).toEqual(
|
|
955
|
+
result.data.contributions.packed.components,
|
|
956
|
+
);
|
|
957
|
+
},
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
it('passes a member another folder declares, which has no source', async () => {
|
|
961
|
+
fs.writeFileSync(
|
|
962
|
+
path.join(tmpDir, 'package.json'),
|
|
963
|
+
`${JSON.stringify({
|
|
964
|
+
name: '@acme/widgets',
|
|
965
|
+
version: '1.0.0',
|
|
966
|
+
files: ['astryx.integration.mjs', 'components'],
|
|
967
|
+
exports: {'./menu': './components/menu/index.ts'},
|
|
968
|
+
})}\n`,
|
|
969
|
+
);
|
|
970
|
+
fs.writeFileSync(
|
|
971
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
972
|
+
"export default {components: './components'};\n",
|
|
973
|
+
);
|
|
974
|
+
const menu = path.join(tmpDir, 'components', 'menu');
|
|
975
|
+
const shared = path.join(tmpDir, 'components', 'shared');
|
|
976
|
+
fs.mkdirSync(menu, {recursive: true});
|
|
977
|
+
fs.mkdirSync(shared, {recursive: true});
|
|
978
|
+
fs.writeFileSync(
|
|
979
|
+
path.join(menu, 'AcmeMenu.doc.mjs'),
|
|
980
|
+
"export default {type: 'component', name: 'AcmeMenu', import: '@acme/widgets/menu', description: 'x', components: [{name: 'AcmeMenuItem', description: 'x', props: []}]};\n",
|
|
981
|
+
);
|
|
982
|
+
fs.writeFileSync(
|
|
983
|
+
path.join(menu, 'AcmeMenu.tsx'),
|
|
984
|
+
'export function AcmeMenu() { return null; }\n',
|
|
985
|
+
);
|
|
986
|
+
fs.writeFileSync(
|
|
987
|
+
path.join(menu, 'index.ts'),
|
|
988
|
+
"export {AcmeMenu} from './AcmeMenu';\nexport {AcmeMenuItem} from '../shared/AcmeMenuItem';\n",
|
|
989
|
+
);
|
|
990
|
+
fs.writeFileSync(
|
|
991
|
+
path.join(shared, 'AcmeMenuItem.tsx'),
|
|
992
|
+
'export function AcmeMenuItem() { return null; }\n',
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
996
|
+
|
|
997
|
+
expect(result.data.issues).toEqual([]);
|
|
998
|
+
expect(result.data.packable).toBe(true);
|
|
999
|
+
expect(result.data.contributions.packed.components).toEqual([
|
|
1000
|
+
'AcmeMenu',
|
|
1001
|
+
'AcmeMenuItem',
|
|
1002
|
+
]);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('reports a family doc the loader rejects once, not once per member', async () => {
|
|
1006
|
+
fs.writeFileSync(
|
|
1007
|
+
path.join(tmpDir, 'package.json'),
|
|
1008
|
+
`${JSON.stringify({
|
|
1009
|
+
name: '@acme/widgets',
|
|
1010
|
+
version: '1.0.0',
|
|
1011
|
+
files: ['astryx.integration.mjs', 'components'],
|
|
1012
|
+
})}\n`,
|
|
1013
|
+
);
|
|
1014
|
+
fs.writeFileSync(
|
|
1015
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
1016
|
+
"export default {components: './components'};\n",
|
|
1017
|
+
);
|
|
1018
|
+
fs.mkdirSync(path.join(tmpDir, 'components'));
|
|
1019
|
+
fs.writeFileSync(
|
|
1020
|
+
path.join(tmpDir, 'components', 'AcmeTabs.tsx'),
|
|
1021
|
+
'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\nexport function AcmeTabPanel() { return null; }\n',
|
|
1022
|
+
);
|
|
1023
|
+
// A displayName that is not a string makes the loader reject the doc.
|
|
1024
|
+
fs.writeFileSync(
|
|
1025
|
+
path.join(tmpDir, 'components', 'AcmeTabs.doc.mjs'),
|
|
1026
|
+
"export default {type: 'component', name: 'AcmeTabs', displayName: 1, description: 'x', components: [{name: 'AcmeTab', description: 'x', props: []}, {name: 'AcmeTabPanel', description: 'x', props: []}]};\n",
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
1030
|
+
const codes = result.data.issues.map(issue => issue.code);
|
|
1031
|
+
|
|
1032
|
+
expect(codes).toContain('component_doc_unloadable');
|
|
1033
|
+
expect(codes.length).toBe(new Set(codes).size);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
757
1036
|
it('passes a generated component through its exact packed export', async () => {
|
|
758
1037
|
fs.writeFileSync(
|
|
759
1038
|
path.join(tmpDir, 'package.json'),
|
|
@@ -811,6 +1090,86 @@ describe('integrationPackCheck', () => {
|
|
|
811
1090
|
);
|
|
812
1091
|
});
|
|
813
1092
|
|
|
1093
|
+
it('does not count a type-only re-export as the component', async () => {
|
|
1094
|
+
fs.writeFileSync(
|
|
1095
|
+
path.join(tmpDir, 'package.json'),
|
|
1096
|
+
`${JSON.stringify({
|
|
1097
|
+
name: '@acme/widgets',
|
|
1098
|
+
version: '1.0.0',
|
|
1099
|
+
files: ['astryx.integration.mjs', 'src'],
|
|
1100
|
+
exports: {'.': './src/index.ts'},
|
|
1101
|
+
})}\n`,
|
|
1102
|
+
);
|
|
1103
|
+
fs.writeFileSync(
|
|
1104
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
1105
|
+
"export default {components: './src'};\n",
|
|
1106
|
+
);
|
|
1107
|
+
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
1108
|
+
fs.writeFileSync(
|
|
1109
|
+
path.join(tmpDir, 'src', 'index.ts'),
|
|
1110
|
+
"export type {AcmeWidget} from './AcmeWidget';\n",
|
|
1111
|
+
);
|
|
1112
|
+
fs.writeFileSync(
|
|
1113
|
+
path.join(tmpDir, 'src', 'AcmeWidget.tsx'),
|
|
1114
|
+
'export function AcmeWidget() { return null; }\n',
|
|
1115
|
+
);
|
|
1116
|
+
fs.writeFileSync(
|
|
1117
|
+
path.join(tmpDir, 'src', 'AcmeWidget.doc.mjs'),
|
|
1118
|
+
"export default {type: 'component', name: 'AcmeWidget', import: '@acme/widgets', description: 'Widget.', props: []};\n",
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
1122
|
+
|
|
1123
|
+
expect(result.data.issues).toContainEqual(
|
|
1124
|
+
expect.objectContaining({code: 'component_export_missing'}),
|
|
1125
|
+
);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('follows a re-export of a folder to its index file', async () => {
|
|
1129
|
+
// `./primitives` names a folder: TypeScript and bundlers load its index file.
|
|
1130
|
+
fs.writeFileSync(
|
|
1131
|
+
path.join(tmpDir, 'package.json'),
|
|
1132
|
+
`${JSON.stringify({
|
|
1133
|
+
name: '@acme/widgets',
|
|
1134
|
+
version: '1.0.0',
|
|
1135
|
+
files: ['astryx.integration.mjs', 'src'],
|
|
1136
|
+
exports: {'.': './src/index.ts'},
|
|
1137
|
+
})}\n`,
|
|
1138
|
+
);
|
|
1139
|
+
fs.writeFileSync(
|
|
1140
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
1141
|
+
"export default {components: './src'};\n",
|
|
1142
|
+
);
|
|
1143
|
+
const primitives = path.join(tmpDir, 'src', 'primitives');
|
|
1144
|
+
fs.mkdirSync(primitives, {recursive: true});
|
|
1145
|
+
fs.writeFileSync(
|
|
1146
|
+
path.join(tmpDir, 'src', 'index.ts'),
|
|
1147
|
+
"export * from './primitives';\n",
|
|
1148
|
+
);
|
|
1149
|
+
fs.writeFileSync(
|
|
1150
|
+
path.join(primitives, 'index.ts'),
|
|
1151
|
+
"export * from './AcmeWidget';\n",
|
|
1152
|
+
);
|
|
1153
|
+
fs.writeFileSync(
|
|
1154
|
+
path.join(primitives, 'AcmeWidget.tsx'),
|
|
1155
|
+
'export function AcmeWidget() { return null; }\n',
|
|
1156
|
+
);
|
|
1157
|
+
fs.writeFileSync(
|
|
1158
|
+
path.join(primitives, 'AcmeWidget.doc.mjs'),
|
|
1159
|
+
"export default {type: 'component', name: 'AcmeWidget', import: '@acme/widgets', description: 'Widget.', props: []};\n",
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
1163
|
+
|
|
1164
|
+
expect(result.data.issues).not.toContainEqual(
|
|
1165
|
+
expect.objectContaining({code: 'component_export_missing'}),
|
|
1166
|
+
);
|
|
1167
|
+
expect(
|
|
1168
|
+
result.data.packable,
|
|
1169
|
+
JSON.stringify(result.data.issues, null, 2),
|
|
1170
|
+
).toBe(true);
|
|
1171
|
+
});
|
|
1172
|
+
|
|
814
1173
|
it('fails when packed template source is hidden by package exports', async () => {
|
|
815
1174
|
writePackage({
|
|
816
1175
|
manifest: "export default {templates: './templates'};\n",
|
package/api/search/search.d.mts
CHANGED
|
@@ -252,4 +252,13 @@ export type Candidate = {
|
|
|
252
252
|
_kind?: "page" | "block" | undefined;
|
|
253
253
|
_resultName?: string | undefined;
|
|
254
254
|
_commandName?: string | undefined;
|
|
255
|
+
/**
|
|
256
|
+
* - How an integration component holds its
|
|
257
|
+
* name, for the owner rule.
|
|
258
|
+
*/
|
|
259
|
+
_hold?: "file" | "doc" | undefined;
|
|
260
|
+
/**
|
|
261
|
+
* - An integration component's doc.
|
|
262
|
+
*/
|
|
263
|
+
_docPath?: string | undefined;
|
|
255
264
|
};
|
package/api/search/search.mjs
CHANGED
|
@@ -81,11 +81,20 @@
|
|
|
81
81
|
|
|
82
82
|
import {withListingCache} from '../../foundation/fs/listing-cache.mjs';
|
|
83
83
|
import {readDocView} from '../../foundation/doc-compiler/read.mjs';
|
|
84
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
discoverExternalPackages,
|
|
86
|
+
findCoreDir,
|
|
87
|
+
} from '../../foundation/fs/paths.mjs';
|
|
85
88
|
import {
|
|
86
89
|
discoverComponents,
|
|
87
90
|
discoverValidIntegrationComponents,
|
|
91
|
+
docStem,
|
|
92
|
+
familyMember,
|
|
88
93
|
findComponentReadme,
|
|
94
|
+
forEachIntegrationDoc,
|
|
95
|
+
integrationMemberEntry,
|
|
96
|
+
integrationRecordHold,
|
|
97
|
+
isIntegrationMember,
|
|
89
98
|
resolveImportPath,
|
|
90
99
|
resolveIntegrationImportPath,
|
|
91
100
|
CORE_PACKAGE,
|
|
@@ -97,6 +106,7 @@ import {
|
|
|
97
106
|
import {
|
|
98
107
|
loadComponentReplacements,
|
|
99
108
|
loadIntegrationsSafely,
|
|
109
|
+
resolveLegacyExternalDoc,
|
|
100
110
|
} from '../component/_adapter.mjs';
|
|
101
111
|
import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
|
|
102
112
|
import {discoverTemplates, extractComponents} from '../template/template.mjs';
|
|
@@ -149,6 +159,9 @@ import {setResultCoverage} from './coverage.mjs';
|
|
|
149
159
|
* @property {'page'|'block'} [_kind]
|
|
150
160
|
* @property {string} [_resultName]
|
|
151
161
|
* @property {string} [_commandName]
|
|
162
|
+
* @property {'file'|'doc'} [_hold] - How an integration component holds its
|
|
163
|
+
* name, for the owner rule.
|
|
164
|
+
* @property {string} [_docPath] - An integration component's doc.
|
|
152
165
|
*/
|
|
153
166
|
|
|
154
167
|
/**
|
|
@@ -1079,7 +1092,11 @@ async function gatherCoreComponents(coreDir) {
|
|
|
1079
1092
|
/** @type {string[]} */
|
|
1080
1093
|
let guidance = [];
|
|
1081
1094
|
if (readme && readme.endsWith('.doc.mjs')) {
|
|
1082
|
-
const
|
|
1095
|
+
const loaded = await loadModuleDoc(readme);
|
|
1096
|
+
// A member documented in its family's doc (VStack in Stack's doc) is
|
|
1097
|
+
// indexed from its own entry: the family's keywords and guidance
|
|
1098
|
+
// describe the family, not the member.
|
|
1099
|
+
const doc = familyMember(loaded, comp) ?? loaded;
|
|
1083
1100
|
if (doc) {
|
|
1084
1101
|
keywords = Array.isArray(doc.keywords) ? doc.keywords : [];
|
|
1085
1102
|
description = doc.usage?.description || doc.description || '';
|
|
@@ -1105,20 +1122,57 @@ async function gatherCoreComponents(coreDir) {
|
|
|
1105
1122
|
* usage/description from each component's .doc.mjs, same as core. Without
|
|
1106
1123
|
* this, an integration component is invisible to `search`/`build` even
|
|
1107
1124
|
* though `component --list`/`component <Name>` already resolve it — the two
|
|
1108
|
-
* discovery paths silently disagreed.
|
|
1125
|
+
* discovery paths silently disagreed. Also returns, per name, the docs that
|
|
1126
|
+
* hold it by their file name and the loadable docs that only record it.
|
|
1109
1127
|
* @param {import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]} loadedIntegrations
|
|
1110
|
-
* @returns {Promise<Candidate[]>}
|
|
1128
|
+
* @returns {Promise<{candidates: Candidate[], fileNamed: Map<string, Set<string>>, documented: Map<string, Set<string>>}>}
|
|
1111
1129
|
*/
|
|
1112
1130
|
async function gatherIntegrationComponents(loadedIntegrations) {
|
|
1113
1131
|
/** @type {Candidate[]} */
|
|
1114
1132
|
const candidates = [];
|
|
1133
|
+
// What the owner rule reads: the docs that hold a name by their file name,
|
|
1134
|
+
// loadable or not and hidden ones too, and the loadable docs that only
|
|
1135
|
+
// record it.
|
|
1136
|
+
/** @type {Map<string, Set<string>>} */
|
|
1137
|
+
const fileNamed = new Map();
|
|
1138
|
+
/** @type {Map<string, Set<string>>} */
|
|
1139
|
+
const documented = new Map();
|
|
1140
|
+
/** @param {Map<string, Set<string>>} map @param {string} name @param {string} docPath */
|
|
1141
|
+
const hold = (map, name, docPath) =>
|
|
1142
|
+
map.set(name, (map.get(name) ?? new Set()).add(docPath));
|
|
1115
1143
|
for (const integration of loadedIntegrations) {
|
|
1116
|
-
const {components} =
|
|
1144
|
+
const {components, discovered} =
|
|
1145
|
+
await discoverValidIntegrationComponents(integration);
|
|
1146
|
+
const renamed = new Set(
|
|
1147
|
+
discovered
|
|
1148
|
+
.filter(rec => integrationRecordHold(rec) === 'doc')
|
|
1149
|
+
.filter(rec => !isIntegrationMember(rec))
|
|
1150
|
+
.map(rec => rec.docPath),
|
|
1151
|
+
);
|
|
1152
|
+
forEachIntegrationDoc(integration.components, (docPath, stem) => {
|
|
1153
|
+
if (!renamed.has(docPath)) hold(fileNamed, stem, docPath);
|
|
1154
|
+
});
|
|
1117
1155
|
for (const rec of components) {
|
|
1118
|
-
const
|
|
1156
|
+
const loaded = await loadModuleDoc(rec.docPath);
|
|
1157
|
+
// A family member is indexed from its own entry: the family's keywords
|
|
1158
|
+
// and guidance describe the family.
|
|
1159
|
+
const member = isIntegrationMember(rec);
|
|
1160
|
+
const doc = member ? integrationMemberEntry(loaded, rec) : loaded;
|
|
1161
|
+
// A doc that names another export of its file still answers to the
|
|
1162
|
+
// file's name, as `component <file name>` does.
|
|
1163
|
+
const fileName = docStem(rec.docPath);
|
|
1164
|
+
if (integrationRecordHold(rec) === 'doc') {
|
|
1165
|
+
hold(documented, rec.name, rec.docPath);
|
|
1166
|
+
}
|
|
1167
|
+
if (renamed.has(rec.docPath) && !member) {
|
|
1168
|
+
hold(documented, fileName, rec.docPath);
|
|
1169
|
+
}
|
|
1119
1170
|
candidates.push({
|
|
1120
1171
|
domain: 'component',
|
|
1121
1172
|
name: rec.name,
|
|
1173
|
+
aliases: !member && fileName !== rec.name ? [fileName] : [],
|
|
1174
|
+
_hold: integrationRecordHold(rec),
|
|
1175
|
+
_docPath: rec.docPath,
|
|
1122
1176
|
keywords: doc && Array.isArray(doc.keywords) ? doc.keywords : [],
|
|
1123
1177
|
description: doc ? doc.usage?.description || doc.description || '' : '',
|
|
1124
1178
|
guidance: guidanceFrom(doc),
|
|
@@ -1138,12 +1192,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
|
|
|
1138
1192
|
packageName: rec.package,
|
|
1139
1193
|
},
|
|
1140
1194
|
rec.name,
|
|
1141
|
-
|
|
1195
|
+
loaded?.import,
|
|
1142
1196
|
),
|
|
1143
1197
|
});
|
|
1144
1198
|
}
|
|
1145
1199
|
}
|
|
1146
|
-
return candidates;
|
|
1200
|
+
return {candidates, fileNamed, documented};
|
|
1147
1201
|
}
|
|
1148
1202
|
|
|
1149
1203
|
/**
|
|
@@ -1155,11 +1209,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
|
|
|
1155
1209
|
*/
|
|
1156
1210
|
async function gatherComponents(coreDir, cwd) {
|
|
1157
1211
|
const loadedIntegrations = await loadIntegrationsSafely(cwd);
|
|
1158
|
-
const [core,
|
|
1212
|
+
const [core, gathered, replacements] = await Promise.all([
|
|
1159
1213
|
gatherCoreComponents(coreDir),
|
|
1160
1214
|
gatherIntegrationComponents(loadedIntegrations),
|
|
1161
1215
|
loadComponentReplacements(coreDir, loadedIntegrations),
|
|
1162
1216
|
]);
|
|
1217
|
+
const {candidates: integrations, fileNamed, documented} = gathered;
|
|
1163
1218
|
// An active replacement answers to the Core name it replaces, so a search
|
|
1164
1219
|
// for that name finds the replacement and not the Core original
|
|
1165
1220
|
// (spec:AST-035 FR11).
|
|
@@ -1170,9 +1225,43 @@ async function gatherComponents(coreDir, cwd) {
|
|
|
1170
1225
|
active.target,
|
|
1171
1226
|
]),
|
|
1172
1227
|
);
|
|
1228
|
+
// The owner rule, as component detail applies it: a candidate `component
|
|
1229
|
+
// <name>` does not open names its package and follows the owner, and a
|
|
1230
|
+
// file-name alias that opens another owner is dropped.
|
|
1231
|
+
const coreNames = new Set(core.map(candidate => candidate.name));
|
|
1232
|
+
/** @param {string} name @param {Candidate} candidate */
|
|
1233
|
+
const opens = (name, candidate) =>
|
|
1234
|
+
!coreNames.has(name) &&
|
|
1235
|
+
findComponentReadme(coreDir, name) == null &&
|
|
1236
|
+
[...(fileNamed.get(name) ?? [])].every(doc => doc === candidate._docPath) &&
|
|
1237
|
+
[...(documented.get(name) ?? [])].every(doc => doc === candidate._docPath);
|
|
1238
|
+
// A legacy docs package keeps an exact name over an export name a doc records.
|
|
1239
|
+
/** @type {ReturnType<typeof discoverExternalPackages> | undefined} */
|
|
1240
|
+
let externals;
|
|
1241
|
+
/** @param {string} name */
|
|
1242
|
+
const legacyDocuments = name =>
|
|
1243
|
+
(externals ??= discoverExternalPackages(cwd)).some(
|
|
1244
|
+
ext => resolveLegacyExternalDoc(ext, name) != null,
|
|
1245
|
+
);
|
|
1246
|
+
const ruled = integrations.map(candidate => {
|
|
1247
|
+
const aliases = (candidate.aliases ?? []).filter(alias =>
|
|
1248
|
+
opens(alias, candidate),
|
|
1249
|
+
);
|
|
1250
|
+
return candidate._hold === 'doc' &&
|
|
1251
|
+
(!opens(candidate.name, candidate) || legacyDocuments(candidate.name))
|
|
1252
|
+
? {
|
|
1253
|
+
...candidate,
|
|
1254
|
+
aliases,
|
|
1255
|
+
_command: `astryx component ${candidate.name} --package ${candidate._package}`,
|
|
1256
|
+
}
|
|
1257
|
+
: {...candidate, aliases};
|
|
1258
|
+
});
|
|
1173
1259
|
return [
|
|
1174
1260
|
...core.filter(candidate => !replacements.forTarget(candidate.name)),
|
|
1175
|
-
...
|
|
1261
|
+
...[
|
|
1262
|
+
...ruled.filter(candidate => !candidate._command),
|
|
1263
|
+
...ruled.filter(candidate => candidate._command),
|
|
1264
|
+
].map(candidate => {
|
|
1176
1265
|
const target = targetOf.get(`${candidate._package}\0${candidate.name}`);
|
|
1177
1266
|
if (target != null) {
|
|
1178
1267
|
return {...candidate, aliases: [...(candidate.aliases ?? []), target]};
|
|
@@ -1225,11 +1314,11 @@ export async function componentKeywords(coreDir, cwd) {
|
|
|
1225
1314
|
.flat()
|
|
1226
1315
|
.map(async name => {
|
|
1227
1316
|
const readme = findComponentReadme(coreDir, name);
|
|
1228
|
-
const
|
|
1317
|
+
const loaded =
|
|
1229
1318
|
readme && readme.endsWith('.doc.mjs')
|
|
1230
1319
|
? await loadModuleDoc(readme)
|
|
1231
1320
|
: null;
|
|
1232
|
-
return {name, keywords: keywordsOf(
|
|
1321
|
+
return {name, keywords: keywordsOf(familyMember(loaded, name) ?? loaded)};
|
|
1233
1322
|
});
|
|
1234
1323
|
const loadedIntegrations = await loadIntegrationsSafely(cwd);
|
|
1235
1324
|
const replacements = await loadComponentReplacements(
|
|
@@ -1239,10 +1328,13 @@ export async function componentKeywords(coreDir, cwd) {
|
|
|
1239
1328
|
const integrations = loadedIntegrations.map(async integration => {
|
|
1240
1329
|
const {components} = await discoverValidIntegrationComponents(integration);
|
|
1241
1330
|
return Promise.all(
|
|
1242
|
-
components.map(async rec =>
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1331
|
+
components.map(async rec => {
|
|
1332
|
+
const loaded = await loadModuleDoc(rec.docPath);
|
|
1333
|
+
return {
|
|
1334
|
+
name: rec.name,
|
|
1335
|
+
keywords: keywordsOf(integrationMemberEntry(loaded, rec) ?? loaded),
|
|
1336
|
+
};
|
|
1337
|
+
}),
|
|
1246
1338
|
);
|
|
1247
1339
|
});
|
|
1248
1340
|
return [
|
|
@@ -616,6 +616,42 @@ describe('search leaf — a theme is found by its names; its description is pros
|
|
|
616
616
|
);
|
|
617
617
|
});
|
|
618
618
|
|
|
619
|
+
describe('search leaf — components whose docs live in a family folder', () => {
|
|
620
|
+
it("indexes a family member from its own entry, not the family's keywords", async () => {
|
|
621
|
+
// HStack and VStack are documented in Stack's doc, whose keywords
|
|
622
|
+
// (horizontal, vertical, row, column, ...) describe Stack.
|
|
623
|
+
const reasons = async (/** @type {string} */ query) =>
|
|
624
|
+
Object.fromEntries(
|
|
625
|
+
(await search(query, {cwd, type: 'component'})).data.results.map(r => [
|
|
626
|
+
r.name,
|
|
627
|
+
r.reason,
|
|
628
|
+
]),
|
|
629
|
+
);
|
|
630
|
+
const column = await reasons('column');
|
|
631
|
+
expect(column.HStack ?? '').not.toMatch(/keyword/);
|
|
632
|
+
expect(column.VStack ?? '').not.toMatch(/keyword/);
|
|
633
|
+
expect(column.Stack).toMatch(/keyword/);
|
|
634
|
+
const vertical = (await search('vertical', {cwd, type: 'component'})).data.results.map(
|
|
635
|
+
r => r.name,
|
|
636
|
+
);
|
|
637
|
+
expect(vertical.indexOf('VStack')).toBeGreaterThanOrEqual(0);
|
|
638
|
+
if (vertical.includes('HStack')) {
|
|
639
|
+
expect(vertical.indexOf('VStack')).toBeLessThan(vertical.indexOf('HStack'));
|
|
640
|
+
}
|
|
641
|
+
}, SLOW);
|
|
642
|
+
|
|
643
|
+
it('ranks VStack, HStack and Heading first for their own names, with their own import', async () => {
|
|
644
|
+
for (const name of ['VStack', 'HStack', 'Heading']) {
|
|
645
|
+
const r = await search(name.toLowerCase(), {cwd, type: 'component'});
|
|
646
|
+
expect(r.data.results[0]).toMatchObject({
|
|
647
|
+
domain: 'component',
|
|
648
|
+
name,
|
|
649
|
+
import: `@astryxdesign/core/${name}`,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}, SLOW);
|
|
653
|
+
});
|
|
654
|
+
|
|
619
655
|
describe('search leaf — integration components', () => {
|
|
620
656
|
/**
|
|
621
657
|
* A minimal consumer project: a stub `@astryxdesign/core` (so `findCoreDir`
|