@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.ebcc404
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/component/_adapter.d.mts +27 -1
- package/api/component/_adapter.mjs +45 -7
- package/api/component/component.mjs +12 -3
- package/api/component/component.test.mjs +420 -167
- package/api/component/integration-export-name.test.mjs +419 -0
- package/api/component/list/list.mjs +42 -4
- package/api/gap-report/gap-report.mjs +66 -4
- package/api/integration/pack-check.mjs +13 -104
- package/api/integration/pack-check.test.mjs +112 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +90 -10
- package/api/search/search.test.mjs +36 -0
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/template/template.test.mjs +120 -3
- package/api/theme/build/build.family.test.mjs +16 -0
- package/api/theme/build/build.mjs +77 -18
- package/api/theme/themeBuild.doc.mjs +3 -3
- package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/working-with-ai.doc.mjs +43 -4
- package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
- package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
- package/assets/templates/pages/detail-page/page.tsx +3 -3
- package/assets/templates/pages/table-inbox/page.tsx +10 -8
- package/authoring/doctypes/base/type.ts +6 -0
- package/authoring/doctypes/component/component.doc.mjs +3 -3
- package/authoring/doctypes/component/type.ts +6 -3
- package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/discovery/component-discovery.d.mts +95 -6
- package/foundation/discovery/component-discovery.export-names.test.mjs +293 -0
- package/foundation/discovery/component-discovery.mjs +524 -49
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +20 -0
- package/foundation/discovery/module-exports.d.mts +21 -0
- package/foundation/discovery/module-exports.mjs +343 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- package/foundation/doc-compiler/doc-loads.test.mjs +5 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -16,6 +16,7 @@ import {fileURLToPath} from 'node:url';
|
|
|
16
16
|
import {
|
|
17
17
|
CORE_PACKAGE,
|
|
18
18
|
discoverComponents,
|
|
19
|
+
familyMember,
|
|
19
20
|
findComponentReadme,
|
|
20
21
|
findComponentSource,
|
|
21
22
|
resolveImportPath,
|
|
@@ -121,6 +122,91 @@ describe('discoverComponents (real core)', () => {
|
|
|
121
122
|
expect(discoverComponents(core)).toEqual({});
|
|
122
123
|
});
|
|
123
124
|
|
|
125
|
+
it('lists HStack, VStack and Heading, whose docs live in their family folders', () => {
|
|
126
|
+
// Each has its own folder and import path; Stack's doc documents HStack and
|
|
127
|
+
// VStack as members, and Heading's own doc sits in the Text folder.
|
|
128
|
+
const comps = discoverComponents(CORE);
|
|
129
|
+
expect(comps.Layout).toEqual(expect.arrayContaining(['HStack', 'Stack', 'VStack']));
|
|
130
|
+
expect(comps.Heading).toEqual(['Heading']);
|
|
131
|
+
expect(findComponentReadme(CORE, 'VStack')).toMatch(/[\\/]Stack[\\/]Stack\.doc\.mjs$/);
|
|
132
|
+
expect(findComponentReadme(CORE, 'HStack')).toMatch(/[\\/]Stack[\\/]Stack\.doc\.mjs$/);
|
|
133
|
+
expect(findComponentReadme(CORE, 'Heading')).toMatch(/[\\/]Text[\\/]Heading\.doc\.mjs$/);
|
|
134
|
+
expect(resolveImportPath(CORE, 'VStack')).toBe('@astryxdesign/core/VStack');
|
|
135
|
+
expect(resolveImportPath(CORE, 'Heading')).toBe('@astryxdesign/core/Heading');
|
|
136
|
+
}, SLOW);
|
|
137
|
+
|
|
138
|
+
it('lists a component folder with no doc when its family doc lists it as a member', () => {
|
|
139
|
+
const core = mkTmp('as-cd-family-');
|
|
140
|
+
const write = (rel, text) => {
|
|
141
|
+
fs.mkdirSync(path.dirname(path.join(core, 'src', rel)), {recursive: true});
|
|
142
|
+
fs.writeFileSync(path.join(core, 'src', rel), text);
|
|
143
|
+
};
|
|
144
|
+
write('Stack/Stack.tsx', 'x');
|
|
145
|
+
write('VStack/VStack.tsx', "import {Stack} from '../Stack/Stack';\n");
|
|
146
|
+
write('Prose/Prose.tsx', "import {Stack} from '../Stack/Stack';\n");
|
|
147
|
+
// Listed by Stack's doc, but builds on nothing in Stack's folder.
|
|
148
|
+
write('Orphan/Orphan.tsx', "import {x} from '../utils';\n");
|
|
149
|
+
write(
|
|
150
|
+
'Stack/Stack.doc.mjs',
|
|
151
|
+
'export const docs = {\n' +
|
|
152
|
+
" name: 'Stack',\n" +
|
|
153
|
+
" group: 'Layout',\n" +
|
|
154
|
+
" relatedComponents: ['Prose'],\n" +
|
|
155
|
+
" usage: {description: 'Pair with Prose.', examples: [{code: \"<Prose label='Prose' />\"}]},\n" +
|
|
156
|
+
" components: [{name: 'Stack'}, {name: 'VStack', description: 'Vertical.'}, {name: 'Orphan'}],\n" +
|
|
157
|
+
'};\n',
|
|
158
|
+
);
|
|
159
|
+
expect(discoverComponents(core)).toEqual({Layout: ['Stack', 'VStack']});
|
|
160
|
+
expect(findComponentReadme(core, 'VStack')).toBe(
|
|
161
|
+
path.join(core, 'src', 'Stack', 'Stack.doc.mjs'),
|
|
162
|
+
);
|
|
163
|
+
// Named only in relatedComponents, prose, and an example: not documented.
|
|
164
|
+
expect(findComponentReadme(core, 'Prose')).toBeNull();
|
|
165
|
+
// A family doc is read only from the folders the component imports.
|
|
166
|
+
expect(findComponentReadme(core, 'Orphan')).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('lists a component folder with no doc when its own doc sits in a family folder', () => {
|
|
170
|
+
const core = mkTmp('as-cd-sibling-');
|
|
171
|
+
for (const dir of ['Text', 'Heading']) {
|
|
172
|
+
fs.mkdirSync(path.join(core, 'src', dir), {recursive: true});
|
|
173
|
+
}
|
|
174
|
+
fs.writeFileSync(path.join(core, 'src', 'Text', 'Text.tsx'), 'x');
|
|
175
|
+
fs.writeFileSync(path.join(core, 'src', 'Heading', 'Heading.tsx'), 'x');
|
|
176
|
+
fs.writeFileSync(
|
|
177
|
+
path.join(core, 'src', 'Text', 'Text.doc.mjs'),
|
|
178
|
+
"export const docs = {name: 'Text', components: [{name: 'Heading'}]};\n",
|
|
179
|
+
);
|
|
180
|
+
fs.writeFileSync(
|
|
181
|
+
path.join(core, 'src', 'Text', 'Heading.doc.mjs'),
|
|
182
|
+
"export const docs = {name: 'Heading'};\n",
|
|
183
|
+
);
|
|
184
|
+
expect(discoverComponents(core)).toEqual({Heading: ['Heading'], Text: ['Text']});
|
|
185
|
+
expect(findComponentReadme(core, 'Heading')).toBe(
|
|
186
|
+
path.join(core, 'src', 'Text', 'Heading.doc.mjs'),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('keeps a member its family hides, and a context folder, unlisted', () => {
|
|
191
|
+
const core = mkTmp('as-cd-family-hidden-');
|
|
192
|
+
for (const dir of ['Stack', 'VStack', 'SizeContext']) {
|
|
193
|
+
fs.mkdirSync(path.join(core, 'src', dir), {recursive: true});
|
|
194
|
+
fs.writeFileSync(
|
|
195
|
+
path.join(core, 'src', dir, `${dir}.tsx`),
|
|
196
|
+
"import {Stack} from '../Stack/Stack';\n",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
fs.writeFileSync(
|
|
200
|
+
path.join(core, 'src', 'Stack', 'Stack.doc.mjs'),
|
|
201
|
+
'export const docs = {\n' +
|
|
202
|
+
" name: 'Stack',\n" +
|
|
203
|
+
" hiddenComponents: ['VStack'],\n" +
|
|
204
|
+
" components: [{name: 'Stack'}, {name: 'VStack'}, {name: 'SizeContext'}],\n" +
|
|
205
|
+
'};\n',
|
|
206
|
+
);
|
|
207
|
+
expect(discoverComponents(core)).toEqual({Stack: ['Stack']});
|
|
208
|
+
});
|
|
209
|
+
|
|
124
210
|
it('returns {} for an empty src/ dir', () => {
|
|
125
211
|
const core = mkTmp('as-cd-emptysrc-');
|
|
126
212
|
fs.mkdirSync(path.join(core, 'src'));
|
|
@@ -133,6 +219,16 @@ describe('discoverComponents (real core)', () => {
|
|
|
133
219
|
});
|
|
134
220
|
});
|
|
135
221
|
|
|
222
|
+
describe('familyMember', () => {
|
|
223
|
+
it("returns a family doc's entry for a member, and null for a component's own doc", () => {
|
|
224
|
+
const stack = {name: 'Stack', components: [{name: 'Stack'}, {name: 'VStack', description: 'v'}]};
|
|
225
|
+
expect(familyMember(stack, 'VStack')).toEqual({name: 'VStack', description: 'v'});
|
|
226
|
+
expect(familyMember(stack, 'Stack')).toBeNull();
|
|
227
|
+
expect(familyMember({name: 'Button'}, 'Button')).toBeNull();
|
|
228
|
+
expect(familyMember(stack, 'HStack')).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
136
232
|
describe('findComponentReadme / findComponentSource / resolveImportPath', () => {
|
|
137
233
|
it('finds the doc + source for a top-level component', () => {
|
|
138
234
|
expect(findComponentReadme(CORE, 'Button')).toMatch(/Button\.doc\.mjs$/);
|
|
@@ -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;
|
|
@@ -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,185 @@ 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
|
+
const TS_WRAPPERS = new Set([
|
|
313
|
+
'TSSatisfiesExpression',
|
|
314
|
+
'TSAsExpression',
|
|
315
|
+
'TSNonNullExpression',
|
|
316
|
+
'ParenthesizedExpression',
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
/** @param {any} node */
|
|
320
|
+
function withoutTypeWrappers(node) {
|
|
321
|
+
let current = node;
|
|
322
|
+
while (current && TS_WRAPPERS.has(current.type)) current = current.expression;
|
|
323
|
+
return current;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The object a module exports, as plain data, read without running it: the
|
|
328
|
+
* first of `exportNames` it exports, an object literal in place or a top-level
|
|
329
|
+
* binding (under `satisfies` or `as` too). Only literal values are kept;
|
|
330
|
+
* anything computed, or overridable by a later spread, is left out.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} file
|
|
333
|
+
* @param {readonly string[]} exportNames `default` or named exports, in order
|
|
334
|
+
* @returns {Record<string, unknown> | null}
|
|
335
|
+
*/
|
|
336
|
+
export function staticExportObject(file, exportNames) {
|
|
337
|
+
/** @type {any[]} */
|
|
338
|
+
let body;
|
|
339
|
+
try {
|
|
340
|
+
body = parseModule(file).program.body;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** @type {Map<string, any>} top-level bindings to their initializers */
|
|
346
|
+
const bindings = new Map();
|
|
347
|
+
for (const statement of body) {
|
|
348
|
+
const declaration =
|
|
349
|
+
statement.type === 'ExportNamedDeclaration'
|
|
350
|
+
? statement.declaration
|
|
351
|
+
: statement;
|
|
352
|
+
if (declaration?.type !== 'VariableDeclaration') continue;
|
|
353
|
+
for (const declarator of declaration.declarations) {
|
|
354
|
+
if (declarator.id?.type === 'Identifier' && declarator.init) {
|
|
355
|
+
bindings.set(declarator.id.name, declarator.init);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** @param {string} name @returns {any} undefined when not exported */
|
|
361
|
+
const exported = name => {
|
|
362
|
+
for (const statement of body) {
|
|
363
|
+
if (name === 'default' && statement.type === 'ExportDefaultDeclaration') {
|
|
364
|
+
return statement.declaration;
|
|
365
|
+
}
|
|
366
|
+
if (
|
|
367
|
+
statement.type !== 'ExportNamedDeclaration' ||
|
|
368
|
+
statement.exportKind === 'type'
|
|
369
|
+
) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (statement.declaration?.type === 'VariableDeclaration') {
|
|
373
|
+
const declarator = statement.declaration.declarations.find(
|
|
374
|
+
(/** @type {any} */ item) =>
|
|
375
|
+
item.id?.type === 'Identifier' && item.id.name === name,
|
|
376
|
+
);
|
|
377
|
+
if (declarator) return declarator.init ?? null;
|
|
378
|
+
}
|
|
379
|
+
if (statement.source) continue;
|
|
380
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
381
|
+
const as = specifier.exported?.name ?? specifier.exported?.value;
|
|
382
|
+
if (specifier.type === 'ExportSpecifier' && as === name) {
|
|
383
|
+
return {type: 'Identifier', name: specifier.local?.name};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {any} node
|
|
392
|
+
* @param {Set<string>} following bindings being followed, against cycles
|
|
393
|
+
* @returns {unknown} undefined when the value is not a literal
|
|
394
|
+
*/
|
|
395
|
+
const literal = (node, following) => {
|
|
396
|
+
const value = withoutTypeWrappers(node);
|
|
397
|
+
switch (value?.type) {
|
|
398
|
+
case 'StringLiteral':
|
|
399
|
+
case 'NumericLiteral':
|
|
400
|
+
case 'BooleanLiteral':
|
|
401
|
+
return value.value;
|
|
402
|
+
case 'NullLiteral':
|
|
403
|
+
return null;
|
|
404
|
+
case 'TemplateLiteral':
|
|
405
|
+
return value.expressions.length === 0
|
|
406
|
+
? value.quasis[0]?.value.cooked
|
|
407
|
+
: undefined;
|
|
408
|
+
case 'Identifier': {
|
|
409
|
+
if (!bindings.has(value.name) || following.has(value.name)) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
const next = new Set(following).add(value.name);
|
|
413
|
+
return literal(bindings.get(value.name), next);
|
|
414
|
+
}
|
|
415
|
+
case 'ArrayExpression':
|
|
416
|
+
return value.elements
|
|
417
|
+
.map((/** @type {any} */ element) =>
|
|
418
|
+
element == null || element.type === 'SpreadElement'
|
|
419
|
+
? undefined
|
|
420
|
+
: literal(element, following),
|
|
421
|
+
)
|
|
422
|
+
.filter((/** @type {unknown} */ item) => item !== undefined);
|
|
423
|
+
case 'ObjectExpression': {
|
|
424
|
+
/** @type {Record<string, unknown>} */
|
|
425
|
+
let object = {};
|
|
426
|
+
for (const property of value.properties) {
|
|
427
|
+
if (property.type === 'SpreadElement') {
|
|
428
|
+
object = {};
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const key = property.computed
|
|
432
|
+
? undefined
|
|
433
|
+
: (property.key?.name ?? property.key?.value);
|
|
434
|
+
if (typeof key !== 'string') continue;
|
|
435
|
+
const item =
|
|
436
|
+
property.type === 'ObjectProperty'
|
|
437
|
+
? literal(property.value, following)
|
|
438
|
+
: undefined;
|
|
439
|
+
if (item === undefined) delete object[key];
|
|
440
|
+
else object[key] = item;
|
|
441
|
+
}
|
|
442
|
+
return object;
|
|
443
|
+
}
|
|
444
|
+
default:
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
for (const exportName of exportNames) {
|
|
450
|
+
const node = exported(exportName);
|
|
451
|
+
if (node === undefined) continue;
|
|
452
|
+
const value = literal(node, new Set());
|
|
453
|
+
return value != null && typeof value === 'object' && !Array.isArray(value)
|
|
454
|
+
? /** @type {Record<string, unknown>} */ (value)
|
|
455
|
+
: null;
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|