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,33 @@
/* eslint-disable no-console */
const path = require('path');
const gm = require('gm');
const SIZES = [48, 96, 180, 192, 256, 384, 512];
const INPUT_ICON = path.join(__dirname, '../public/static/logo.png');
const OUTPUT_DIR = path.join(__dirname, '../public/static/icons');
console.log('Generating Icons');
const promises = SIZES.map(
(size) =>
new Promise((resolve, reject) => {
gm(INPUT_ICON)
.resize(size, size)
.write(path.join(OUTPUT_DIR, `${size}x${size}.png`), (err) => {
if (err) {
reject(err);
return;
}
resolve();
console.log(`Size ${size} created`);
});
}),
);
Promise.all(promises).catch((err) => {
setTimeout(() => {
console.log(err);
throw err;
});
});

View File

@@ -0,0 +1,25 @@
/* eslint-disable no-console */
const path = require('path');
const fs = require('node:fs/promises');
async function prepend(file, string) {
const data = await fs.readFile(file, 'utf8');
await fs.writeFile(file, string + data, 'utf8');
}
async function run() {
const swDest = path.join(__dirname, '../export/sw.js');
const swSrc = path.join(__dirname, '../src/sw.js');
await fs.cp(swSrc, swDest, { recursive: true });
await prepend(
swDest,
`
// uuid: ${new Date()}
`,
);
console.log('Successfully built service worker');
}
run();

View File

