@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-docs-infra",
3
- "version": "0.13.1-canary.0",
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": "764e3b3cfc14293e74e93f3413609025f502bab9"
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
- /** Suffix for data attributes enum name (default: 'DataAttributes') */
24
- dataAttributesSuffix?: string;
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
- }, allExports: tae.ExportNode[], typeNameMap: Record<string, string>, rewriteContext: TypeRewriteContext, options?: FormatComponentOptions): Promise<ComponentTypeMeta>;
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, allExports, typeNameMap, rewriteContext, options = {}) {
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
- // Find data attributes and CSS variables in a single loop
38
- let dataAttributes;
39
- let cssVariables;
40
-
41
- // For DataAttributes/CssVars lookup, use the originalName (before transformations).
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 && dataAttributes.type.kind === 'enum' ? sortObjectByKeys(await formatEnum(dataAttributes.type, descriptionReplacements), options.ordering?.dataAttributes ?? memberOrder.dataAttributes) : {},
135
- cssVariables: cssVariables && cssVariables.type.kind === 'enum' ? sortObjectByKeys(await formatEnum(cssVariables.type, descriptionReplacements), options.ordering?.cssVariables ?? memberOrder.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' | 'css-variables' | 'data-attributes';
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 DataAttributes types, the component name this type belongs to.
50
+ * For constant groups holding a component's data attributes, that component's name.
50
51
  */
51
52
  dataAttributesOf?: string;
52
53
  /**
53
- * For CssVars types, the component name this type belongs to.
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
- const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap);
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
- return raw;
66
- }
67
-
68
- // Handle DataAttributes types
69
- if (displayName.endsWith('.DataAttributes')) {
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 { readFile, stat } from 'node:fs/promises';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
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 { findMetaFiles } from "./findMetaFiles.mjs";
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.allTypes);
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.allTypes,
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.allTypes, variantResult.typeNameMap || {}, rewriteContext, {
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(/^(.+)\.(Props|State|DataAttributes|CssVars)$/);
394
+ const parts = typeMeta.name.match(/^(.+)\.Props$/);
474
395
  if (!parts) {
475
396
  return typeMeta;
476
397
  }
477
- const [, componentName, suffix] = parts;
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 (suffix === 'Props' && correspondingComponent.data.props) {
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
- lib,
20
- parserOptions
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 parseFromProgram(entryPaths[0], program, {
52
- ...PARSER_OPTIONS,
53
- ...parserOptions
54
- }).exports;
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
- allTypes: ExportNode[];
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 { transformConstantGroup } from "./transformConstantGroup.mjs";
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.allTypes) {
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.allEntrypoints, {}, tracker, functionName, [request.relativePath]);
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
- exports
293
- } = parseFromProgram(entrypoint, program, PARSER_OPTIONS);
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, ...request.metaFiles, ...entrypointDependencies];
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
- allTypes,
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 and debug info
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
- const componentName = data.dataAttributesOf;
604
- const anchorId = componentName.toLowerCase().replace(/\./g, '');
605
- nodes.push(md.paragraph([md.text('Data attributes for '), md.link(`#${anchorId}`, componentName), md.text(' component.')]));
606
- } else if (data.cssVarsOf) {
607
- const componentName = data.cssVarsOf;
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
- }