@astryxdesign/cli 0.7.2-canary.e2909cf → 0.7.2-canary.ebcc404
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 +27 -1
- package/api/component/_adapter.mjs +45 -7
- package/api/component/component.mjs +12 -3
- package/api/component/integration-export-name.test.mjs +419 -0
- package/api/gap-report/gap-report.mjs +66 -4
- package/api/integration/pack-check.mjs +3 -94
- package/api/integration/pack-check.test.mjs +112 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +82 -7
- package/api/swizzle/copy/copy.mjs +28 -14
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/authoring/doctypes/component/component.doc.mjs +1 -1
- package/authoring/doctypes/component/type.ts +6 -3
- package/foundation/discovery/component-discovery.d.mts +83 -6
- package/foundation/discovery/component-discovery.export-names.test.mjs +293 -0
- package/foundation/discovery/component-discovery.mjs +313 -49
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +20 -0
- package/foundation/discovery/module-exports.d.mts +21 -0
- package/foundation/discovery/module-exports.mjs +343 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/package.json +9 -9
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import {createRequire} from 'node:module';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
+
import {DOC_EXPORTS} from '../doc-compiler/read.mjs';
|
|
10
11
|
import {listDir, listDirEntries, pathExists} from '../fs/listing-cache.mjs';
|
|
11
12
|
import {existsCaseExact} from '../fs/paths.mjs';
|
|
12
13
|
import {CORE_PROVIDER_ID} from '../identity/providers.mjs';
|
|
13
14
|
import {loadComponentDoc} from './component-loader.mjs';
|
|
15
|
+
import {moduleExportsName, staticExportObject} from './module-exports.mjs';
|
|
14
16
|
|
|
15
17
|
const require = createRequire(import.meta.url);
|
|
16
18
|
|
|
@@ -82,12 +84,35 @@ function componentNameFromFile(fileName) {
|
|
|
82
84
|
const GROUP_RE = /(?:^|\n) {0,2}group:\s*['"]([^'"]+)['"]/;
|
|
83
85
|
const HIDDEN_COMPONENTS_RE = /(?:^|\n) {0,2}hiddenComponents:\s*\[([^\]]*)\]/;
|
|
84
86
|
const HIDDEN_RE = /(?:^|\n) {0,2}hidden:\s*true/;
|
|
87
|
+
// Every top-level `name: '...'` (or `"name": "..."`) in the file: a quick check
|
|
88
|
+
// that a doc is named after its file before anything parses it.
|
|
89
|
+
const NAME_FIELD_RE = /(?:^|\n) {0,2}['"]?name['"]?\s*:\s*['"]([^'"\n]+)['"]/g;
|
|
90
|
+
// A top-level `replaces: '...'`: the Core name a doc claims for its package.
|
|
91
|
+
const REPLACES_FIELD_RE =
|
|
92
|
+
/(?:^|\n) {0,2}['"]?replaces['"]?\s*:\s*['"]([^'"\n]+)['"]/;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What a quick scan of a doc's text finds before anything parses it: the
|
|
96
|
+
* top-level `name` values it writes (none when a spread could set the name)
|
|
97
|
+
* and the Core name it replaces.
|
|
98
|
+
* @param {string} text
|
|
99
|
+
* @returns {{names: string[], replaces: string | null}}
|
|
100
|
+
*/
|
|
101
|
+
function scanDocText(text) {
|
|
102
|
+
return {
|
|
103
|
+
names: text.includes('...')
|
|
104
|
+
? []
|
|
105
|
+
: [...text.matchAll(NAME_FIELD_RE)].map(match => match[1]),
|
|
106
|
+
replaces: REPLACES_FIELD_RE.exec(text)?.[1] ?? null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
85
109
|
|
|
86
110
|
/**
|
|
87
111
|
* Read the `group`, `hiddenComponents`, and `hidden` fields from a
|
|
88
|
-
* component's .doc.mjs file (synchronous)
|
|
112
|
+
* component's .doc.mjs file (synchronous), with what {@link scanDocText}
|
|
113
|
+
* finds in it.
|
|
89
114
|
* @param {string} docPath
|
|
90
|
-
* @returns {{group: string | null, hiddenComponents: Set<string>, hidden: boolean}}
|
|
115
|
+
* @returns {{group: string | null, hiddenComponents: Set<string>, hidden: boolean, names: string[], replaces: string | null}}
|
|
91
116
|
*/
|
|
92
117
|
function readDocMeta(docPath) {
|
|
93
118
|
try {
|
|
@@ -106,9 +131,16 @@ function readDocMeta(docPath) {
|
|
|
106
131
|
group: groupMatch ? groupMatch[1] : null,
|
|
107
132
|
hiddenComponents: hiddenSet,
|
|
108
133
|
hidden,
|
|
134
|
+
...scanDocText(content),
|
|
109
135
|
};
|
|
110
136
|
} catch {
|
|
111
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
group: null,
|
|
139
|
+
hiddenComponents: new Set(),
|
|
140
|
+
hidden: false,
|
|
141
|
+
names: [],
|
|
142
|
+
replaces: null,
|
|
143
|
+
};
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
|
|
@@ -843,6 +875,95 @@ export function findExternalComponentDoc(docsDir, name) {
|
|
|
843
875
|
// commands — and the future integration-component swizzle — can disambiguate
|
|
844
876
|
// by package and route source/issues correctly.
|
|
845
877
|
|
|
878
|
+
// A doc's `name` can name another export of its file; every other doc reads as
|
|
879
|
+
// before.
|
|
880
|
+
const COMPONENT_NAME_RE = /^[A-Z][\w$]*$/;
|
|
881
|
+
|
|
882
|
+
/** @type {Map<string, {stamp: string, value: Record<string, any> | null}>} doc reads, reused while the doc file is unchanged */
|
|
883
|
+
const docReads = new Map();
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* A file's identity for a cache: ctime and inode change on a rewrite even when
|
|
887
|
+
* npm stamps the same size and mtime.
|
|
888
|
+
* @param {string} file
|
|
889
|
+
*/
|
|
890
|
+
function fileStamp(file) {
|
|
891
|
+
try {
|
|
892
|
+
const {size, mtimeMs, ctimeMs, ino} = fs.statSync(file);
|
|
893
|
+
return `${size}:${mtimeMs}:${ctimeMs}:${ino}`;
|
|
894
|
+
} catch {
|
|
895
|
+
return 'missing';
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** UTF-16 code-unit order, the same in every locale. @param {string} a @param {string} b */
|
|
900
|
+
function byCodeUnit(a, b) {
|
|
901
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* A component doc's exported object as plain data, read without running it.
|
|
906
|
+
* @param {string} docPath
|
|
907
|
+
* @returns {Record<string, any> | null}
|
|
908
|
+
*/
|
|
909
|
+
function docObject(docPath) {
|
|
910
|
+
const stamp = fileStamp(docPath);
|
|
911
|
+
const cached = docReads.get(docPath);
|
|
912
|
+
if (cached?.stamp === stamp) return cached.value;
|
|
913
|
+
const value = staticExportObject(docPath, DOC_EXPORTS.components);
|
|
914
|
+
docReads.set(docPath, {stamp, value});
|
|
915
|
+
return value;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** @param {string} docPath */
|
|
919
|
+
export function docStem(docPath) {
|
|
920
|
+
return path.basename(docPath).replace(/\.doc\.(ts|mjs|js)$/, '');
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* The export a component doc names when its same-stem source is not named
|
|
925
|
+
* after an export, or null to keep the stem.
|
|
926
|
+
* @param {{docPath: string, sourcePath: string|null, stem: string, names: string[]}} doc
|
|
927
|
+
* `names`: top-level names a quick scan found (none: the doc is read)
|
|
928
|
+
* @returns {string | null}
|
|
929
|
+
*/
|
|
930
|
+
function otherExportName({docPath, sourcePath, stem, names}) {
|
|
931
|
+
if (sourcePath == null) return null;
|
|
932
|
+
if (names.length > 0 && names.every(name => name === stem)) return null;
|
|
933
|
+
const name = docObject(docPath)?.name;
|
|
934
|
+
return typeof name === 'string' &&
|
|
935
|
+
name !== stem &&
|
|
936
|
+
COMPONENT_NAME_RE.test(name) &&
|
|
937
|
+
moduleExportsName(sourcePath, name) &&
|
|
938
|
+
!moduleExportsName(sourcePath, stem)
|
|
939
|
+
? name
|
|
940
|
+
: null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Visit every component doc under an integration's components root, hidden
|
|
945
|
+
* ones included, in discovery order, with its stem.
|
|
946
|
+
* @param {string | undefined} componentsDir
|
|
947
|
+
* @param {(docPath: string, stem: string) => void} visit
|
|
948
|
+
*/
|
|
949
|
+
export function forEachIntegrationDoc(componentsDir, visit) {
|
|
950
|
+
if (!componentsDir || !pathExists(componentsDir)) return;
|
|
951
|
+
/** @param {string} dirPath */
|
|
952
|
+
function scanDir(dirPath) {
|
|
953
|
+
for (const entry of listDirEntries(dirPath)) {
|
|
954
|
+
if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
955
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
956
|
+
if (entry.isDirectory()) {
|
|
957
|
+
scanDir(fullPath);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const suffix = INTEGRATION_DOC_SUFFIXES.find(s => entry.name.endsWith(s));
|
|
961
|
+
if (suffix) visit(fullPath, entry.name.slice(0, -suffix.length));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
scanDir(componentsDir);
|
|
965
|
+
}
|
|
966
|
+
|
|
846
967
|
/**
|
|
847
968
|
* Given an integration component doc path, return the sibling component source
|
|
848
969
|
* (`{Name}.tsx`) if one exists, else null.
|
|
@@ -860,49 +981,71 @@ function integrationSourceForDoc(docPath) {
|
|
|
860
981
|
/**
|
|
861
982
|
* Discover ownership records for the components contributed by a single loaded
|
|
862
983
|
* integration. Scans the integration's resolved `components` dir for same-stem
|
|
863
|
-
* doc files and records each with owner package + issuesUrl + sourcePath.
|
|
984
|
+
* doc files and records each with owner package + issuesUrl + sourcePath. A doc
|
|
985
|
+
* that names a differently named export of its file is recorded under that
|
|
986
|
+
* export unless the name is taken: every doc's file name (a hidden doc's too),
|
|
987
|
+
* a Core name a doc in the package replaces, and a name an earlier doc records.
|
|
864
988
|
*
|
|
865
989
|
* @param {{name: string, components?: string, issuesUrl?: string}} integration
|
|
866
990
|
* a single entry from `loadedIntegrations` (lib/integrations.mjs)
|
|
991
|
+
* @param {{documentedNames?: boolean}} [options] `documentedNames: false`
|
|
992
|
+
* records each doc under its file name alone, without reading the names
|
|
993
|
+
* docs document: for a caller that needs only what each doc says
|
|
867
994
|
* @returns {Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>}
|
|
868
995
|
*/
|
|
869
|
-
export function discoverIntegrationComponents(
|
|
996
|
+
export function discoverIntegrationComponents(
|
|
997
|
+
integration,
|
|
998
|
+
{documentedNames = true} = {},
|
|
999
|
+
) {
|
|
870
1000
|
const componentsDir = integration?.components;
|
|
871
1001
|
if (!componentsDir || !pathExists(componentsDir)) return [];
|
|
872
1002
|
|
|
873
1003
|
/** @type {Map<string, {name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>} */
|
|
874
1004
|
const byName = new Map();
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1005
|
+
/** @type {Map<string, {names: string[], replaces: string | null}>} stem → quick scan */
|
|
1006
|
+
const scans = new Map();
|
|
1007
|
+
/** @type {Set<string>} names no doc can record under another name */
|
|
1008
|
+
const taken = new Set();
|
|
1009
|
+
|
|
1010
|
+
forEachIntegrationDoc(componentsDir, (fullPath, name) => {
|
|
1011
|
+
taken.add(name);
|
|
1012
|
+
const {group, hidden, names, replaces} = readDocMeta(fullPath);
|
|
1013
|
+
if (hidden) return;
|
|
1014
|
+
// First doc wins per name (precedence matches INTEGRATION_DOC_SUFFIXES).
|
|
1015
|
+
if (byName.has(name)) return;
|
|
1016
|
+
byName.set(name, {
|
|
1017
|
+
name,
|
|
1018
|
+
package: integration.name,
|
|
1019
|
+
docPath: fullPath,
|
|
1020
|
+
sourcePath: integrationSourceForDoc(fullPath),
|
|
1021
|
+
issuesUrl: integration.issuesUrl,
|
|
1022
|
+
group: group ?? null,
|
|
1023
|
+
});
|
|
1024
|
+
scans.set(name, {names, replaces});
|
|
1025
|
+
});
|
|
1026
|
+
if (!documentedNames) return [...byName.values()];
|
|
1027
|
+
|
|
1028
|
+
// A doc that names a differently named export of its file is listed under
|
|
1029
|
+
// that export, unless the name is taken.
|
|
1030
|
+
for (const {replaces} of scans.values()) if (replaces) taken.add(replaces);
|
|
1031
|
+
const records = [...byName.values()];
|
|
1032
|
+
const byPath = [...records].sort((a, b) => byCodeUnit(a.docPath, b.docPath));
|
|
1033
|
+
/** @type {Map<string, string>} stem → export name */
|
|
1034
|
+
const exportNames = new Map();
|
|
1035
|
+
for (const record of byPath) {
|
|
1036
|
+
const exportName = otherExportName({
|
|
1037
|
+
...record,
|
|
1038
|
+
stem: record.name,
|
|
1039
|
+
names: scans.get(record.name)?.names ?? [],
|
|
1040
|
+
});
|
|
1041
|
+
if (exportName == null || taken.has(exportName)) continue;
|
|
1042
|
+
taken.add(exportName);
|
|
1043
|
+
exportNames.set(record.name, exportName);
|
|
902
1044
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1045
|
+
return records.map(record => {
|
|
1046
|
+
const name = exportNames.get(record.name);
|
|
1047
|
+
return name ? {...record, name} : record;
|
|
1048
|
+
});
|
|
906
1049
|
}
|
|
907
1050
|
|
|
908
1051
|
/**
|
|
@@ -910,17 +1053,20 @@ export function discoverIntegrationComponents(integration) {
|
|
|
910
1053
|
* Invalid metadata or a missing same-stem source removes only that component;
|
|
911
1054
|
* callers can report the returned errors while retaining valid siblings. A
|
|
912
1055
|
* component whose doc sets `replaces` carries the value as written; the
|
|
913
|
-
* component replacement resolver decides what it means.
|
|
1056
|
+
* component replacement resolver decides what it means. A doc that fails to
|
|
1057
|
+
* load is reported under its file name: what it says cannot be trusted.
|
|
914
1058
|
*
|
|
915
1059
|
* @param {{name: string, components?: string, issuesUrl?: string}} integration
|
|
1060
|
+
* @param {{documentedNames?: boolean}} [options] as for
|
|
1061
|
+
* {@link discoverIntegrationComponents}
|
|
916
1062
|
* @returns {Promise<{
|
|
917
1063
|
* components: Array<{name: string, package: string, docPath: string, sourcePath: string, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>,
|
|
918
1064
|
* discovered: Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>,
|
|
919
1065
|
* errors: Array<{name: string, message: string}>,
|
|
920
1066
|
* }>}
|
|
921
1067
|
*/
|
|
922
|
-
export async function discoverValidIntegrationComponents(integration) {
|
|
923
|
-
const discovered = discoverIntegrationComponents(integration);
|
|
1068
|
+
export async function discoverValidIntegrationComponents(integration, options) {
|
|
1069
|
+
const discovered = discoverIntegrationComponents(integration, options);
|
|
924
1070
|
/** @type {Array<{name: string, package: string, docPath: string, sourcePath: string, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>} */
|
|
925
1071
|
const components = [];
|
|
926
1072
|
/** @type {Array<{name: string, message: string}>} */
|
|
@@ -946,9 +1092,10 @@ export async function discoverValidIntegrationComponents(integration) {
|
|
|
946
1092
|
: valid,
|
|
947
1093
|
);
|
|
948
1094
|
} catch (err) {
|
|
1095
|
+
const name = docStem(record.docPath);
|
|
949
1096
|
errors.push({
|
|
950
|
-
name
|
|
951
|
-
message: `Component "${
|
|
1097
|
+
name,
|
|
1098
|
+
message: `Component "${name}" has invalid metadata: ${
|
|
952
1099
|
err instanceof Error ? err.message : String(err)
|
|
953
1100
|
}`,
|
|
954
1101
|
});
|
|
@@ -958,19 +1105,122 @@ export async function discoverValidIntegrationComponents(integration) {
|
|
|
958
1105
|
return {components, discovered, errors};
|
|
959
1106
|
}
|
|
960
1107
|
|
|
1108
|
+
/**
|
|
1109
|
+
* How an owner holds a component name, which the owner rule
|
|
1110
|
+
* ({@link unscopedComponentOwners}) reads: `core` when Core documents it;
|
|
1111
|
+
* `file` when an integration doc named after its file is recorded under it;
|
|
1112
|
+
* `doc` when only a doc records it: an export name, or the file name of a doc
|
|
1113
|
+
* recorded under another export.
|
|
1114
|
+
* @typedef {'core' | 'file' | 'doc'} NameHold
|
|
1115
|
+
*/
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* How a discovered integration record holds its name.
|
|
1119
|
+
* @param {{name: string, docPath: string|null}} record
|
|
1120
|
+
* @returns {'file' | 'doc'}
|
|
1121
|
+
*/
|
|
1122
|
+
export function integrationRecordHold(record) {
|
|
1123
|
+
return record.docPath == null || docStem(record.docPath) === record.name
|
|
1124
|
+
? 'file'
|
|
1125
|
+
: 'doc';
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* The doc that records `name` though its file is named otherwise, with its
|
|
1130
|
+
* source, reading only docs that write the name as a string.
|
|
1131
|
+
*
|
|
1132
|
+
* @param {string} componentsDir
|
|
1133
|
+
* @param {string} name
|
|
1134
|
+
* @returns {{docPath: string, sourcePath: string|null} | null}
|
|
1135
|
+
*/
|
|
1136
|
+
function findDocumentedComponent(componentsDir, name) {
|
|
1137
|
+
if (!COMPONENT_NAME_RE.test(name)) return null;
|
|
1138
|
+
const literals = [`'${name}'`, `"${name}"`, `\`${name}\``];
|
|
1139
|
+
/** @type {Set<string>} */
|
|
1140
|
+
const stems = new Set();
|
|
1141
|
+
/** @type {Array<{docPath: string, stem: string, names: string[], replaces: string | null, sourcePath: string|null}>} */
|
|
1142
|
+
const candidates = [];
|
|
1143
|
+
forEachIntegrationDoc(componentsDir, (docPath, stem) => {
|
|
1144
|
+
let text;
|
|
1145
|
+
try {
|
|
1146
|
+
text = fs.readFileSync(docPath, 'utf-8');
|
|
1147
|
+
} catch {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
// The docs discovery records: the first visible doc for each stem.
|
|
1151
|
+
if (HIDDEN_RE.test(text) || stems.has(stem)) return;
|
|
1152
|
+
stems.add(stem);
|
|
1153
|
+
if (!literals.some(literal => text.includes(literal))) return;
|
|
1154
|
+
candidates.push({
|
|
1155
|
+
docPath,
|
|
1156
|
+
stem,
|
|
1157
|
+
...scanDocText(text),
|
|
1158
|
+
sourcePath: integrationSourceForDoc(docPath),
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
// A Core name a doc in the package replaces is the replacement's to answer.
|
|
1162
|
+
if (candidates.some(candidate => candidate.replaces === name)) return null;
|
|
1163
|
+
candidates.sort((a, b) => byCodeUnit(a.docPath, b.docPath));
|
|
1164
|
+
const found = candidates.find(
|
|
1165
|
+
candidate => otherExportName(candidate) === name,
|
|
1166
|
+
);
|
|
1167
|
+
return found ? {docPath: found.docPath, sourcePath: found.sourcePath} : null;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* The integration component a name finds, as discovery records it, and its
|
|
1172
|
+
* hold: the same-stem doc first, then a doc that records the name.
|
|
1173
|
+
*
|
|
1174
|
+
* @param {{components?: string}} integration
|
|
1175
|
+
* @param {string} name
|
|
1176
|
+
* @returns {{docPath: string, sourcePath: string|null, hold: 'file' | 'doc'} | null}
|
|
1177
|
+
*/
|
|
1178
|
+
export function findIntegrationComponent(integration, name) {
|
|
1179
|
+
const componentsDir = integration?.components;
|
|
1180
|
+
if (!componentsDir || !pathExists(componentsDir)) return null;
|
|
1181
|
+
const sameStem = findSameStemDoc(componentsDir, name);
|
|
1182
|
+
if (sameStem) {
|
|
1183
|
+
const exportName = otherExportName({
|
|
1184
|
+
docPath: sameStem,
|
|
1185
|
+
sourcePath: integrationSourceForDoc(sameStem),
|
|
1186
|
+
stem: name,
|
|
1187
|
+
names: readDocMeta(sameStem).names,
|
|
1188
|
+
});
|
|
1189
|
+
const renamed =
|
|
1190
|
+
exportName != null &&
|
|
1191
|
+
findSameStemDoc(componentsDir, exportName) == null &&
|
|
1192
|
+
findDocumentedComponent(componentsDir, exportName)?.docPath === sameStem;
|
|
1193
|
+
return {
|
|
1194
|
+
docPath: sameStem,
|
|
1195
|
+
sourcePath: integrationSourceForDoc(sameStem),
|
|
1196
|
+
hold: renamed ? 'doc' : 'file',
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
const found = findDocumentedComponent(componentsDir, name);
|
|
1200
|
+
return found && {...found, hold: 'doc'};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
961
1203
|
/**
|
|
962
1204
|
* Find an integration component's doc file by name within a loaded
|
|
963
1205
|
* integration's resolved `components` dir. Honors the same-stem convention
|
|
964
|
-
* (`{Name}.doc.{ts,mjs,js}`), preferring `.ts` → `.mjs` → `.js
|
|
1206
|
+
* (`{Name}.doc.{ts,mjs,js}`), preferring `.ts` → `.mjs` → `.js`, and finds a
|
|
1207
|
+
* doc that records the component under a differently named export of its file.
|
|
965
1208
|
*
|
|
966
1209
|
* @param {{components?: string}} integration
|
|
967
1210
|
* @param {string} name bare component name (no `XDS`/`Astryx` prefix)
|
|
968
1211
|
* @returns {string|null}
|
|
969
1212
|
*/
|
|
970
1213
|
export function findIntegrationComponentDoc(integration, name) {
|
|
971
|
-
|
|
972
|
-
|
|
1214
|
+
return findIntegrationComponent(integration, name)?.docPath ?? null;
|
|
1215
|
+
}
|
|
973
1216
|
|
|
1217
|
+
/**
|
|
1218
|
+
* The first `{Name}.doc.{ts,mjs,js}` under `componentsDir`, shallowest first.
|
|
1219
|
+
* @param {string} componentsDir
|
|
1220
|
+
* @param {string} name
|
|
1221
|
+
* @returns {string|null}
|
|
1222
|
+
*/
|
|
1223
|
+
function findSameStemDoc(componentsDir, name) {
|
|
974
1224
|
/**
|
|
975
1225
|
* @param {string} dirPath
|
|
976
1226
|
* @returns {string | null}
|
|
@@ -996,18 +1246,32 @@ export function findIntegrationComponentDoc(integration, name) {
|
|
|
996
1246
|
}
|
|
997
1247
|
|
|
998
1248
|
/**
|
|
999
|
-
* Find an integration component's swizzleable source file
|
|
1000
|
-
*
|
|
1001
|
-
* when the integration ships docs without source.
|
|
1249
|
+
* Find an integration component's swizzleable source file by name within a
|
|
1250
|
+
* loaded integration's resolved `components` dir: the same-stem `{Name}.tsx`
|
|
1251
|
+
* of its doc. Returns null when the integration ships docs without source.
|
|
1002
1252
|
*
|
|
1003
1253
|
* @param {{components?: string}} integration
|
|
1004
1254
|
* @param {string} name bare component name
|
|
1005
1255
|
* @returns {string|null}
|
|
1006
1256
|
*/
|
|
1007
1257
|
export function findIntegrationComponentSource(integration, name) {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1258
|
+
return findIntegrationComponent(integration, name)?.sourcePath ?? null;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* The owner rule: an owner that holds a name only because a doc records it
|
|
1263
|
+
* (`hold: 'doc'`) yields to Core and to every file-named owner; `--package`
|
|
1264
|
+
* still reaches it. Surfaces resolve a replacement first, then this rule, then
|
|
1265
|
+
* which docs load.
|
|
1266
|
+
*
|
|
1267
|
+
* @template {{hold?: string}} T
|
|
1268
|
+
* @param {T[]} owners each with its {@link NameHold}; an owner without one
|
|
1269
|
+
* counts as Core's or file-named
|
|
1270
|
+
* @returns {T[]}
|
|
1271
|
+
*/
|
|
1272
|
+
export function unscopedComponentOwners(owners) {
|
|
1273
|
+
const primary = owners.filter(owner => owner.hold !== 'doc');
|
|
1274
|
+
return primary.length > 0 ? primary : owners;
|
|
1011
1275
|
}
|
|
1012
1276
|
|
|
1013
1277
|
/**
|
|
@@ -161,6 +161,24 @@ export async function resolveComponentReplacements(
|
|
|
161
161
|
typeof name === 'string' ? byTarget.get(name) : undefined,
|
|
162
162
|
};
|
|
163
163
|
|
|
164
|
+
// A first read under file names alone settles the common case, no
|
|
165
|
+
// declaration anywhere, without reading the names docs document.
|
|
166
|
+
let declares = false;
|
|
167
|
+
for (const integration of loadedIntegrations) {
|
|
168
|
+
if (declares || !integration?.components || integration.__loadError)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const {components} = await discoverValidIntegrationComponents(
|
|
172
|
+
integration,
|
|
173
|
+
{documentedNames: false},
|
|
174
|
+
);
|
|
175
|
+
declares = components.some(record => record.replaces !== undefined);
|
|
176
|
+
} catch {
|
|
177
|
+
// Read again below, where a root that cannot be read is skipped.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!declares) return result;
|
|
181
|
+
|
|
164
182
|
/** @type {Declaration[]} */
|
|
165
183
|
const declarations = [];
|
|
166
184
|
/** @type {Array<{name: string, package: string}>} */
|
|
@@ -259,6 +259,26 @@ describe('resolveComponentReplacements', () => {
|
|
|
259
259
|
]);
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
+
it('applies a replacement under the export a doc names when its file is named otherwise', async () => {
|
|
263
|
+
// Nav.doc.mjs documents AcmeSideNav, the export of a Nav.tsx that exports
|
|
264
|
+
// nothing named Nav.
|
|
265
|
+
const acme = integrationWithDocSource(
|
|
266
|
+
'@acme/nav',
|
|
267
|
+
'Nav',
|
|
268
|
+
BASE_DOC.replace("name: 'AcmeNav'", "name: 'AcmeSideNav'"),
|
|
269
|
+
);
|
|
270
|
+
fs.writeFileSync(
|
|
271
|
+
path.join(acme.components, 'Nav.tsx'),
|
|
272
|
+
'export function AcmeSideNav() { return null; }\n',
|
|
273
|
+
);
|
|
274
|
+
const result = await resolveComponentReplacements(CORE_DIR, [acme]);
|
|
275
|
+
expect(result.findings).toEqual([]);
|
|
276
|
+
expect(result.forTarget('SideNav')).toMatchObject({
|
|
277
|
+
name: 'AcmeSideNav',
|
|
278
|
+
package: '@acme/nav',
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
262
282
|
it('lets a replacement keep the Core name it replaces', async () => {
|
|
263
283
|
const acme = integration(
|
|
264
284
|
'@acme/nav',
|
|
@@ -6,5 +6,26 @@
|
|
|
6
6
|
* @param {string} file
|
|
7
7
|
* @param {string} exportName
|
|
8
8
|
* @param {Set<string>} [seen]
|
|
9
|
+
* @returns {boolean}
|
|
9
10
|
*/
|
|
10
11
|
export function moduleExportsName(file: string, exportName: string, seen?: Set<string>): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The file that declares a module's runtime export, without executing it (the
|
|
14
|
+
* module, or where a local re-export leads); null when it cannot be proved.
|
|
15
|
+
* @param {string} file
|
|
16
|
+
* @param {string} exportName
|
|
17
|
+
* @param {Set<string>} [seen]
|
|
18
|
+
* @returns {string | null}
|
|
19
|
+
*/
|
|
20
|
+
export function moduleExportSource(file: string, exportName: string, seen?: Set<string>): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The object a module exports, as plain data, read without running it: the
|
|
23
|
+
* first of `exportNames` it exports, an object literal in place or a top-level
|
|
24
|
+
* binding (under `satisfies` or `as` too). Only literal values are kept;
|
|
25
|
+
* anything computed, or overridable by a later spread, is left out.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} file
|
|
28
|
+
* @param {readonly string[]} exportNames `default` or named exports, in order
|
|
29
|
+
* @returns {Record<string, unknown> | null}
|
|
30
|
+
*/
|
|
31
|
+
export function staticExportObject(file: string, exportNames: readonly string[]): Record<string, unknown> | null;
|