init project
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled

This commit is contained in:
how2ice
2025-12-12 14:26:25 +09:00
commit 005cf56baf
43188 changed files with 1079531 additions and 0 deletions

View File

@@ -0,0 +1 @@
.tsbuildinfo

View File

@@ -0,0 +1,13 @@
# Changelog
## 1.0.3
Renamed the package to @mui/internal-docs-utils
## 1.0.2
Fixed incorrectly released package.
## 1.0.0
Initial release as an npm package.

View File

@@ -0,0 +1,9 @@
# @mui/internal-docs-utils
This package contains utilities shared between MUI docs generation scripts.
This is an internal package not meant for general use.
## Release
1. Build the project: `pnpm build`
2. Publish the build artifacts to npm: `pnpm release:publish`

View File

@@ -0,0 +1,30 @@
{
"name": "@mui/internal-docs-utils",
"version": "2.0.5",
"author": "MUI Team",
"description": "Utilities for MUI docs. This is an internal package not meant for general use.",
"main": "./build/index.js",
"exports": {
".": "./build/index.js"
},
"types": "./build/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/mui/material-ui.git",
"directory": "packages-internal/docs-utils"
},
"scripts": {
"prebuild": "rimraf ./build",
"build": "tsc -p tsconfig.build.json",
"typescript": "tsc -p tsconfig.json",
"release:publish": "pnpm build && pnpm publish --tag latest",
"release:publish:dry-run": "pnpm build && pnpm publish --tag latest --registry=\"http://localhost:4873/\""
},
"dependencies": {
"rimraf": "^6.1.2",
"typescript": "^5.9.3"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,11 @@
/**
* @deprecated Import if from '@mui-internal/api-docs-builder'
*/
export interface ComponentClassDefinition {
key: string;
className: string;
description: string;
isGlobal: boolean;
isDeprecated?: boolean;
deprecationInfo?: string;
}

View File

@@ -0,0 +1,131 @@
import path from 'path';
import fs from 'fs';
import * as ts from 'typescript';
export interface TypeScriptProject {
name: string;
rootPath: string;
exports: Record<string, ts.Symbol>;
program: ts.Program;
checker: ts.TypeChecker;
}
export interface CreateTypeScriptProjectOptions {
name: string;
rootPath: string;
/**
* Config to use to build this package.
* The path must be relative to the root path.
* @default 'tsconfig.build.json`
*/
tsConfigPath?: string;
/**
* File used as root of the package.
* This property is used to gather the exports of the project.
* The path must be relative to the root path.
*/
entryPointPath?: string;
/**
* Files to include in the project.
* By default, it will use the files defined in the tsconfig.
*/
files?: string[];
}
export const createTypeScriptProject = (
options: CreateTypeScriptProjectOptions,
): TypeScriptProject => {
const {
name,
rootPath,
tsConfigPath: inputTsConfigPath = 'tsconfig.build.json',
entryPointPath: inputEntryPointPath,
files,
} = options;
const tsConfigPath = path.join(rootPath, inputTsConfigPath);
const tsConfigFile = ts.readConfigFile(tsConfigPath, (filePath) =>
fs.readFileSync(filePath).toString(),
);
if (tsConfigFile.error) {
throw tsConfigFile.error;
}
// The build config does not parse the `.d.ts` files, but we sometimes need them to get the exports.
if (tsConfigFile.config.exclude) {
tsConfigFile.config.exclude = tsConfigFile.config.exclude.filter(
(pattern: string) => pattern !== 'src/**/*.d.ts',
);
}
const tsConfigFileContent = ts.parseJsonConfigFileContent(
tsConfigFile.config,
ts.sys,
path.dirname(tsConfigPath),
);
if (tsConfigFileContent.errors.length > 0) {
throw tsConfigFileContent.errors[0];
}
const program = ts.createProgram({
rootNames: files ?? tsConfigFileContent.fileNames,
options: tsConfigFileContent.options,
});
const checker = program.getTypeChecker();
let exports: TypeScriptProject['exports'];
if (inputEntryPointPath) {
const entryPointPath = path.join(rootPath, inputEntryPointPath);
const sourceFile = program.getSourceFile(entryPointPath);
exports = Object.fromEntries(
checker.getExportsOfModule(checker.getSymbolAtLocation(sourceFile!)!).map((symbol) => {
return [symbol.name, symbol];
}),
);
} else {
exports = {};
}
return {
name,
rootPath,
exports,
program,
checker,
};
};
export type TypeScriptProjectBuilder = (
projectName: string,
options?: { files?: string[] },
) => TypeScriptProject;
export const createTypeScriptProjectBuilder = (
projectsConfig: Record<string, Omit<CreateTypeScriptProjectOptions, 'name'>>,
): TypeScriptProjectBuilder => {
const projects = new Map<string, TypeScriptProject>();
return (projectName: string, options: { files?: string[] } = {}) => {
const cachedProject = projects.get(projectName);
if (cachedProject != null) {
return cachedProject;
}
// eslint-disable-next-line no-console
console.log(`Building new TS project: ${projectName}`);
const project = createTypeScriptProject({
name: projectName,
...projectsConfig[projectName],
...options,
});
projects.set(projectName, project);
return project;
};
};

View File

@@ -0,0 +1,358 @@
import * as ts from 'typescript';
import { TypeScriptProject } from './createTypeScriptProject';
export interface ParsedProp {
/**
* If `true`, some signatures do not contain this property.
* For example: `id` in `{ id: number, value: string } | { value: string }`
*/
onlyUsedInSomeSignatures: boolean;
signatures: { symbol: ts.Symbol; componentType: ts.Type }[];
}
export interface ParsedComponent {
name: string;
location: ts.Node;
type: ts.Type;
sourceFile: ts.SourceFile | undefined;
props: Record<string, ParsedProp>;
}
function isTypeJSXElementLike(type: ts.Type, project: TypeScriptProject): boolean {
const symbol = type.symbol ?? type.aliasSymbol;
if (symbol) {
const name = project.checker.getFullyQualifiedName(symbol);
return (
// Remove once global JSX namespace is no longer used by React
name === 'global.JSX.Element' ||
name === 'React.JSX.Element' ||
name === 'React.ReactElement' ||
name === 'React.ReactNode'
);
}
if (type.isUnion()) {
return type.types.every(
// eslint-disable-next-line no-bitwise
(subType) => subType.flags & ts.TypeFlags.Null || isTypeJSXElementLike(subType, project),
);
}
return false;
}
function isStyledFunction(node: ts.VariableDeclaration): boolean {
return (
!!node.initializer &&
ts.isCallExpression(node.initializer) &&
ts.isCallExpression(node.initializer.expression) &&
ts.isIdentifier(node.initializer.expression.expression) &&
node.initializer.expression.expression.escapedText === 'styled'
);
}
function getJSXLikeReturnValueFromFunction(type: ts.Type, project: TypeScriptProject) {
return type
.getCallSignatures()
.filter((signature) => isTypeJSXElementLike(signature.getReturnType(), project));
}
function parsePropsType({
name,
type,
shouldInclude = () => true,
location,
sourceFile,
}: {
name: string;
type: ts.Type;
location: ts.Node;
shouldInclude?: (data: { name: string; depth: number }) => boolean;
sourceFile: ts.SourceFile | undefined;
}): ParsedComponent {
const parsedProps: Record<string, ParsedProp> = {};
type
.getProperties()
.filter((property) => shouldInclude({ name: property.getName(), depth: 1 }))
.forEach((property) => {
parsedProps[property.getName()] = {
signatures: [
{
symbol: property,
componentType: type,
},
],
onlyUsedInSomeSignatures: false,
};
});
return {
name,
location,
type,
sourceFile,
props: parsedProps,
};
}
function parseFunctionComponent({
node,
shouldInclude,
project,
}: {
node: ts.VariableDeclaration | ts.FunctionDeclaration;
shouldInclude?: (data: { name: string; depth: number }) => boolean;
project: TypeScriptProject;
}): ParsedComponent | null {
if (!node.name) {
return null;
}
const symbol = project.checker.getSymbolAtLocation(node.name);
if (!symbol) {
return null;
}
const componentName = node.name.getText();
// Discriminate render functions to components
if (componentName[0].toUpperCase() !== componentName[0]) {
return null;
}
const signatures = getJSXLikeReturnValueFromFunction(
project.checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!),
project,
);
if (signatures.length === 0) {
return null;
}
const parsedComponents = signatures.map((signature) =>
parsePropsType({
shouldInclude,
name: componentName,
type: project.checker.getTypeOfSymbolAtLocation(
signature.parameters[0],
signature.parameters[0].valueDeclaration!,
),
location: signature.parameters[0].valueDeclaration!,
sourceFile: node.getSourceFile(),
}),
);
const squashedProps: Record<string, ParsedProp> = {};
parsedComponents.forEach((parsedComponent) => {
Object.keys(parsedComponent.props).forEach((propName) => {
if (!squashedProps[propName]) {
squashedProps[propName] = parsedComponent.props[propName];
} else {
squashedProps[propName].signatures = [
...squashedProps[propName].signatures,
...parsedComponent.props[propName].signatures,
];
}
});
});
const squashedParsedComponent: ParsedComponent = {
...parsedComponents[0],
props: squashedProps,
};
Object.keys(squashedParsedComponent.props).forEach((propName) => {
squashedParsedComponent.props[propName].onlyUsedInSomeSignatures =
squashedParsedComponent.props[propName].signatures.length < signatures.length;
});
return squashedParsedComponent;
}
export interface GetPropsFromComponentDeclarationOptions {
project: TypeScriptProject;
node: ts.Node;
/**
* Called before a PropType is added to a component/object
* @returns true to include the prop, false to skip it
*/
shouldInclude?: (data: { name: string; depth: number }) => boolean;
/**
* Control if const declarations should be checked
* @default false
* @example declare const Component: React.JSXElementConstructor<Props>;
*/
checkDeclarations?: boolean;
}
function getPropsFromVariableDeclaration({
node,
project,
checkDeclarations,
shouldInclude,
}: { node: ts.VariableDeclaration } & Pick<
GetPropsFromComponentDeclarationOptions,
'project' | 'checkDeclarations' | 'shouldInclude'
>) {
const type = project.checker.getTypeAtLocation(node.name);
if (!node.initializer) {
if (
checkDeclarations &&
type.aliasSymbol &&
type.aliasTypeArguments &&
project.checker.getFullyQualifiedName(type.aliasSymbol) === 'React.JSXElementConstructor'
) {
const propsType = type.aliasTypeArguments[0];
if (propsType === undefined) {
throw new TypeError(
'Unable to find symbol for `props`. This is a bug in typescript-to-proptypes.',
);
}
return parsePropsType({
name: node.name.getText(),
type: propsType,
location: node.name,
shouldInclude,
sourceFile: node.getSourceFile(),
});
}
if (checkDeclarations) {
return parseFunctionComponent({
node,
shouldInclude,
project,
});
}
return null;
}
if (
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
node.initializer.parameters.length === 1
) {
return parseFunctionComponent({
node,
shouldInclude,
project,
});
}
// x = React.memo((props:type) { return <div/> })
// x = React.forwardRef((props:type) { return <div/> })
if (ts.isCallExpression(node.initializer) && node.initializer.arguments.length > 0) {
const potentialComponent = node.initializer.arguments[0];
if (
(ts.isArrowFunction(potentialComponent) || ts.isFunctionExpression(potentialComponent)) &&
potentialComponent.parameters.length > 0 &&
getJSXLikeReturnValueFromFunction(
project.checker.getTypeAtLocation(potentialComponent),
project,
).length > 0
) {
const propsSymbol = project.checker.getSymbolAtLocation(
potentialComponent.parameters[0].name,
);
if (propsSymbol) {
return parsePropsType({
name: node.name.getText(),
type: project.checker.getTypeOfSymbolAtLocation(
propsSymbol,
propsSymbol.valueDeclaration!,
),
location: propsSymbol.valueDeclaration!,
shouldInclude,
sourceFile: node.getSourceFile(),
});
}
}
}
// handle component factories: x = createComponent()
if (
checkDeclarations &&
node.initializer &&
!isStyledFunction(node) &&
getJSXLikeReturnValueFromFunction(type, project).length > 0
) {
return parseFunctionComponent({
node,
shouldInclude,
project,
});
}
return null;
}
export function getPropsFromComponentNode({
node,
shouldInclude,
project,
checkDeclarations,
}: GetPropsFromComponentDeclarationOptions): ParsedComponent | null {
let parsedComponent: ParsedComponent | null = null;
// function x(props: type) { return <div/> }
if (
ts.isFunctionDeclaration(node) &&
node.name &&
node.parameters.length === 1 &&
getJSXLikeReturnValueFromFunction(project.checker.getTypeAtLocation(node.name), project)
.length > 0
) {
parsedComponent = parseFunctionComponent({ node, shouldInclude, project });
} else if (ts.isVariableDeclaration(node)) {
parsedComponent = getPropsFromVariableDeclaration({
node,
project,
checkDeclarations,
shouldInclude,
});
} else if (ts.isVariableStatement(node)) {
// const x = ...
ts.forEachChild(node.declarationList, (variableNode) => {
if (parsedComponent != null) {
return;
}
// x = (props: type) => { return <div/> }
// x = function(props: type) { return <div/> }
// x = function y(props: type) { return <div/> }
// x = react.memo((props:type) { return <div/> })
if (ts.isVariableDeclaration(variableNode) && variableNode.name) {
parsedComponent = getPropsFromVariableDeclaration({
node: variableNode,
project,
checkDeclarations,
shouldInclude,
});
}
if (
ts.isClassDeclaration(variableNode) &&
variableNode.name &&
variableNode.heritageClauses &&
variableNode.heritageClauses.length === 1
) {
const heritage = variableNode.heritageClauses[0];
if (heritage.types.length !== 1) {
return;
}
const arg = heritage.types[0];
if (!arg.typeArguments) {
return;
}
parsedComponent = parsePropsType({
shouldInclude,
name: variableNode.name.getText(),
location: arg.typeArguments[0],
type: project.checker.getTypeAtLocation(arg.typeArguments[0]),
sourceFile: node.getSourceFile(),
});
}
});
}
return parsedComponent;
}