@@ -0,0 +1,244 @@
/* eslint-disable no-console */
/**
* Transpiles TypeScript demos to formatted JavaScript.
* Can be used to verify that JS and TS demos are equivalent. No introduced change
* would indicate equivalence.
*/
/**
* List of demos or folders to ignore when transpiling.
* Only ignore files that aren't used in the UI.
*/
const ignoreList = ['/pages.ts', 'docs/data/joy/getting-started/templates'];
const path = require('path');
const fs = require('node:fs');
const babel = require('@babel/core');
const prettier = require('prettier');
const {
getPropTypesFromFile,
injectPropTypesInFile,
} = require('@mui/internal-scripts/typescript-to-proptypes');
const {
createTypeScriptProjectBuilder,
} = require('@mui-internal/api-docs-builder/utils/createTypeScriptProject');
const { default: yargs } = require('yargs');
const { hideBin } = require('yargs/helpers');
const { fixBabelGeneratorIssues, fixLineEndings } = require('@mui/internal-docs-utils');
const { default: CORE_TYPESCRIPT_PROJECTS } = require('../../scripts/coreTypeScriptProjects');
const babelConfig = {
presets: ['@babel/preset-typescript'],
plugins: [],
generatorOpts: { retainLines: true },
babelrc: false,
configFile: false,
shouldPrintComment: (comment) => !comment.startsWith(' @babel-ignore-comment-in-output'),
};
const workspaceRoot = path.join(__dirname, '../../');
async function getFiles(root) {
const files = [];
try {
await Promise.all(
(await fs.promises.readdir(root)).map(async (name) => {
const filePath = path.join(root, name);
const stat = await fs.promises.stat(filePath);
if (
stat.isDirectory() &&
!ignoreList.some((ignorePath) =>
filePath.startsWith(path.normalize(`${workspaceRoot}/${ignorePath}`)),
)
) {
files.push(...(await getFiles(filePath)));
} else if (
stat.isFile() &&
/\.tsx?$/.test(filePath) &&
!filePath.endsWith('types.ts') &&
!filePath.endsWith('.d.ts') &&
!ignoreList.some((ignorePath) => filePath.endsWith(path.normalize(ignorePath)))
) {
files.push(filePath);
}
}),
);
} catch (error) {
if (error.message?.includes('no such file or directory')) {
return [];
}
throw error;
}
return files;
}
const TranspileResult = {
Success: 0,
Failed: 1,
};
async function transpileFile(tsxPath, project) {
const jsPath = tsxPath.replace(/\.tsx?$/, '.js');
try {
const source = await fs.promises.readFile(tsxPath, 'utf8');
const transformOptions = { ...babelConfig, filename: tsxPath };
const enableJSXPreview =
!tsxPath.includes(path.join('pages', 'premium-themes')) &&
!tsxPath.includes(path.join('getting-started', 'templates'));
if (enableJSXPreview) {
transformOptions.plugins = transformOptions.plugins.concat([
[
require.resolve('docs/src/modules/utils/babel-plugin-jsx-preview'),
{ maxLines: 16, outputFilename: `${tsxPath}.preview` },
],
]);
}
const { code } = await babel.transformAsync(source, transformOptions);
if (/import \w* from 'prop-types'/.test(code)) {
throw new Error('TypeScript demo contains prop-types, please remove them');
}
console.log(tsxPath);
const propTypesAST = getPropTypesFromFile({
project,
filePath: tsxPath,
shouldResolveObject: ({ name }) => {
if (name === 'classes' || name === 'ownerState' || name === 'popper') {
return false;
}
return undefined;
},
});
const codeWithPropTypes = injectPropTypesInFile({ components: propTypesAST, target: code });
const prettierConfig = await prettier.resolveConfig(jsPath, {
config: path.join(workspaceRoot, 'prettier.config.mjs'),
});
const prettierFormat = async (jsSource) =>
prettier.format(jsSource, { ...prettierConfig, filepath: jsPath });
const codeWithoutTsIgnoreComments = codeWithPropTypes.replace(/^\s*\/\/ @ts-ignore.*$/gm, '');
const prettified = await prettierFormat(codeWithoutTsIgnoreComments);
const formatted = fixBabelGeneratorIssues(prettified);
const correctedLineEndings = fixLineEndings(source, formatted);
// removed blank lines change potential formatting
await fs.promises.writeFile(jsPath, await prettierFormat(correctedLineEndings));
return TranspileResult.Success;
} catch (err) {
console.error('Something went wrong transpiling %s\n%s\n', tsxPath, err);
return TranspileResult.Failed;
}
}
async function main(argv) {
const { watch: watchMode, disableCache, pattern } = argv;
// TODO: Remove at some point.
// Though not too soon so that it isn't disruptive.
// It's a no-op anyway.
if (disableCache !== undefined) {
console.warn(
'--disable-cache does not have any effect since it is the default. In the future passing this flag will throw.',
);
}
const filePattern = new RegExp(pattern);
if (pattern.length > 0) {
console.log(`Only considering demos matching ${filePattern}`);
}
const tsxFiles = [
...(await getFiles(path.join(workspaceRoot, 'docs/src/pages'))), // old structure
...(await getFiles(path.join(workspaceRoot, 'docs/data'))), // new structure
].filter((fileName) => filePattern.test(fileName));
const buildProject = createTypeScriptProjectBuilder(CORE_TYPESCRIPT_PROJECTS);
const project = buildProject('docs', { files: tsxFiles });
let successful = 0;
let failed = 0;
(
await Promise.all(
tsxFiles.map((file) => {
return transpileFile(file, project);
}),
)
).forEach((result) => {
switch (result) {
case TranspileResult.Success: {
successful += 1;
break;
}
case TranspileResult.Failed: {
failed += 1;
break;
}
default: {
throw new Error(`No handler for ${result}`);
}
}
});
console.log(
[
'------ Summary ------',
'%i demo(s) were successfully transpiled',
'%i demo(s) were unsuccessful',
].join('\n'),
successful,
failed,
);
if (!watchMode) {
if (failed > 0) {
process.exit(1);
}
return;
}
tsxFiles.forEach((filePath) => {
fs.watchFile(filePath, { interval: 500 }, async () => {
if ((await transpileFile(filePath, project, true)) === 0) {
console.log('Success - %s', filePath);
}
});
});
console.log('\nWatching for file changes...');
}
yargs()
.command({
command: '$0',
description: 'transpile TypeScript demos',
builder: (command) => {
return command
.option('watch', {
default: false,
description: 'transpiles demos as soon as they changed',
type: 'boolean',
})
.option('disable-cache', {
description: 'No longer supported. The cache is disabled by default.',
type: 'boolean',
})
.option('pattern', {
default: '',
description:
'Transpiles only the TypeScript demos whose filename matches the given pattern.',
type: 'string',
});
},
handler: main,
})
.help()
.strict(true)
.version(false)
.parse(hideBin(process.argv));

View File

