@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
|
@@ -6,5 +6,32 @@
|
|
|
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;
|
|
32
|
+
/**
|
|
33
|
+
* Marks an object {@link staticExportObject} returns (or one inside it) whose
|
|
34
|
+
* source also has properties or spreads it could not read as literals. Not
|
|
35
|
+
* enumerable, so the object compares and serializes as its literal properties.
|
|
36
|
+
*/
|
|
37
|
+
export const HAS_COMPUTED: unique symbol;
|
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
* Proves a runtime export without executing package code. Local ESM re-exports
|
|
7
7
|
* are followed recursively; unresolved or unparsable modules fail closed.
|
|
8
8
|
*
|
|
9
|
-
* @input A module path and runtime export name
|
|
10
|
-
*
|
|
9
|
+
* @input A module path and runtime export name, or a module path and the
|
|
10
|
+
* exports to try
|
|
11
|
+
* @output Whether that export can be proved statically and the file that
|
|
12
|
+
* declares it, or the exported object literal as plain data
|
|
11
13
|
* @position packages/cli/foundation/discovery — shared package inspection
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import * as fs from 'node:fs';
|
|
15
17
|
import * as path from 'node:path';
|
|
16
|
-
import
|
|
18
|
+
import {parse} from '@babel/parser';
|
|
17
19
|
|
|
18
20
|
const MODULE_EXTENSIONS = [
|
|
19
21
|
'.mjs',
|
|
@@ -25,7 +27,141 @@ const MODULE_EXTENSIONS = [
|
|
|
25
27
|
'.tsx',
|
|
26
28
|
'.jsx',
|
|
27
29
|
];
|
|
28
|
-
|
|
30
|
+
// The options jscodeshift's tsx parser passes to Babel, so the same sources
|
|
31
|
+
// parse, without tokens or the recast layer: these reads only look at the tree.
|
|
32
|
+
/** @type {import('@babel/parser').ParserOptions} */
|
|
33
|
+
const PARSE_OPTIONS = /** @type {any} */ ({
|
|
34
|
+
sourceType: 'module',
|
|
35
|
+
allowImportExportEverywhere: true,
|
|
36
|
+
allowReturnOutsideFunction: true,
|
|
37
|
+
plugins: [
|
|
38
|
+
'jsx',
|
|
39
|
+
'asyncGenerators',
|
|
40
|
+
'decoratorAutoAccessors',
|
|
41
|
+
'bigInt',
|
|
42
|
+
'classPrivateMethods',
|
|
43
|
+
'classPrivateProperties',
|
|
44
|
+
'classProperties',
|
|
45
|
+
'decorators-legacy',
|
|
46
|
+
'doExpressions',
|
|
47
|
+
'dynamicImport',
|
|
48
|
+
'exportDefaultFrom',
|
|
49
|
+
'exportExtensions',
|
|
50
|
+
'exportNamespaceFrom',
|
|
51
|
+
'functionBind',
|
|
52
|
+
'functionSent',
|
|
53
|
+
'importAttributes',
|
|
54
|
+
'importMeta',
|
|
55
|
+
'nullishCoalescingOperator',
|
|
56
|
+
'numericSeparator',
|
|
57
|
+
'objectRestSpread',
|
|
58
|
+
'optionalCatchBinding',
|
|
59
|
+
'optionalChaining',
|
|
60
|
+
['pipelineOperator', {proposal: 'minimal'}],
|
|
61
|
+
'throwExpressions',
|
|
62
|
+
'typescript',
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Modules parsed in this process, by path, reused while the file is unchanged:
|
|
68
|
+
* one command can ask about the same module more than once.
|
|
69
|
+
* @type {Map<string, {stamp: string, ast: any}>}
|
|
70
|
+
*/
|
|
71
|
+
const parsedModules = new Map();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Module texts read in this process, by path, reused while the file is
|
|
75
|
+
* unchanged.
|
|
76
|
+
* @type {Map<string, {stamp: string, text: string}>}
|
|
77
|
+
*/
|
|
78
|
+
const moduleTexts = new Map();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A file's identity for these caches: size and mtime, plus ctime and inode,
|
|
82
|
+
* which change when a file is rewritten even if npm stamps the same mtime on it.
|
|
83
|
+
* @param {string} file
|
|
84
|
+
*/
|
|
85
|
+
function fileStamp(file) {
|
|
86
|
+
const {size, mtimeMs, ctimeMs, ino} = fs.statSync(file);
|
|
87
|
+
return `${size}:${mtimeMs}:${ctimeMs}:${ino}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {string} file */
|
|
91
|
+
function moduleText(file) {
|
|
92
|
+
const stamp = fileStamp(file);
|
|
93
|
+
const cached = moduleTexts.get(file);
|
|
94
|
+
if (cached?.stamp === stamp) return cached.text;
|
|
95
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
96
|
+
moduleTexts.set(file, {stamp, text});
|
|
97
|
+
return text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {string} file */
|
|
101
|
+
function parseModule(file) {
|
|
102
|
+
const stamp = fileStamp(file);
|
|
103
|
+
const cached = parsedModules.get(file);
|
|
104
|
+
if (cached?.stamp === stamp) return cached.ast;
|
|
105
|
+
const ast = parse(moduleText(file), PARSE_OPTIONS);
|
|
106
|
+
parsedModules.set(file, {stamp, ast});
|
|
107
|
+
return ast;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// `export *`: the one way a module exports a name it never writes.
|
|
111
|
+
const EXPORT_ALL_RE = /\bexport\s*\*/;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether `text` has `word` with no identifier character on either side.
|
|
115
|
+
* @param {string} text
|
|
116
|
+
* @param {string} word
|
|
117
|
+
*/
|
|
118
|
+
function writesWord(text, word) {
|
|
119
|
+
for (
|
|
120
|
+
let at = text.indexOf(word);
|
|
121
|
+
at !== -1;
|
|
122
|
+
at = text.indexOf(word, at + 1)
|
|
123
|
+
) {
|
|
124
|
+
const before = text[at - 1] ?? '';
|
|
125
|
+
const after = text[at + word.length] ?? '';
|
|
126
|
+
if (!/[\w$]/.test(before) && !/[\w$]/.test(after)) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const NOT_CHILDREN = new Set([
|
|
132
|
+
'loc',
|
|
133
|
+
'start',
|
|
134
|
+
'end',
|
|
135
|
+
'extra',
|
|
136
|
+
'comments',
|
|
137
|
+
'leadingComments',
|
|
138
|
+
'trailingComments',
|
|
139
|
+
'innerComments',
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every node of `type` in the tree, at any depth, in source order.
|
|
144
|
+
* @param {any} node
|
|
145
|
+
* @param {string} type
|
|
146
|
+
* @param {any[]} [found]
|
|
147
|
+
* @returns {any[]}
|
|
148
|
+
*/
|
|
149
|
+
function nodesOfType(node, type, found = []) {
|
|
150
|
+
if (Array.isArray(node)) {
|
|
151
|
+
for (const child of node) nodesOfType(child, type, found);
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
if (node == null || typeof node !== 'object') return found;
|
|
155
|
+
if (node.type === type) found.push(node);
|
|
156
|
+
for (const key in node) {
|
|
157
|
+
if (NOT_CHILDREN.has(key)) continue;
|
|
158
|
+
const child = node[key];
|
|
159
|
+
if (child != null && typeof child === 'object') {
|
|
160
|
+
nodesOfType(child, type, found);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
29
165
|
|
|
30
166
|
/** @param {unknown} specifier @param {string} fromFile */
|
|
31
167
|
function resolveLocalModule(specifier, fromFile) {
|
|
@@ -76,32 +212,56 @@ function runtimeDefault(declaration) {
|
|
|
76
212
|
* @param {string} file
|
|
77
213
|
* @param {string} exportName
|
|
78
214
|
* @param {Set<string>} [seen]
|
|
215
|
+
* @returns {boolean}
|
|
79
216
|
*/
|
|
80
217
|
export function moduleExportsName(file, exportName, seen = new Set()) {
|
|
218
|
+
return moduleExportSource(file, exportName, seen) != null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The file that declares a module's runtime export, without executing it (the
|
|
223
|
+
* module, or where a local re-export leads); null when it cannot be proved.
|
|
224
|
+
* @param {string} file
|
|
225
|
+
* @param {string} exportName
|
|
226
|
+
* @param {Set<string>} [seen]
|
|
227
|
+
* @returns {string | null}
|
|
228
|
+
*/
|
|
229
|
+
export function moduleExportSource(file, exportName, seen = new Set()) {
|
|
81
230
|
const identity = `${file}\0${exportName}`;
|
|
82
|
-
if (seen.has(identity)) return
|
|
231
|
+
if (seen.has(identity)) return null;
|
|
83
232
|
seen.add(identity);
|
|
233
|
+
let text;
|
|
84
234
|
try {
|
|
85
|
-
if (!fs.statSync(file).isFile()) return
|
|
235
|
+
if (!fs.statSync(file).isFile()) return null;
|
|
236
|
+
text = moduleText(file);
|
|
86
237
|
} catch {
|
|
87
|
-
return
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
// A module whose text writes neither the export's name as a word nor
|
|
241
|
+
// `export *` cannot export it, so it is not parsed. Any `export *` text, even
|
|
242
|
+
// in a comment, leaves the answer to the parsed tree below.
|
|
243
|
+
if (
|
|
244
|
+
exportName !== 'default' &&
|
|
245
|
+
!writesWord(text, exportName) &&
|
|
246
|
+
!EXPORT_ALL_RE.test(text)
|
|
247
|
+
) {
|
|
248
|
+
return null;
|
|
88
249
|
}
|
|
89
250
|
|
|
90
|
-
let
|
|
251
|
+
let ast;
|
|
91
252
|
try {
|
|
92
|
-
|
|
253
|
+
ast = parseModule(file);
|
|
93
254
|
} catch {
|
|
94
|
-
return
|
|
255
|
+
return null;
|
|
95
256
|
}
|
|
96
257
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (found ||
|
|
102
|
-
const node = exportPath.node;
|
|
258
|
+
/** @type {string | null} */
|
|
259
|
+
let found = null;
|
|
260
|
+
nodesOfType(ast.program, 'ExportNamedDeclaration').forEach(
|
|
261
|
+
(/** @type {any} */ node) => {
|
|
262
|
+
if (found || node.exportKind === 'type') return;
|
|
103
263
|
if (declarationExports(node.declaration, exportName)) {
|
|
104
|
-
found =
|
|
264
|
+
found = file;
|
|
105
265
|
return;
|
|
106
266
|
}
|
|
107
267
|
for (const specifier of node.specifiers ?? []) {
|
|
@@ -114,33 +274,198 @@ export function moduleExportsName(file, exportName, seen = new Set()) {
|
|
|
114
274
|
const exported = specifier.exported?.name ?? specifier.exported?.value;
|
|
115
275
|
if (exported !== exportName) continue;
|
|
116
276
|
if (!node.source) {
|
|
117
|
-
found =
|
|
277
|
+
found = file;
|
|
118
278
|
return;
|
|
119
279
|
}
|
|
120
280
|
const target = resolveLocalModule(node.source.value, file);
|
|
121
281
|
const imported =
|
|
122
282
|
specifier.local?.name ?? specifier.local?.value ?? exportName;
|
|
123
|
-
|
|
124
|
-
|
|
283
|
+
const declared = target
|
|
284
|
+
? moduleExportSource(target, imported, seen)
|
|
285
|
+
: null;
|
|
286
|
+
if (declared) {
|
|
287
|
+
found = declared;
|
|
125
288
|
return;
|
|
126
289
|
}
|
|
127
290
|
}
|
|
128
|
-
}
|
|
129
|
-
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
if (found) return found;
|
|
130
294
|
|
|
131
295
|
if (exportName === 'default') {
|
|
132
|
-
const defaults =
|
|
296
|
+
const defaults = nodesOfType(ast.program, 'ExportDefaultDeclaration');
|
|
133
297
|
if (defaults.some((/** @type {any} */ node) => runtimeDefault(node.declaration))) {
|
|
134
|
-
return
|
|
298
|
+
return file;
|
|
135
299
|
}
|
|
136
300
|
}
|
|
137
301
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
302
|
+
nodesOfType(ast.program, 'ExportAllDeclaration').forEach(
|
|
303
|
+
(/** @type {any} */ node) => {
|
|
304
|
+
if (found || node.exportKind === 'type') return;
|
|
305
|
+
const target = resolveLocalModule(node.source?.value, file);
|
|
306
|
+
if (target) found = moduleExportSource(target, exportName, seen);
|
|
307
|
+
},
|
|
308
|
+
);
|
|
145
309
|
return found;
|
|
146
310
|
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Marks an object {@link staticExportObject} returns (or one inside it) whose
|
|
314
|
+
* source also has properties or spreads it could not read as literals. Not
|
|
315
|
+
* enumerable, so the object compares and serializes as its literal properties.
|
|
316
|
+
*/
|
|
317
|
+
export const HAS_COMPUTED = Symbol('hasComputed');
|
|
318
|
+
|
|
319
|
+
const TS_WRAPPERS = new Set([
|
|
320
|
+
'TSSatisfiesExpression',
|
|
321
|
+
'TSAsExpression',
|
|
322
|
+
'TSNonNullExpression',
|
|
323
|
+
'ParenthesizedExpression',
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
/** @param {any} node */
|
|
327
|
+
function withoutTypeWrappers(node) {
|
|
328
|
+
let current = node;
|
|
329
|
+
while (current && TS_WRAPPERS.has(current.type)) current = current.expression;
|
|
330
|
+
return current;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The object a module exports, as plain data, read without running it: the
|
|
335
|
+
* first of `exportNames` it exports, an object literal in place or a top-level
|
|
336
|
+
* binding (under `satisfies` or `as` too). Only literal values are kept;
|
|
337
|
+
* anything computed, or overridable by a later spread, is left out.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} file
|
|
340
|
+
* @param {readonly string[]} exportNames `default` or named exports, in order
|
|
341
|
+
* @returns {Record<string, unknown> | null}
|
|
342
|
+
*/
|
|
343
|
+
export function staticExportObject(file, exportNames) {
|
|
344
|
+
/** @type {any[]} */
|
|
345
|
+
let body;
|
|
346
|
+
try {
|
|
347
|
+
body = parseModule(file).program.body;
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** @type {Map<string, any>} top-level bindings to their initializers */
|
|
353
|
+
const bindings = new Map();
|
|
354
|
+
for (const statement of body) {
|
|
355
|
+
const declaration =
|
|
356
|
+
statement.type === 'ExportNamedDeclaration'
|
|
357
|
+
? statement.declaration
|
|
358
|
+
: statement;
|
|
359
|
+
if (declaration?.type !== 'VariableDeclaration') continue;
|
|
360
|
+
for (const declarator of declaration.declarations) {
|
|
361
|
+
if (declarator.id?.type === 'Identifier' && declarator.init) {
|
|
362
|
+
bindings.set(declarator.id.name, declarator.init);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** @param {string} name @returns {any} undefined when not exported */
|
|
368
|
+
const exported = name => {
|
|
369
|
+
for (const statement of body) {
|
|
370
|
+
if (name === 'default' && statement.type === 'ExportDefaultDeclaration') {
|
|
371
|
+
return statement.declaration;
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
statement.type !== 'ExportNamedDeclaration' ||
|
|
375
|
+
statement.exportKind === 'type'
|
|
376
|
+
) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (statement.declaration?.type === 'VariableDeclaration') {
|
|
380
|
+
const declarator = statement.declaration.declarations.find(
|
|
381
|
+
(/** @type {any} */ item) =>
|
|
382
|
+
item.id?.type === 'Identifier' && item.id.name === name,
|
|
383
|
+
);
|
|
384
|
+
if (declarator) return declarator.init ?? null;
|
|
385
|
+
}
|
|
386
|
+
if (statement.source) continue;
|
|
387
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
388
|
+
const as = specifier.exported?.name ?? specifier.exported?.value;
|
|
389
|
+
if (specifier.type === 'ExportSpecifier' && as === name) {
|
|
390
|
+
return {type: 'Identifier', name: specifier.local?.name};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return undefined;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @param {any} node
|
|
399
|
+
* @param {Set<string>} following bindings being followed, against cycles
|
|
400
|
+
* @returns {unknown} undefined when the value is not a literal
|
|
401
|
+
*/
|
|
402
|
+
const literal = (node, following) => {
|
|
403
|
+
const value = withoutTypeWrappers(node);
|
|
404
|
+
switch (value?.type) {
|
|
405
|
+
case 'StringLiteral':
|
|
406
|
+
case 'NumericLiteral':
|
|
407
|
+
case 'BooleanLiteral':
|
|
408
|
+
return value.value;
|
|
409
|
+
case 'NullLiteral':
|
|
410
|
+
return null;
|
|
411
|
+
case 'TemplateLiteral':
|
|
412
|
+
return value.expressions.length === 0
|
|
413
|
+
? value.quasis[0]?.value.cooked
|
|
414
|
+
: undefined;
|
|
415
|
+
case 'Identifier': {
|
|
416
|
+
if (!bindings.has(value.name) || following.has(value.name)) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
const next = new Set(following).add(value.name);
|
|
420
|
+
return literal(bindings.get(value.name), next);
|
|
421
|
+
}
|
|
422
|
+
case 'ArrayExpression':
|
|
423
|
+
return value.elements
|
|
424
|
+
.map((/** @type {any} */ element) =>
|
|
425
|
+
element == null || element.type === 'SpreadElement'
|
|
426
|
+
? undefined
|
|
427
|
+
: literal(element, following),
|
|
428
|
+
)
|
|
429
|
+
.filter((/** @type {unknown} */ item) => item !== undefined);
|
|
430
|
+
case 'ObjectExpression': {
|
|
431
|
+
/** @type {Record<string, unknown>} */
|
|
432
|
+
let object = {};
|
|
433
|
+
let computed = false;
|
|
434
|
+
for (const property of value.properties) {
|
|
435
|
+
if (property.type === 'SpreadElement') {
|
|
436
|
+
object = {};
|
|
437
|
+
computed = true;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const key = property.computed
|
|
441
|
+
? undefined
|
|
442
|
+
: (property.key?.name ?? property.key?.value);
|
|
443
|
+
const item =
|
|
444
|
+
typeof key === 'string' && property.type === 'ObjectProperty'
|
|
445
|
+
? literal(property.value, following)
|
|
446
|
+
: undefined;
|
|
447
|
+
if (item === undefined) computed = true;
|
|
448
|
+
if (typeof key !== 'string') continue;
|
|
449
|
+
if (item === undefined) delete object[key];
|
|
450
|
+
else object[key] = item;
|
|
451
|
+
}
|
|
452
|
+
if (computed) {
|
|
453
|
+
Object.defineProperty(object, HAS_COMPUTED, {value: true});
|
|
454
|
+
}
|
|
455
|
+
return object;
|
|
456
|
+
}
|
|
457
|
+
default:
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
for (const exportName of exportNames) {
|
|
463
|
+
const node = exported(exportName);
|
|
464
|
+
if (node === undefined) continue;
|
|
465
|
+
const value = literal(node, new Set());
|
|
466
|
+
return value != null && typeof value === 'object' && !Array.isArray(value)
|
|
467
|
+
? /** @type {Record<string, unknown>} */ (value)
|
|
468
|
+
: null;
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Static reads of a module's exports, without running it: the file that
|
|
5
|
+
* declares an export, and an exported object literal as plain data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import {moduleExportSource, staticExportObject} from './module-exports.mjs';
|
|
13
|
+
|
|
14
|
+
/** @type {string} */
|
|
15
|
+
let dir;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-export-field-'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} file
|
|
27
|
+
* @param {string} source
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function write(file, source) {
|
|
31
|
+
const filePath = path.join(dir, file);
|
|
32
|
+
fs.writeFileSync(filePath, source);
|
|
33
|
+
return filePath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DOC_EXPORTS = ['default', 'docs'];
|
|
37
|
+
|
|
38
|
+
describe('staticExportObject', () => {
|
|
39
|
+
it('reads a default-exported object literal as plain data', () => {
|
|
40
|
+
const file = write(
|
|
41
|
+
'A.doc.mjs',
|
|
42
|
+
"export default {type: 'component', name: 'AcmeCarousel', order: 2, hidden: false, extra: null};\n",
|
|
43
|
+
);
|
|
44
|
+
expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
|
|
45
|
+
type: 'component',
|
|
46
|
+
name: 'AcmeCarousel',
|
|
47
|
+
order: 2,
|
|
48
|
+
hidden: false,
|
|
49
|
+
extra: null,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reads a named export, an export list, and the first export it looks for', () => {
|
|
54
|
+
const named = write(
|
|
55
|
+
'B.doc.mjs',
|
|
56
|
+
"/** @type {object} */\nexport const docs = {\n name: 'AcmeCarousel',\n};\n",
|
|
57
|
+
);
|
|
58
|
+
const both = write(
|
|
59
|
+
'C.doc.mjs',
|
|
60
|
+
"export const docs = {name: 'Legacy'};\nexport default {name: 'Stamped'};\n",
|
|
61
|
+
);
|
|
62
|
+
const list = write(
|
|
63
|
+
'E.doc.mjs',
|
|
64
|
+
"const local = {name: 'AcmeCarousel'};\nexport {local as docs};\n",
|
|
65
|
+
);
|
|
66
|
+
expect(staticExportObject(named, DOC_EXPORTS)?.name).toBe('AcmeCarousel');
|
|
67
|
+
expect(staticExportObject(both, DOC_EXPORTS)?.name).toBe('Stamped');
|
|
68
|
+
expect(staticExportObject(list, DOC_EXPORTS)?.name).toBe('AcmeCarousel');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('follows top-level bindings through TypeScript wrappers, at any depth', () => {
|
|
72
|
+
const file = write(
|
|
73
|
+
'D.doc.ts',
|
|
74
|
+
[
|
|
75
|
+
"const slide = {name: 'AcmeSlide', description: 'One slide.'} as const;",
|
|
76
|
+
"const doc = {name: 'AcmeCarousel', components: [slide, {name: 'AcmeDots', description: 'Dots.'}]} satisfies {name: string};",
|
|
77
|
+
'export default doc;',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n'),
|
|
80
|
+
);
|
|
81
|
+
expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
|
|
82
|
+
name: 'AcmeCarousel',
|
|
83
|
+
components: [
|
|
84
|
+
{name: 'AcmeSlide', description: 'One slide.'},
|
|
85
|
+
{name: 'AcmeDots', description: 'Dots.'},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('leaves out what it cannot read without running the module', () => {
|
|
91
|
+
const file = write(
|
|
92
|
+
'F.doc.mjs',
|
|
93
|
+
[
|
|
94
|
+
"const prefix = 'Acme';",
|
|
95
|
+
'const shared = {group: "Media"};',
|
|
96
|
+
'export default {',
|
|
97
|
+
' // A computed string, a call and a spread: none is read.',
|
|
98
|
+
' name: `${prefix}Carousel`,',
|
|
99
|
+
' usage: makeUsage(),',
|
|
100
|
+
" group: 'Overridden',",
|
|
101
|
+
' ...shared,',
|
|
102
|
+
" category: 'Media',",
|
|
103
|
+
" components: [...more, {name: 'AcmeSlide'}],",
|
|
104
|
+
'};',
|
|
105
|
+
'',
|
|
106
|
+
].join('\n'),
|
|
107
|
+
);
|
|
108
|
+
expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
|
|
109
|
+
category: 'Media',
|
|
110
|
+
components: [{name: 'AcmeSlide'}],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('answers null when there is no exported object', () => {
|
|
115
|
+
const files = [
|
|
116
|
+
write(
|
|
117
|
+
'G.doc.mjs',
|
|
118
|
+
"export const docs = {name: 'Legacy'};\nexport default makeDoc();\n",
|
|
119
|
+
),
|
|
120
|
+
write('H.doc.mjs', 'export default {name: '),
|
|
121
|
+
write('I.doc.mjs', "export const other = {name: 'AcmeCarousel'};\n"),
|
|
122
|
+
write('J.doc.mjs', "export default ['AcmeCarousel'];\n"),
|
|
123
|
+
];
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
expect(staticExportObject(file, DOC_EXPORTS)).toBeNull();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('moduleExportSource', () => {
|
|
131
|
+
it('names the file that declares an export, following local re-exports', () => {
|
|
132
|
+
const impl = write(
|
|
133
|
+
'impl.tsx',
|
|
134
|
+
'export function AcmeSlide() { return null; }\n',
|
|
135
|
+
);
|
|
136
|
+
const barrel = write(
|
|
137
|
+
'index.ts',
|
|
138
|
+
"export {AcmeSlide} from './impl';\nexport * from './dots';\nexport function AcmeCarousel() { return null; }\n",
|
|
139
|
+
);
|
|
140
|
+
const dots = write('dots.tsx', 'export const AcmeDots = () => null;\n');
|
|
141
|
+
expect(moduleExportSource(barrel, 'AcmeSlide')).toBe(impl);
|
|
142
|
+
expect(moduleExportSource(barrel, 'AcmeDots')).toBe(dots);
|
|
143
|
+
expect(moduleExportSource(barrel, 'AcmeCarousel')).toBe(barrel);
|
|
144
|
+
expect(moduleExportSource(barrel, 'AcmeMissing')).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('does not follow a re-export written in a comment', () => {
|
|
148
|
+
write('legacy.tsx', 'export function AcmeTab() { return null; }\n');
|
|
149
|
+
const barrel = write(
|
|
150
|
+
'tabs.ts',
|
|
151
|
+
"// export * from './legacy';\nexport function AcmeTabs() { return null; }\n",
|
|
152
|
+
);
|
|
153
|
+
expect(moduleExportSource(barrel, 'AcmeTab')).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('does not count a type-only export', () => {
|
|
157
|
+
const file = write('types.ts', 'export type AcmeShape = {id: string};\n');
|
|
158
|
+
expect(moduleExportSource(file, 'AcmeShape')).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('sees a file npm replaced with one of the same size and mtime', () => {
|
|
162
|
+
// npm stamps every installed file with one mtime and writes new files.
|
|
163
|
+
const npmTime = new Date('1985-10-26T08:15:00Z');
|
|
164
|
+
const file = write('Panel.tsx', 'export function AcmePanel() {}\n');
|
|
165
|
+
fs.utimesSync(file, npmTime, npmTime);
|
|
166
|
+
expect(moduleExportSource(file, 'AcmePanel')).toBe(file);
|
|
167
|
+
const next = write('Panel.next.tsx', 'export function AcmePanol() {}\n');
|
|
168
|
+
fs.utimesSync(next, npmTime, npmTime);
|
|
169
|
+
fs.renameSync(next, file);
|
|
170
|
+
expect(moduleExportSource(file, 'AcmePanel')).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {any} ts the project's own `typescript`
|
|
6
|
+
* @param {string} packageDir
|
|
7
|
+
* @param {string[]} sources the files to read
|
|
8
|
+
* @returns {{options: any, declarations: string[], error?: string}}
|
|
9
|
+
*/
|
|
10
|
+
export function readPackageTsconfig(ts: any, packageDir: string, sources: string[]): {
|
|
11
|
+
options: any;
|
|
12
|
+
declarations: string[];
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|