@astryxdesign/cli 0.7.2-canary.fbc03f7 → 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/integration-export-name.test.mjs +644 -0
- 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 +51 -97
- 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 +100 -13
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/theme/build/build.family.test.mjs +54 -6
- package/api/theme/build/build.mjs +114 -17
- package/api/theme/themeBuild.doc.mjs +4 -4
- 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/theme.template.ts +1 -1
- package/authoring/doctypes/component/component.doc.mjs +2 -2
- 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 +155 -21
- 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/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/component-discovery.d.mts +178 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +659 -62
- 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/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 +6 -0
- 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/package.json +9 -9
|
@@ -7,10 +7,18 @@
|
|
|
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 {
|
|
16
|
+
HAS_COMPUTED,
|
|
17
|
+
moduleExportSource,
|
|
18
|
+
moduleExportsName,
|
|
19
|
+
staticExportObject,
|
|
20
|
+
} from './module-exports.mjs';
|
|
21
|
+
import {inspectPackageExport} from './package-exports.mjs';
|
|
14
22
|
|
|
15
23
|
const require = createRequire(import.meta.url);
|
|
16
24
|
|
|
@@ -82,12 +90,40 @@ function componentNameFromFile(fileName) {
|
|
|
82
90
|
const GROUP_RE = /(?:^|\n) {0,2}group:\s*['"]([^'"]+)['"]/;
|
|
83
91
|
const HIDDEN_COMPONENTS_RE = /(?:^|\n) {0,2}hiddenComponents:\s*\[([^\]]*)\]/;
|
|
84
92
|
const HIDDEN_RE = /(?:^|\n) {0,2}hidden:\s*true/;
|
|
93
|
+
// Every top-level `name: '...'` (or `"name": "..."`) in the file: a quick check
|
|
94
|
+
// that a doc is named after its file before anything parses it.
|
|
95
|
+
const NAME_FIELD_RE = /(?:^|\n) {0,2}['"]?name['"]?\s*:\s*['"]([^'"\n]+)['"]/g;
|
|
96
|
+
// A `components` field anywhere in the file, written out or as a shorthand: the
|
|
97
|
+
// quick check for a family doc, before anything parses it. A false match only
|
|
98
|
+
// costs the parse.
|
|
99
|
+
const MEMBERS_FIELD_RE = /\bcomponents\b/;
|
|
100
|
+
// A top-level `replaces: '...'`: the Core name a doc claims for its package.
|
|
101
|
+
const REPLACES_FIELD_RE =
|
|
102
|
+
/(?:^|\n) {0,2}['"]?replaces['"]?\s*:\s*['"]([^'"\n]+)['"]/;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What a quick scan of a doc's text finds before anything parses it: the
|
|
106
|
+
* top-level `name` values it writes (none when a spread could set the name),
|
|
107
|
+
* whether it may list family members, and the Core name it replaces.
|
|
108
|
+
* @param {string} text
|
|
109
|
+
* @returns {{names: string[], hasMembers: boolean, replaces: string | null}}
|
|
110
|
+
*/
|
|
111
|
+
function scanDocText(text) {
|
|
112
|
+
return {
|
|
113
|
+
names: text.includes('...')
|
|
114
|
+
? []
|
|
115
|
+
: [...text.matchAll(NAME_FIELD_RE)].map(match => match[1]),
|
|
116
|
+
hasMembers: MEMBERS_FIELD_RE.test(text),
|
|
117
|
+
replaces: REPLACES_FIELD_RE.exec(text)?.[1] ?? null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
85
120
|
|
|
86
121
|
/**
|
|
87
122
|
* Read the `group`, `hiddenComponents`, and `hidden` fields from a
|
|
88
|
-
* component's .doc.mjs file (synchronous)
|
|
123
|
+
* component's .doc.mjs file (synchronous), with what {@link scanDocText}
|
|
124
|
+
* finds in it.
|
|
89
125
|
* @param {string} docPath
|
|
90
|
-
* @returns {{group: string | null, hiddenComponents: Set<string>, hidden: boolean}}
|
|
126
|
+
* @returns {{group: string | null, hiddenComponents: Set<string>, hidden: boolean, names: string[], hasMembers: boolean, replaces: string | null}}
|
|
91
127
|
*/
|
|
92
128
|
function readDocMeta(docPath) {
|
|
93
129
|
try {
|
|
@@ -106,9 +142,17 @@ function readDocMeta(docPath) {
|
|
|
106
142
|
group: groupMatch ? groupMatch[1] : null,
|
|
107
143
|
hiddenComponents: hiddenSet,
|
|
108
144
|
hidden,
|
|
145
|
+
...scanDocText(content),
|
|
109
146
|
};
|
|
110
147
|
} catch {
|
|
111
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
group: null,
|
|
150
|
+
hiddenComponents: new Set(),
|
|
151
|
+
hidden: false,
|
|
152
|
+
names: [],
|
|
153
|
+
hasMembers: false,
|
|
154
|
+
replaces: null,
|
|
155
|
+
};
|
|
112
156
|
}
|
|
113
157
|
}
|
|
114
158
|
|
|
@@ -148,9 +192,12 @@ function docMemberNames(source) {
|
|
|
148
192
|
let ast;
|
|
149
193
|
try {
|
|
150
194
|
babelParser ??= require('@babel/parser');
|
|
151
|
-
ast = /** @type {typeof import('@babel/parser')} */ (babelParser).parse(
|
|
152
|
-
|
|
153
|
-
|
|
195
|
+
ast = /** @type {typeof import('@babel/parser')} */ (babelParser).parse(
|
|
196
|
+
source,
|
|
197
|
+
{
|
|
198
|
+
sourceType: 'module',
|
|
199
|
+
},
|
|
200
|
+
);
|
|
154
201
|
} catch {
|
|
155
202
|
return [];
|
|
156
203
|
}
|
|
@@ -165,12 +212,16 @@ function docMemberNames(source) {
|
|
|
165
212
|
for (const declarator of declaration.declarations) {
|
|
166
213
|
if (declarator.id.type !== 'Identifier' || !declarator.init) continue;
|
|
167
214
|
constants.set(declarator.id.name, declarator.init);
|
|
168
|
-
if (
|
|
215
|
+
if (
|
|
216
|
+
node.type === 'ExportNamedDeclaration' &&
|
|
217
|
+
declarator.id.name === 'docs'
|
|
218
|
+
) {
|
|
169
219
|
docsExport = declarator.init;
|
|
170
220
|
}
|
|
171
221
|
}
|
|
172
222
|
}
|
|
173
|
-
if (node.type === 'ExportDefaultDeclaration')
|
|
223
|
+
if (node.type === 'ExportDefaultDeclaration')
|
|
224
|
+
defaultExport = node.declaration;
|
|
174
225
|
}
|
|
175
226
|
let doc = defaultExport ?? docsExport;
|
|
176
227
|
if (doc?.type === 'Identifier') doc = constants.get(doc.name);
|
|
@@ -388,7 +439,9 @@ export function discoverComponents(coreDir) {
|
|
|
388
439
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
389
440
|
const name = entry.name;
|
|
390
441
|
if (componentGroups.has(name) || name.endsWith('Context')) continue;
|
|
391
|
-
if (
|
|
442
|
+
if (
|
|
443
|
+
listDir(path.join(srcDir, name)).some(file => file.endsWith('.doc.mjs'))
|
|
444
|
+
) {
|
|
392
445
|
continue;
|
|
393
446
|
}
|
|
394
447
|
if (!hasOwnFolder(srcDir, name)) continue;
|
|
@@ -843,6 +896,345 @@ export function findExternalComponentDoc(docsDir, name) {
|
|
|
843
896
|
// commands — and the future integration-component swizzle — can disambiguate
|
|
844
897
|
// by package and route source/issues correctly.
|
|
845
898
|
|
|
899
|
+
// A doc's `name` can name another export of its file, and a family doc's full
|
|
900
|
+
// entries are components under their own names; every other doc reads as before.
|
|
901
|
+
const COMPONENT_NAME_RE = /^[A-Z][\w$]*$/;
|
|
902
|
+
|
|
903
|
+
/** @type {Map<string, {stamp: string, value: Record<string, any> | null}>} doc reads, reused while the doc file is unchanged */
|
|
904
|
+
const docReads = new Map();
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* A file's identity for a cache: ctime and inode change on a rewrite even when
|
|
908
|
+
* npm stamps the same size and mtime.
|
|
909
|
+
* @param {string} file
|
|
910
|
+
*/
|
|
911
|
+
function fileStamp(file) {
|
|
912
|
+
try {
|
|
913
|
+
const {size, mtimeMs, ctimeMs, ino} = fs.statSync(file);
|
|
914
|
+
return `${size}:${mtimeMs}:${ctimeMs}:${ino}`;
|
|
915
|
+
} catch {
|
|
916
|
+
return 'missing';
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** UTF-16 code-unit order, the same in every locale. @param {string} a @param {string} b */
|
|
921
|
+
function byCodeUnit(a, b) {
|
|
922
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* A component doc's exported object as plain data, read without running it.
|
|
927
|
+
* @param {string} docPath
|
|
928
|
+
* @returns {Record<string, any> | null}
|
|
929
|
+
*/
|
|
930
|
+
function docObject(docPath) {
|
|
931
|
+
const stamp = fileStamp(docPath);
|
|
932
|
+
const cached = docReads.get(docPath);
|
|
933
|
+
if (cached?.stamp === stamp) return cached.value;
|
|
934
|
+
const value = staticExportObject(docPath, DOC_EXPORTS.components);
|
|
935
|
+
docReads.set(docPath, {stamp, value});
|
|
936
|
+
return value;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** @param {string} docPath */
|
|
940
|
+
export function docStem(docPath) {
|
|
941
|
+
return path.basename(docPath).replace(/\.doc\.(ts|mjs|js)$/, '');
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* The export a component doc names when its same-stem source is not named
|
|
946
|
+
* after an export, or null to keep the stem.
|
|
947
|
+
* @param {{docPath: string, sourcePath: string|null, stem: string, names: string[]}} doc
|
|
948
|
+
* `names`: top-level names a quick scan found (none: the doc is read)
|
|
949
|
+
* @returns {string | null}
|
|
950
|
+
*/
|
|
951
|
+
function otherExportName({docPath, sourcePath, stem, names}) {
|
|
952
|
+
if (sourcePath == null) return null;
|
|
953
|
+
if (names.length > 0 && names.every(name => name === stem)) return null;
|
|
954
|
+
const name = docObject(docPath)?.name;
|
|
955
|
+
return typeof name === 'string' &&
|
|
956
|
+
name !== stem &&
|
|
957
|
+
COMPONENT_NAME_RE.test(name) &&
|
|
958
|
+
moduleExportsName(sourcePath, name) &&
|
|
959
|
+
!moduleExportsName(sourcePath, stem)
|
|
960
|
+
? name
|
|
961
|
+
: null;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* The file Node loads for a package without `exports`: its `main`, else its
|
|
966
|
+
* `index` file.
|
|
967
|
+
* @param {string} packageDir
|
|
968
|
+
* @returns {string | null}
|
|
969
|
+
*/
|
|
970
|
+
function packageMain(packageDir) {
|
|
971
|
+
let main;
|
|
972
|
+
try {
|
|
973
|
+
main = JSON.parse(
|
|
974
|
+
fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'),
|
|
975
|
+
).main;
|
|
976
|
+
} catch {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
const entry =
|
|
980
|
+
typeof main === 'string' && main !== ''
|
|
981
|
+
? `./${path.posix.normalize(main).replace(/^\.\//, '')}`
|
|
982
|
+
: './index';
|
|
983
|
+
return inspectPackageExport({}, packageDir, entry).target;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* The local module a doc's import names: the doc's own `import`, else the
|
|
988
|
+
* subpath its directory publishes, read through the package's `exports` (its
|
|
989
|
+
* `main` when it has none). Undefined when the package directory is unknown;
|
|
990
|
+
* null when the import names another package or resolves to no file.
|
|
991
|
+
*
|
|
992
|
+
* @param {{name?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
993
|
+
* @param {string} docPath
|
|
994
|
+
* @param {Record<string, any> | null} doc
|
|
995
|
+
* @returns {string | null | undefined}
|
|
996
|
+
*/
|
|
997
|
+
function importedModule(integration, docPath, doc) {
|
|
998
|
+
const packageName = integration?.name;
|
|
999
|
+
const packageDir = integration?.__packageDir;
|
|
1000
|
+
if (!packageName || !packageDir) return undefined;
|
|
1001
|
+
const exportsMap =
|
|
1002
|
+
integration.__packageExports === undefined
|
|
1003
|
+
? readPackageExports(packageDir)
|
|
1004
|
+
: integration.__packageExports;
|
|
1005
|
+
const specifier = resolveIntegrationImportPath(
|
|
1006
|
+
{exportsMap, packageDir, docPath, packageName},
|
|
1007
|
+
docStem(docPath),
|
|
1008
|
+
typeof doc?.import === 'string' ? doc.import : null,
|
|
1009
|
+
);
|
|
1010
|
+
if (specifier !== packageName && !specifier.startsWith(`${packageName}/`)) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
if (specifier === packageName && exportsMap == null) {
|
|
1014
|
+
return packageMain(packageDir);
|
|
1015
|
+
}
|
|
1016
|
+
const subpath =
|
|
1017
|
+
specifier === packageName
|
|
1018
|
+
? '.'
|
|
1019
|
+
: `./${specifier.slice(packageName.length + 1)}`;
|
|
1020
|
+
return inspectPackageExport(
|
|
1021
|
+
{name: packageName, exports: exportsMap ?? undefined},
|
|
1022
|
+
packageDir,
|
|
1023
|
+
subpath,
|
|
1024
|
+
).target;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Whether `file` sits in `folder` or below it.
|
|
1029
|
+
* @param {string} folder
|
|
1030
|
+
* @param {string} file
|
|
1031
|
+
*/
|
|
1032
|
+
function isWithin(folder, file) {
|
|
1033
|
+
const relative = path.relative(folder, file);
|
|
1034
|
+
return (
|
|
1035
|
+
relative !== '' &&
|
|
1036
|
+
!path.isAbsolute(relative) &&
|
|
1037
|
+
relative.split(path.sep)[0] !== '..'
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* The `.tsx` file in `folder`, or below it, that declares `name`, taking files
|
|
1043
|
+
* in path order: the source of a member whose import never reaches a `.tsx`
|
|
1044
|
+
* file, as when the import names built output.
|
|
1045
|
+
*
|
|
1046
|
+
* @param {string} folder
|
|
1047
|
+
* @param {string} name
|
|
1048
|
+
* @returns {string | null}
|
|
1049
|
+
*/
|
|
1050
|
+
export function folderSourceFor(folder, name) {
|
|
1051
|
+
/** @type {string[]} */
|
|
1052
|
+
const files = [];
|
|
1053
|
+
/** @param {string} dirPath */
|
|
1054
|
+
const scan = dirPath => {
|
|
1055
|
+
for (const entry of listDirEntries(dirPath)) {
|
|
1056
|
+
if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
1057
|
+
if (entry.name.startsWith('.')) continue;
|
|
1058
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
1059
|
+
if (entry.isDirectory()) scan(fullPath);
|
|
1060
|
+
else if (
|
|
1061
|
+
entry.name.endsWith('.tsx') &&
|
|
1062
|
+
!/\.(?:test|spec|stories)\.tsx$/.test(entry.name)
|
|
1063
|
+
) {
|
|
1064
|
+
files.push(fullPath);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
scan(folder);
|
|
1069
|
+
for (const file of files.sort(byCodeUnit)) {
|
|
1070
|
+
const declared = moduleExportSource(file, name);
|
|
1071
|
+
if (
|
|
1072
|
+
declared != null &&
|
|
1073
|
+
declared.endsWith('.tsx') &&
|
|
1074
|
+
isWithin(folder, declared)
|
|
1075
|
+
) {
|
|
1076
|
+
return declared;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* The full entries a family doc lists in `components` that the module its
|
|
1084
|
+
* import names exports, each with its source. An entry is full when it says
|
|
1085
|
+
* more than its `name`; the name must be static PascalCase, not hidden, and
|
|
1086
|
+
* neither the doc's stem nor the export the doc is recorded under. A member's
|
|
1087
|
+
* source is the `.tsx` file in the family doc's folder, or below it, that
|
|
1088
|
+
* declares it: the one the family's own source or the imported module reaches,
|
|
1089
|
+
* else the one in that folder that declares the name, as when the import names
|
|
1090
|
+
* built output. A member that only a bundle or a file outside the folder
|
|
1091
|
+
* declares has no source.
|
|
1092
|
+
*
|
|
1093
|
+
* @param {{name?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
1094
|
+
* @param {{docPath: string, sourcePath: string|null}} record the doc's own
|
|
1095
|
+
* @param {string | null | undefined} ownName the export the doc is recorded
|
|
1096
|
+
* under, when it names one
|
|
1097
|
+
* @param {string} [only] look for this member alone
|
|
1098
|
+
* @returns {Array<{name: string, sourcePath: string | null}>}
|
|
1099
|
+
*/
|
|
1100
|
+
function familyMembers(integration, record, ownName, only) {
|
|
1101
|
+
const familySource = record.sourcePath;
|
|
1102
|
+
const doc = docObject(record.docPath);
|
|
1103
|
+
const entries = Array.isArray(doc?.components) ? doc.components : [];
|
|
1104
|
+
const hidden = new Set(
|
|
1105
|
+
Array.isArray(doc?.hiddenComponents) ? doc.hiddenComponents : [],
|
|
1106
|
+
);
|
|
1107
|
+
const stem = docStem(record.docPath);
|
|
1108
|
+
/** @type {string[]} */
|
|
1109
|
+
const names = [];
|
|
1110
|
+
for (const entry of entries) {
|
|
1111
|
+
if (entry == null || typeof entry !== 'object') continue;
|
|
1112
|
+
const name = entry.name;
|
|
1113
|
+
const full = Object.keys(entry).length > 1 || entry[HAS_COMPUTED] === true;
|
|
1114
|
+
if (typeof name !== 'string' || !full) continue;
|
|
1115
|
+
if (!COMPONENT_NAME_RE.test(name) || hidden.has(name)) continue;
|
|
1116
|
+
if (name === stem || name === ownName) continue;
|
|
1117
|
+
if (only != null && name !== only) continue;
|
|
1118
|
+
names.push(name);
|
|
1119
|
+
}
|
|
1120
|
+
if (names.length === 0) return [];
|
|
1121
|
+
const imported = importedModule(integration, record.docPath, doc);
|
|
1122
|
+
const module = imported === undefined ? familySource : imported;
|
|
1123
|
+
if (module == null) return [];
|
|
1124
|
+
const folder = path.dirname(record.docPath);
|
|
1125
|
+
/** @param {string | null} file */
|
|
1126
|
+
const inFolder = file =>
|
|
1127
|
+
file != null && file.endsWith('.tsx') && isWithin(folder, file);
|
|
1128
|
+
return names.flatMap(name => {
|
|
1129
|
+
const exported = moduleExportSource(module, name);
|
|
1130
|
+
if (exported == null) return [];
|
|
1131
|
+
// A member's source is a `.tsx` file in the family's folder, as a doc's own
|
|
1132
|
+
// is: never a bundle, even one under a family at the package root.
|
|
1133
|
+
const reached = [
|
|
1134
|
+
familySource == null ? null : moduleExportSource(familySource, name),
|
|
1135
|
+
exported,
|
|
1136
|
+
].find(inFolder);
|
|
1137
|
+
return [{name, sourcePath: reached ?? folderSourceFor(folder, name)}];
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Each family doc under an integration's components root that names an
|
|
1143
|
+
* import, with the file the import resolves to: null when it resolves to none,
|
|
1144
|
+
* such as build output that does not exist yet.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {{name: string, components?: string, issuesUrl?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
1147
|
+
* @returns {Array<{docPath: string, module: string | null}>}
|
|
1148
|
+
*/
|
|
1149
|
+
export function familyImports(integration) {
|
|
1150
|
+
return discoverIntegrationComponents(integration).flatMap(record => {
|
|
1151
|
+
if (record.docPath == null || !isOwnRecord(record)) return [];
|
|
1152
|
+
const doc = docObject(record.docPath);
|
|
1153
|
+
if (!Array.isArray(doc?.components) || doc.components.length === 0) {
|
|
1154
|
+
return [];
|
|
1155
|
+
}
|
|
1156
|
+
const module = importedModule(integration, record.docPath, doc);
|
|
1157
|
+
return module === undefined ? [] : [{docPath: record.docPath, module}];
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Whether `record` is its doc's own component (under the doc's stem or the
|
|
1163
|
+
* export its `name` documents) rather than a member the doc lists.
|
|
1164
|
+
*
|
|
1165
|
+
* @param {{name: string, docPath: string|null}} record
|
|
1166
|
+
* @returns {boolean}
|
|
1167
|
+
*/
|
|
1168
|
+
function isOwnRecord(record) {
|
|
1169
|
+
if (record.docPath == null) return true;
|
|
1170
|
+
const stem = docStem(record.docPath);
|
|
1171
|
+
if (stem === record.name) return true;
|
|
1172
|
+
return (
|
|
1173
|
+
otherExportName({
|
|
1174
|
+
docPath: record.docPath,
|
|
1175
|
+
sourcePath: integrationSourceForDoc(record.docPath),
|
|
1176
|
+
stem,
|
|
1177
|
+
names: [],
|
|
1178
|
+
}) === record.name
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Whether a discovered integration record is a member its family doc lists,
|
|
1184
|
+
* decided from the files, not from whether the doc loads.
|
|
1185
|
+
*
|
|
1186
|
+
* @param {{name: string, docPath: string|null}} record
|
|
1187
|
+
* @returns {boolean}
|
|
1188
|
+
*/
|
|
1189
|
+
export function isIntegrationMember(record) {
|
|
1190
|
+
return !isOwnRecord(record);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* The entry a family doc gives a member that discovery lists under its own
|
|
1195
|
+
* name, or null when the record is the doc's own component or the loaded doc
|
|
1196
|
+
* has no entry for it.
|
|
1197
|
+
*
|
|
1198
|
+
* @param {any} doc the doc loaded from `record.docPath`
|
|
1199
|
+
* @param {{name: string, docPath: string|null}} record
|
|
1200
|
+
* @returns {any}
|
|
1201
|
+
*/
|
|
1202
|
+
export function integrationMemberEntry(doc, record) {
|
|
1203
|
+
if (doc == null || typeof doc !== 'object' || isOwnRecord(record)) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
if (!Array.isArray(doc.components)) return null;
|
|
1207
|
+
return (
|
|
1208
|
+
doc.components.find(
|
|
1209
|
+
(/** @type {any} */ entry) => entry?.name === record.name,
|
|
1210
|
+
) ?? null
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Visit every component doc under an integration's components root, hidden
|
|
1216
|
+
* ones included, in discovery order, with its stem.
|
|
1217
|
+
* @param {string | undefined} componentsDir
|
|
1218
|
+
* @param {(docPath: string, stem: string) => void} visit
|
|
1219
|
+
*/
|
|
1220
|
+
export function forEachIntegrationDoc(componentsDir, visit) {
|
|
1221
|
+
if (!componentsDir || !pathExists(componentsDir)) return;
|
|
1222
|
+
/** @param {string} dirPath */
|
|
1223
|
+
function scanDir(dirPath) {
|
|
1224
|
+
for (const entry of listDirEntries(dirPath)) {
|
|
1225
|
+
if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
1226
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
1227
|
+
if (entry.isDirectory()) {
|
|
1228
|
+
scanDir(fullPath);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
const suffix = INTEGRATION_DOC_SUFFIXES.find(s => entry.name.endsWith(s));
|
|
1232
|
+
if (suffix) visit(fullPath, entry.name.slice(0, -suffix.length));
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
scanDir(componentsDir);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
846
1238
|
/**
|
|
847
1239
|
* Given an integration component doc path, return the sibling component source
|
|
848
1240
|
* (`{Name}.tsx`) if one exists, else null.
|
|
@@ -850,7 +1242,7 @@ export function findExternalComponentDoc(docsDir, name) {
|
|
|
850
1242
|
* @param {string} docPath absolute path to a `{Name}.doc.{ts,mjs,js}` file
|
|
851
1243
|
* @returns {string|null}
|
|
852
1244
|
*/
|
|
853
|
-
function integrationSourceForDoc(docPath) {
|
|
1245
|
+
export function integrationSourceForDoc(docPath) {
|
|
854
1246
|
const dir = path.dirname(docPath);
|
|
855
1247
|
const base = path.basename(docPath).replace(/\.doc\.(ts|mjs|js)$/, '');
|
|
856
1248
|
const candidate = path.join(dir, `${base}.tsx`);
|
|
@@ -860,49 +1252,85 @@ function integrationSourceForDoc(docPath) {
|
|
|
860
1252
|
/**
|
|
861
1253
|
* Discover ownership records for the components contributed by a single loaded
|
|
862
1254
|
* integration. Scans the integration's resolved `components` dir for same-stem
|
|
863
|
-
* doc files and records each with owner package + issuesUrl + sourcePath.
|
|
1255
|
+
* doc files and records each with owner package + issuesUrl + sourcePath. A doc
|
|
1256
|
+
* that names a differently named export of its file is recorded under that
|
|
1257
|
+
* export, and each family member it lists, under the member's name, unless the
|
|
1258
|
+
* name is taken: every doc's file name (a hidden doc's too), a Core name a doc
|
|
1259
|
+
* in the package replaces, and a name an earlier doc records.
|
|
864
1260
|
*
|
|
865
|
-
* @param {{name: string, components?: string, issuesUrl?: string}} integration
|
|
1261
|
+
* @param {{name: string, components?: string, issuesUrl?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
866
1262
|
* a single entry from `loadedIntegrations` (lib/integrations.mjs)
|
|
1263
|
+
* @param {{documentedNames?: boolean}} [options] `documentedNames: false`
|
|
1264
|
+
* records each doc under its file name alone, without reading the names
|
|
1265
|
+
* docs document: for a caller that needs only what each doc says
|
|
867
1266
|
* @returns {Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>}
|
|
868
1267
|
*/
|
|
869
|
-
export function discoverIntegrationComponents(
|
|
1268
|
+
export function discoverIntegrationComponents(
|
|
1269
|
+
integration,
|
|
1270
|
+
{documentedNames = true} = {},
|
|
1271
|
+
) {
|
|
870
1272
|
const componentsDir = integration?.components;
|
|
871
1273
|
if (!componentsDir || !pathExists(componentsDir)) return [];
|
|
872
1274
|
|
|
873
1275
|
/** @type {Map<string, {name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>} */
|
|
874
1276
|
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
|
-
|
|
1277
|
+
/** @type {Map<string, {names: string[], hasMembers: boolean, replaces: string | null}>} stem → quick scan */
|
|
1278
|
+
const scans = new Map();
|
|
1279
|
+
/** @type {Set<string>} names no doc can record under another name */
|
|
1280
|
+
const taken = new Set();
|
|
1281
|
+
|
|
1282
|
+
forEachIntegrationDoc(componentsDir, (fullPath, name) => {
|
|
1283
|
+
taken.add(name);
|
|
1284
|
+
const {group, hidden, names, hasMembers, replaces} = readDocMeta(fullPath);
|
|
1285
|
+
if (hidden) return;
|
|
1286
|
+
// First doc wins per name (precedence matches INTEGRATION_DOC_SUFFIXES).
|
|
1287
|
+
if (byName.has(name)) return;
|
|
1288
|
+
byName.set(name, {
|
|
1289
|
+
name,
|
|
1290
|
+
package: integration.name,
|
|
1291
|
+
docPath: fullPath,
|
|
1292
|
+
sourcePath: integrationSourceForDoc(fullPath),
|
|
1293
|
+
issuesUrl: integration.issuesUrl,
|
|
1294
|
+
group: group ?? null,
|
|
1295
|
+
});
|
|
1296
|
+
scans.set(name, {names, hasMembers, replaces});
|
|
1297
|
+
});
|
|
1298
|
+
if (!documentedNames) return [...byName.values()];
|
|
1299
|
+
|
|
1300
|
+
// A doc that names a differently named export of its file is listed under
|
|
1301
|
+
// that export, and each member a family doc lists under the member's name,
|
|
1302
|
+
// unless the name is taken.
|
|
1303
|
+
for (const {replaces} of scans.values()) if (replaces) taken.add(replaces);
|
|
1304
|
+
const records = [...byName.values()];
|
|
1305
|
+
const byPath = [...records].sort((a, b) => byCodeUnit(a.docPath, b.docPath));
|
|
1306
|
+
/** @type {Map<string, string>} stem → export name */
|
|
1307
|
+
const exportNames = new Map();
|
|
1308
|
+
for (const record of byPath) {
|
|
1309
|
+
const exportName = otherExportName({
|
|
1310
|
+
...record,
|
|
1311
|
+
stem: record.name,
|
|
1312
|
+
names: scans.get(record.name)?.names ?? [],
|
|
1313
|
+
});
|
|
1314
|
+
if (exportName == null || taken.has(exportName)) continue;
|
|
1315
|
+
taken.add(exportName);
|
|
1316
|
+
exportNames.set(record.name, exportName);
|
|
1317
|
+
}
|
|
1318
|
+
/** @type {typeof records} */
|
|
1319
|
+
const members = [];
|
|
1320
|
+
for (const record of byPath) {
|
|
1321
|
+
if (!scans.get(record.name)?.hasMembers) continue;
|
|
1322
|
+
const ownName = exportNames.get(record.name);
|
|
1323
|
+
for (const member of familyMembers(integration, record, ownName)) {
|
|
1324
|
+
if (taken.has(member.name)) continue;
|
|
1325
|
+
taken.add(member.name);
|
|
1326
|
+
members.push({...record, ...member});
|
|
901
1327
|
}
|
|
902
1328
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1329
|
+
const named = records.map(record => {
|
|
1330
|
+
const name = exportNames.get(record.name);
|
|
1331
|
+
return name ? {...record, name} : record;
|
|
1332
|
+
});
|
|
1333
|
+
return [...named, ...members];
|
|
906
1334
|
}
|
|
907
1335
|
|
|
908
1336
|
/**
|
|
@@ -910,24 +1338,33 @@ export function discoverIntegrationComponents(integration) {
|
|
|
910
1338
|
* Invalid metadata or a missing same-stem source removes only that component;
|
|
911
1339
|
* callers can report the returned errors while retaining valid siblings. A
|
|
912
1340
|
* component whose doc sets `replaces` carries the value as written; the
|
|
913
|
-
* component replacement resolver decides what it means.
|
|
1341
|
+
* component replacement resolver decides what it means. A family member is
|
|
1342
|
+
* read through its family's doc: it never carries the family's `replaces`, and
|
|
1343
|
+
* a family doc that fails to load is reported once. A doc that fails to load
|
|
1344
|
+
* is reported under its file name: what it says cannot be trusted.
|
|
914
1345
|
*
|
|
915
1346
|
* @param {{name: string, components?: string, issuesUrl?: string}} integration
|
|
1347
|
+
* @param {{documentedNames?: boolean}} [options] as for
|
|
1348
|
+
* {@link discoverIntegrationComponents}
|
|
916
1349
|
* @returns {Promise<{
|
|
917
|
-
* components: Array<{name: string, package: string, docPath: string, sourcePath: string, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>,
|
|
1350
|
+
* components: Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>,
|
|
918
1351
|
* discovered: Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>,
|
|
919
1352
|
* errors: Array<{name: string, message: string}>,
|
|
920
1353
|
* }>}
|
|
921
1354
|
*/
|
|
922
|
-
export async function discoverValidIntegrationComponents(integration) {
|
|
923
|
-
const discovered = discoverIntegrationComponents(integration);
|
|
924
|
-
/** @type {Array<{name: string, package: string, docPath: string, sourcePath: string, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>} */
|
|
1355
|
+
export async function discoverValidIntegrationComponents(integration, options) {
|
|
1356
|
+
const discovered = discoverIntegrationComponents(integration, options);
|
|
1357
|
+
/** @type {Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>} */
|
|
925
1358
|
const components = [];
|
|
926
1359
|
/** @type {Array<{name: string, message: string}>} */
|
|
927
1360
|
const errors = [];
|
|
1361
|
+
/** @type {Set<string>} docs that failed to load */
|
|
1362
|
+
const failedDocs = new Set();
|
|
928
1363
|
|
|
929
1364
|
for (const record of discovered) {
|
|
930
|
-
|
|
1365
|
+
// A doc's own component needs its same-stem source. A member has none when
|
|
1366
|
+
// only a bundle or a file outside its family's folder declares it.
|
|
1367
|
+
if (record.sourcePath == null && isOwnRecord(record)) {
|
|
931
1368
|
errors.push({
|
|
932
1369
|
name: record.name,
|
|
933
1370
|
message: `Component "${record.name}" is missing its same-stem source file ${record.name}.tsx.`,
|
|
@@ -937,18 +1374,24 @@ export async function discoverValidIntegrationComponents(integration) {
|
|
|
937
1374
|
try {
|
|
938
1375
|
const doc = await loadComponentDoc(record.docPath);
|
|
939
1376
|
const valid =
|
|
940
|
-
/** @type {{name: string, package: string, docPath: string, sourcePath: string, issuesUrl: string|undefined, group: string|null}} */ (
|
|
1377
|
+
/** @type {{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}} */ (
|
|
941
1378
|
record
|
|
942
1379
|
);
|
|
943
1380
|
components.push(
|
|
944
|
-
|
|
1381
|
+
isOwnRecord(record) &&
|
|
1382
|
+
doc != null &&
|
|
1383
|
+
typeof doc === 'object' &&
|
|
1384
|
+
doc.replaces !== undefined
|
|
945
1385
|
? {...valid, replaces: doc.replaces}
|
|
946
1386
|
: valid,
|
|
947
1387
|
);
|
|
948
1388
|
} catch (err) {
|
|
1389
|
+
if (failedDocs.has(record.docPath)) continue;
|
|
1390
|
+
failedDocs.add(record.docPath);
|
|
1391
|
+
const name = docStem(record.docPath);
|
|
949
1392
|
errors.push({
|
|
950
|
-
name
|
|
951
|
-
message: `Component "${
|
|
1393
|
+
name,
|
|
1394
|
+
message: `Component "${name}" has invalid metadata: ${
|
|
952
1395
|
err instanceof Error ? err.message : String(err)
|
|
953
1396
|
}`,
|
|
954
1397
|
});
|
|
@@ -958,19 +1401,158 @@ export async function discoverValidIntegrationComponents(integration) {
|
|
|
958
1401
|
return {components, discovered, errors};
|
|
959
1402
|
}
|
|
960
1403
|
|
|
1404
|
+
/**
|
|
1405
|
+
* How an owner holds a component name, which the owner rule
|
|
1406
|
+
* ({@link unscopedComponentOwners}) reads: `core` when Core documents it;
|
|
1407
|
+
* `file` when an integration doc named after its file is recorded under it;
|
|
1408
|
+
* `doc` when only a doc records it: an export name, a member, or the file name
|
|
1409
|
+
* of a doc recorded under another export.
|
|
1410
|
+
* @typedef {'core' | 'file' | 'doc'} NameHold
|
|
1411
|
+
*/
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* How a discovered integration record holds its name.
|
|
1415
|
+
* @param {{name: string, docPath: string|null}} record
|
|
1416
|
+
* @returns {'file' | 'doc'}
|
|
1417
|
+
*/
|
|
1418
|
+
export function integrationRecordHold(record) {
|
|
1419
|
+
return record.docPath == null || docStem(record.docPath) === record.name
|
|
1420
|
+
? 'file'
|
|
1421
|
+
: 'doc';
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* The doc that records `name` as its export or a member though its file is
|
|
1426
|
+
* named otherwise, with the source, reading only docs that write the name.
|
|
1427
|
+
*
|
|
1428
|
+
* @param {{name?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
1429
|
+
* @param {string} componentsDir
|
|
1430
|
+
* @param {string} name
|
|
1431
|
+
* @returns {{docPath: string, sourcePath: string|null} | null}
|
|
1432
|
+
*/
|
|
1433
|
+
function findDocumentedComponent(integration, componentsDir, name) {
|
|
1434
|
+
if (!COMPONENT_NAME_RE.test(name)) return null;
|
|
1435
|
+
const literals = [`'${name}'`, `"${name}"`, `\`${name}\``];
|
|
1436
|
+
/** @type {Set<string>} */
|
|
1437
|
+
const stems = new Set();
|
|
1438
|
+
/** @type {Array<{docPath: string, stem: string, names: string[], hasMembers: boolean, replaces: string | null, sourcePath: string|null}>} */
|
|
1439
|
+
const candidates = [];
|
|
1440
|
+
forEachIntegrationDoc(componentsDir, (docPath, stem) => {
|
|
1441
|
+
let text;
|
|
1442
|
+
try {
|
|
1443
|
+
text = fs.readFileSync(docPath, 'utf-8');
|
|
1444
|
+
} catch {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
// The docs discovery records: the first visible doc for each stem.
|
|
1448
|
+
if (HIDDEN_RE.test(text) || stems.has(stem)) return;
|
|
1449
|
+
stems.add(stem);
|
|
1450
|
+
if (!literals.some(literal => text.includes(literal))) return;
|
|
1451
|
+
candidates.push({
|
|
1452
|
+
docPath,
|
|
1453
|
+
stem,
|
|
1454
|
+
...scanDocText(text),
|
|
1455
|
+
sourcePath: integrationSourceForDoc(docPath),
|
|
1456
|
+
});
|
|
1457
|
+
});
|
|
1458
|
+
// A Core name a doc in the package replaces is the replacement's to answer.
|
|
1459
|
+
if (candidates.some(candidate => candidate.replaces === name)) return null;
|
|
1460
|
+
candidates.sort((a, b) => byCodeUnit(a.docPath, b.docPath));
|
|
1461
|
+
for (const candidate of candidates) {
|
|
1462
|
+
if (otherExportName(candidate) === name) {
|
|
1463
|
+
return {docPath: candidate.docPath, sourcePath: candidate.sourcePath};
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
for (const candidate of candidates) {
|
|
1467
|
+
if (!candidate.hasMembers) continue;
|
|
1468
|
+
const ownName = otherExportName(candidate);
|
|
1469
|
+
const [member] = familyMembers(integration, candidate, ownName, name);
|
|
1470
|
+
if (member) {
|
|
1471
|
+
return {docPath: candidate.docPath, sourcePath: member.sourcePath};
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* The integration component a name finds, as discovery records it, and its
|
|
1479
|
+
* hold: the same-stem doc first, then a doc that records the name as an export
|
|
1480
|
+
* or a member.
|
|
1481
|
+
*
|
|
1482
|
+
* @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
1483
|
+
* @param {string} name
|
|
1484
|
+
* @returns {{docPath: string, sourcePath: string|null, hold: 'file' | 'doc'} | null}
|
|
1485
|
+
*/
|
|
1486
|
+
export function findIntegrationComponent(integration, name) {
|
|
1487
|
+
const componentsDir = integration?.components;
|
|
1488
|
+
if (!componentsDir || !pathExists(componentsDir)) return null;
|
|
1489
|
+
const sameStem = findSameStemDoc(componentsDir, name);
|
|
1490
|
+
if (sameStem) {
|
|
1491
|
+
const exportName = otherExportName({
|
|
1492
|
+
docPath: sameStem,
|
|
1493
|
+
sourcePath: integrationSourceForDoc(sameStem),
|
|
1494
|
+
stem: name,
|
|
1495
|
+
names: readDocMeta(sameStem).names,
|
|
1496
|
+
});
|
|
1497
|
+
const renamed =
|
|
1498
|
+
exportName != null &&
|
|
1499
|
+
findSameStemDoc(componentsDir, exportName) == null &&
|
|
1500
|
+
findDocumentedComponent(integration, componentsDir, exportName)
|
|
1501
|
+
?.docPath === sameStem;
|
|
1502
|
+
return {
|
|
1503
|
+
docPath: sameStem,
|
|
1504
|
+
sourcePath: integrationSourceForDoc(sameStem),
|
|
1505
|
+
hold: renamed ? 'doc' : 'file',
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
const found = findDocumentedComponent(integration, componentsDir, name);
|
|
1509
|
+
return found && {...found, hold: 'doc'};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* The name discovery records a component doc under: the export its `name`
|
|
1514
|
+
* documents when the doc is recorded under it, else the doc's file name.
|
|
1515
|
+
*
|
|
1516
|
+
* @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
1517
|
+
* @param {string} docPath
|
|
1518
|
+
* @returns {string}
|
|
1519
|
+
*/
|
|
1520
|
+
export function recordedDocName(integration, docPath) {
|
|
1521
|
+
const stem = docStem(docPath);
|
|
1522
|
+
const found = findIntegrationComponent(integration, stem);
|
|
1523
|
+
if (found?.docPath !== docPath || found.hold !== 'doc') return stem;
|
|
1524
|
+
return (
|
|
1525
|
+
otherExportName({
|
|
1526
|
+
docPath,
|
|
1527
|
+
sourcePath: integrationSourceForDoc(docPath),
|
|
1528
|
+
stem,
|
|
1529
|
+
names: readDocMeta(docPath).names,
|
|
1530
|
+
}) ?? stem
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
961
1534
|
/**
|
|
962
1535
|
* Find an integration component's doc file by name within a loaded
|
|
963
1536
|
* integration's resolved `components` dir. Honors the same-stem convention
|
|
964
|
-
* (`{Name}.doc.{ts,mjs,js}`), preferring `.ts` → `.mjs` → `.js
|
|
1537
|
+
* (`{Name}.doc.{ts,mjs,js}`), preferring `.ts` → `.mjs` → `.js`, and finds a
|
|
1538
|
+
* doc that records the component under another name: a differently named
|
|
1539
|
+
* export of its file, or a member its family doc lists.
|
|
965
1540
|
*
|
|
966
|
-
* @param {{components?: string}} integration
|
|
1541
|
+
* @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
967
1542
|
* @param {string} name bare component name (no `XDS`/`Astryx` prefix)
|
|
968
1543
|
* @returns {string|null}
|
|
969
1544
|
*/
|
|
970
1545
|
export function findIntegrationComponentDoc(integration, name) {
|
|
971
|
-
|
|
972
|
-
|
|
1546
|
+
return findIntegrationComponent(integration, name)?.docPath ?? null;
|
|
1547
|
+
}
|
|
973
1548
|
|
|
1549
|
+
/**
|
|
1550
|
+
* The first `{Name}.doc.{ts,mjs,js}` under `componentsDir`, shallowest first.
|
|
1551
|
+
* @param {string} componentsDir
|
|
1552
|
+
* @param {string} name
|
|
1553
|
+
* @returns {string|null}
|
|
1554
|
+
*/
|
|
1555
|
+
function findSameStemDoc(componentsDir, name) {
|
|
974
1556
|
/**
|
|
975
1557
|
* @param {string} dirPath
|
|
976
1558
|
* @returns {string | null}
|
|
@@ -996,18 +1578,33 @@ export function findIntegrationComponentDoc(integration, name) {
|
|
|
996
1578
|
}
|
|
997
1579
|
|
|
998
1580
|
/**
|
|
999
|
-
* Find an integration component's swizzleable source file
|
|
1000
|
-
*
|
|
1581
|
+
* Find an integration component's swizzleable source file by name within a
|
|
1582
|
+
* loaded integration's resolved `components` dir: the same-stem `{Name}.tsx`
|
|
1583
|
+
* of its doc, or, for a family member, the file that declares it. Returns null
|
|
1001
1584
|
* when the integration ships docs without source.
|
|
1002
1585
|
*
|
|
1003
|
-
* @param {{components?: string}} integration
|
|
1586
|
+
* @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
|
|
1004
1587
|
* @param {string} name bare component name
|
|
1005
1588
|
* @returns {string|null}
|
|
1006
1589
|
*/
|
|
1007
1590
|
export function findIntegrationComponentSource(integration, name) {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1591
|
+
return findIntegrationComponent(integration, name)?.sourcePath ?? null;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* The owner rule: an owner that holds a name only because a doc records it
|
|
1596
|
+
* (`hold: 'doc'`) yields to Core and to every file-named owner; `--package`
|
|
1597
|
+
* still reaches it. Surfaces resolve a replacement first, then this rule, then
|
|
1598
|
+
* which docs load.
|
|
1599
|
+
*
|
|
1600
|
+
* @template {{hold?: string}} T
|
|
1601
|
+
* @param {T[]} owners each with its {@link NameHold}; an owner without one
|
|
1602
|
+
* counts as Core's or file-named
|
|
1603
|
+
* @returns {T[]}
|
|
1604
|
+
*/
|
|
1605
|
+
export function unscopedComponentOwners(owners) {
|
|
1606
|
+
const primary = owners.filter(owner => owner.hold !== 'doc');
|
|
1607
|
+
return primary.length > 0 ? primary : owners;
|
|
1011
1608
|
}
|
|
1012
1609
|
|
|
1013
1610
|
/**
|