@@ -0,0 +1,47 @@
import fs from 'fs';
import { Feed } from 'feed';
import { BlogPost } from 'docs/lib/sourcing';
import ROUTES from 'docs/src/route';
export default function generateRssFeed(allBlogPosts: Array<BlogPost>) {
if (process.env.NODE_ENV !== 'production') {
return;
}
const siteUrl = 'https://mui.com';
const feed = new Feed({
title: 'MUI - Blog',
description:
'Follow the MUI blog to learn about new product features, latest advancements in UI development, and business initiatives.',
id: `${siteUrl}/blog`,
link: `${siteUrl}/blog`,
language: 'en',
image: `${siteUrl}/static/logo.svg`,
favicon: `${siteUrl}/favicon.ico`,
copyright: `Copyright © ${new Date().getFullYear()} Material UI SAS, trading as MUI.`,
feedLinks: {
rss2: `${siteUrl}/public${ROUTES.rssFeed}`,
},
});
allBlogPosts.forEach((post) => {
const postAuthors = post.authors && post.authors.map((author) => ({ name: author }));
const postDate = post.date ? new Date(post.date) : new Date();
const postCategory = post.tags.map((tag) => ({ name: tag }));
const postLink = `${siteUrl}/blog/${post.slug}`;
feed.addItem({
title: post.title,
image: post.image,
id: postLink,
link: postLink,
description: post.description,
category: postCategory,
date: postDate,
author: postAuthors,
});
});
fs.mkdirSync(`public${ROUTES.rssFeed.replace('rss.xml', '')}`, { recursive: true });
fs.writeFileSync(`public${ROUTES.rssFeed}`, feed.rss2());
}

View File

@@ -0,0 +1,162 @@
import fs from 'fs/promises';
import path from 'path';
import { chromium } from '@playwright/test';
/**
* README
*
* Usage:
* - `pnpm template:screenshot` to generate all screenshots
* - `pnpm template:screenshot material-ui` to generate all screenshots for Material-UI templates
* - `pnpm template:screenshot order-dashboard` to generate screenshots for file named `order-dashboard.tsx`
* - `pnpm template:screenshot material-ui dashboard` to generate screenshots for file named `dashboard.tsx` of Material UI templates
*
* Note:
* - The screenshot with `-dark` suffix is generated if the page has a button with id `toggle-mode`
* <button data-screenshot="toggle-mode" onClick={toggleMode}>Toggle Mode</button>
*
* - The screenshot with `-default` suffix is generated if the page has a button with id `toggle-default-theme`
* <button data-screenshot="toggle-default-theme" onClick={toggleDefaultTheme}>Toggle Default Theme</button>
*
* - The screenshot with `-default-dark` suffix is generated if the page has both buttons
*
* Debug:
* - Set `chromium.launch({ headless: false })` in line:50 to see the browser
*/
function sleep(duration: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
}
const host = process.env.DEPLOY_PREVIEW || 'http://localhost:3000';
/**
* project key should be `mui.com/<project-key>/*`
*/
const projects = {
'material-ui': {
input: path.join(process.cwd(), 'docs/pages/material-ui/getting-started/templates'),
output: 'docs/public/static/screenshots',
viewport: { width: 813 * 2, height: 457 * 2 },
},
'joy-ui': {
input: path.join(process.cwd(), 'docs/pages/joy-ui/getting-started/templates'),
output: 'docs/public/static/screenshots',
viewport: { width: 1600, height: 800 },
},
};
const names = new Set(process.argv.slice(2));
(async () => {
// eslint-disable-next-line no-console
console.info('Host:', host);
const browser = await chromium.launch({ headless: true });
await Promise.all(
Object.entries(projects)
.filter(([project]) => names.size === 0 || names.has(project))
.map(async ([project, { input, output, viewport }]) => {
const page = await browser.newPage({
viewport,
reducedMotion: 'reduce',
});
names.delete(project);
const files = await fs.readdir(input);
const urls = files
.filter(
(file) =>
!file.startsWith('index') &&
(names.size === 0 || names.has(file.replace(/\.(js|tsx)$/, ''))),
)
.map(
(file) => `/${project}/getting-started/templates/${file.replace(/\.(js|tsx)$/, '/')}`,
);
async function toggleMode() {
await page.locator('css=[data-screenshot="toggle-mode"]').locator('visible=true').click();
}
async function captureDarkMode(outputPath: string) {
const btn = await page.$('[data-screenshot="toggle-mode"]');
if (btn) {
if ((await btn.getAttribute('aria-haspopup')) === 'true') {
await toggleMode();
await page.getByRole('menuitem').filter({ hasText: /dark/i }).click();
await page.waitForLoadState('networkidle'); // changing to dark mode might trigger image loading
await sleep(100); // give time for image decoding, resizing, rendering
await page.screenshot({ path: outputPath, animations: 'disabled' });
await toggleMode();
await page
.getByRole('menuitem')
.filter({ hasText: /system/i })
.click(); // switch back to light
} else if ((await btn.getAttribute('aria-haspopup')) === 'listbox') {
await toggleMode();
await page.getByRole('option').filter({ hasText: /dark/i }).click();
await page.waitForLoadState('networkidle'); // changing to dark mode might trigger image loading
await sleep(100); // give time for image decoding, resizing, rendering
await page.screenshot({ path: outputPath, animations: 'disabled' });
await toggleMode();
await page
.getByRole('option')
.filter({ hasText: /system/i })
.click(); // switch back to light
} else {
await toggleMode();
await page.waitForLoadState('networkidle'); // changing to dark mode might trigger image loading
await sleep(100); // give time for image decoding, resizing, rendering
await page.screenshot({ path: outputPath, animations: 'disabled' });
await toggleMode(); // switch back to light
}
}
}
try {
await Promise.resolve().then(() =>
urls.reduce(async (sequence, aUrl) => {
await sequence;
await page.goto(`${host}${aUrl}?hideFrame=true`, { waitUntil: 'networkidle' });
const filePath = `${output}${aUrl.replace(/\/$/, '')}.jpg`;
// eslint-disable-next-line no-console
console.info('Saving screenshot to:', filePath);
await page.screenshot({ path: filePath, animations: 'disabled' });
await captureDarkMode(filePath.replace('.jpg', '-dark.jpg'));
// capture custom theme
const toggleTheme = await page.$('[data-screenshot="toggle-default-theme"]');
if (toggleTheme) {
await page.click('[data-screenshot="toggle-default-theme"]');
await page.screenshot({
path: filePath.replace('.jpg', '-default.jpg'),
animations: 'disabled',
});
await captureDarkMode(filePath.replace('.jpg', '-default-dark.jpg'));
}
return Promise.resolve();
}, Promise.resolve()),
);
} catch (error) {
console.error(error);
}
}),
);
await browser.close();
})();