View File

@@ -0,0 +1,55 @@
import { EOL } from 'os';
export * from './createTypeScriptProject';
export { type ComponentClassDefinition } from './ComponentClassDefinition';
export * from './getPropsFromComponentNode';
export function getLineFeed(source: string): string {
const match = source.match(/\r?\n/);
return match === null ? EOL : match[0];
}
const fixBabelIssuesRegExp = /(?<=(\/>)|,)(\r?\n){2}/g;
export function fixBabelGeneratorIssues(source: string): string {
return source.replace(fixBabelIssuesRegExp, '\n');
}
export function fixLineEndings(source: string, target: string): string {
return target.replace(/\r?\n/g, getLineFeed(source));
}
/**
* Converts styled or regular component d.ts file to unstyled d.ts
* @param filename - the file of the styled or regular mui component
*/
export function getUnstyledFilename(filename: string, definitionFile: boolean = false): string {
if (filename.includes('mui-base')) {
return filename;
}
let unstyledFile = '';
const separator = filename.includes('/') ? '/' : '\\';
if (!filename.includes('mui-base')) {
unstyledFile = filename
.replace(/.d.ts$/, '')
.replace(/.tsx?$/, '')
.replace(/.js$/, '');
unstyledFile = unstyledFile.replace(/Styled/g, '');
if (separator === '/') {
unstyledFile = unstyledFile.replace(
/packages\/mui-lab|packages\/mui-material/g,
'packages/mui-base',
);
} else {
unstyledFile = unstyledFile.replace(
/packages\\mui-lab|packages\\mui-material/g,
'packages\\mui-base',
);
}
}
return definitionFile ? `${unstyledFile}.d.ts` : `${unstyledFile}.js`;
}

View File

@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build",
"declaration": true,
"noEmit": false,
"composite": true,
"tsBuildInfoFile": "./build/.tsbuildinfo",
"target": "ES2020",
"types": ["node"]
},
"exclude": ["./test/*.ts"]
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"noEmit": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"types": ["node"],
"strict": true,
"esModuleInterop": true,
"isolatedModules": true
},
"include": ["./src/**/*.ts"]
}