@mui/internal-docs-infra 0.13.1-canary.0 → 0.13.1-canary.1
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/package.json +2 -2
- package/pipeline/loadServerTypesMeta/constantGroups.d.mts +48 -0
- package/pipeline/loadServerTypesMeta/constantGroups.mjs +202 -0
- package/pipeline/loadServerTypesMeta/formatComponent.d.mts +4 -5
- package/pipeline/loadServerTypesMeta/formatComponent.mjs +8 -90
- package/pipeline/loadServerTypesMeta/formatRaw.d.mts +8 -3
- package/pipeline/loadServerTypesMeta/formatRaw.mjs +12 -33
- package/pipeline/loadServerTypesMeta/formatType.d.mts +6 -1
- package/pipeline/loadServerTypesMeta/formatType.mjs +1 -1
- package/pipeline/loadServerTypesMeta/loadServerTypesMeta.d.mts +0 -1
- package/pipeline/loadServerTypesMeta/loadServerTypesMeta.mjs +15 -136
- package/pipeline/loadServerTypesMeta/parseTestSources.d.mts +11 -1
- package/pipeline/loadServerTypesMeta/parseTestSources.mjs +19 -7
- package/pipeline/loadServerTypesMeta/processTypes.d.mts +2 -8
- package/pipeline/loadServerTypesMeta/processTypes.mjs +14 -49
- package/pipeline/syncTypes/generateTypesMarkdown.mjs +10 -14
- package/pipeline/loadServerTypesMeta/findMetaFiles.d.mts +0 -9
- package/pipeline/loadServerTypesMeta/findMetaFiles.mjs +0 -45
- package/pipeline/loadServerTypesMeta/transformConstantGroup.d.mts +0 -14
- package/pipeline/loadServerTypesMeta/transformConstantGroup.mjs +0 -80
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-docs-infra",
|
|
3
|
-
"version": "0.13.1-canary.
|
|
3
|
+
"version": "0.13.1-canary.1",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "MUI Infra - internal documentation creation tools.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -814,5 +814,5 @@
|
|
|
814
814
|
"bin": {
|
|
815
815
|
"docs-infra": "./cli/index.mjs"
|
|
816
816
|
},
|
|
817
|
-
"gitSha": "
|
|
817
|
+
"gitSha": "41611dd904d88de9bbc8dce94594452c144fd2ea"
|
|
818
818
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type * as tae from 'typescript-api-extractor';
|
|
3
|
+
/** Which of a component's tables a constant group holds. */
|
|
4
|
+
export type ConstantGroupKind = 'dataAttributes' | 'cssVariables';
|
|
5
|
+
export interface ConstantGroupTarget {
|
|
6
|
+
/** The component's name as the entrypoint exports it, e.g. `Toolbar.Button` */
|
|
7
|
+
component: string;
|
|
8
|
+
kind: ConstantGroupKind;
|
|
9
|
+
}
|
|
10
|
+
/** The constant groups holding one component's tables. */
|
|
11
|
+
export type ComponentConstantGroups = Partial<Record<ConstantGroupKind, tae.EnumNode>>;
|
|
12
|
+
/** The value of a constant in a constant group. */
|
|
13
|
+
type ConstantValue = string | number;
|
|
14
|
+
/**
|
|
15
|
+
* Finds the entrypoint's namespace exports whose module holds nothing but literal constants,
|
|
16
|
+
* e.g. `export * as ButtonDataAttributes from './ButtonDataAttributes'`, with their values.
|
|
17
|
+
* Namespaces nested in other namespace exports are found too, under their dotted path
|
|
18
|
+
* (`Toolbar.ButtonDataAttributes`).
|
|
19
|
+
*
|
|
20
|
+
* The parser flattens such a namespace into one export per member and cannot tell a constant
|
|
21
|
+
* from a type alias of the same literal, so the checker is asked instead.
|
|
22
|
+
*
|
|
23
|
+
* @returns Member values keyed by namespace path, then member name
|
|
24
|
+
*/
|
|
25
|
+
export declare function findConstantNamespaces(entrypoint: string, program: ts.Program): Map<string, Map<string, ConstantValue>>;
|
|
26
|
+
/**
|
|
27
|
+
* Collapses the flattened members of each constant namespace (`ButtonDataAttributes.open`)
|
|
28
|
+
* into a single enum-shaped export named after the namespace, in place of its first member.
|
|
29
|
+
*/
|
|
30
|
+
export declare function foldConstantNamespaces(exports: tae.ExportNode[], namespaces: Map<string, Map<string, ConstantValue>>): tae.ExportNode[];
|
|
31
|
+
/**
|
|
32
|
+
* Writes the declaration of a constant group as it is exported: a namespace of constants
|
|
33
|
+
* for a namespace export, an enum otherwise.
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatConstantGroupDeclaration(name: string, group: tae.EnumNode, asNamespace: boolean): string;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves which component each constant group documents from its name: the component's
|
|
38
|
+
* name with its dots removed, followed by `DataAttributes` or `CssVariables`.
|
|
39
|
+
*
|
|
40
|
+
* Throws when the name before the suffix is not exactly one of the exported components.
|
|
41
|
+
*
|
|
42
|
+
* @returns Targets keyed by the group's export name, and each component's groups
|
|
43
|
+
*/
|
|
44
|
+
export declare function matchConstantGroups(exports: tae.ExportNode[]): {
|
|
45
|
+
targets: Map<string, ConstantGroupTarget>;
|
|
46
|
+
byComponent: Map<string, ComponentConstantGroups>;
|
|
47
|
+
};
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { EnumMember, EnumNode, ExportNode, TypeName } from 'typescript-api-extractor';
|
|
3
|
+
import { formatPropertyComment } from "./formatType.mjs";
|
|
4
|
+
import { isComponentType, isEnumType } from "./typeGuards.mjs";
|
|
5
|
+
|
|
6
|
+
/** Which of a component's tables a constant group holds. */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A constant group is a named set of constant values an entrypoint publishes, either as an
|
|
10
|
+
* enum or as a namespace of constants (`export * as ButtonDataAttributes from './…'`); both
|
|
11
|
+
* are represented as an enum-shaped export. Its name tells which component table it holds:
|
|
12
|
+
* the component's name with its dots removed, followed by one of these suffixes, e.g.
|
|
13
|
+
* `ToolbarButtonDataAttributes` holds the data attributes of `Toolbar.Button`.
|
|
14
|
+
*/
|
|
15
|
+
const KIND_SUFFIXES = {
|
|
16
|
+
dataAttributes: 'DataAttributes',
|
|
17
|
+
cssVariables: 'CssVariables'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** The constant groups holding one component's tables. */
|
|
21
|
+
|
|
22
|
+
function resolveAlias(symbol, checker) {
|
|
23
|
+
// eslint-disable-next-line no-bitwise -- TypeScript symbol flags are a bitmask
|
|
24
|
+
return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The value of a constant in a constant group. */
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads the value of a `const` holding a string or number literal.
|
|
31
|
+
*/
|
|
32
|
+
function readLiteralConstant(symbol, checker) {
|
|
33
|
+
const declaration = symbol.valueDeclaration;
|
|
34
|
+
if (!declaration || !ts.isVariableDeclaration(declaration) ||
|
|
35
|
+
// eslint-disable-next-line no-bitwise -- TypeScript node flags are a bitmask
|
|
36
|
+
!(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const type = checker.getTypeOfSymbol(symbol);
|
|
40
|
+
return type.isStringLiteral() || type.isNumberLiteral() ? type.value : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads a module's exports as constant values, when it exports nothing but literal constants.
|
|
45
|
+
*/
|
|
46
|
+
function readConstantModule(moduleSymbol, checker) {
|
|
47
|
+
const values = new Map();
|
|
48
|
+
const members = checker.getExportsOfModule(moduleSymbol);
|
|
49
|
+
const isConstantModule = members.length > 0 && members.every(member => {
|
|
50
|
+
const value = readLiteralConstant(resolveAlias(member, checker), checker);
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
values.set(member.name, value);
|
|
55
|
+
return true;
|
|
56
|
+
});
|
|
57
|
+
return isConstantModule ? values : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Finds the entrypoint's namespace exports whose module holds nothing but literal constants,
|
|
62
|
+
* e.g. `export * as ButtonDataAttributes from './ButtonDataAttributes'`, with their values.
|
|
63
|
+
* Namespaces nested in other namespace exports are found too, under their dotted path
|
|
64
|
+
* (`Toolbar.ButtonDataAttributes`).
|
|
65
|
+
*
|
|
66
|
+
* The parser flattens such a namespace into one export per member and cannot tell a constant
|
|
67
|
+
* from a type alias of the same literal, so the checker is asked instead.
|
|
68
|
+
*
|
|
69
|
+
* @returns Member values keyed by namespace path, then member name
|
|
70
|
+
*/
|
|
71
|
+
export function findConstantNamespaces(entrypoint, program) {
|
|
72
|
+
const namespaces = new Map();
|
|
73
|
+
const sourceFile = program.getSourceFile(entrypoint);
|
|
74
|
+
const checker = program.getTypeChecker();
|
|
75
|
+
const entrypointSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile);
|
|
76
|
+
const visited = new Set();
|
|
77
|
+
const visit = (moduleSymbol, prefix) => {
|
|
78
|
+
for (const exportSymbol of checker.getExportsOfModule(moduleSymbol)) {
|
|
79
|
+
if (exportSymbol.declarations?.some(ts.isNamespaceExport)) {
|
|
80
|
+
const target = checker.getAliasedSymbol(exportSymbol);
|
|
81
|
+
const path = `${prefix}${exportSymbol.name}`;
|
|
82
|
+
const values = readConstantModule(target, checker);
|
|
83
|
+
if (values) {
|
|
84
|
+
namespaces.set(path, values);
|
|
85
|
+
} else if (!visited.has(target)) {
|
|
86
|
+
visited.add(target);
|
|
87
|
+
visit(target, `${path}.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
if (entrypointSymbol) {
|
|
93
|
+
visit(entrypointSymbol, '');
|
|
94
|
+
}
|
|
95
|
+
return namespaces;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Collapses the flattened members of each constant namespace (`ButtonDataAttributes.open`)
|
|
100
|
+
* into a single enum-shaped export named after the namespace, in place of its first member.
|
|
101
|
+
*/
|
|
102
|
+
export function foldConstantNamespaces(exports, namespaces) {
|
|
103
|
+
if (namespaces.size === 0) {
|
|
104
|
+
return exports;
|
|
105
|
+
}
|
|
106
|
+
const groups = new Map();
|
|
107
|
+
const folded = [];
|
|
108
|
+
for (const node of exports) {
|
|
109
|
+
// Members sit directly under their namespace, which may itself be nested
|
|
110
|
+
// (`Toolbar.ButtonDataAttributes.pressed`).
|
|
111
|
+
const dot = node.name.lastIndexOf('.');
|
|
112
|
+
const namespace = dot === -1 ? undefined : node.name.slice(0, dot);
|
|
113
|
+
const memberName = node.name.slice(dot + 1);
|
|
114
|
+
const value = namespace === undefined ? undefined : namespaces.get(namespace)?.get(memberName);
|
|
115
|
+
if (namespace === undefined || value === undefined) {
|
|
116
|
+
folded.push(node);
|
|
117
|
+
} else {
|
|
118
|
+
let members = groups.get(namespace);
|
|
119
|
+
if (!members) {
|
|
120
|
+
members = [];
|
|
121
|
+
groups.set(namespace, members);
|
|
122
|
+
folded.push(new ExportNode(namespace, new EnumNode(new TypeName(namespace), members, undefined), undefined));
|
|
123
|
+
}
|
|
124
|
+
// Typed as a string, but the parser stores numeric enum values as numbers too; do
|
|
125
|
+
// the same so both forms render alike.
|
|
126
|
+
members.push(new EnumMember(memberName, value, node.documentation));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return folded;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Writes the declaration of a constant group as it is exported: a namespace of constants
|
|
134
|
+
* for a namespace export, an enum otherwise.
|
|
135
|
+
*/
|
|
136
|
+
export function formatConstantGroupDeclaration(name, group, asNamespace) {
|
|
137
|
+
const members = group.members.map(member => {
|
|
138
|
+
// Enum members hold the literal's value, numbers included; a computed member has none.
|
|
139
|
+
const value = member.value;
|
|
140
|
+
const literal = typeof value === 'number' ? String(value) : JSON.stringify(value);
|
|
141
|
+
const comment = member.documentation ? formatPropertyComment(member.documentation) : undefined;
|
|
142
|
+
let declaration = `${member.name} = ${literal},`;
|
|
143
|
+
if (asNamespace) {
|
|
144
|
+
declaration = `const ${member.name}: ${literal};`;
|
|
145
|
+
} else if (value === undefined) {
|
|
146
|
+
declaration = `${member.name},`;
|
|
147
|
+
}
|
|
148
|
+
return comment ? `${comment}\n${declaration}` : declaration;
|
|
149
|
+
});
|
|
150
|
+
return asNamespace ? `declare namespace ${name} {\n${members.join('\n')}\n}` : `enum ${name} {\n${members.join('\n')}\n}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolves which component each constant group documents from its name: the component's
|
|
155
|
+
* name with its dots removed, followed by `DataAttributes` or `CssVariables`.
|
|
156
|
+
*
|
|
157
|
+
* Throws when the name before the suffix is not exactly one of the exported components.
|
|
158
|
+
*
|
|
159
|
+
* @returns Targets keyed by the group's export name, and each component's groups
|
|
160
|
+
*/
|
|
161
|
+
export function matchConstantGroups(exports) {
|
|
162
|
+
const targets = new Map();
|
|
163
|
+
const byComponent = new Map();
|
|
164
|
+
const componentsByFlatName = new Map();
|
|
165
|
+
for (const node of exports) {
|
|
166
|
+
if (isComponentType(node.type)) {
|
|
167
|
+
const flatName = node.name.replaceAll('.', '');
|
|
168
|
+
const names = componentsByFlatName.get(flatName);
|
|
169
|
+
if (names) {
|
|
170
|
+
names.push(node.name);
|
|
171
|
+
} else {
|
|
172
|
+
componentsByFlatName.set(flatName, [node.name]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const kinds = Object.keys(KIND_SUFFIXES);
|
|
177
|
+
for (const node of exports) {
|
|
178
|
+
// Names are compared without dots, like the component names they stand for, so a group
|
|
179
|
+
// exported inside a namespace (`Toolbar.ButtonDataAttributes`) matches too.
|
|
180
|
+
const groupName = node.name.replaceAll('.', '');
|
|
181
|
+
const kind = kinds.find(candidate => groupName.endsWith(KIND_SUFFIXES[candidate]) && groupName.length > KIND_SUFFIXES[candidate].length);
|
|
182
|
+
if (kind && isEnumType(node.type)) {
|
|
183
|
+
const flatName = groupName.slice(0, -KIND_SUFFIXES[kind].length);
|
|
184
|
+
const components = componentsByFlatName.get(flatName) ?? [];
|
|
185
|
+
if (components.length !== 1) {
|
|
186
|
+
throw new Error(components.length === 0 ? `[constantGroups] ${node.name} - no exported component is named ${flatName}` : `[constantGroups] ${node.name} - ${components.join(', ')} are all named ${flatName}`);
|
|
187
|
+
}
|
|
188
|
+
const [component] = components;
|
|
189
|
+
targets.set(node.name, {
|
|
190
|
+
component,
|
|
191
|
+
kind
|
|
192
|
+
});
|
|
193
|
+
const groups = byComponent.get(component) ?? {};
|
|
194
|
+
groups[kind] ??= node.type;
|
|
195
|
+
byComponent.set(component, groups);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
targets,
|
|
200
|
+
byComponent
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as tae from 'typescript-api-extractor';
|
|
2
2
|
import type { FormattedProperty, FormattedEnumMember, FormatInlineTypeOptions, DescriptionReplacement } from "./format.mjs";
|
|
3
|
+
import type { ComponentConstantGroups } from "./constantGroups.mjs";
|
|
3
4
|
import type { TypeRewriteContext } from "./rewriteTypes.mjs";
|
|
4
5
|
import type { ExternalTypesCollector } from "./externalTypes.mjs";
|
|
5
6
|
import type { HastRoot } from "../../CodeHighlighter/types.mjs";
|
|
@@ -20,10 +21,8 @@ export type ComponentTypeMeta = {
|
|
|
20
21
|
* Options for customizing component data formatting.
|
|
21
22
|
*/
|
|
22
23
|
export interface FormatComponentOptions {
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
/** Suffix for CSS variables enum name (default: 'CssVars') */
|
|
26
|
-
cssVariablesSuffix?: string;
|
|
24
|
+
/** The constant groups holding this component's tables (see `matchConstantGroups`) */
|
|
25
|
+
constantGroups?: ComponentConstantGroups;
|
|
27
26
|
/** Pattern/replacement pairs to apply to descriptions */
|
|
28
27
|
descriptionReplacements?: DescriptionReplacement[];
|
|
29
28
|
/** Options for inline type formatting (e.g., unionPrintWidth) */
|
|
@@ -44,7 +43,7 @@ export interface FormatComponentOptions {
|
|
|
44
43
|
*/
|
|
45
44
|
export declare function formatComponentData(component: tae.ExportNode & {
|
|
46
45
|
type: tae.ComponentNode;
|
|
47
|
-
},
|
|
46
|
+
}, typeNameMap: Record<string, string>, rewriteContext: TypeRewriteContext, options?: FormatComponentOptions): Promise<ComponentTypeMeta>;
|
|
48
47
|
/**
|
|
49
48
|
* Type guard to check if an export is a public component that should be documented.
|
|
50
49
|
*
|
|
@@ -20,10 +20,8 @@ import * as memberOrder from "../loadServerTypesText/order.mjs";
|
|
|
20
20
|
*
|
|
21
21
|
* The component must be validated with `isPublicComponent()` before calling this function.
|
|
22
22
|
*/
|
|
23
|
-
export async function formatComponentData(component,
|
|
23
|
+
export async function formatComponentData(component, typeNameMap, rewriteContext, options = {}) {
|
|
24
24
|
const {
|
|
25
|
-
dataAttributesSuffix = 'DataAttributes',
|
|
26
|
-
cssVariablesSuffix = 'CssVars',
|
|
27
25
|
descriptionReplacements,
|
|
28
26
|
formatting,
|
|
29
27
|
externalTypes
|
|
@@ -34,91 +32,11 @@ export async function formatComponentData(component, allExports, typeNameMap, re
|
|
|
34
32
|
const descriptionText = component.documentation?.description ? applyDescriptionReplacements(component.documentation.description, descriptionReplacements) : undefined;
|
|
35
33
|
const description = descriptionText ? await parseMarkdownToHast(descriptionText) : undefined;
|
|
36
34
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Example for re-exported component:
|
|
43
|
-
// - Component: ContextMenu.Backdrop (transformed from MenuBackdrop)
|
|
44
|
-
// - originalName: MenuBackdrop
|
|
45
|
-
// - We look for: MenuBackdropDataAttributes (using originalName + suffix)
|
|
46
|
-
// - That export's originalName will also be MenuBackdropDataAttributes
|
|
47
|
-
const originalName = component.originalName;
|
|
48
|
-
const componentNameForLookup = originalName || component.name.replace(/\./g, '');
|
|
49
|
-
const dataAttributesName = `${componentNameForLookup}${dataAttributesSuffix}`;
|
|
50
|
-
const cssVariablesName = `${componentNameForLookup}${cssVariablesSuffix}`;
|
|
51
|
-
|
|
52
|
-
// Get the component's short name (e.g., "Trigger" from "AlertDialog.Trigger")
|
|
53
|
-
const componentShortName = component.name.split('.').pop() || component.name;
|
|
54
|
-
const dataAttributesSuffixWithShortName = `${componentShortName}${dataAttributesSuffix}`;
|
|
55
|
-
const cssVariablesSuffixWithShortName = `${componentShortName}${cssVariablesSuffix}`;
|
|
56
|
-
|
|
57
|
-
// Look for DataAttributes/CssVars by checking originalName on each export
|
|
58
|
-
// First pass: exact match
|
|
59
|
-
for (const node of allExports) {
|
|
60
|
-
const nodeOriginalName = node.originalName;
|
|
61
|
-
const nodeName = nodeOriginalName || node.name;
|
|
62
|
-
if (nodeName === dataAttributesName) {
|
|
63
|
-
dataAttributes = node;
|
|
64
|
-
} else if (nodeName === cssVariablesName) {
|
|
65
|
-
cssVariables = node;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Early exit if we found both
|
|
69
|
-
if (dataAttributes && cssVariables) {
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Fallback: For re-exported components (like AlertDialog.Trigger which re-exports DialogTrigger),
|
|
75
|
-
// the DataAttributes file uses the original component name (DialogTriggerDataAttributes).
|
|
76
|
-
// If we didn't find an exact match, look for any DataAttributes ending with the component's
|
|
77
|
-
// short name (e.g., "TriggerDataAttributes").
|
|
78
|
-
// Priority: prefer DataAttributes whose prefix is contained in the component's namespace
|
|
79
|
-
// (e.g., for AlertDialog.Trigger, prefer DialogTriggerDataAttributes over MenuTriggerDataAttributes)
|
|
80
|
-
if (!dataAttributes || !cssVariables) {
|
|
81
|
-
// Get the component's namespace (e.g., "AlertDialog" from "AlertDialog.Trigger")
|
|
82
|
-
const componentNamespace = component.name.includes('.') ? component.name.substring(0, component.name.lastIndexOf('.')) : '';
|
|
83
|
-
|
|
84
|
-
// Collect all matching candidates
|
|
85
|
-
const dataAttributesCandidates = [];
|
|
86
|
-
const cssVariablesCandidates = [];
|
|
87
|
-
for (const node of allExports) {
|
|
88
|
-
const nodeOriginalName = node.originalName;
|
|
89
|
-
const nodeName = nodeOriginalName || node.name;
|
|
90
|
-
|
|
91
|
-
// Check if this export ends with the component's short name + suffix
|
|
92
|
-
if (!dataAttributes && nodeName.endsWith(dataAttributesSuffixWithShortName)) {
|
|
93
|
-
// Extract the prefix (e.g., "Dialog" from "DialogTriggerDataAttributes")
|
|
94
|
-
const prefix = nodeName.slice(0, -dataAttributesSuffixWithShortName.length);
|
|
95
|
-
// Priority: 2 if prefix is contained in namespace (related component), 1 otherwise
|
|
96
|
-
const priority = componentNamespace.includes(prefix) ? 2 : 1;
|
|
97
|
-
dataAttributesCandidates.push({
|
|
98
|
-
node,
|
|
99
|
-
priority
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (!cssVariables && nodeName.endsWith(cssVariablesSuffixWithShortName)) {
|
|
103
|
-
const prefix = nodeName.slice(0, -cssVariablesSuffixWithShortName.length);
|
|
104
|
-
const priority = componentNamespace.includes(prefix) ? 2 : 1;
|
|
105
|
-
cssVariablesCandidates.push({
|
|
106
|
-
node,
|
|
107
|
-
priority
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Select the highest priority candidate
|
|
113
|
-
if (!dataAttributes && dataAttributesCandidates.length > 0) {
|
|
114
|
-
dataAttributesCandidates.sort((a, b) => b.priority - a.priority);
|
|
115
|
-
dataAttributes = dataAttributesCandidates[0].node;
|
|
116
|
-
}
|
|
117
|
-
if (!cssVariables && cssVariablesCandidates.length > 0) {
|
|
118
|
-
cssVariablesCandidates.sort((a, b) => b.priority - a.priority);
|
|
119
|
-
cssVariables = cssVariablesCandidates[0].node;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
35
|
+
// The component's data attributes and CSS variables are the constant groups matched to it.
|
|
36
|
+
const {
|
|
37
|
+
dataAttributes,
|
|
38
|
+
cssVariables
|
|
39
|
+
} = options.constantGroups ?? {};
|
|
122
40
|
const raw = {
|
|
123
41
|
name: component.name,
|
|
124
42
|
description,
|
|
@@ -131,8 +49,8 @@ export async function formatComponentData(component, allExports, typeNameMap, re
|
|
|
131
49
|
externalTypes,
|
|
132
50
|
descriptionReplacements
|
|
133
51
|
}), options.ordering?.props ?? memberOrder.props),
|
|
134
|
-
dataAttributes: dataAttributes
|
|
135
|
-
cssVariables: cssVariables
|
|
52
|
+
dataAttributes: dataAttributes ? sortObjectByKeys(await formatEnum(dataAttributes, descriptionReplacements), options.ordering?.dataAttributes ?? memberOrder.dataAttributes) : {},
|
|
53
|
+
cssVariables: cssVariables ? sortObjectByKeys(await formatEnum(cssVariables, descriptionReplacements), options.ordering?.cssVariables ?? memberOrder.cssVariables) : {}
|
|
136
54
|
};
|
|
137
55
|
|
|
138
56
|
// Post-process type strings to align naming across re-exports and hide internal suffixes.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as tae from 'typescript-api-extractor';
|
|
2
2
|
import type { FormattedProperty, FormatInlineTypeOptions, DescriptionReplacement } from "./format.mjs";
|
|
3
|
+
import type { ConstantGroupTarget } from "./constantGroups.mjs";
|
|
3
4
|
import type { TypeRewriteContext } from "./rewriteTypes.mjs";
|
|
4
5
|
import type { ExternalTypesCollector } from "./externalTypes.mjs";
|
|
5
6
|
import type { HastRoot } from "../../CodeHighlighter/types.mjs";
|
|
@@ -12,7 +13,7 @@ export interface ReExportInfo {
|
|
|
12
13
|
/** Anchor slug for linking (e.g., "#trigger") */
|
|
13
14
|
slug: string;
|
|
14
15
|
/** What kind of type this re-exports */
|
|
15
|
-
suffix: 'props'
|
|
16
|
+
suffix: 'props';
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
19
|
* Formatted raw type metadata with the type declaration as a formatted code string.
|
|
@@ -46,11 +47,11 @@ export type RawTypeMeta = {
|
|
|
46
47
|
*/
|
|
47
48
|
reExportOf?: ReExportInfo;
|
|
48
49
|
/**
|
|
49
|
-
* For
|
|
50
|
+
* For constant groups holding a component's data attributes, that component's name.
|
|
50
51
|
*/
|
|
51
52
|
dataAttributesOf?: string;
|
|
52
53
|
/**
|
|
53
|
-
* For
|
|
54
|
+
* For constant groups holding a component's CSS variables, that component's name.
|
|
54
55
|
*/
|
|
55
56
|
cssVarsOf?: string;
|
|
56
57
|
/**
|
|
@@ -75,6 +76,10 @@ export interface FormatRawOptions {
|
|
|
75
76
|
externalTypes?: ExternalTypesCollector;
|
|
76
77
|
/** Pattern/replacement pairs to apply to descriptions */
|
|
77
78
|
descriptionReplacements?: DescriptionReplacement[];
|
|
79
|
+
/** The component table this constant group holds (see `matchConstantGroups`) */
|
|
80
|
+
constantGroup?: ConstantGroupTarget;
|
|
81
|
+
/** Whether the constant group was exported as a namespace of constants rather than an enum */
|
|
82
|
+
constantNamespace?: boolean;
|
|
78
83
|
}
|
|
79
84
|
/**
|
|
80
85
|
* Formats a raw type export into a structured metadata object with formatted code.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { prettyFormat, parseMarkdownToHast, applyDescriptionReplacements, formatProperties, extractTypeParameters } from "./format.mjs";
|
|
2
2
|
import { formatType } from "./formatType.mjs";
|
|
3
3
|
import { isEnumType, isObjectType } from "./typeGuards.mjs";
|
|
4
|
+
import { formatConstantGroupDeclaration } from "./constantGroups.mjs";
|
|
4
5
|
import { rewriteTypeStringsDeep } from "./rewriteTypes.mjs";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -33,7 +34,9 @@ import { rewriteTypeStringsDeep } from "./rewriteTypes.mjs";
|
|
|
33
34
|
*/
|
|
34
35
|
export async function formatRawData(exportNode, displayName, typeNameMap, rewriteContext, _options = {}) {
|
|
35
36
|
const {
|
|
36
|
-
descriptionReplacements
|
|
37
|
+
descriptionReplacements,
|
|
38
|
+
constantGroup,
|
|
39
|
+
constantNamespace
|
|
37
40
|
} = _options;
|
|
38
41
|
const descriptionText = exportNode.documentation?.description ? applyDescriptionReplacements(exportNode.documentation.description, descriptionReplacements) : undefined;
|
|
39
42
|
const description = descriptionText ? await parseMarkdownToHast(descriptionText) : undefined;
|
|
@@ -50,8 +53,9 @@ export async function formatRawData(exportNode, displayName, typeNameMap, rewrit
|
|
|
50
53
|
};
|
|
51
54
|
}));
|
|
52
55
|
|
|
53
|
-
// For enums, still generate the code block but also include members
|
|
54
|
-
|
|
56
|
+
// For enums, still generate the code block but also include members. The declaration
|
|
57
|
+
// follows how the group is exported: an enum, or a namespace of constants.
|
|
58
|
+
const formattedCode = await prettyFormat(formatConstantGroupDeclaration(displayName.replace(/\./g, ''), exportNode.type, Boolean(constantNamespace)), null);
|
|
55
59
|
|
|
56
60
|
// Rewrite type names in descriptions (but NOT in formattedCode which is valid TypeScript syntax)
|
|
57
61
|
const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined;
|
|
@@ -62,36 +66,11 @@ export async function formatRawData(exportNode, displayName, typeNameMap, rewrit
|
|
|
62
66
|
formattedCode,
|
|
63
67
|
enumMembers
|
|
64
68
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const componentName = displayName.replace('.DataAttributes', '');
|
|
71
|
-
const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap);
|
|
72
|
-
const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined;
|
|
73
|
-
const raw = {
|
|
74
|
-
name: displayName,
|
|
75
|
-
description,
|
|
76
|
-
descriptionText: rewrittenDescriptionText,
|
|
77
|
-
formattedCode,
|
|
78
|
-
dataAttributesOf: componentName
|
|
79
|
-
};
|
|
80
|
-
return raw;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Handle CssVars types
|
|
84
|
-
if (displayName.endsWith('.CssVars')) {
|
|
85
|
-
const componentName = displayName.replace('.CssVars', '');
|
|
86
|
-
const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap);
|
|
87
|
-
const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined;
|
|
88
|
-
const raw = {
|
|
89
|
-
name: displayName,
|
|
90
|
-
description,
|
|
91
|
-
descriptionText: rewrittenDescriptionText,
|
|
92
|
-
formattedCode,
|
|
93
|
-
cssVarsOf: componentName
|
|
94
|
-
};
|
|
69
|
+
if (constantGroup?.kind === 'dataAttributes') {
|
|
70
|
+
raw.dataAttributesOf = constantGroup.component;
|
|
71
|
+
} else if (constantGroup?.kind === 'cssVariables') {
|
|
72
|
+
raw.cssVarsOf = constantGroup.component;
|
|
73
|
+
}
|
|
95
74
|
return raw;
|
|
96
75
|
}
|
|
97
76
|
|
|
@@ -20,4 +20,9 @@ export declare function formatType(type: tae.AnyType, options: FormatTypeOptions
|
|
|
20
20
|
* processing, then runs the output through `prettyFormat()` for consistent styling.
|
|
21
21
|
*/
|
|
22
22
|
export declare function prettyFormatType(type: tae.AnyType, options: FormatTypeOptions): Promise<string>;
|
|
23
|
-
export declare function getFullyQualifiedName(typeName: tae.TypeName, exportNames: string[], typeNameMap: Record<string, string>, preserveTypeParameters?: boolean): string;
|
|
23
|
+
export declare function getFullyQualifiedName(typeName: tae.TypeName, exportNames: string[], typeNameMap: Record<string, string>, preserveTypeParameters?: boolean): string;
|
|
24
|
+
/**
|
|
25
|
+
* Formats a JSDoc comment block from property documentation.
|
|
26
|
+
* Returns undefined if no meaningful content to document.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatPropertyComment(documentation: tae.Documentation): string | undefined;
|
|
@@ -818,7 +818,7 @@ function normalizeQuotes(str) {
|
|
|
818
818
|
* Formats a JSDoc comment block from property documentation.
|
|
819
819
|
* Returns undefined if no meaningful content to document.
|
|
820
820
|
*/
|
|
821
|
-
function formatPropertyComment(documentation) {
|
|
821
|
+
export function formatPropertyComment(documentation) {
|
|
822
822
|
const lines = [];
|
|
823
823
|
if (documentation.description) {
|
|
824
824
|
lines.push(...documentation.description.split('\n'));
|
|
@@ -119,7 +119,6 @@ export interface LoadServerTypesMetaResult extends OrganizeTypesResult<TypesMeta
|
|
|
119
119
|
* This function handles:
|
|
120
120
|
* - Loading TypeScript configuration
|
|
121
121
|
* - Resolving library source files and variants
|
|
122
|
-
* - Finding meta files (DataAttributes, CssVars)
|
|
123
122
|
* - Processing types via worker thread
|
|
124
123
|
* - Formatting component, hook, function, and raw types
|
|
125
124
|
* - Collecting external types referenced in props/params
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Can use node: imports here since this is server-only code
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { parseImportsAndComments, extractNameAndSlugFromUrl } from "../loaderUtils/index.mjs";
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { extractNameAndSlugFromUrl } from "../loaderUtils/index.mjs";
|
|
6
5
|
import { nameMark, performanceMeasure } from "../loadPrecomputedCodeHighlighter/performanceLogger.mjs";
|
|
7
6
|
import { loadTypescriptConfig } from "./loadTypescriptConfig.mjs";
|
|
8
7
|
import { resolveLibrarySourceFiles } from "./resolveLibrarySourceFiles.mjs";
|
|
@@ -13,7 +12,7 @@ import { formatFunctionData, isPublicFunction } from "./formatFunction.mjs";
|
|
|
13
12
|
import { formatRawData } from "./formatRaw.mjs";
|
|
14
13
|
import { prettyFormat } from "./format.mjs";
|
|
15
14
|
import { buildTypeCompatibilityMap } from "./rewriteTypes.mjs";
|
|
16
|
-
import {
|
|
15
|
+
import { matchConstantGroups } from "./constantGroups.mjs";
|
|
17
16
|
import { getWorkerManager } from "./workerManager.mjs";
|
|
18
17
|
import { reconstructPerformanceLogs } from "./performanceTracking.mjs";
|
|
19
18
|
import { typeSuffixes as defaultTypeSuffixes } from "../loadServerTypesText/order.mjs";
|
|
@@ -25,7 +24,6 @@ const functionName = 'Load Server Types Meta';
|
|
|
25
24
|
* This function handles:
|
|
26
25
|
* - Loading TypeScript configuration
|
|
27
26
|
* - Resolving library source files and variants
|
|
28
|
-
* - Finding meta files (DataAttributes, CssVars)
|
|
29
27
|
* - Processing types via worker thread
|
|
30
28
|
* - Formatting component, hook, function, and raw types
|
|
31
29
|
* - Collecting external types referenced in props/params
|
|
@@ -74,94 +72,12 @@ export async function loadServerTypesMeta(options) {
|
|
|
74
72
|
}, [functionName, relativePath]);
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
// Collect all entrypoints for optimized program creation
|
|
78
|
-
// Include both the component entrypoints and their meta files (DataAttributes, CssVars)
|
|
79
|
-
// These are file:// URLs from resolveLibrarySourceFiles
|
|
80
|
-
const resolvedEntrypointUrls = Array.from(resolvedVariantMap.values());
|
|
81
|
-
|
|
82
|
-
// Parse exports from library source files to find re-exported directories
|
|
83
|
-
// This helps us discover DataAttributes/CssVars files from re-exported components
|
|
84
|
-
const reExportedDirUrls = new Set();
|
|
85
|
-
await Promise.all(resolvedEntrypointUrls.map(async entrypointUrl => {
|
|
86
|
-
try {
|
|
87
|
-
// Convert file:// URL to filesystem path for Node.js fs APIs
|
|
88
|
-
const fsEntrypoint = fileURLToPath(entrypointUrl);
|
|
89
|
-
const sourceCode = await readFile(fsEntrypoint, 'utf-8');
|
|
90
|
-
const parsed = parseImportsAndComments(sourceCode, entrypointUrl);
|
|
91
|
-
|
|
92
|
-
// Look for relative exports (e.g., '../menu/', './Button', etc.)
|
|
93
|
-
await Promise.all(Object.keys(parsed.relative || {}).map(async exportPath => {
|
|
94
|
-
if (exportPath.startsWith('..') || exportPath.startsWith('.')) {
|
|
95
|
-
// Resolve to absolute filesystem path
|
|
96
|
-
const absoluteFsPath = path.resolve(path.dirname(fsEntrypoint), exportPath);
|
|
97
|
-
|
|
98
|
-
// Check if this path exists as a directory
|
|
99
|
-
// If not, it might be a module reference (e.g., '../menu/backdrop/MenuBackdrop' -> MenuBackdrop.tsx)
|
|
100
|
-
// In that case, we want to add the parent directory
|
|
101
|
-
try {
|
|
102
|
-
const stats = await stat(absoluteFsPath);
|
|
103
|
-
if (stats.isDirectory()) {
|
|
104
|
-
// It's a directory, add it with trailing slash so path.dirname returns this directory
|
|
105
|
-
reExportedDirUrls.add(pathToFileURL(`${absoluteFsPath}/`).href);
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
// Path doesn't exist as-is. Check if it exists with common extensions
|
|
109
|
-
const extensions = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
|
|
110
|
-
for (const ext of extensions) {
|
|
111
|
-
try {
|
|
112
|
-
// eslint-disable-next-line no-await-in-loop
|
|
113
|
-
const fileStats = await stat(absoluteFsPath + ext);
|
|
114
|
-
if (fileStats.isFile()) {
|
|
115
|
-
// It's a file reference, add the parent directory as file:// URL
|
|
116
|
-
// Add trailing slash so path.dirname returns this directory, not its parent
|
|
117
|
-
const parentDir = path.dirname(absoluteFsPath);
|
|
118
|
-
reExportedDirUrls.add(pathToFileURL(`${parentDir}/`).href);
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
} catch {
|
|
122
|
-
// Continue checking other extensions
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// If not found as file or directory, it might be a bare module reference - skip it
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}));
|
|
130
|
-
} catch (error) {
|
|
131
|
-
// If we can't parse a file, just skip it
|
|
132
|
-
console.warn(`[Main] Failed to parse exports from ${entrypointUrl}:`, error instanceof Error ? error.message : error);
|
|
133
|
-
}
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
// Find meta files from the library source directories and re-exported directories
|
|
137
|
-
// Convert file:// URLs to filesystem paths for findMetaFiles
|
|
138
|
-
const entrypointFiles = resolvedEntrypointUrls.map(url => fileURLToPath(url));
|
|
139
|
-
const reExportedDirs = Array.from(reExportedDirUrls).map(url => fileURLToPath(url));
|
|
140
|
-
|
|
141
|
-
// findMetaFiles accepts filesystem paths and returns filesystem paths
|
|
142
|
-
// We search both entrypoint files and re-exported directories for meta files,
|
|
143
|
-
// but only include actual files (entrypoints + found meta files), not directories
|
|
144
|
-
const metaFilesFromEntrypoints = await Promise.all(entrypointFiles.map(fsPath => findMetaFiles(fsPath))).then(results => results.flat());
|
|
145
|
-
const metaFilesFromReExports = await Promise.all(reExportedDirs.map(fsPath => findMetaFiles(fsPath))).then(results => results.flat());
|
|
146
|
-
|
|
147
|
-
// Meta files are DataAttributes/CssVars files that aren't imported but contain type info
|
|
148
|
-
const metaFiles = [...metaFilesFromEntrypoints, ...metaFilesFromReExports];
|
|
149
|
-
|
|
150
|
-
// All files needed for the TypeScript program (entrypoints + meta files)
|
|
151
|
-
const allEntrypoints = [...entrypointFiles, ...metaFiles];
|
|
152
|
-
currentMark = performanceMeasure(currentMark, {
|
|
153
|
-
mark: 'Meta Files Resolved',
|
|
154
|
-
measure: 'Meta Files Resolution'
|
|
155
|
-
}, [functionName, relativePath]);
|
|
156
|
-
|
|
157
75
|
// Process types — use the worker manager singleton (which adapts to main vs worker thread)
|
|
158
76
|
const workerManager = getWorkerManager();
|
|
159
77
|
const workerStartTime = performance.now();
|
|
160
78
|
const workerResult = await workerManager.processTypes({
|
|
161
79
|
projectPath: config.projectPath,
|
|
162
80
|
compilerOptions: config.options,
|
|
163
|
-
allEntrypoints,
|
|
164
|
-
metaFiles,
|
|
165
81
|
resolvedVariantMap: Array.from(resolvedVariantMap.entries()),
|
|
166
82
|
dependencies: config.dependencies,
|
|
167
83
|
rootContextDir,
|
|
@@ -199,7 +115,7 @@ export async function loadServerTypesMeta(options) {
|
|
|
199
115
|
|
|
200
116
|
// Build type compatibility map once from all exports across all variants
|
|
201
117
|
// This map is used to rewrite type references (e.g., Dialog.Trigger.State -> AlertDialog.Trigger.State)
|
|
202
|
-
const allRawExports = Object.values(rawVariantData).flatMap(v => v.
|
|
118
|
+
const allRawExports = Object.values(rawVariantData).flatMap(v => v.exports);
|
|
203
119
|
const allExportNames = Array.from(new Set(allRawExports.map(exp => exp.name)));
|
|
204
120
|
const typeCompatibilityMap = buildTypeCompatibilityMap(allRawExports, allExportNames);
|
|
205
121
|
|
|
@@ -223,18 +139,21 @@ export async function loadServerTypesMeta(options) {
|
|
|
223
139
|
// Each variant shares the same collected map so types are deduplicated automatically.
|
|
224
140
|
const externalTypesCollector = {
|
|
225
141
|
collected: collectedExternalTypes,
|
|
226
|
-
allExports: variantResult.
|
|
142
|
+
allExports: variantResult.exports,
|
|
227
143
|
pattern: externalTypesPatternRegex,
|
|
228
144
|
typeNameMap: variantResult.typeNameMap
|
|
229
145
|
};
|
|
146
|
+
const constantGroups = matchConstantGroups(variantResult.exports);
|
|
147
|
+
const constantNamespaces = new Set(variantResult.constantNamespaces);
|
|
230
148
|
|
|
231
149
|
// Process all exports in parallel within each variant
|
|
232
150
|
const types = await Promise.all(variantResult.exports.map(async exportNode => {
|
|
233
151
|
if (isPublicComponent(exportNode)) {
|
|
234
|
-
const formattedData = await formatComponentData(exportNode, variantResult.
|
|
152
|
+
const formattedData = await formatComponentData(exportNode, variantResult.typeNameMap || {}, rewriteContext, {
|
|
235
153
|
formatting: formattingOptions,
|
|
236
154
|
externalTypes: externalTypesCollector,
|
|
237
155
|
ordering: options.ordering,
|
|
156
|
+
constantGroups: constantGroups.byComponent.get(exportNode.name),
|
|
238
157
|
descriptionReplacements: options.descriptionReplacements
|
|
239
158
|
});
|
|
240
159
|
return {
|
|
@@ -284,7 +203,9 @@ export async function loadServerTypesMeta(options) {
|
|
|
284
203
|
const formattedData = await formatRawData(exportNode, exportNode.name, variantResult.typeNameMap || {}, rewriteContext, {
|
|
285
204
|
formatting: formattingOptions,
|
|
286
205
|
externalTypes: externalTypesCollector,
|
|
287
|
-
descriptionReplacements: options.descriptionReplacements
|
|
206
|
+
descriptionReplacements: options.descriptionReplacements,
|
|
207
|
+
constantGroup: constantGroups.targets.get(exportNode.name),
|
|
208
|
+
constantNamespace: constantNamespaces.has(exportNode.name)
|
|
288
209
|
});
|
|
289
210
|
return {
|
|
290
211
|
type: 'raw',
|
|
@@ -470,11 +391,11 @@ export async function loadServerTypesMeta(options) {
|
|
|
470
391
|
|
|
471
392
|
// Extract component name and suffix (e.g., "ButtonProps" -> component: "Button", suffix: "Props")
|
|
472
393
|
// Handle both namespaced (ContextMenu.Root.Props) and non-namespaced (ButtonProps) names
|
|
473
|
-
const parts = typeMeta.name.match(/^(.+)\.
|
|
394
|
+
const parts = typeMeta.name.match(/^(.+)\.Props$/);
|
|
474
395
|
if (!parts) {
|
|
475
396
|
return typeMeta;
|
|
476
397
|
}
|
|
477
|
-
const [, componentName
|
|
398
|
+
const [, componentName] = parts;
|
|
478
399
|
|
|
479
400
|
// Find the corresponding component by checking both the full name and just the last part
|
|
480
401
|
// e.g., for "ContextMenu.Root.Props", check both "ContextMenu.Root" and "Root"
|
|
@@ -484,7 +405,7 @@ export async function loadServerTypesMeta(options) {
|
|
|
484
405
|
}
|
|
485
406
|
|
|
486
407
|
// Check if Props is a re-export of the component's props
|
|
487
|
-
if (
|
|
408
|
+
if (correspondingComponent.data.props) {
|
|
488
409
|
const hasProps = Object.keys(correspondingComponent.data.props).length > 0;
|
|
489
410
|
if (hasProps) {
|
|
490
411
|
// Extract the display name (last part after dot) for the link text
|
|
@@ -504,48 +425,6 @@ export async function loadServerTypesMeta(options) {
|
|
|
504
425
|
};
|
|
505
426
|
}
|
|
506
427
|
}
|
|
507
|
-
|
|
508
|
-
// Check if DataAttributes is a re-export of the component's data attributes
|
|
509
|
-
if (suffix === 'DataAttributes' && correspondingComponent.data.dataAttributes) {
|
|
510
|
-
const hasDataAttributes = Object.keys(correspondingComponent.data.dataAttributes).length > 0;
|
|
511
|
-
if (hasDataAttributes) {
|
|
512
|
-
// Extract the display name (last part after dot) for the link text
|
|
513
|
-
const displayName = componentName.includes('.') ? componentName.split('.').pop() : componentName;
|
|
514
|
-
return {
|
|
515
|
-
type: 'raw',
|
|
516
|
-
name: typeMeta.name,
|
|
517
|
-
data: {
|
|
518
|
-
...typeMeta.data,
|
|
519
|
-
reExportOf: {
|
|
520
|
-
name: displayName,
|
|
521
|
-
slug: `#${displayName.toLowerCase()}`,
|
|
522
|
-
suffix: 'data-attributes'
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Check if CssVars is a re-export of the component's CSS variables
|
|
530
|
-
if (suffix === 'CssVars' && correspondingComponent.data.cssVariables) {
|
|
531
|
-
const hasCssVariables = Object.keys(correspondingComponent.data.cssVariables).length > 0;
|
|
532
|
-
if (hasCssVariables) {
|
|
533
|
-
// Extract the display name (last part after dot) for the link text
|
|
534
|
-
const displayName = componentName.includes('.') ? componentName.split('.').pop() : componentName;
|
|
535
|
-
return {
|
|
536
|
-
type: 'raw',
|
|
537
|
-
name: typeMeta.name,
|
|
538
|
-
data: {
|
|
539
|
-
...typeMeta.data,
|
|
540
|
-
reExportOf: {
|
|
541
|
-
name: displayName,
|
|
542
|
-
slug: `#${displayName.toLowerCase()}`,
|
|
543
|
-
suffix: 'css-variables'
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
428
|
return typeMeta;
|
|
550
429
|
});
|
|
551
430
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
1
2
|
import type * as tae from 'typescript-api-extractor';
|
|
2
3
|
export interface ParseTestSourcesOptions {
|
|
3
4
|
/**
|
|
@@ -21,4 +22,13 @@ export interface ParseTestSourcesOptions {
|
|
|
21
22
|
* Sources are keyed by file name (e.g. `ComponentRootDataAttributes.ts`) and may import
|
|
22
23
|
* each other by relative path. The first entry is the entrypoint.
|
|
23
24
|
*/
|
|
24
|
-
export declare function parseTestSources(sources: Record<string, string>, options?: ParseTestSourcesOptions): tae.ExportNode[];
|
|
25
|
+
export declare function parseTestSources(sources: Record<string, string>, options?: ParseTestSourcesOptions): tae.ExportNode[];
|
|
26
|
+
/**
|
|
27
|
+
* Builds a TypeScript program over in-memory sources, for tests that need the checker
|
|
28
|
+
* itself rather than parsed exports. Sources are keyed and resolved as in
|
|
29
|
+
* `parseTestSources`, and `entrypoint` is the path of the first one.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createTestProgram(sources: Record<string, string>, lib?: string): {
|
|
32
|
+
program: ts.Program;
|
|
33
|
+
entrypoint: string;
|
|
34
|
+
};
|
|
@@ -16,9 +16,21 @@ const LIB_PATH = `${ROOT}/lib.d.ts`;
|
|
|
16
16
|
*/
|
|
17
17
|
export function parseTestSources(sources, options = {}) {
|
|
18
18
|
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} = options;
|
|
19
|
+
program,
|
|
20
|
+
entrypoint
|
|
21
|
+
} = createTestProgram(sources, options.lib);
|
|
22
|
+
return parseFromProgram(entrypoint, program, {
|
|
23
|
+
...PARSER_OPTIONS,
|
|
24
|
+
...options.parserOptions
|
|
25
|
+
}).exports;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builds a TypeScript program over in-memory sources, for tests that need the checker
|
|
30
|
+
* itself rather than parsed exports. Sources are keyed and resolved as in
|
|
31
|
+
* `parseTestSources`, and `entrypoint` is the path of the first one.
|
|
32
|
+
*/
|
|
33
|
+
export function createTestProgram(sources, lib) {
|
|
22
34
|
const sourceFiles = new Map();
|
|
23
35
|
if (lib !== undefined) {
|
|
24
36
|
sourceFiles.set(LIB_PATH, ts.createSourceFile(LIB_PATH, lib, ts.ScriptTarget.ESNext, true));
|
|
@@ -48,8 +60,8 @@ export function parseTestSources(sources, options = {}) {
|
|
|
48
60
|
rootDir: ROOT,
|
|
49
61
|
noLib: lib === undefined
|
|
50
62
|
}, host);
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
63
|
+
return {
|
|
64
|
+
program,
|
|
65
|
+
entrypoint: entryPaths[0]
|
|
66
|
+
};
|
|
55
67
|
}
|
|
@@ -4,7 +4,8 @@ import type { InheritedExternalPropsConfig } from "./inheritedExternalProps.mjs"
|
|
|
4
4
|
import type { PerformanceLog } from "./performanceTracking.mjs";
|
|
5
5
|
export interface VariantResult {
|
|
6
6
|
exports: ExportNode[];
|
|
7
|
-
|
|
7
|
+
/** Names of the exports folded from namespaces of constants (see `findConstantNamespaces`) */
|
|
8
|
+
constantNamespaces: string[];
|
|
8
9
|
namespaces: string[];
|
|
9
10
|
typeNameMap?: Record<string, string>;
|
|
10
11
|
}
|
|
@@ -12,10 +13,6 @@ export interface WorkerRequest {
|
|
|
12
13
|
requestId?: number;
|
|
13
14
|
projectPath: string;
|
|
14
15
|
compilerOptions: CompilerOptions;
|
|
15
|
-
/** All files for the TypeScript program (entrypoints + meta files) */
|
|
16
|
-
allEntrypoints: string[];
|
|
17
|
-
/** Meta files (DataAttributes, CssVars) - not entrypoints, just additional type info */
|
|
18
|
-
metaFiles: string[];
|
|
19
16
|
/** Map serialized as array of [variantName, fileUrl] tuples where fileUrl uses file:// protocol */
|
|
20
17
|
resolvedVariantMap: Array<[string, string]>;
|
|
21
18
|
/** Dependency paths (filesystem paths, not URLs) */
|
|
@@ -37,9 +34,6 @@ export interface WorkerResponse {
|
|
|
37
34
|
allDependencies?: string[];
|
|
38
35
|
performanceLogs?: PerformanceLog[];
|
|
39
36
|
error?: string;
|
|
40
|
-
debug?: {
|
|
41
|
-
metaFilesCount?: number;
|
|
42
|
-
};
|
|
43
37
|
}
|
|
44
38
|
/**
|
|
45
39
|
* Process TypeScript types for the given request.
|
|
@@ -4,7 +4,7 @@ import { parseFromProgram } from 'typescript-api-extractor';
|
|
|
4
4
|
import ts from 'typescript';
|
|
5
5
|
import { createOptimizedProgram } from "./createOptimizedProgram.mjs";
|
|
6
6
|
import { augmentComponentsWithInheritedProps } from "./inheritedExternalProps.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { findConstantNamespaces, foldConstantNamespaces } from "./constantGroups.mjs";
|
|
8
8
|
import { PARSER_OPTIONS } from "./constants.mjs";
|
|
9
9
|
import { extractJSDocText, isJSDocNodeArray } from "./extractJSDocText.mjs";
|
|
10
10
|
import { PerformanceTracker } from "./performanceTracking.mjs";
|
|
@@ -76,7 +76,7 @@ function collectDocumentedNames(variantData) {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
for (const variant of Object.values(variantData)) {
|
|
79
|
-
for (const node of variant.
|
|
79
|
+
for (const node of variant.exports) {
|
|
80
80
|
addName(node.name);
|
|
81
81
|
}
|
|
82
82
|
if (variant.typeNameMap) {
|
|
@@ -266,10 +266,9 @@ export async function processTypes(request) {
|
|
|
266
266
|
try {
|
|
267
267
|
// Create optimized TypeScript program
|
|
268
268
|
const programWrapperStart = tracker.mark(nameMark(functionName, 'Program Creation Start', [request.relativePath], true));
|
|
269
|
-
const program = createOptimizedProgram(request.projectPath, request.compilerOptions, request.
|
|
269
|
+
const program = createOptimizedProgram(request.projectPath, request.compilerOptions, request.resolvedVariantMap.map(([, fileUrl]) => fileURLToPath(fileUrl)), {}, tracker, functionName, [request.relativePath]);
|
|
270
270
|
const programWrapperEnd = tracker.mark(nameMark(functionName, 'Program Creation End', [request.relativePath], true));
|
|
271
271
|
tracker.measure(nameMark(functionName, 'Program Creation', [request.relativePath], true), programWrapperStart, programWrapperEnd);
|
|
272
|
-
const internalTypesCache = {};
|
|
273
272
|
|
|
274
273
|
// Process variants in parallel
|
|
275
274
|
const resolvedVariantMap = new Map(request.resolvedVariantMap);
|
|
@@ -288,9 +287,12 @@ export async function processTypes(request) {
|
|
|
288
287
|
|
|
289
288
|
// Use parseFromProgram directly - it now handles namespace exports,
|
|
290
289
|
// type aliases, and re-exports properly
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
const parsed = parseFromProgram(entrypoint, program, PARSER_OPTIONS);
|
|
291
|
+
|
|
292
|
+
// Modules of constants exported as a namespace (`export * as ButtonDataAttributes`)
|
|
293
|
+
// arrive flattened into one export per member; document each as a single group.
|
|
294
|
+
const constantNamespaces = findConstantNamespaces(entrypoint, program);
|
|
295
|
+
const exports = foldConstantNamespaces(parsed.exports, constantNamespaces);
|
|
294
296
|
|
|
295
297
|
// Re-add configured props that the parser dropped because they are
|
|
296
298
|
// inherited from an externally declared type in node_modules
|
|
@@ -317,36 +319,7 @@ export async function processTypes(request) {
|
|
|
317
319
|
// Include files from the TypeScript program for hot reloading support
|
|
318
320
|
// We collect only files imported by THIS entrypoint, not all files in the program
|
|
319
321
|
const entrypointDependencies = collectSourceFileDependencies(sourceFile, program, new Set());
|
|
320
|
-
const dependencies = [...request.dependencies, entrypoint, ...
|
|
321
|
-
|
|
322
|
-
// Parse meta files (DataAttributes, CssVars) for additional type information
|
|
323
|
-
const allInternalTypes = request.metaFiles.map(file => {
|
|
324
|
-
if (internalTypesCache[file]) {
|
|
325
|
-
return internalTypesCache[file];
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Ensure the file is loaded in the program first
|
|
329
|
-
// This is important for meta files (DataAttributes, CssVars) that aren't imported
|
|
330
|
-
const fileSourceFile = program.getSourceFile(file);
|
|
331
|
-
if (!fileSourceFile) {
|
|
332
|
-
console.warn(`[processTypes] ${variantName} - Could not load source file: ${file}`);
|
|
333
|
-
return [];
|
|
334
|
-
}
|
|
335
|
-
const {
|
|
336
|
-
exports: internalExport
|
|
337
|
-
} = parseFromProgram(file, program, PARSER_OPTIONS);
|
|
338
|
-
|
|
339
|
-
// Metadata files may declare their members as an enum or as named constants;
|
|
340
|
-
// normalize both to a single constant group named after the file.
|
|
341
|
-
const groupExports = transformConstantGroup(file, internalExport);
|
|
342
|
-
internalTypesCache[file] = groupExports;
|
|
343
|
-
return groupExports;
|
|
344
|
-
});
|
|
345
|
-
const internalTypes = allInternalTypes.reduce((acc, cur) => {
|
|
346
|
-
acc.push(...cur);
|
|
347
|
-
return acc;
|
|
348
|
-
}, []);
|
|
349
|
-
const allTypes = [...exports, ...internalTypes];
|
|
322
|
+
const dependencies = [...request.dependencies, entrypoint, ...entrypointDependencies];
|
|
350
323
|
const parseEnd = tracker.mark(nameMark(functionName, `Variant ${variantName} Parsed`, [request.relativePath]));
|
|
351
324
|
tracker.measure(nameMark(functionName, `Variant ${variantName} Parsing`, [request.relativePath]), parseStart, parseEnd);
|
|
352
325
|
const variantEnd = tracker.mark(nameMark(functionName, `Variant ${variantName} Complete`, [request.relativePath], true));
|
|
@@ -355,15 +328,12 @@ export async function processTypes(request) {
|
|
|
355
328
|
variantName,
|
|
356
329
|
variantData: {
|
|
357
330
|
exports,
|
|
358
|
-
|
|
331
|
+
constantNamespaces: Array.from(constantNamespaces.keys()),
|
|
359
332
|
namespaces,
|
|
360
333
|
// Convert Map to Record for serialization across worker boundary
|
|
361
334
|
typeNameMap: mergedTypeNameMap.size > 0 ? Object.fromEntries(mergedTypeNameMap) : undefined
|
|
362
335
|
},
|
|
363
|
-
dependencies
|
|
364
|
-
debug: {
|
|
365
|
-
metaFilesCount: request.metaFiles.length
|
|
366
|
-
}
|
|
336
|
+
dependencies
|
|
367
337
|
};
|
|
368
338
|
} catch (error) {
|
|
369
339
|
throw new Error(`Failed to parse variant ${variantName} (${fileUrl}): \n${error && typeof error === 'object' && 'message' in error && error.message}`);
|
|
@@ -371,19 +341,15 @@ export async function processTypes(request) {
|
|
|
371
341
|
});
|
|
372
342
|
const variantResults = await Promise.all(variantPromises);
|
|
373
343
|
|
|
374
|
-
// Process results and collect dependencies
|
|
344
|
+
// Process results and collect dependencies
|
|
375
345
|
const variantData = {};
|
|
376
346
|
const allDependencies = [];
|
|
377
|
-
const debugInfo = {};
|
|
378
347
|
for (const result of variantResults) {
|
|
379
348
|
if (result) {
|
|
380
349
|
variantData[result.variantName] = result.variantData;
|
|
381
350
|
result.dependencies.forEach(file => {
|
|
382
351
|
allDependencies.push(file);
|
|
383
352
|
});
|
|
384
|
-
if (result.debug) {
|
|
385
|
-
debugInfo[result.variantName] = result.debug;
|
|
386
|
-
}
|
|
387
353
|
}
|
|
388
354
|
}
|
|
389
355
|
|
|
@@ -394,8 +360,7 @@ export async function processTypes(request) {
|
|
|
394
360
|
success: true,
|
|
395
361
|
variantData: serializedVariantData,
|
|
396
362
|
allDependencies,
|
|
397
|
-
performanceLogs: tracker.getLogs()
|
|
398
|
-
debug: Object.keys(debugInfo).length > 0 ? debugInfo[Object.keys(debugInfo)[0]] : undefined
|
|
363
|
+
performanceLogs: tracker.getLogs()
|
|
399
364
|
};
|
|
400
365
|
} catch (error) {
|
|
401
366
|
return {
|
|
@@ -201,6 +201,9 @@ export async function generateTypesMarkdown(options) {
|
|
|
201
201
|
// If there are multiple unique prefixes, don't strip anything
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Strips the common prefix, as component headings always are
|
|
205
|
+
const stripCommonPrefix = part => commonPrefix && part.startsWith(`${commonPrefix}.`) ? part.slice(commonPrefix.length + 1) : part;
|
|
206
|
+
|
|
204
207
|
// Helper function to generate markdown chunks for a single type
|
|
205
208
|
// When useFullName is true, the type name is not stripped of the common prefix
|
|
206
209
|
async function generateSingleTypeMarkdown(typeMeta, useFullName = false) {
|
|
@@ -228,12 +231,7 @@ export async function generateTypesMarkdown(options) {
|
|
|
228
231
|
};
|
|
229
232
|
|
|
230
233
|
// Helper to get display name - either full name or stripped prefix
|
|
231
|
-
const getDisplayName = part =>
|
|
232
|
-
if (useFullName) {
|
|
233
|
-
return part;
|
|
234
|
-
}
|
|
235
|
-
return commonPrefix && part.startsWith(`${commonPrefix}.`) ? part.slice(commonPrefix.length + 1) : part;
|
|
236
|
-
};
|
|
234
|
+
const getDisplayName = part => useFullName ? part : stripCommonPrefix(part);
|
|
237
235
|
if (typeMeta.type === 'component') {
|
|
238
236
|
// Use transformed name (e.g., "Component.Part" instead of "ComponentPart")
|
|
239
237
|
const part = typeMeta.name;
|
|
@@ -599,14 +597,12 @@ export async function generateTypesMarkdown(options) {
|
|
|
599
597
|
addHeading(3, displayName);
|
|
600
598
|
if (data.reExportOf) {
|
|
601
599
|
nodes.push(md.paragraph([md.text('Re-export of '), md.link(data.reExportOf.slug, data.reExportOf.name), md.text(` ${data.reExportOf.suffix}.`)]));
|
|
602
|
-
} else if (data.dataAttributesOf) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const anchorId = componentName.toLowerCase().replace(/\./g, '');
|
|
609
|
-
nodes.push(md.paragraph([md.text('CSS variables for '), md.link(`#${anchorId}`, componentName), md.text(' component.')]));
|
|
600
|
+
} else if (data.dataAttributesOf || data.cssVarsOf) {
|
|
601
|
+
// Link to the component whose table this group holds, then declare the group
|
|
602
|
+
// Link to the component's heading, named the way the heading itself is
|
|
603
|
+
const componentHeading = stripCommonPrefix(data.dataAttributesOf ?? data.cssVarsOf);
|
|
604
|
+
nodes.push(md.paragraph([md.text(data.dataAttributesOf ? 'Data attributes of ' : 'CSS variables of '), md.link(`#${componentHeading.toLowerCase().replaceAll('.', '')}`, componentHeading), md.text('.')]));
|
|
605
|
+
addCodeBlock(data.formattedCode, 'typescript');
|
|
610
606
|
} else if (data.enumMembers && data.enumMembers.length > 0) {
|
|
611
607
|
// Render enum as a table
|
|
612
608
|
if (data.descriptionText) {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Finds metadata files (DataAttributes, CssVars) in the directory of the given entrypoint file,
|
|
3
|
-
* or recursively within the given directory.
|
|
4
|
-
*
|
|
5
|
-
* @param entrypoint - A filesystem path pointing to the entrypoint file or a directory
|
|
6
|
-
* @param suffixes - File suffixes to search for (default: ['DataAttributes', 'CssVars'])
|
|
7
|
-
* @returns Array of filesystem paths for matching files
|
|
8
|
-
*/
|
|
9
|
-
export declare function findMetaFiles(entrypoint: string, suffixes?: string[]): Promise<string[]>;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// eslint-disable-next-line n/prefer-node-protocol
|
|
2
|
-
import path from 'path';
|
|
3
|
-
// eslint-disable-next-line n/prefer-node-protocol
|
|
4
|
-
import fs from 'fs/promises';
|
|
5
|
-
function hasSuffix(suffixes, filename) {
|
|
6
|
-
return suffixes.some(suffix => filename.endsWith(`${suffix}.d.ts`) || filename.endsWith(`${suffix}.ts`) || filename.endsWith(`${suffix}.tsx`));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Finds metadata files (DataAttributes, CssVars) in the directory of the given entrypoint file,
|
|
11
|
-
* or recursively within the given directory.
|
|
12
|
-
*
|
|
13
|
-
* @param entrypoint - A filesystem path pointing to the entrypoint file or a directory
|
|
14
|
-
* @param suffixes - File suffixes to search for (default: ['DataAttributes', 'CssVars'])
|
|
15
|
-
* @returns Array of filesystem paths for matching files
|
|
16
|
-
*/
|
|
17
|
-
export async function findMetaFiles(entrypoint, suffixes = ['DataAttributes', 'CssVars']) {
|
|
18
|
-
// Check if the path ends with / (directory hint) or check via stat
|
|
19
|
-
let dir;
|
|
20
|
-
if (entrypoint.endsWith('/')) {
|
|
21
|
-
// Path ends with / - it's explicitly a directory, walk it directly
|
|
22
|
-
dir = entrypoint.slice(0, -1); // Remove trailing slash for fs operations
|
|
23
|
-
} else {
|
|
24
|
-
// It's a file - walk its parent directory
|
|
25
|
-
dir = path.dirname(entrypoint);
|
|
26
|
-
}
|
|
27
|
-
const files = [];
|
|
28
|
-
async function walkDirectory(currentDir) {
|
|
29
|
-
const entries = await fs.readdir(currentDir, {
|
|
30
|
-
withFileTypes: true
|
|
31
|
-
});
|
|
32
|
-
const subdirectories = [];
|
|
33
|
-
for (const entry of entries) {
|
|
34
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
35
|
-
if (entry.isDirectory()) {
|
|
36
|
-
subdirectories.push(fullPath);
|
|
37
|
-
} else if (entry.isFile()) {
|
|
38
|
-
files.push(fullPath);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
await Promise.all(subdirectories.map(subdir => walkDirectory(subdir)));
|
|
42
|
-
}
|
|
43
|
-
await walkDirectory(dir);
|
|
44
|
-
return files.filter(file => hasSuffix(suffixes, file));
|
|
45
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type * as tae from 'typescript-api-extractor';
|
|
2
|
-
/**
|
|
3
|
-
* Normalizes a metadata file's exports into a single constant group named after the file.
|
|
4
|
-
*
|
|
5
|
-
* A constant group is a named, documented set of key/value constants belonging to one
|
|
6
|
-
* component — the data attributes it sets, or the CSS variables it reads. Authors may
|
|
7
|
-
* declare it as an enum named after the file, or as named literal constants; both end up
|
|
8
|
-
* as the same enum-shaped export so the rest of the pipeline sees one representation.
|
|
9
|
-
*
|
|
10
|
-
* Exports matching no known authoring style are returned unchanged. Once a group is formed
|
|
11
|
-
* it replaces the file's exports, so a metadata file that also exports something which is
|
|
12
|
-
* not a constant is a mistake: it throws rather than dropping those exports silently.
|
|
13
|
-
*/
|
|
14
|
-
export declare function transformConstantGroup(filePath: string, exports: tae.ExportNode[]): tae.ExportNode[];
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { EnumMember, EnumNode, ExportNode, TypeName } from 'typescript-api-extractor';
|
|
2
|
-
import { fileUrlToPortablePath } from "../loaderUtils/fileUrlToPortablePath.mjs";
|
|
3
|
-
import { getFileNameFromUrl } from "../loaderUtils/getFileNameFromUrl.mjs";
|
|
4
|
-
import { isEnumType, isLiteralType } from "./typeGuards.mjs";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Derives a constant group's name from its file path, e.g.
|
|
8
|
-
* `/src/accordion/panel/AccordionPanelCssVars.ts` becomes `AccordionPanelCssVars`.
|
|
9
|
-
*/
|
|
10
|
-
function getGroupName(filePath) {
|
|
11
|
-
const {
|
|
12
|
-
fileName,
|
|
13
|
-
extension
|
|
14
|
-
} = getFileNameFromUrl(fileUrlToPortablePath(filePath));
|
|
15
|
-
return extension ? fileName.slice(0, -extension.length) : fileName;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Reads a constant's value from its literal type.
|
|
20
|
-
*
|
|
21
|
-
* String literals arrive wrapped in quotes (`"data-open"`) while numeric literals do not,
|
|
22
|
-
* so both are normalized to the bare string an enum member would have carried. Anything
|
|
23
|
-
* else — objects, functions, booleans — is not a documentable constant.
|
|
24
|
-
*/
|
|
25
|
-
function readLiteralValue(value) {
|
|
26
|
-
if (typeof value === 'number') {
|
|
27
|
-
return String(value);
|
|
28
|
-
}
|
|
29
|
-
if (typeof value === 'string' && value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
30
|
-
return value.slice(1, -1);
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Normalizes a metadata file's exports into a single constant group named after the file.
|
|
37
|
-
*
|
|
38
|
-
* A constant group is a named, documented set of key/value constants belonging to one
|
|
39
|
-
* component — the data attributes it sets, or the CSS variables it reads. Authors may
|
|
40
|
-
* declare it as an enum named after the file, or as named literal constants; both end up
|
|
41
|
-
* as the same enum-shaped export so the rest of the pipeline sees one representation.
|
|
42
|
-
*
|
|
43
|
-
* Exports matching no known authoring style are returned unchanged. Once a group is formed
|
|
44
|
-
* it replaces the file's exports, so a metadata file that also exports something which is
|
|
45
|
-
* not a constant is a mistake: it throws rather than dropping those exports silently.
|
|
46
|
-
*/
|
|
47
|
-
export function transformConstantGroup(filePath, exports) {
|
|
48
|
-
const groupName = getGroupName(filePath);
|
|
49
|
-
|
|
50
|
-
// Already an enum declaration named after its file, so there is nothing to normalize.
|
|
51
|
-
if (exports.some(node => node.name === groupName && isEnumType(node.type))) {
|
|
52
|
-
return exports;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Literal constants are folded into members of a single group rather than left alongside
|
|
56
|
-
// it: names this generic (`open`, `index`, `disabled`) would shadow real types when
|
|
57
|
-
// resolving `{@link}` references.
|
|
58
|
-
const members = [];
|
|
59
|
-
const discarded = [];
|
|
60
|
-
for (const node of exports) {
|
|
61
|
-
const value = isLiteralType(node.type) ? readLiteralValue(node.type.value) : undefined;
|
|
62
|
-
if (value === undefined) {
|
|
63
|
-
discarded.push(node.name);
|
|
64
|
-
} else {
|
|
65
|
-
members.push(new EnumMember(node.name, value, node.documentation));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (members.length === 0) {
|
|
69
|
-
return exports;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// The group replaces the file's exports wholesale, so anything that is not a constant —
|
|
73
|
-
// a helper function, a type alias, an enum under another name, a constant widened off its
|
|
74
|
-
// literal type — would be documented nowhere. Metadata files are expected to hold
|
|
75
|
-
// constants only, so fail the build rather than drop these exports silently.
|
|
76
|
-
if (discarded.length > 0) {
|
|
77
|
-
throw new Error(`[transformConstantGroup] ${groupName} - metadata files must export only constants, but these are not: ${discarded.join(', ')}`);
|
|
78
|
-
}
|
|
79
|
-
return [new ExportNode(groupName, new EnumNode(new TypeName(groupName), members, undefined), undefined)];
|
|
80
|
-
}
|