52
docs/scripts/i18n.ts Normal file
View File

@@ -0,0 +1,52 @@
// @ts-check
import path from 'path';
import fs from 'node:fs/promises';
import { pageToTitle } from 'docs/src/modules/utils/helpers';
import materialPages from 'docs/data/material/pages';
import systemPages from 'docs/data/system/pages';
import joyPages from 'docs/data/joy/pages';
import { MuiPage } from 'docs/src/MuiPage';
const EXCLUDES = ['/api', '/blog', '/x/react-', '/toolpad'];
async function run() {
const translationsFilename = path.join(__dirname, '../translations/translations.json');
const translationsFile = await fs.readFile(translationsFilename, 'utf8');
/**
* @type {{ pages: Record<String, string> }}
*/
const output = JSON.parse(translationsFile);
output.pages = {};
/**
* @param {readonly import('docs/src/MuiPage').MuiPage[]} pages
*/
const traverse = (pages: MuiPage[]) => {
pages.forEach((page) => {
if (
(page.pathname !== '/' && page.pathname === '/api-docs') ||
!EXCLUDES.some((exclude) => page.pathname.includes(exclude))
) {
const title = pageToTitle(page);
if (title) {
const pathname = page.subheader || page.pathname;
output.pages[pathname] = title;
}
}
if (page.children) {
traverse(page.children);
}
});
};
traverse([...systemPages, ...materialPages, ...joyPages]);
await fs.writeFile(translationsFilename, `${JSON.stringify(output, null, 2)}\n`);
}
run().catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,5 @@
export default function Playground() {
return (
<div>A playground for a fast iteration development cycle in isolation outside of git.</div>
);
}

View File

@@ -0,0 +1,41 @@
import * as path from 'path';
import { crawl } from '@mui/internal-code-infra/brokenLinksChecker';
import { globby } from 'globby';
async function main() {
// The /blog/ page has pagination that's not persisted in the url.
const blogPages = await globby('blog/**/*.js', {
cwd: path.resolve(import.meta.dirname, '../pages'),
});
const blogSeedUrls = blogPages.map((page) => {
const pathname = page.replace(/(?:\/index)?\.js$/, '');
return `/${pathname}`;
});
const { issues } = await crawl({
startCommand: 'pnpm start --no-request-logging -p 3001',
host: 'http://localhost:3001/',
seedUrls: ['/', ...blogSeedUrls],
outPath: path.resolve(import.meta.dirname, '../export/material-ui/link-structure.json'),
// Target paths to ignore during link checking
ignoredPaths: [
// Internal links not on this server
// TODO: Seed crawler with stored links from e.g. mui.com/x/link-structure.json
/^\/(x|base-ui|joy-ui|store|toolpad)(\/|$)/,
],
// CSS selectors for content to ignore during link checking
ignoredContent: [
// Links used in demos under MemoryRouter
// TODO: Create an easier way to identify content under MemoryRouter
// (e.g. a class or an option on the demo)
'[id^="demo-"] a[href^="/inbox"]',
'[id^="demo-"] a[href^="/trash"]',
'[id^="demo-"] a[href^="/spam"]',
'[id^="demo-"] a[href^="/drafts"]',
],
});
process.exit(issues.length);
}
main();

