@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.
@@ -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 {group: null, hiddenComponents: new Set(), hidden: false};
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(integration) {
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
- /** @param {string} dirPath */
877
- function scanDir(dirPath) {
878
- const entries = listDirEntries(dirPath);
879
- for (const entry of entries) {
880
- if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
881
- const fullPath = path.join(dirPath, entry.name);
882
- if (entry.isDirectory()) {
883
- scanDir(fullPath);
884
- continue;
885
- }
886
- const suffix = INTEGRATION_DOC_SUFFIXES.find(s => entry.name.endsWith(s));
887
- if (!suffix) continue;
888
- const name = entry.name.slice(0, -suffix.length);
889
- const {group, hidden} = readDocMeta(fullPath);
890
- if (hidden) continue;
891
- // First doc wins per name (precedence matches INTEGRATION_DOC_SUFFIXES).
892
- if (byName.has(name)) continue;
893
- byName.set(name, {
894
- name,
895
- package: integration.name,
896
- docPath: fullPath,
897
- sourcePath: integrationSourceForDoc(fullPath),
898
- issuesUrl: integration.issuesUrl,
899
- group: group ?? null,
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
- scanDir(componentsDir);
905
- return [...byName.values()];
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: record.name,
951
- message: `Component "${record.name}" has invalid metadata: ${
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
- const componentsDir = integration?.components;
972
- if (!componentsDir || !pathExists(componentsDir)) return null;
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 (`{Name}.tsx`) by
1000
- * name within a loaded integration's resolved `components` dir. Returns null
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
- const docPath = findIntegrationComponentDoc(integration, name);
1009
- if (!docPath) return null;
1010
- return integrationSourceForDoc(docPath);
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;