Skip to content

Commit d666e92

Browse files
committed
Move the config to scripts
1 parent b58ccca commit d666e92

File tree

4 files changed

+307
-310
lines changed

4 files changed

+307
-310
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"preinstall": "npx only-allow pnpm",
7-
"proptypes": "tsx ./packages/proptypes-builder/cli.ts",
7+
"proptypes": "tsx ./scripts/generateProptypes.ts",
88
"deduplicate": "pnpm dedupe",
99
"benchmark:browser": "pnpm --filter benchmark browser",
1010
"build": "lerna run --scope \"@mui/*\" build",
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/* eslint-disable no-console */
2+
import * as path from 'path';
3+
import * as fse from 'fs-extra';
4+
import * as prettier from 'prettier';
5+
import glob from 'fast-glob';
6+
import * as _ from 'lodash';
7+
import {
8+
CreateTypeScriptProjectOptions,
9+
fixBabelGeneratorIssues,
10+
fixLineEndings,
11+
getUnstyledFilename,
12+
createTypeScriptProjectBuilder,
13+
TypeScriptProject,
14+
} from '@mui-internal/docs-utils';
15+
import {
16+
getPropTypesFromFile,
17+
injectPropTypesInFile,
18+
InjectPropTypesInFileOptions,
19+
LiteralType,
20+
} from '@mui-internal/typescript-to-proptypes';
21+
import { ProjectSettings } from './ProjectSettings';
22+
23+
export async function generatePropTypes(
24+
projectsSettings: ProjectSettings[],
25+
pattern: RegExp | null,
26+
prettierConfigPath: string,
27+
) {
28+
if (pattern != null) {
29+
console.log(`Only considering declaration files matching ${pattern.source}`);
30+
}
31+
32+
const allTypeScriptProjects = projectsSettings
33+
.map((setting) => setting.typeScriptProject)
34+
.reduce((acc, project) => {
35+
acc[project.name] = project;
36+
return acc;
37+
}, {} as Record<string, CreateTypeScriptProjectOptions>);
38+
39+
const buildTypeScriptProject = createTypeScriptProjectBuilder(allTypeScriptProjects);
40+
41+
const prettierConfig = prettier.resolveConfig.sync(process.cwd(), {
42+
config: prettierConfigPath,
43+
});
44+
45+
const promises: Promise<void>[] = [];
46+
for (let i = 0; i < projectsSettings.length; i += 1) {
47+
const projectSettings = projectsSettings[i];
48+
const typeScriptProject = buildTypeScriptProject(projectSettings.typeScriptProject.name);
49+
50+
generatePropTypesForProject(typeScriptProject, projectSettings, pattern, prettierConfig);
51+
}
52+
53+
const results = await Promise.allSettled(promises);
54+
55+
const fails = results.filter((result): result is PromiseRejectedResult => {
56+
return result.status === 'rejected';
57+
});
58+
59+
fails.forEach((result) => {
60+
console.error(result.reason);
61+
});
62+
if (fails.length > 0) {
63+
process.exit(1);
64+
}
65+
}
66+
67+
function sortBreakpointsLiteralByViewportAscending(a: LiteralType, b: LiteralType) {
68+
// default breakpoints ordered by their size ascending
69+
const breakpointOrder: readonly unknown[] = ['"xs"', '"sm"', '"md"', '"lg"', '"xl"'];
70+
71+
return breakpointOrder.indexOf(a.value) - breakpointOrder.indexOf(b.value);
72+
}
73+
74+
function sortSizeByScaleAscending(a: LiteralType, b: LiteralType) {
75+
const sizeOrder: readonly unknown[] = ['"small"', '"medium"', '"large"'];
76+
return sizeOrder.indexOf(a.value) - sizeOrder.indexOf(b.value);
77+
}
78+
79+
async function generatePropTypesForProject(
80+
typeScriptProject: TypeScriptProject,
81+
projectSettings: ProjectSettings,
82+
pattern: RegExp | null,
83+
prettierConfig: prettier.Options | null,
84+
) {
85+
// Matches files where the folder and file both start with uppercase letters
86+
// Example: AppBar/AppBar.d.ts
87+
const allFiles = await glob('+([A-Z])*/+([A-Z])*.*@(d.ts|ts|tsx)', {
88+
absolute: true,
89+
cwd: projectSettings.rootPath,
90+
});
91+
92+
const files = _.flatten(allFiles)
93+
.filter((filePath) => {
94+
// Filter out files where the directory name and filename doesn't match
95+
// Example: Modal/ModalManager.d.ts
96+
let folderName = path.basename(path.dirname(filePath));
97+
const fileName = path.basename(filePath).replace(/(\.d\.ts|\.tsx|\.ts)/g, '');
98+
99+
// An exception is if the folder name starts with Unstable_/unstable_
100+
// Example: Unstable_Grid2/Grid2.tsx
101+
if (/(u|U)nstable_/g.test(folderName)) {
102+
folderName = folderName.slice(9);
103+
}
104+
105+
return fileName === folderName;
106+
})
107+
.filter((filePath) => pattern?.test(filePath) ?? true);
108+
109+
const promises = files.map<Promise<void>>(async (tsFile) => {
110+
const sourceFile = tsFile.includes('.d.ts') ? tsFile.replace('.d.ts', '.js') : tsFile;
111+
try {
112+
await generatePropTypesForFile(
113+
typeScriptProject,
114+
projectSettings,
115+
sourceFile,
116+
tsFile,
117+
prettierConfig,
118+
);
119+
} catch (error: any) {
120+
error.message = `${tsFile}: ${error.message}`;
121+
throw error;
122+
}
123+
});
124+
125+
return promises;
126+
}
127+
128+
// Custom order of literal unions by component
129+
const getSortLiteralUnions: InjectPropTypesInFileOptions['getSortLiteralUnions'] = (
130+
component,
131+
propType,
132+
) => {
133+
if (
134+
component.name === 'Hidden' &&
135+
(propType.name === 'initialWidth' || propType.name === 'only')
136+
) {
137+
return sortBreakpointsLiteralByViewportAscending;
138+
}
139+
140+
if (propType.name === 'size') {
141+
return sortSizeByScaleAscending;
142+
}
143+
144+
return undefined;
145+
};
146+
147+
async function generatePropTypesForFile(
148+
project: TypeScriptProject,
149+
projectSettings: ProjectSettings,
150+
sourceFile: string,
151+
tsFile: string,
152+
prettierConfig: prettier.Options | null,
153+
): Promise<void> {
154+
const components = getPropTypesFromFile({
155+
filePath: tsFile,
156+
project,
157+
shouldResolveObject: ({ name }) => {
158+
if (
159+
name.toLowerCase().endsWith('classes') ||
160+
name === 'theme' ||
161+
name === 'ownerState' ||
162+
(name.endsWith('Props') && name !== 'componentsProps' && name !== 'slotProps')
163+
) {
164+
return false;
165+
}
166+
return undefined;
167+
},
168+
checkDeclarations: true,
169+
});
170+
171+
if (components.length === 0) {
172+
return;
173+
}
174+
175+
// exclude internal slot components, e.g. ButtonRoot
176+
const cleanComponents = components.filter((component) => {
177+
if (component.propsFilename?.endsWith('.tsx')) {
178+
// only check for .tsx
179+
const match = component.propsFilename.match(/.*\/([A-Z][a-zA-Z]+)\.tsx/);
180+
if (match) {
181+
return component.name === match[1];
182+
}
183+
}
184+
return true;
185+
});
186+
187+
const { useExternalDocumentation = {}, ignoreExternalDocumentation = {} } = projectSettings;
188+
189+
cleanComponents.forEach((component) => {
190+
component.types.forEach((prop) => {
191+
if (
192+
!prop.jsDoc ||
193+
(project.name !== 'base' &&
194+
ignoreExternalDocumentation[component.name] &&
195+
ignoreExternalDocumentation[component.name].includes(prop.name))
196+
) {
197+
prop.jsDoc = '@ignore';
198+
}
199+
});
200+
});
201+
202+
const sourceContent = await fse.readFile(sourceFile, 'utf8');
203+
const isTsFile = /(\.(ts|tsx))/.test(sourceFile);
204+
// If the component inherits the props from some unstyled components
205+
// we don't want to add those propTypes again in the Material UI/Joy UI propTypes
206+
const unstyledFile = getUnstyledFilename(tsFile, true);
207+
const unstyledPropsFile = unstyledFile.replace('.d.ts', '.types.ts');
208+
209+
// TODO remove, should only have .types.ts
210+
const propsFile = tsFile.replace(/(\.d\.ts|\.tsx|\.ts)/g, 'Props.ts');
211+
const propsFileAlternative = tsFile.replace(/(\.d\.ts|\.tsx|\.ts)/g, '.types.ts');
212+
const generatedForTypeScriptFile = sourceFile === tsFile;
213+
const result = injectPropTypesInFile({
214+
components,
215+
target: sourceContent,
216+
options: {
217+
disablePropTypesTypeChecking: generatedForTypeScriptFile,
218+
babelOptions: {
219+
filename: sourceFile,
220+
},
221+
comment: [
222+
'┌────────────────────────────── Warning ──────────────────────────────┐',
223+
'│ These PropTypes are generated from the TypeScript type definitions. │',
224+
isTsFile
225+
? '│ To update them, edit the TypeScript types and run `pnpm proptypes`. │'
226+
: '│ To update them, edit the d.ts file and run `pnpm proptypes`. │',
227+
'└─────────────────────────────────────────────────────────────────────┘',
228+
].join('\n'),
229+
ensureBabelPluginTransformReactRemovePropTypesIntegration: true,
230+
getSortLiteralUnions,
231+
reconcilePropTypes: (prop, previous, generated) => {
232+
const usedCustomValidator = previous !== undefined && !previous.startsWith('PropTypes');
233+
const ignoreGenerated =
234+
previous !== undefined &&
235+
previous.startsWith('PropTypes /* @typescript-to-proptypes-ignore */');
236+
237+
if (
238+
ignoreGenerated &&
239+
// `ignoreGenerated` implies that `previous !== undefined`
240+
previous!
241+
.replace('PropTypes /* @typescript-to-proptypes-ignore */', 'PropTypes')
242+
.replace(/\s/g, '') === generated.replace(/\s/g, '')
243+
) {
244+
throw new Error(
245+
`Unused \`@typescript-to-proptypes-ignore\` directive for prop '${prop.name}'.`,
246+
);
247+
}
248+
249+
if (usedCustomValidator || ignoreGenerated) {
250+
// `usedCustomValidator` and `ignoreGenerated` narrow `previous` to `string`
251+
return previous!;
252+
}
253+
254+
return generated;
255+
},
256+
shouldInclude: ({ component, prop }) => {
257+
if (prop.name === 'children') {
258+
return true;
259+
}
260+
let shouldDocument;
261+
const { name: componentName } = component;
262+
263+
prop.filenames.forEach((filename) => {
264+
const isExternal = filename !== tsFile;
265+
const implementedByUnstyledVariant =
266+
filename === unstyledFile || filename === unstyledPropsFile;
267+
const implementedBySelfPropsFile =
268+
filename === propsFile || filename === propsFileAlternative;
269+
if (!isExternal || implementedByUnstyledVariant || implementedBySelfPropsFile) {
270+
shouldDocument = true;
271+
}
272+
});
273+
274+
if (
275+
useExternalDocumentation[componentName] &&
276+
(useExternalDocumentation[componentName] === '*' ||
277+
useExternalDocumentation[componentName].includes(prop.name))
278+
) {
279+
shouldDocument = true;
280+
}
281+
282+
return shouldDocument;
283+
},
284+
},
285+
});
286+
287+
if (!result) {
288+
throw new Error('Unable to produce inject propTypes into code.');
289+
}
290+
291+
const prettified = prettier.format(result, { ...prettierConfig, filepath: sourceFile });
292+
const formatted = fixBabelGeneratorIssues(prettified);
293+
const correctedLineEndings = fixLineEndings(sourceContent, formatted);
294+
295+
await fse.writeFile(sourceFile, correctedLineEndings);
296+
}

0 commit comments

Comments
 (0)