View File

@@ -0,0 +1,30 @@
/* eslint-disable no-console */
import url from 'url';
import * as fs from 'fs/promises';
import path from 'path';
async function run() {
// Same as https://tools-public.mui.com/prod/pages/muicomabout
const response = await fetch(
'https://tools-public.mui.com/prod/api/data/muicomabout/queryAbout',
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
},
);
const apiResponse = await response.json();
const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url));
await fs.writeFile(
path.resolve(currentDirectory, '../data/about/teamMembers.json'),
JSON.stringify(apiResponse.data),
'utf8',
);
console.log('done');
}
run();

View File

@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"include": ["*.ts"],
"compilerOptions": {
"allowJs": true,
"isolatedModules": true,
"noEmit": true,
"noUnusedLocals": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"esModuleInterop": true,
"types": ["node"]
}
}

View File

@@ -0,0 +1,99 @@
/* eslint-disable no-console */
import path from 'path';
import fs from 'node:fs';
import fetch from 'cross-fetch';
import * as mui from '@mui/icons-material';
import synonyms from 'docs/data/material/components/material-icons/synonyms';
// eslint-disable-next-line import/no-relative-packages
import myDestRewriter from '../../packages/mui-icons-material/renameFilters/material-design-icons';
function not(a, b) {
return a.filter((value) => !b.includes(value));
}
function union(a, b) {
return [...new Set([...a, ...b])];
}
async function run() {
try {
const response = await fetch('https://fonts.google.com/metadata/icons');
const text = await response.text();
const data = JSON.parse(text.replace(")]}'", ''));
const materialIcons = data.icons.reduce((acc, icon) => {
icon.tags = not(icon.tags, icon.name.replace('_')) // remove the icon name strings from the tags
.filter((t) => {
// remove invalid tags
if (
t.includes('Remove') ||
t.includes('Duplicate') ||
t.includes('Same as') ||
t.includes('remove others')
) {
console.log(`Skipping invalid tag (${t}) in ${icon.name}`);
return false;
}
return true;
})
.map((t) => t.replace(/'/g, ''));
// Fix names that can't be exported as ES modules.
icon.name = myDestRewriter({ base: icon.name });
acc[icon.name] = icon.tags;
return acc;
}, {});
const npmPackageIcons = Object.keys(mui).reduce((acc, icon) => {
const name = icon.replace(/(Outlined|TwoTone|Rounded|Sharp)$/, '');
acc[name] = true;
return acc;
}, {});
const iconList = union(Object.keys(materialIcons), Object.keys(synonyms))
.filter((icon) => {
// The icon is not in @mui/material so no point in having synonyms.
return npmPackageIcons[icon];
})
.sort((a, b) => -b.localeCompare(a));
let newSynonyms = 'const synonyms = {\n';
iconList.forEach((icon) => {
const synonymsIconStrings = synonyms[icon] ? synonyms[icon].split(' ') : [];
// Some MD tags have multiple words in a string, so we separate those out to dedupe them
const materialIconStrings = materialIcons[icon]
? materialIcons[icon].reduce((tags, tag) => tags.concat(tag.split(' ')), [])
: [];
let mergedStrings = union(synonymsIconStrings, materialIconStrings);
mergedStrings = mergedStrings
// remove strings that are substrings of others
.filter((tag) => !mergedStrings.some((one) => one.includes(tag) && one !== tag))
.sort()
.join(' ');
if (mergedStrings !== '') {
newSynonyms += ` ${icon}: '${mergedStrings}',\n`;
}
});
newSynonyms += '};\n\nexport default synonyms;\n';
fs.writeFileSync(
path.join(__dirname, `../../docs/data/material/components/material-icons/synonyms.js`),
newSynonyms,
);
console.log('Stats:');
console.log(`${iconList.length} synonyms icons in the generated file`);
console.log(`${Object.keys(npmPackageIcons).length} icons in @mui/material`);
console.log(`${Object.keys(materialIcons).length} icons in Material Design`);
} catch (err) {
console.log('err', err);
throw err;
}
}
run();