@astryxdesign/cli 0.7.2-canary.fc12a18 → 0.7.2-canary.fdd384f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/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.d.mts +11 -0
- package/api/docs/_adapter.mjs +244 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +8 -4
- package/api/integration/add-contribution.test.mjs +4 -2
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/check/check.d.mts +96 -0
- package/api/integration/check/check.mjs +357 -0
- package/api/integration/check/check.test.mjs +489 -0
- package/api/integration/integration-check.type.d.mts +118 -0
- package/api/integration/integration-check.type.mjs +84 -0
- package/api/integration/integrationCheck.doc.d.mts +10 -0
- package/api/integration/integrationCheck.doc.mjs +73 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.mjs +8 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/integration/validate-integration.type.d.mts +8 -0
- package/api/integration/validate-integration.type.mjs +12 -1
- package/api/integration/validateIntegration.doc.mjs +17 -3
- package/api/search/search.d.mts +9 -0
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +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 +64 -0
- package/api/theme/build/build.mjs +180 -24
- package/api/theme/themeBuild.doc.mjs +4 -4
- 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/block-template.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +196 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
- package/assets/docs/tree/checks.doc.mjs +21 -13
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/links.doc.mjs +2 -2
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- package/assets/docs/tree/single-component.doc.mjs +3 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/versioning.doc.mjs +1 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/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/template.doc.mjs +1 -1
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +8 -1
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +837 -0
- package/clients/cli/commands/doctor.mjs +190 -0
- package/clients/cli/commands/integration-check.doc.mjs +85 -0
- package/clients/cli/commands/integration-check.test.mjs +267 -0
- package/clients/cli/commands/integration.doc.mjs +6 -2
- package/clients/cli/commands/integration.mjs +107 -1
- package/clients/cli/commands/text-json-parity.test.mjs +5 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/index.mjs +1 -0
- package/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/integration-doc-files.d.mts +22 -0
- package/foundation/discovery/integration-doc-files.mjs +231 -0
- package/foundation/discovery/integration-doc-files.test.mjs +246 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +278 -0
- package/foundation/discovery/prop-drift.mjs +2263 -0
- package/foundation/discovery/prop-drift.test.mjs +2219 -0
- package/foundation/discovery/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 +11 -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 +7 -2
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
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`
|
|
@@ -21,8 +21,10 @@ import {checkGhCli} from '../_github.mjs';
|
|
|
21
21
|
import {Project} from '../../../foundation/config/project.mjs';
|
|
22
22
|
import {
|
|
23
23
|
CORE_PACKAGE,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
docStem,
|
|
25
|
+
findComponentReadme,
|
|
26
|
+
findIntegrationComponent,
|
|
27
|
+
unscopedComponentOwners,
|
|
26
28
|
} from '../../../foundation/discovery/component-discovery.mjs';
|
|
27
29
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
28
30
|
import {AstryxError, writeFailed} from '../../error.mjs';
|
|
@@ -126,33 +128,38 @@ async function loadConfigSafely(cwd) {
|
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* Build the set of OWNER packages that provide a component named `name` across
|
|
129
|
-
* core + every loaded integration.
|
|
131
|
+
* core + every loaded integration. Core holds every name it documents, but has
|
|
132
|
+
* source to copy (`sourceDir`) only for a name with its own folder.
|
|
130
133
|
* @param {string} coreDir
|
|
131
134
|
* @param {Array<{name: string, components?: string, issuesUrl?: string}>} loadedIntegrations
|
|
132
135
|
* @param {string} name
|
|
133
136
|
* @param {string|undefined} coreIssuesUrl
|
|
134
|
-
* @returns {Array<{package: string, sourceDir: string|null, ownerPackage: string, issuesUrl: string|undefined}>}
|
|
137
|
+
* @returns {Array<{package: string, sourceDir: string|null, docPath: string|null, ownerPackage: string, issuesUrl: string|undefined, hold: string}>}
|
|
135
138
|
*/
|
|
136
139
|
function resolveOwners(coreDir, loadedIntegrations, name, coreIssuesUrl) {
|
|
137
140
|
const owners = [];
|
|
138
141
|
const coreComponentDir = path.join(coreDir, 'src', name);
|
|
139
|
-
|
|
142
|
+
const coreFolder = fs.existsSync(coreComponentDir);
|
|
143
|
+
if (coreFolder || findComponentReadme(coreDir, name)) {
|
|
140
144
|
owners.push({
|
|
141
145
|
package: CORE_PACKAGE,
|
|
142
|
-
sourceDir: coreComponentDir,
|
|
146
|
+
sourceDir: coreFolder ? coreComponentDir : null,
|
|
147
|
+
docPath: null,
|
|
143
148
|
ownerPackage: CORE_PACKAGE,
|
|
144
149
|
issuesUrl: coreIssuesUrl || DEFAULT_ISSUES_URL,
|
|
150
|
+
hold: 'core',
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
153
|
for (const integration of loadedIntegrations) {
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
150
|
-
const sourcePath = findIntegrationComponentSource(integration, name);
|
|
154
|
+
const found = findIntegrationComponent(integration, name);
|
|
155
|
+
if (!found) continue;
|
|
151
156
|
owners.push({
|
|
152
157
|
package: integration.name,
|
|
153
|
-
sourceDir: sourcePath ? path.dirname(sourcePath) : null,
|
|
158
|
+
sourceDir: found.sourcePath ? path.dirname(found.sourcePath) : null,
|
|
159
|
+
docPath: found.docPath,
|
|
154
160
|
ownerPackage: integration.name,
|
|
155
161
|
issuesUrl: integration.issuesUrl,
|
|
162
|
+
hold: found.hold,
|
|
156
163
|
});
|
|
157
164
|
}
|
|
158
165
|
return owners;
|
|
@@ -222,25 +229,32 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
222
229
|
: undefined;
|
|
223
230
|
// An active integration replacement answers to the Core name it replaces,
|
|
224
231
|
// bare or qualified by its own package (spec:AST-035 FR11): copy it under its
|
|
225
|
-
//
|
|
232
|
+
// doc's file name, the folder it has always gone to. `--package
|
|
233
|
+
// @astryxdesign/core` still copies the Core original.
|
|
226
234
|
if (pkg !== CORE_PACKAGE) {
|
|
227
235
|
const replacement = await resolveComponentReplacement(
|
|
228
236
|
coreDir,
|
|
229
237
|
loadedIntegrations,
|
|
230
238
|
dirName,
|
|
231
239
|
);
|
|
240
|
+
const copyName = replacement ? docStem(replacement.docPath) : null;
|
|
232
241
|
if (
|
|
233
242
|
replacement &&
|
|
234
|
-
|
|
243
|
+
copyName &&
|
|
244
|
+
(!pkg || (pkg === replacement.package && copyName !== dirName))
|
|
235
245
|
) {
|
|
236
|
-
return swizzleCopy(
|
|
246
|
+
return swizzleCopy(copyName, {
|
|
237
247
|
...options,
|
|
238
248
|
package: replacement.package,
|
|
239
249
|
});
|
|
240
250
|
}
|
|
241
251
|
}
|
|
242
252
|
|
|
243
|
-
const
|
|
253
|
+
const found = resolveOwners(coreDir, loadedIntegrations, dirName, coreIssuesUrl);
|
|
254
|
+
// The owner rule, then a Core name without its own folder drops out.
|
|
255
|
+
const allOwners = (pkg ? found : unscopedComponentOwners(found)).filter(
|
|
256
|
+
owner => owner.package !== CORE_PACKAGE || owner.sourceDir != null,
|
|
257
|
+
);
|
|
244
258
|
|
|
245
259
|
if (allOwners.length === 0) {
|
|
246
260
|
throw new AstryxError(
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
3
6
|
import {describe, it, expect} from 'vitest';
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
stripTemplateAssetRefs,
|
|
9
|
+
template,
|
|
10
|
+
extractComponents,
|
|
11
|
+
} from './template.mjs';
|
|
5
12
|
|
|
6
13
|
describe('stripTemplateAssetRefs', () => {
|
|
7
14
|
it('replaces a /template-assets image path with an inline data URI', () => {
|
|
@@ -31,8 +38,7 @@ describe('stripTemplateAssetRefs', () => {
|
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
it('preserves surrounding source structure', () => {
|
|
34
|
-
const src =
|
|
35
|
-
"const data = [{src: '/template-assets/x.png', alt: 'X'}];";
|
|
41
|
+
const src = "const data = [{src: '/template-assets/x.png', alt: 'X'}];";
|
|
36
42
|
const out = stripTemplateAssetRefs(src);
|
|
37
43
|
expect(out).toContain("alt: 'X'");
|
|
38
44
|
expect(out).toContain('const data = [{src:');
|
|
@@ -102,4 +108,115 @@ describe('template --skeleton component extraction (prefix-agnostic)', () => {
|
|
|
102
108
|
|
|
103
109
|
expect(result.data.skeleton).toContain('columns={{minWidth: 200}}');
|
|
104
110
|
});
|
|
111
|
+
|
|
112
|
+
it('filters out local helper components declared in the same file', () => {
|
|
113
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
114
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
pagePath,
|
|
117
|
+
`
|
|
118
|
+
import {Card} from '@astryxdesign/core/Layout';
|
|
119
|
+
|
|
120
|
+
function TimelineSection() {
|
|
121
|
+
return <div />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default function Page() {
|
|
125
|
+
return (
|
|
126
|
+
<Card>
|
|
127
|
+
<TimelineSection />
|
|
128
|
+
</Card>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const components = extractComponents(pagePath);
|
|
136
|
+
expect(components).toContain('Card');
|
|
137
|
+
expect(components).not.toContain('Timeline');
|
|
138
|
+
} finally {
|
|
139
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('filters out a type-annotated local helper', () => {
|
|
144
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
145
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
pagePath,
|
|
148
|
+
`
|
|
149
|
+
import type {FC} from 'react';
|
|
150
|
+
import {Card} from '@astryxdesign/core/Layout';
|
|
151
|
+
|
|
152
|
+
const TimelineSection: FC = () => <div />;
|
|
153
|
+
|
|
154
|
+
export default function Page() {
|
|
155
|
+
return (
|
|
156
|
+
<Card>
|
|
157
|
+
<TimelineSection />
|
|
158
|
+
</Card>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
);
|
|
163
|
+
try {
|
|
164
|
+
const components = extractComponents(pagePath);
|
|
165
|
+
expect(components).toContain('Card');
|
|
166
|
+
expect(components).not.toContain('Timeline');
|
|
167
|
+
} finally {
|
|
168
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('keeps an imported component when declaration-like text appears in a comment or string', () => {
|
|
173
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
174
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
pagePath,
|
|
177
|
+
`
|
|
178
|
+
import {Card} from '@astryxdesign/core/Layout';
|
|
179
|
+
|
|
180
|
+
// const Card = () => null;
|
|
181
|
+
const note = 'function Card() {}';
|
|
182
|
+
|
|
183
|
+
export default function Page() {
|
|
184
|
+
return <Card>{note}</Card>;
|
|
185
|
+
}
|
|
186
|
+
`,
|
|
187
|
+
);
|
|
188
|
+
try {
|
|
189
|
+
expect(extractComponents(pagePath)).toContain('Card');
|
|
190
|
+
} finally {
|
|
191
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('keeps a bare imported component whose name is itself a stripped suffix', () => {
|
|
196
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
197
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
198
|
+
fs.writeFileSync(
|
|
199
|
+
pagePath,
|
|
200
|
+
`
|
|
201
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
202
|
+
|
|
203
|
+
function SectionHeading() {
|
|
204
|
+
return <div />;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default function Page() {
|
|
208
|
+
return (
|
|
209
|
+
<Section>
|
|
210
|
+
<SectionHeading />
|
|
211
|
+
</Section>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
`,
|
|
215
|
+
);
|
|
216
|
+
try {
|
|
217
|
+
expect(extractComponents(pagePath)).toContain('Section');
|
|
218
|
+
} finally {
|
|
219
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
105
222
|
});
|
|
@@ -13,6 +13,7 @@ import {themeBuild, themeBuildFamily} from './build.mjs';
|
|
|
13
13
|
import {interceptCore} from './core-interception.mjs';
|
|
14
14
|
import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
|
|
15
15
|
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
16
17
|
vi.setConfig({testTimeout: 120_000});
|
|
17
18
|
const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
|
|
18
19
|
const FAMILY_KEY = 'ocean-family';
|
|
@@ -252,6 +253,69 @@ describe('themeBuildFamily()', () => {
|
|
|
252
253
|
expect(fs.existsSync(dtsPath)).toBe(false);
|
|
253
254
|
});
|
|
254
255
|
|
|
256
|
+
it('refuses to write family outputs outside cwd, and checks them read-only', async () => {
|
|
257
|
+
const dir = makeDir();
|
|
258
|
+
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
259
|
+
const app = path.join(dir, 'app');
|
|
260
|
+
fs.mkdirSync(app);
|
|
261
|
+
|
|
262
|
+
await expect(
|
|
263
|
+
themeBuildFamily(files, {familyKey: FAMILY_KEY}, {cwd: app}),
|
|
264
|
+
).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
|
|
265
|
+
const checked = await themeBuildFamily(
|
|
266
|
+
files,
|
|
267
|
+
{familyKey: FAMILY_KEY, check: true},
|
|
268
|
+
{cwd: app},
|
|
269
|
+
);
|
|
270
|
+
expect(checked?.type).toBe('theme.build.check');
|
|
271
|
+
expect(checked?.data.upToDate).toBe(false);
|
|
272
|
+
// The `.css.d.ts` import stub may be absent in check mode.
|
|
273
|
+
expect(checked?.data.stale).toEqual(
|
|
274
|
+
OUTPUTS.filter(file => !file.endsWith('.css.d.ts')).map(file => ({
|
|
275
|
+
path: `../themes/${file}`,
|
|
276
|
+
reason: 'missing',
|
|
277
|
+
})),
|
|
278
|
+
);
|
|
279
|
+
for (const output of OUTPUTS) {
|
|
280
|
+
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(false);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('ends a stale family report for outputs outside cwd with a remedy that works', async () => {
|
|
285
|
+
const dir = makeDir();
|
|
286
|
+
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
287
|
+
const app = path.join(dir, 'app');
|
|
288
|
+
fs.mkdirSync(app);
|
|
289
|
+
const family = ['--family', ...files, '--family-key', FAMILY_KEY];
|
|
290
|
+
|
|
291
|
+
const stale = await runCli(['theme', 'build', ...family, '--check'], app);
|
|
292
|
+
expect(stale.code).toBe(1);
|
|
293
|
+
// The old remedy ran the build from cwd, which refuses these outputs.
|
|
294
|
+
expect(stale.stderr).not.toContain('Rebuild with:');
|
|
295
|
+
|
|
296
|
+
const lines = stale.stderr.split('\n').map(line => line.trim());
|
|
297
|
+
const runDir = lines.find(line => line.startsWith('cd '))?.slice(3);
|
|
298
|
+
expect(runDir).toBe(fs.realpathSync(path.join(dir, 'themes')));
|
|
299
|
+
const stem = `${getCliInvocation(runDir)} theme build `;
|
|
300
|
+
const [buildArgs, checkArgs] = lines
|
|
301
|
+
.filter(line => line.startsWith(stem))
|
|
302
|
+
.map(line => line.slice(stem.length).split(' '));
|
|
303
|
+
expect(checkArgs).toEqual([...buildArgs, '--check']);
|
|
304
|
+
|
|
305
|
+
// Following the remedy builds, and the check from there passes.
|
|
306
|
+
const built = await runCli(['theme', 'build', ...buildArgs], runDir);
|
|
307
|
+
expect(built.code).toBe(0);
|
|
308
|
+
for (const output of OUTPUTS) {
|
|
309
|
+
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(true);
|
|
310
|
+
}
|
|
311
|
+
const checked = await runCli(
|
|
312
|
+
['--json', 'theme', 'build', ...checkArgs],
|
|
313
|
+
runDir,
|
|
314
|
+
);
|
|
315
|
+
expect(checked.code).toBe(0);
|
|
316
|
+
expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
255
319
|
it('fails without publishing a mixed trio when staging a write fails', async () => {
|
|
256
320
|
const dir = makeDir();
|
|
257
321
|
const files = writeFamily(dir);
|