Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/app/docs-infra/pipeline/load-server-types-meta/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export const accordionPanelHeight = '--accordion-panel-height';

Both produce identical documentation. The constant form lets application bundlers inline and tree-shake the values, since nothing references a runtime enum object. Constants re-exported from another meta file (`export * from '../combobox/clear/ComboboxClearDataAttributes'`) are collected too, and belong to the group named after the re-exporting file.

Type-only exports (type aliases, interfaces) have no runtime constant to document and may sit next to either style as typing helpers. Mixing the two styles in one file is rejected: standalone constants next to the file's enum would silently disappear from the documentation, so the build fails instead.

### 4. Process Types in Worker

Type extraction is offloaded to a dedicated worker thread. Inside the worker:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ describe('transformConstantGroup', () => {
]);
});

it("throws when standalone constants sit alongside the file's enum, naming them", () => {
expect(() =>
transformSources({
'ComponentRootDataAttributes.ts': `
export enum ComponentRootDataAttributes {
/** Present when open. */
open = 'data-open',
}
/** Present when disabled. */
export const disabled = 'data-disabled';
`,
}),
).toThrow(/disabled/);
});

it("leaves type helpers next to the file's enum untouched", () => {
const exports = transformSources({
'ComponentRootDataAttributes.ts': `
export enum ComponentRootDataAttributes {
/** Present when open. */
open = 'data-open',
}
export type ComponentRootDataAttribute = keyof typeof ComponentRootDataAttributes;
`,
});

expect(exports.map((node) => node.name)).toEqual([
'ComponentRootDataAttributes',
'ComponentRootDataAttribute',
]);
});

it('throws when an enum under a different name sits alongside constants, naming it', () => {
expect(() =>
transformSources({
Expand Down Expand Up @@ -225,15 +257,31 @@ describe('transformConstantGroup', () => {
]);
});

it('throws when non-constant exports sit alongside constants, naming them', () => {
it('skips type-only exports, documenting only the runtime constants', () => {
const group = groupOf(
transformSources({
'ComponentRootDataAttributes.ts': `
export type AttributeName = 'data-open';

/** Present when open. */
export const open: AttributeName = 'data-open';
`,
}),
);

expect(membersOf(group.type)).toEqual([
{ name: 'open', value: 'data-open', description: 'Present when open.', type: undefined },
]);
});

it('throws when runtime exports that are not constants sit alongside constants, naming them', () => {
let message = '';
try {
transformSources({
'ComponentRootDataAttributes.ts': `
/** Present when open. */
export const open = 'data-open';

export type Attribute = string;
export function helper(): string {
return 'data-open';
}
Expand All @@ -244,7 +292,6 @@ describe('transformConstantGroup', () => {
}

expect(message).toContain('ComponentRootDataAttributes');
expect(message).toContain('Attribute');
expect(message).toContain('helper');
});

Expand All @@ -263,6 +310,17 @@ describe('transformConstantGroup', () => {
});

describe('unrecognized files', () => {
it('leaves a file exporting only types untouched', () => {
const exports = transformSources({
'ComponentRootDataAttributes.ts': `
export type AttributeName = 'data-open';
`,
});

expect(exports.every((node) => node.type.kind !== 'enum')).toBe(true);
expect(exports.map((node) => node.name)).toEqual(['AttributeName']);
});

it('leaves a file with no literal constants untouched', () => {
const exports = transformSources({
'ComponentRootDataAttributes.ts': `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,39 @@ function readLiteralValue(value: unknown): string | undefined {
return undefined;
}

/**
* Reads an export's constant value, or `undefined` when its type is not a
* supported literal.
*/
function readConstantValue(node: tae.ExportNode): string | undefined {
return isLiteralType(node.type) ? readLiteralValue(node.type.value) : undefined;
}

/**
* Whether an export is a documentable constant: a runtime value narrowed to a
* supported literal type. Type-only exports resolve to the same literal nodes
* (`export type X = 'data-open'`), so the declaration space is checked, not just
* the type shape.
*/
function isConstantExport(node: tae.ExportNode): boolean {
return node.isValue && readConstantValue(node) !== undefined;
}

/**
* Normalizes a metadata file's exports into a single constant group named after the file.
*
* A constant group is a named, documented set of key/value constants belonging to one
* component — the data attributes it sets, or the CSS variables it reads. Authors may
* declare it as an enum named after the file, or as named literal constants; both end up
* as the same enum-shaped export so the rest of the pipeline sees one representation.
* Non-value exports are skipped, never discarded: a typing helper next to the
* constants is not an authoring mistake.
*
* Exports matching no known authoring style are returned unchanged. Once a group is formed
* it replaces the file's exports, so a metadata file that also exports something which is
* not a constant is a mistake: it throws rather than dropping those exports silently.
* it replaces the file's exports, so a metadata file that also exports a runtime value
* which is not a constant is a mistake: it throws rather than dropping those exports
* silently. Mixing standalone constants with the file's enum throws for the same reason —
* downstream matching reads only the enum, so the constants would vanish from the docs.
*/
export function transformConstantGroup(
filePath: string,
Expand All @@ -56,7 +78,17 @@ export function transformConstantGroup(
const groupName = getGroupName(filePath);

// Already an enum declaration named after its file, so there is nothing to normalize.
if (exports.some((node) => node.name === groupName && isEnumType(node.type))) {
const groupEnum = exports.find((node) => node.name === groupName && isEnumType(node.type));
if (groupEnum) {
const mixedConstants = exports
.filter((node) => node !== groupEnum && isConstantExport(node))
.map((node) => node.name);
if (mixedConstants.length > 0) {
throw new Error(
`[transformConstantGroup] ${groupName} - metadata files must not mix standalone constants with the file's enum, move these into the enum: ${mixedConstants.join(', ')}`,
);
}

return exports;
}

Expand All @@ -67,7 +99,11 @@ export function transformConstantGroup(
const discarded: string[] = [];

for (const node of exports) {
const value = isLiteralType(node.type) ? readLiteralValue(node.type.value) : undefined;
if (!node.isValue) {
continue;
}

const value = readConstantValue(node);
if (value === undefined) {
discarded.push(node.name);
} else {
Expand All @@ -79,8 +115,8 @@ export function transformConstantGroup(
return exports;
}

// The group replaces the file's exports wholesale, so anything that is not a constant —
// a helper function, a type alias, an enum under another name, a constant widened off its
// The group replaces the file's exports wholesale, so a runtime export that is not a
// constant — a helper function, an enum under another name, a constant widened off its
// literal type — would be documented nowhere. Metadata files are expected to hold
// constants only, so fail the build rather than drop these exports silently.
if (discarded.length > 0) {
Expand All @@ -90,6 +126,15 @@ export function transformConstantGroup(
}

return [
new ExportNode(groupName, new EnumNode(new TypeName(groupName), members, undefined), undefined),
new ExportNode(
groupName,
new EnumNode(new TypeName(groupName), members, undefined),
undefined,
{
isValue: true,
isType: true,
isNamespace: false,
},
),
];
}
Loading