diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts new file mode 100644 index 00000000000000..8951bdb06eaa01 --- /dev/null +++ b/docs/data/base/pages.ts @@ -0,0 +1,12 @@ +import pagesApi from './pagesApi'; + +const pages = [ + { + title: 'Component API', + pathname: '/base/api', + icon: 'CodeIcon', + children: pagesApi, + }, +]; + +export default pages; diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js new file mode 100644 index 00000000000000..e0a30c5dfa3e4f --- /dev/null +++ b/docs/data/base/pagesApi.js @@ -0,0 +1 @@ +module.exports = []; diff --git a/docs/data/material/pages.ts b/docs/data/material/pages.ts new file mode 100644 index 00000000000000..7a16d417fb1774 --- /dev/null +++ b/docs/data/material/pages.ts @@ -0,0 +1,240 @@ +import pagesApi from './pagesApi'; + +const pages = [ + { + pathname: '/material/getting-started', + icon: 'DescriptionIcon', + children: [ + { pathname: '/material/getting-started/installation' }, + { pathname: '/material/getting-started/usage' }, + { pathname: '/material/getting-started/example-projects' }, + { pathname: '/material/getting-started/templates' }, + { pathname: '/material/getting-started/learn' }, + { pathname: '/material/getting-started/faq', title: 'FAQs' }, + { pathname: '/material/getting-started/supported-components' }, + { pathname: '/material/getting-started/supported-platforms' }, + { pathname: '/material/getting-started/support' }, + ], + }, + { + pathname: '/material/react-', + title: 'Components', + icon: 'ToggleOnIcon', + children: [ + { + pathname: '/material/components/inputs', + subheader: 'inputs', + children: [ + { pathname: '/material/react-autocomplete' }, + { pathname: '/material/react-buttons', title: 'Button' }, + { pathname: '/material/react-button-group' }, + { pathname: '/material/react-checkboxes', title: 'Checkbox' }, + { pathname: '/material/react-floating-action-button' }, + { pathname: '/material/react-radio-buttons', title: 'Radio button' }, + { pathname: '/material/react-rating' }, + { pathname: '/material/react-selects', title: 'Select' }, + { pathname: '/material/react-slider' }, + { pathname: '/material/react-switches', title: 'Switch' }, + { pathname: '/material/react-text-fields', title: 'Text field' }, + { pathname: '/material/react-transfer-list' }, + { pathname: '/material/react-toggle-button' }, + ], + }, + { + pathname: '/material/components/data-display', + subheader: 'data-display', + children: [ + { pathname: '/material/react-avatars', title: 'Avatar' }, + { pathname: '/material/react-badges', title: 'Badge' }, + { pathname: '/material/react-chips', title: 'Chip' }, + { pathname: '/material/react-dividers', title: 'Divider' }, + { pathname: '/material/react-icons' }, + { pathname: '/material/react-material-icons' }, + { pathname: '/material/react-lists', title: 'List' }, + { pathname: '/material/react-tables', title: 'Table' }, + { pathname: '/material/react-tooltips', title: 'Tooltip' }, + { pathname: '/material/react-typography' }, + ], + }, + { + pathname: '/material/components/feedback', + subheader: 'feedback', + children: [ + { pathname: '/material/react-alert' }, + { pathname: '/material/react-backdrop' }, + { pathname: '/material/react-dialogs' }, + { pathname: '/material/react-progress' }, + { pathname: '/material/react-skeleton' }, + { pathname: '/material/react-snackbars', title: 'Snackbar' }, + ], + }, + { + pathname: '/material/components/surfaces', + subheader: 'surfaces', + children: [ + { pathname: '/material/react-accordion' }, + { pathname: '/material/react-app-bar' }, + { pathname: '/material/react-cards', title: 'Card' }, + { pathname: '/material/react-paper' }, + ], + }, + { + pathname: '/material/components/navigation', + subheader: 'navigation', + children: [ + { pathname: '/material/react-bottom-navigation' }, + { pathname: '/material/react-breadcrumbs' }, + { pathname: '/material/react-drawers', title: 'Drawer' }, + { pathname: '/material/react-links', title: 'Link' }, + { pathname: '/material/react-menus', title: 'Menu' }, + { pathname: '/material/react-pagination' }, + { pathname: '/material/react-speed-dial' }, + { pathname: '/material/react-steppers', title: 'Stepper' }, + { pathname: '/material/react-tabs' }, + ], + }, + { + pathname: '/material/components/layout', + subheader: 'layout', + children: [ + { pathname: '/material/react-box' }, + { pathname: '/material/react-container' }, + { pathname: '/material/react-grid' }, + { pathname: '/material/react-stack' }, + { pathname: '/material/react-image-list' }, + { pathname: '/material/react-hidden' }, + ], + }, + { + pathname: '/material/components/utils', + subheader: 'utils', + children: [ + { pathname: '/material/react-click-away-listener' }, + { pathname: '/material/react-css-baseline', title: 'CSS Baseline' }, + { pathname: '/material/react-modal' }, + { pathname: '/material/react-no-ssr', title: 'No SSR' }, + { pathname: '/material/react-popover' }, + { pathname: '/material/react-popper' }, + { pathname: '/material/react-portal' }, + { pathname: '/material/react-textarea-autosize' }, + { pathname: '/material/react-transitions' }, + { pathname: '/material/react-use-media-query', title: 'useMediaQuery' }, + ], + }, + { + pathname: '/x/data-grid', + subheader: 'data-grid', + }, + { + pathname: '/material', + subheader: 'lab', + children: [ + { pathname: '/material/about-the-lab', title: 'About the lab 🧪' }, + { + pathname: '/material/lab/pickers', + subheader: 'pickers', + title: 'Date / Time', + children: [ + { pathname: '/material/react-pickers', title: 'Introduction' }, + { pathname: '/material/react-date-picker' }, + { + pathname: '/material/react-date-range-picker', + title: 'Date Range Picker ⚡️', + }, + { pathname: '/material/react-date-time-picker' }, + { pathname: '/material/react-time-picker' }, + ], + }, + { pathname: '/material/react-masonry' }, + { pathname: '/material/react-timeline' }, + { pathname: '/material/react-trap-focus' }, + { pathname: '/material/react-tree-view' }, + ], + }, + ], + }, + { + title: 'Component API', + pathname: '/material/api', + icon: 'CodeIcon', + children: [ + ...pagesApi, + { + pathname: '/x/api/mui-data-grid', + title: 'Data Grid', + }, + ], + }, + { + pathname: '/material/customization', + icon: 'CreateIcon', + children: [ + { + pathname: '/material/customization', + subheader: '/material/customization/theme', + children: [ + { pathname: '/material/customization/theming' }, + { pathname: '/material/customization/palette' }, + { pathname: '/material/customization/dark-mode', title: 'Dark mode' }, + { pathname: '/material/customization/typography' }, + { pathname: '/material/customization/spacing' }, + { pathname: '/material/customization/breakpoints' }, + { pathname: '/material/customization/density' }, + { pathname: '/material/customization/z-index', title: 'z-index' }, + { pathname: '/material/customization/transitions' }, + { pathname: '/material/customization/theme-components', title: 'Components' }, + { pathname: '/material/customization/default-theme', title: 'Default Theme' }, + ], + }, + { pathname: '/material/customization/how-to-customize' }, + { pathname: '/material/customization/color' }, + { pathname: '/material/customization/unstyled-components' }, + ], + }, + { + pathname: '/material/guides', + title: 'How To Guides', + icon: 'VisibilityIcon', + children: [ + { pathname: '/material/guides/api', title: 'API Design Approach' }, + { pathname: '/material/guides/classname-generator', title: 'ClassName Generator' }, + { pathname: '/material/guides/understand-mui-packages', title: 'Understand MUI packages' }, + { pathname: '/material/guides/typescript', title: 'TypeScript' }, + { pathname: '/material/guides/interoperability', title: 'Style Library Interoperability' }, + { pathname: '/material/guides/styled-engine' }, + { pathname: '/material/guides/minimizing-bundle-size' }, + { pathname: '/material/guides/composition' }, + { pathname: '/material/guides/routing' }, + { pathname: '/material/guides/server-rendering' }, + { pathname: '/material/guides/responsive-ui', title: 'Responsive UI' }, + { + pathname: '/material/guides/pickers-migration', + title: 'Migration from @material-ui/pickers', + }, + { pathname: '/material/guides/migration-v4', title: 'Migration From v4' }, + { pathname: '/material/guides/migration-v3', title: 'Migration From v3' }, + { pathname: '/material/guides/migration-v0x', title: 'Migration From v0.x' }, + { pathname: '/material/guides/testing' }, + { pathname: '/material/guides/localization' }, + { pathname: '/material/guides/content-security-policy', title: 'Content Security Policy' }, + { pathname: '/material/guides/right-to-left', title: 'Right-to-left' }, + { pathname: '/material/guides/flow' }, + ], + }, + { + pathname: '/material/discover-more', + icon: 'AddIcon', + children: [ + { pathname: '/material/discover-more/showcase' }, + { pathname: '/material/discover-more/related-projects' }, + { pathname: '/material/discover-more/roadmap' }, + { pathname: '/material/discover-more/backers', title: 'Sponsors & Backers' }, + { pathname: '/material/discover-more/vision' }, + { pathname: '/material/discover-more/changelog' }, + { pathname: '/material/discover-more/languages' }, + { pathname: '/about', title: 'About us' }, + ], + }, +]; + +export default pages; diff --git a/docs/data/material/pagesApi.js b/docs/data/material/pagesApi.js new file mode 100644 index 00000000000000..e0a30c5dfa3e4f --- /dev/null +++ b/docs/data/material/pagesApi.js @@ -0,0 +1 @@ +module.exports = []; diff --git a/docs/data/styles/pages.ts b/docs/data/styles/pages.ts new file mode 100644 index 00000000000000..7ff5a6e8c4ce93 --- /dev/null +++ b/docs/data/styles/pages.ts @@ -0,0 +1,14 @@ +const pages = [ + { + pathname: '/styles', + title: 'Styles (legacy)', + icon: 'StyleIcon', + children: [ + { pathname: '/styles/basics' }, + { pathname: '/styles/advanced' }, + { pathname: '/styles/api', title: 'API' }, + ], + }, +]; + +export default pages; diff --git a/docs/data/system/pages.ts b/docs/data/system/pages.ts new file mode 100644 index 00000000000000..a497e79626902b --- /dev/null +++ b/docs/data/system/pages.ts @@ -0,0 +1,27 @@ +const pages = [ + { + pathname: '/system', + icon: 'BuildIcon', + children: [ + { pathname: '/system/basics' }, + { pathname: '/system/properties' }, + { pathname: '/system/the-sx-prop', title: 'The sx prop' }, + { pathname: '/system/borders' }, + { pathname: '/system/display' }, + { pathname: '/system/flexbox' }, + { pathname: '/system/grid' }, + { pathname: '/system/palette' }, + { pathname: '/system/positions' }, + { pathname: '/system/shadows' }, + { pathname: '/system/sizing' }, + { pathname: '/system/spacing' }, + { pathname: '/system/screen-readers' }, + { pathname: '/system/typography' }, + { pathname: '/system/advanced' }, + { pathname: '/system/box' }, + { pathname: '/system/styled', title: 'styled' }, + ], + }, +]; + +export default pages; diff --git a/docs/next.config.js b/docs/next.config.js index c0fbed64b1336b..c4f544a0f87d73 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -3,6 +3,7 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const pkg = require('../package.json'); const { findPages } = require('./src/modules/utils/find'); const { LANGUAGES, LANGUAGES_SSR } = require('./src/modules/constants'); +const FEATURE_TOGGLE = require('./src/featureToggle'); const workspaceRoot = path.join(__dirname, '../'); @@ -226,6 +227,65 @@ module.exports = { { source: '/api/:rest*/', destination: '/api-docs/:rest*/' }, ]; }, + async redirects() { + if (FEATURE_TOGGLE.enable_redirects) { + return [ + { + source: '/getting-started/:path*', + destination: '/material/getting-started/:path*', + permanent: false, + }, + { + source: '/customization/:path*', + destination: '/material/customization/:path*', + permanent: false, + }, + { + source: '/guides/:path*', + destination: '/material/guides/:path*', + permanent: false, + }, + { + source: '/discover-more/:path*', + destination: '/material/discover-more/:path*', + permanent: false, + }, + { + source: '/components/about-the-lab', + destination: '/material/about-the-lab', + permanent: false, + }, + { + source: '/components/data-grid/:path*', + destination: '/x/react-data-grid/:path*', + permanent: false, + }, + { + source: '/components/:path*', + destination: '/material/react-:path*', + permanent: false, + }, + { + source: '/api/data-grid/:path*', + destination: '/x/api/mui-data-grid/:path*', + permanent: false, + }, + { + source: + // if this regex change, make sure to update `replaceMarkdownLinks` + '/api/:path(loading-button|tab-list|tab-panel|date-picker|date-time-picker|time-picker|calendar-picker|calendar-picker-skeleton|desktop-picker|mobile-date-picker|month-picker|pickers-day|static-date-picker|year-picker|masonry|timeline|timeline-connector|timeline-content|timeline-dot|timeline-item|timeline-opposite-content|timeline-separator|unstable-trap-focus|tree-item|tree-view)', + destination: '/material/api/mui-lab/:path*', + permanent: false, + }, + { + source: '/api/:path*', + destination: '/material/api/mui-material/:path*', + permanent: false, + }, + ]; + } + return []; + }, // Can be turned on when https://github.com/vercel/next.js/issues/24640 is fixed optimizeFonts: false, }; diff --git a/docs/packages/markdown/loader.js b/docs/packages/markdown/loader.js index 08693a6dc5c196..ebe2d6d910781b 100644 --- a/docs/packages/markdown/loader.js +++ b/docs/packages/markdown/loader.js @@ -1,4 +1,4 @@ -const { promises: fs } = require('fs'); +const { promises: fs, readdirSync } = require('fs'); const path = require('path'); const { prepareMarkdown } = require('./parseMarkdown'); @@ -29,6 +29,45 @@ function moduleIDToJSIdentifier(moduleID) { .join(''); } +const componentPackageMapping = { + material: {}, + base: {}, +}; + +const packages = [ + { + name: 'mui-material', + product: 'material', + paths: [ + path.join(__dirname, '../../../packages/mui-lab/src'), + path.join(__dirname, '../../../packages/mui-material/src'), + path.join(__dirname, '../../../packages/mui-base/src'), + ], + }, + { + name: 'mui-base', + product: 'base', + paths: [path.join(__dirname, '../../../packages/mui-base/src')], + }, +]; + +packages.forEach((pkg) => { + pkg.paths.forEach((pkgPath) => { + const match = pkgPath.match(/packages\/([^/]+)\/src/); + const packageName = match ? match[1] : null; + if (!packageName) { + throw new Error(`cannot find package name from path: ${pkgPath}`); + } + const filePaths = readdirSync(pkgPath); + filePaths.forEach((folder) => { + if (folder.match(/^[A-Z]/)) { + // filename starts with Uppercase = component + componentPackageMapping[pkg.product][folder] = packageName; + } + }); + }); +}); + /** * @type {import('webpack').loader.Loader} */ @@ -81,7 +120,7 @@ module.exports = async function demoLoader() { // win32 to posix .replace(/\\/g, '/') .replace(/^\/src\/pages\//, ''); - const { docs } = prepareMarkdown({ pageFilename, translations }); + const { docs } = prepareMarkdown({ pageFilename, translations, componentPackageMapping }); const demos = {}; const demoModuleIDs = new Set(); diff --git a/docs/packages/markdown/parseMarkdown.js b/docs/packages/markdown/parseMarkdown.js index df90d278eb45d5..231df587eb3cab 100644 --- a/docs/packages/markdown/parseMarkdown.js +++ b/docs/packages/markdown/parseMarkdown.js @@ -242,7 +242,7 @@ function createRender(context) { * @param {string} config.pageFilename - posix filename relative to nextjs pages directory */ function prepareMarkdown(config) { - const { pageFilename, translations } = config; + const { pageFilename, translations, componentPackageMapping = {} } = config; const demos = {}; /** @@ -251,6 +251,25 @@ function prepareMarkdown(config) { const docs = {}; const headingHashes = {}; + /** + * @param {string} product + * @example 'material' + * @param {string} componentPkg + * @example 'mui-base' + * @param {string} component + * @example 'ButtonUnstyled' + * @returns {string} + */ + function resolveComponentApiUrl(product, componentPkg, component) { + if (!product || !componentPkg) { + return `/api/${kebabCase(component)}/`; + } + if (componentPkg === 'mui-base') { + return `/base/api/${componentPkg}/${kebabCase(component)}/`; + } + return `/${product}/api/${componentPkg}/${kebabCase(component)}/`; + } + translations // Process the English markdown before the other locales. // English ToC anchor links are used in all languages @@ -267,12 +286,19 @@ function prepareMarkdown(config) { ## API ${headers.components - .map( - (component) => - `- [\`<${component} />\`](${headers.product ? `/${headers.product}` : ''}/api/${kebabCase( - component, - )}/)`, - ) + .map((component) => { + return `- [\`<${component} />\`](/api/${kebabCase(component)}/)`; + + // TODO: enable the code below once the migration is done. + // eslint-disable-next-line no-unreachable + const componentPkgMap = componentPackageMapping[headers.product]; + const componentPkg = componentPkgMap ? componentPkgMap[component] : null; + return `- [\`<${component} />\`](${resolveComponentApiUrl( + headers.product, + componentPkg, + component, + )})`; + }) .join('\n')} `); } diff --git a/docs/pages/_app.js b/docs/pages/_app.js index cbbf2a0391a4b4..7ea5b1fe488b27 100644 --- a/docs/pages/_app.js +++ b/docs/pages/_app.js @@ -7,7 +7,6 @@ LicenseInfo.setLicenseKey(process.env.NEXT_PUBLIC_MUI_LICENSE); import 'docs/src/modules/components/bootstrap'; // --- Post bootstrap ----- import * as React from 'react'; -import find from 'lodash/find'; import { loadCSS } from 'fg-loadcss/src/loadCSS'; import NextHead from 'next/head'; import PropTypes from 'prop-types'; @@ -27,6 +26,7 @@ import { } from 'docs/src/modules/utils/i18n'; import DocsStyledEngineProvider from 'docs/src/modules/utils/StyledEngineProvider'; import createEmotionCache from 'docs/src/createEmotionCache'; +import findActivePage from 'docs/src/modules/utils/findActivePage'; // Client-side cache, shared for the whole session of the user in the browser. const clientSideEmotionCache = createEmotionCache(); @@ -156,32 +156,6 @@ Tip: you can access the documentation \`theme\` object directly in the console. 'font-family:monospace;color:#1976d2;font-size:12px;', ); } - -function findActivePage(currentPages, pathname) { - const activePage = find(currentPages, (page) => { - if (page.children) { - if (pathname.indexOf(`${page.pathname}/`) === 0) { - // Check if one of the children matches (for /components) - return findActivePage(page.children, pathname); - } - } - - // Should be an exact match if no children - return pathname === page.pathname; - }); - - if (!activePage) { - return null; - } - - // We need to drill down - if (activePage.pathname !== pathname) { - return findActivePage(activePage.children, pathname); - } - - return activePage; -} - function AppWrapper(props) { const { children, emotionCache, pageProps } = props; diff --git a/docs/scripts/ApiBuilders/ComponentApiBuilder.ts b/docs/scripts/ApiBuilders/ComponentApiBuilder.ts index 3d3338f326c8b4..03aeebd673a7e8 100644 --- a/docs/scripts/ApiBuilders/ComponentApiBuilder.ts +++ b/docs/scripts/ApiBuilders/ComponentApiBuilder.ts @@ -15,7 +15,6 @@ import generatePropTypeDescription, { getChained, } from 'docs/src/modules/utils/generatePropTypeDescription'; import { renderInline as renderMarkdownInline } from '@mui/markdown'; -import { pageToTitle } from 'docs/src/modules/utils/helpers'; import createDescribeableProp, { DescribeablePropDescriptor, } from 'docs/src/modules/utils/createDescribeableProp'; @@ -23,27 +22,24 @@ import generatePropDescription from 'docs/src/modules/utils/generatePropDescript import parseStyles, { Styles } from 'docs/src/modules/utils/parseStyles'; import generateUtilityClass from '@mui/base/generateUtilityClass'; import * as ttp from 'typescript-to-proptypes'; -import { getLineFeed, getUnstyledFilename } from '../helpers'; -import { findComponentDemos, getMuiName } from '../buildApiUtils'; +import { getUnstyledFilename } from '../helpers'; +import { ComponentInfo } from '../buildApiUtils'; const DEFAULT_PRETTIER_CONFIG_PATH = path.join(process.cwd(), 'prettier.config.js'); -interface ReactApi extends ReactDocgenApi { - /** - * list of page pathnames - * @example ['/components/Accordion'] - */ - demos: readonly string[]; +export interface ReactApi extends ReactDocgenApi { + demos: ReturnType; EOL: string; filename: string; - apiUrl: string; + apiPathname: string; forwardsRefTo: string | undefined; - inheritance: { component: string; pathname: string } | null; + inheritance: ReturnType; /** * react component name * @example 'Accordion' */ name: string; + muiName: string; description: string; spread: boolean | undefined; /** @@ -88,21 +84,6 @@ export function writePrettifiedFile( }); } -const parseFile = (filename: string) => { - const src = readFileSync(filename, 'utf8'); - return { - src, - shouldSkip: - filename.indexOf('internal') !== -1 || - !!src.match(/@ignore - internal component\./) || - !!src.match(/@ignore - do not document\./), - spread: !src.match(/ = exactProp\(/), - name: path.parse(filename).name, - EOL: getLineFeed(src), - inheritedComponent: src.match(/\/\/ @inheritedComponent (.*)/)?.[1], - }; -}; - /** * Produces markdown of the description that can be hosted anywhere. * @@ -218,11 +199,11 @@ async function annotateComponentDefinition(api: ReactApi) { let inheritanceAPILink = null; if (api.inheritance !== null) { - const url = api.inheritance.pathname.startsWith('/') - ? `${HOST}${api.inheritance.pathname}` - : api.inheritance.pathname; - - inheritanceAPILink = `[${api.inheritance.component} API](${url})`; + inheritanceAPILink = `[${api.inheritance.name} API](${ + api.inheritance.apiPathname.startsWith('http') + ? api.inheritance.apiPathname + : `${HOST}${api.inheritance.apiPathname}` + })`; } const markdownLines = (await computeApiDescription(api, { host: HOST })).split('\n'); @@ -233,13 +214,21 @@ async function annotateComponentDefinition(api: ReactApi) { markdownLines.push( 'Demos:', '', - ...api.demos.map((demoPathname) => { - return `- [${pageToTitle({ pathname: demoPathname })}](${HOST}${demoPathname}/)`; + ...api.demos.map((item) => { + return `- [${item.name}](${ + item.demoPathname.startsWith('http') ? item.demoPathname : `${HOST}${item.demoPathname}` + })`; }), '', ); - markdownLines.push('API:', '', `- [${api.name} API](${HOST}${api.apiUrl}/)`); + markdownLines.push( + 'API:', + '', + `- [${api.name} API](${ + api.apiPathname.startsWith('http') ? api.apiPathname : `${HOST}${api.apiPathname}` + })`, + ); if (api.inheritance !== null) { markdownLines.push(`- inherits ${inheritanceAPILink}`); } @@ -284,18 +273,6 @@ function extractClassConditions(descriptions: any) { return classConditions; } -/** - * Generate list of component demos - */ -function generateDemoList(reactAPI: ReactApi): string { - return ``; -} - /** * @param filepath - absolute path * @example toGithubPath('/home/user/material-ui/packages/Accordion') === '/packages/Accordion' @@ -371,8 +348,15 @@ const generateApiPage = (outputDirectory: string, reactApi: ReactApi) => { spread: reactApi.spread, forwardsRefTo: reactApi.forwardsRefTo, filename: toGithubPath(reactApi.filename), - inheritance: reactApi.inheritance, - demos: generateDemoList(reactApi), + inheritance: reactApi.inheritance + ? { + component: reactApi.inheritance.name, + pathname: reactApi.inheritance.apiPathname, + } + : null, + demos: ``, cssComponent: cssComponents.indexOf(reactApi.name) >= 0, }; @@ -520,25 +504,26 @@ const attachPropsTable = (reactApi: ReactApi) => { * - Add the comment in the component filename with its demo & API urls (including the inherited component). * this process is done by sourcing markdown files and filter matched `components` in the frontmatter */ -const generateComponentApi = async ( - filename: string, - options: { - outputPagesDirectory: string; - productUrlPrefix: string; - apiUrl: string; - ttpProgram: ttp.ts.Program; - pagesMarkdown: Array<{ components: string[]; pathname: string }>; - }, -) => { - const { ttpProgram: program, pagesMarkdown, outputPagesDirectory } = options; - const { shouldSkip, name, spread, EOL, inheritedComponent } = parseFile(filename); +const generateComponentApi = async (componentInfo: ComponentInfo, program: ttp.ts.Program) => { + const { + filename, + name, + muiName, + apiPathname, + apiPagesDirectory, + getInheritance, + getDemos, + readFile, + } = componentInfo; + + const { shouldSkip, spread, EOL, src } = readFile(); if (shouldSkip) { return null; } const reactApi: ReactApi = docgenParse( - readFileSync(filename, 'utf8'), + src, null, defaultHandlers.concat(muiDefaultPropsHandler), { filename }, @@ -588,9 +573,10 @@ const generateComponentApi = async ( } reactApi.filename = filename; reactApi.name = name; - reactApi.apiUrl = options.apiUrl; + reactApi.muiName = muiName; + reactApi.apiPathname = apiPathname; reactApi.EOL = EOL; - reactApi.demos = findComponentDemos(name, pagesMarkdown); + reactApi.demos = getDemos(); if (reactApi.demos.length === 0) { throw new Error( 'Unable to find demos. \n' + @@ -603,30 +589,14 @@ const generateComponentApi = async ( // no Object.assign to visually check for collisions reactApi.forwardsRefTo = testInfo.forwardsRefTo; reactApi.spread = testInfo.spread ?? spread; - - const inheritedComponentName = testInfo.inheritComponent || inheritedComponent; - if (inheritedComponentName) { - reactApi.inheritance = { - component: inheritedComponentName, - pathname: - inheritedComponentName === 'Transition' - ? 'http://reactcommunity.org/react-transition-group/transition/#Transition-props' - : `${options.productUrlPrefix}/api/${kebabCase(inheritedComponentName)}/`, - }; - } else { - reactApi.inheritance = null; - } - + reactApi.inheritance = getInheritance(testInfo.inheritComponent); reactApi.styles = await parseStyles(reactApi, program); if (reactApi.styles.classes.length > 0 && !reactApi.name.endsWith('Unstyled')) { - reactApi.styles.name = getMuiName(reactApi.name); + reactApi.styles.name = reactApi.muiName; } reactApi.styles.classes.forEach((key) => { - const globalClass = generateUtilityClass( - reactApi.styles.name || getMuiName(reactApi.name), - key, - ); + const globalClass = generateUtilityClass(reactApi.styles.name || reactApi.muiName, key); reactApi.styles.globalClasses[key] = globalClass; }); @@ -638,7 +608,7 @@ const generateComponentApi = async ( // Generate pages, json and translations generateApiTranslations(path.join(process.cwd(), 'docs/translations/api-docs'), reactApi); - generateApiPage(outputPagesDirectory, reactApi); + generateApiPage(apiPagesDirectory, reactApi); // Add comment about demo & api links (including inherited component) to the component file await annotateComponentDefinition(reactApi); diff --git a/docs/scripts/buildApi.ts b/docs/scripts/buildApi.ts index 5a2c66d7e2ace4..d625ff39d1cbfa 100644 --- a/docs/scripts/buildApi.ts +++ b/docs/scripts/buildApi.ts @@ -1,56 +1,25 @@ -import { mkdirSync, readFileSync } from 'fs'; +import { mkdirSync } from 'fs'; import * as fse from 'fs-extra'; import path from 'path'; -import * as _ from 'lodash'; import kebabCase from 'lodash/kebabCase'; import * as yargs from 'yargs'; -import { ReactDocgenApi } from 'react-docgen'; -import { findPages, findPagesMarkdown, findComponents } from 'docs/src/modules/utils/find'; -import { getHeaders } from '@mui/markdown'; -import { Styles } from 'docs/src/modules/utils/parseStyles'; import * as ttp from 'typescript-to-proptypes'; -import { getGeneralPathInfo, getMaterialPathInfo, getBasePathInfo } from './buildApiUtils'; -import buildComponentApi, { writePrettifiedFile } from './ApiBuilders/ComponentApiBuilder'; +import { findPages, findComponents } from 'docs/src/modules/utils/find'; +import FEATURE_TOGGLE from 'docs/src/featureToggle'; +import { + ComponentInfo, + getGenericComponentInfo, + getMaterialComponentInfo, + getBaseComponentInfo, + extractApiPage, +} from 'docs/scripts/buildApiUtils'; +import buildComponentApi, { + writePrettifiedFile, + ReactApi, +} from 'docs/scripts/ApiBuilders/ComponentApiBuilder'; const apiDocsTranslationsDirectory = path.resolve('docs', 'translations', 'api-docs'); -interface ReactApi extends ReactDocgenApi { - /** - * list of page pathnames - * @example ['/components/Accordion'] - */ - demos: readonly string[]; - EOL: string; - filename: string; - apiUrl: string; - forwardsRefTo: string | undefined; - inheritance: { component: string; pathname: string } | null; - /** - * react component name - * @example 'Accordion' - */ - name: string; - description: string; - spread: boolean | undefined; - /** - * result of path.readFileSync from the `filename` in utf-8 - */ - src: string; - styles: Styles; - propsTable: _.Dictionary<{ - default: string | undefined; - required: boolean | undefined; - type: { name: string | undefined; description: string | undefined }; - deprecated: true | undefined; - deprecationInfo: string | undefined; - }>; - translations: { - componentDescription: string; - propDescriptions: { [key: string]: string | undefined }; - classDescriptions: { [key: string]: { description: string; conditions?: string } }; - }; -} - async function removeOutdatedApiDocsTranslations(components: readonly ReactApi[]): Promise { const componentDirectories = new Set(); const files = await fse.readdir(apiDocsTranslationsDirectory); @@ -82,39 +51,71 @@ async function removeOutdatedApiDocsTranslations(components: readonly ReactApi[] ); } +const getAllFiles = (dirPath: string, arrayOfFiles: string[] = []) => { + const files = fse.readdirSync(dirPath); + + files.forEach((file) => { + if (fse.statSync(`${dirPath}/${file}`).isDirectory()) { + arrayOfFiles = getAllFiles(`${dirPath}/${file}`, arrayOfFiles); + } else { + arrayOfFiles.push(path.join(__dirname, dirPath, '/', file)); + } + }); + + return arrayOfFiles; +}; + +function findApiPages(relativeFolder: string) { + let pages: Array<{ pathname: string }> = []; + let filePaths = []; + try { + filePaths = getAllFiles(path.join(process.cwd(), relativeFolder)); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + return []; + } + filePaths.forEach((itemPath) => { + if (itemPath.endsWith('.js')) { + const data = extractApiPage(itemPath); + + pages.push({ pathname: data.apiPathname }); + } + }); + + // sort by pathnames without '-' so that e.g. card comes before card-action + pages = pages.sort((a, b) => { + const pathnameA = a.pathname.replace(/-/g, ''); + const pathnameB = b.pathname.replace(/-/g, ''); + if (pathnameA < pathnameB) { + return -1; + } + if (pathnameA > pathnameB) { + return 1; + } + return 0; + }); + + return pages; +} + interface Settings { input: { /** * Component directories to be used to generate API */ libDirectory: string[]; - /** - * The directory to get api pathnames to generate pagesApi - */ - pageDirectory: string; - /** - * The directory that contains markdown files to be used to find demos - * related to the processed component - */ - markdownDirectory: string; }; output: { - /** - * API page + json content output directory - */ - pagesDirectory: string; /** * The output path of `pagesApi` generated from `input.pageDirectory` */ apiManifestPath: string; }; - productUrlPrefix: string; - getPathInfo: (filename: string) => { apiUrl: string; demoUrl: string }; + getApiPages: () => Array<{ pathname: string }>; + getComponentInfo: (filename: string) => ComponentInfo; } -/** - * This is the refactored version of the current API building process, nothing's changed. - */ const BEFORE_MIGRATION_SETTINGS: Settings[] = [ { input: { @@ -123,127 +124,86 @@ const BEFORE_MIGRATION_SETTINGS: Settings[] = [ path.join(process.cwd(), 'packages/mui-material/src'), path.join(process.cwd(), 'packages/mui-lab/src'), ], - pageDirectory: path.join(process.cwd(), 'docs/pages'), - markdownDirectory: path.join(process.cwd(), 'docs/src/pages'), }, output: { - pagesDirectory: path.join(process.cwd(), 'docs/pages/api-docs'), apiManifestPath: path.join(process.cwd(), 'docs/src/pagesApi.js'), }, - productUrlPrefix: '', - getPathInfo: getGeneralPathInfo, + getApiPages: () => { + const pages = findPages({ front: true }, path.join(process.cwd(), 'docs/pages')); + return pages.find(({ pathname }) => pathname.indexOf('api') !== -1)?.children ?? []; + }, + getComponentInfo: getGenericComponentInfo, }, ]; -/** - * Once the preparation is done (as described in https://github.com/mui-org/material-ui/issues/30091), swithc to this settings. - * It will generate API for the current & `/material` paths, then set the redirect to link `/api/*` to `/material/api/*` - * At this point, `mui-base` content is still live in with `mui-material`. - */ -// @ts-ignore -// eslint-disable-next-line @typescript-eslint/no-unused-vars const MIGRATION_SETTINGS: Settings[] = [ - ...BEFORE_MIGRATION_SETTINGS, { input: { libDirectory: [ - path.join(process.cwd(), 'packages/mui-base/src'), path.join(process.cwd(), 'packages/mui-material/src'), path.join(process.cwd(), 'packages/mui-lab/src'), ], - pageDirectory: path.join(process.cwd(), 'docs/pages/material'), - markdownDirectory: path.join(process.cwd(), 'docs/data'), }, output: { - pagesDirectory: path.join(process.cwd(), 'docs/pages/material/api-docs'), apiManifestPath: path.join(process.cwd(), 'docs/data/material/pagesApi.js'), }, - productUrlPrefix: '/material', - getPathInfo: getMaterialPathInfo, + getApiPages: () => findApiPages('docs/pages/material/api'), + getComponentInfo: getMaterialComponentInfo, }, -]; - -/** - * Once redirects are stable - * - Create `mui-base` content in `docs/pages/base/*` and switch to this settings. - * - Remove old content directories, eg. `docs/pages/components/*`, ...etc - */ -// @ts-ignore -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const POST_MIGRATION_SETTINGS: Settings[] = [ { input: { - libDirectory: [ - path.join(process.cwd(), 'packages/mui-material/src'), - path.join(process.cwd(), 'packages/mui-lab/src'), - ], - pageDirectory: path.join(process.cwd(), 'docs/pages/material'), - markdownDirectory: path.join(process.cwd(), 'docs/data'), + libDirectory: [path.join(process.cwd(), 'packages/mui-base/src')], }, output: { - pagesDirectory: path.join(process.cwd(), 'docs/pages/material/api-docs'), - apiManifestPath: path.join(process.cwd(), 'docs/data/material/pagesApi.js'), + apiManifestPath: path.join(process.cwd(), 'docs/data/base/pagesApi.js'), }, - productUrlPrefix: '/material', - getPathInfo: getMaterialPathInfo, + getApiPages: () => findApiPages('docs/pages/base/api'), + getComponentInfo: getBaseComponentInfo, }, + // add other products, eg. joy, data-grid, ...etc { + // use old config so that component type definition does not change by `annotateComponentDefinition` + // TODO: remove this setting at cleanup phase input: { - libDirectory: [path.join(process.cwd(), 'packages/mui-base/src')], - pageDirectory: path.join(process.cwd(), 'docs/pages/base'), - markdownDirectory: path.join(process.cwd(), 'docs/data'), + libDirectory: [ + path.join(process.cwd(), 'packages/mui-base/src'), + path.join(process.cwd(), 'packages/mui-material/src'), + path.join(process.cwd(), 'packages/mui-lab/src'), + ], }, output: { - pagesDirectory: path.join(process.cwd(), 'docs/pages/base/api-docs'), - apiManifestPath: path.join(process.cwd(), 'docs/data/base/pagesApi.js'), + apiManifestPath: path.join(process.cwd(), 'docs/src/pagesApi.js'), }, - productUrlPrefix: '/base', - getPathInfo: getBasePathInfo, + getApiPages: () => { + const pages = findPages({ front: true }, path.join(process.cwd(), 'docs/pages')); + return pages.find(({ pathname }) => pathname.indexOf('api') !== -1)?.children ?? []; + }, + getComponentInfo: getGenericComponentInfo, }, - // add other products, eg. joy, data-grid, ...etc ]; -const ACTIVE_SETTINGS = BEFORE_MIGRATION_SETTINGS; +// TODO: Switch to MIGRATION_SETTINGS once ready to migrate content +const ACTIVE_SETTINGS = FEATURE_TOGGLE.enable_product_scope + ? MIGRATION_SETTINGS + : BEFORE_MIGRATION_SETTINGS; async function run(argv: { grep?: string }) { const grep = argv.grep == null ? null : new RegExp(argv.grep); let allBuilds: Array> = []; await ACTIVE_SETTINGS.reduce(async (resolvedPromise, setting) => { + await resolvedPromise; const workspaceRoot = path.resolve(__dirname, '../../'); /** * @type {string[]} */ const componentDirectories = setting.input.libDirectory; const apiPagesManifestPath = setting.output.apiManifestPath; - const pagesDirectory = setting.output.pagesDirectory; - mkdirSync(pagesDirectory, { mode: 0o777, recursive: true }); const manifestDir = apiPagesManifestPath.match(/(.*)\/[^/]+\./)?.[1]; if (manifestDir) { mkdirSync(manifestDir, { recursive: true }); } - /** - * pageMarkdown: Array<{ components: string[]; filename: string; pathname: string }> - * - * e.g.: - * [{ - * pathname: '/components/accordion', - * filename: '/Users/user/Projects/material-ui/docs/src/pages/components/badges/accordion-ja.md', - * components: [ 'Accordion', 'AccordionActions', 'AccordionDetails', 'AccordionSummary' ] - * }, ...] - */ - const pagesMarkdown = findPagesMarkdown(setting.input.markdownDirectory) - .map((markdown) => { - const markdownSource = readFileSync(markdown.filename, 'utf8'); - return { - ...markdown, - pathname: setting.getPathInfo(markdown.filename).demoUrl, - components: getHeaders(markdownSource).components, - }; - }) - .filter((markdown) => markdown.components.length > 0); - /** * components: Array<{ filename: string }> * e.g. @@ -282,15 +242,11 @@ async function run(argv: { grep?: string }) { const componentBuilds = components.map(async (component) => { try { const { filename } = component; - const pathInfo = setting.getPathInfo(filename); + const componentInfo = setting.getComponentInfo(filename); - return buildComponentApi(filename, { - ttpProgram: program, - pagesMarkdown, - apiUrl: pathInfo.apiUrl, - productUrlPrefix: setting.productUrlPrefix, - outputPagesDirectory: setting.output.pagesDirectory, - }); + mkdirSync(componentInfo.apiPagesDirectory, { mode: 0o777, recursive: true }); + + return buildComponentApi(componentInfo, program); } catch (error: any) { error.message = `${path.relative(process.cwd(), component.filename)}: ${error.message}`; throw error; @@ -312,16 +268,9 @@ async function run(argv: { grep?: string }) { allBuilds = [...allBuilds, ...builds]; - const pages = findPages({ front: true }, setting.input.pageDirectory); - const apiPages = pages.find(({ pathname }) => pathname.indexOf('api') !== -1)?.children; - if (apiPages === undefined) { - throw new TypeError('Unable to find pages under /api'); - } - - const source = `module.exports = ${JSON.stringify(apiPages)}`; + const source = `module.exports = ${JSON.stringify(setting.getApiPages())}`; writePrettifiedFile(apiPagesManifestPath, source); - - await resolvedPromise; + return Promise.resolve(); }, Promise.resolve()); if (grep === null) { diff --git a/docs/scripts/buildApiUtils.test.ts b/docs/scripts/buildApiUtils.test.ts index 080ba4acad3cdb..fe8fe55a67c11b 100644 --- a/docs/scripts/buildApiUtils.test.ts +++ b/docs/scripts/buildApiUtils.test.ts @@ -1,89 +1,223 @@ +import path from 'path'; +import fs from 'fs'; import { expect } from 'chai'; +import sinon from 'sinon'; +import FEATURE_TOGGLE from '../src/featureToggle'; import { - findComponentDemos, - getMuiName, - getGeneralPathInfo, - getMaterialPathInfo, - getBasePathInfo, + extractApiPage, + extractPackageFile, + getGenericComponentInfo, + getMaterialComponentInfo, + getBaseComponentInfo, } from './buildApiUtils'; describe('buildApiUtils', () => { - it('findComponentDemos return matched component', () => { - expect( - findComponentDemos('Accordion', [ - { - pathname: '/material/components/accordion', - components: ['Accordion', 'AccordionDetails'], - }, - { - pathname: '/material/components/accordion-details', - components: ['Accordion', 'AccordionDetails'], - }, - ]), - ).to.deep.equal(['/material/components/accordion', '/material/components/accordion-details']); + describe('extractApiPage', () => { + it('return info for api page', () => { + expect( + extractApiPage('/material-ui/docs/pages/material/api/mui-material/accordion-actions.js'), + ).to.deep.equal({ + apiPathname: '/material/api/mui-material/accordion-actions', + }); + }); }); - it('getMuiName return name without Unstyled', () => { - expect(getMuiName('ButtonUnstyled')).to.equal('MuiButton'); - }); + describe('extractPackageFilePath', () => { + it('return info if path is a package (material)', () => { + const result = extractPackageFile('/material-ui/packages/mui-material/src/Button/Button.js'); + sinon.assert.match(result, { + packagePath: 'mui-material', + muiPackage: 'mui-material', + name: 'Button', + }); + }); - it('getMuiName return name without Styled', () => { - expect(getMuiName('StyledInputBase')).to.equal('MuiInputBase'); - }); + it('return info if path is a package (lab)', () => { + const result = extractPackageFile( + '/material-ui/packages/mui-lab/src/LoadingButton/LoadingButton.js', + ); + sinon.assert.match(result, { + packagePath: 'mui-lab', + muiPackage: 'mui-lab', + name: 'LoadingButton', + }); + }); - describe('getGeneralPathInfo', () => { - it('return correct apiUrl', () => { - const info = getGeneralPathInfo(`/packages/mui-material/src/Button/Button.js`); - expect(info.apiUrl).to.equal(`/api/button`); + it('return info if path is a package (base)', () => { + const result = extractPackageFile( + '/material-ui/packages/mui-base/src/TabUnstyled/TabUnstyled.tsx', + ); + sinon.assert.match(result, { + packagePath: 'mui-base', + muiPackage: 'mui-base', + name: 'TabUnstyled', + }); }); - it('return correct demoUrl', () => { - const info = getGeneralPathInfo(`/docs/src/pages/components/buttons/buttons.md`); - expect(info.demoUrl).to.equal(`/components/buttons`); + it('return info if path is a package (data-grid)', () => { + const result = extractPackageFile('/material-ui/packages/grid/x-data-grid/src/DataGrid.tsx'); + sinon.assert.match(result, { + packagePath: 'x-data-grid', + muiPackage: 'mui-data-grid', + name: 'DataGrid', + }); }); - }); - describe('getMaterialPathInfo', () => { - it('[mui-material] return correct apiUrl', () => { - const info = getMaterialPathInfo(`/packages/mui-material/src/Button/Button.js`); - expect(info.apiUrl).to.equal(`/material/api/button`); + it('return info if path is a package (data-grid-pro)', () => { + const result = extractPackageFile( + '/material-ui/packages/grid/x-data-grid-pro/src/DataGridPro.tsx', + ); + sinon.assert.match(result, { + packagePath: 'x-data-grid-pro', + muiPackage: 'mui-data-grid-pro', + name: 'DataGridPro', + }); }); - it('[mui-material] return correct demoUrl', () => { - const info = getMaterialPathInfo(`/docs/data/material/components/buttons/buttons.md`); - expect(info.demoUrl).to.equal(`/material/components/buttons`); + it('return null if path is not a package', () => { + const result = extractPackageFile( + '/material-ui/docs/pages/material/getting-started/getting-started.md', + ); + sinon.assert.match(result, { + packagePath: null, + name: null, + }); }); + }); + + describe('getGenericComponentInfo', () => { + it('return correct apiPathname', () => { + const info = getGenericComponentInfo( + path.join(process.cwd(), `/packages/mui-material/src/Button/Button.js`), + ); + sinon.assert.match(info, { + name: 'Button', + apiPathname: '/api/button/', + muiName: 'MuiButton', + apiPagesDirectory: sinon.match((value) => value.endsWith('docs/pages/api-docs')), + }); + + expect(info.getInheritance('ButtonBase')).to.deep.equal({ + name: 'ButtonBase', + apiPathname: '/api/button-base/', + }); - it('[mui-lab] return correct apiUrl', () => { - const info = getMaterialPathInfo(`/packages/mui-lab/src/LoadingButton/LoadingButton.js`); - expect(info.apiUrl).to.equal(`/material/api/loading-button`); + expect(info.getDemos()).to.deep.equal([ + { + name: 'Button Group', + demoPathname: '/components/button-group/', + }, + { + name: 'Buttons', + demoPathname: '/components/buttons/', + }, + ]); }); - it('[mui-lab] return correct demoUrl', () => { - const info = getMaterialPathInfo(`/docs/data/material/components/buttons/buttons.md`); - expect(info.demoUrl).to.equal(`/material/components/buttons`); + it('Icon return correct Demos annotation', () => { + const info = getGenericComponentInfo( + path.join(process.cwd(), `/packages/mui-material/src/Icon/Icon.js`), + ); + sinon.assert.match(info, { + name: 'Icon', + apiPathname: '/api/icon/', + muiName: 'MuiIcon', + apiPagesDirectory: sinon.match((value) => value.endsWith('docs/pages/api-docs')), + }); + + expect(info.getDemos()).to.deep.equal([ + { + name: 'Icons', + demoPathname: '/components/icons/', + }, + { + name: 'Material Icons', + demoPathname: '/components/material-icons/', + }, + ]); }); + }); - it('[mui-base] return correct apiUrl', () => { - const info = getMaterialPathInfo(`/packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.tsx`); - expect(info.apiUrl).to.equal(`/material/api/button-unstyled`); + describe('getMaterialComponentInfo', () => { + beforeEach(function test() { + if (!FEATURE_TOGGLE.enable_product_scope) { + this.skip(); + } }); + it('return correct info for material component file', () => { + const info = getMaterialComponentInfo( + path.join(process.cwd(), `/packages/mui-material/src/Button/Button.js`), + ); + sinon.assert.match(info, { + name: 'Button', + apiPathname: '/material/api/mui-material/button/', + muiName: 'MuiButton', + apiPagesDirectory: sinon.match((value) => + value.endsWith('docs/pages/material/api/mui-material'), + ), + }); - it('[mui-base] return correct demoUrl', () => { - const info = getMaterialPathInfo(`/docs/data/material/components/buttons/buttons.md`); - expect(info.demoUrl).to.equal(`/material/components/buttons`); + expect(info.getInheritance('ButtonBase')).to.deep.equal({ + name: 'ButtonBase', + apiPathname: '/material/api/mui-material/button-base/', + }); + + let existed = false; + try { + fs.readdirSync(path.join(process.cwd(), 'docs/data')); + existed = true; + // eslint-disable-next-line no-empty + } catch (error) {} + if (existed) { + expect(info.getDemos()).to.deep.equal([ + { + name: 'Button Group', + demoPathname: '/material/react-button-group/', + }, + { + name: 'Buttons', + demoPathname: '/material/react-buttons/', + }, + ]); + } }); }); - describe('getBasePathInfo', () => { - it('return correct apiUrl', () => { - const info = getBasePathInfo(`/packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.tsx`); - expect(info.apiUrl).to.equal(`/base/api/button-unstyled`); + describe('getBaseComponentInfo', () => { + beforeEach(function test() { + if (!FEATURE_TOGGLE.enable_product_scope) { + this.skip(); + } }); + it('return correct info for base component file', () => { + const info = getBaseComponentInfo( + path.join(process.cwd(), `/packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.tsx`), + ); + sinon.assert.match(info, { + name: 'ButtonUnstyled', + apiPathname: '/base/api/mui-base/button-unstyled/', + muiName: 'MuiButton', + apiPagesDirectory: sinon.match((value) => value.endsWith('docs/pages/base/api/mui-base')), + }); + + info.readFile(); + + expect(info.getInheritance()).to.deep.equal(null); - it('return correct demoUrl', () => { - const info = getBasePathInfo(`/docs/data/base/components/button-unstyled/button-unstyled.md`); - expect(info.demoUrl).to.equal(`/base/components/button-unstyled`); + let existed = false; + try { + fs.readdirSync(path.join(process.cwd(), 'docs/data')); + existed = true; + // eslint-disable-next-line no-empty + } catch (error) {} + if (existed) { + expect(info.getDemos()).to.deep.equal([ + { + name: 'Buttons', + demoPathname: '/material/react-buttons/', + }, + ]); + } }); }); }); diff --git a/docs/scripts/buildApiUtils.ts b/docs/scripts/buildApiUtils.ts index 30579aa88dc1ac..acf0caec7025cc 100644 --- a/docs/scripts/buildApiUtils.ts +++ b/docs/scripts/buildApiUtils.ts @@ -1,57 +1,256 @@ +import fs from 'fs'; import path from 'path'; import kebabCase from 'lodash/kebabCase'; +import { getHeaders } from '@mui/markdown'; +import { findPagesMarkdown, findPagesMarkdownNew } from 'docs/src/modules/utils/find'; +import { getLineFeed } from 'docs/scripts/helpers'; +import { pageToTitle } from 'docs/src/modules/utils/helpers'; -export function findComponentDemos( +function findComponentDemos( componentName: string, pagesMarkdown: ReadonlyArray<{ pathname: string; components: readonly string[] }>, ) { - const demos = pagesMarkdown - .filter((page) => { - return page.components.includes(componentName); - }) - .map((page) => { - return page.pathname; - }); - - return Array.from(new Set(demos)); + const filteredMarkdowns = pagesMarkdown + .filter((page) => page.components.includes(componentName)) + .map((page) => page.pathname); + return Array.from(new Set(filteredMarkdowns)) // get unique filenames + .map((pathname) => ({ + name: pageToTitle({ pathname }) || '', + demoPathname: `${pathname}/`, + })); } -export function getMuiName(name: string) { +function getMuiName(name: string) { return `Mui${name.replace('Unstyled', '').replace('Styled', '')}`; } -function normalizeFilePath(filename: string) { - return filename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); -} +const componentPackageMapping = { + material: {} as Record, + base: {} as Record, +}; + +const packages = [ + { + name: 'mui-material', + product: 'material', + paths: [ + path.join(__dirname, '../../packages/mui-lab/src'), + path.join(__dirname, '../../packages/mui-material/src'), + path.join(__dirname, '../../packages/mui-base/src'), + ], + }, + { + name: 'mui-base', + product: 'base', + paths: [path.join(__dirname, '../../packages/mui-base/src')], + }, +]; + +packages.forEach((pkg) => { + pkg.paths.forEach((pkgPath) => { + const packageName = pkgPath.match(/packages\/([^/]+)\/src/)?.[1]; + if (!packageName) { + throw new Error(`cannot find package name from path: ${pkgPath}`); + } + const filePaths = fs.readdirSync(pkgPath); + filePaths.forEach((folder) => { + if (folder.match(/^[A-Z]/)) { + // @ts-ignore + componentPackageMapping[pkg.product][folder] = packageName; + } + }); + }); +}); + +export const extractPackageFile = (filePath: string) => { + filePath = filePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); + const match = filePath.match( + /.*\/packages.*\/(?[^/]+)\/src\/(.*\/)?(?[^/]+)\.(js|tsx|ts|d\.ts)/, + ); + const result = { + packagePath: match ? match.groups?.packagePath! : null, + name: match ? match.groups?.name! : null, + }; + return { + ...result, + muiPackage: result.packagePath?.replace('x-', 'mui-'), + }; +}; + +export const extractApiPage = (filePath: string) => { + filePath = filePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); + return { + apiPathname: filePath + .replace(/^.*\/pages/, '') + .replace(/\.(js|tsx)/, '') + .replace(/^\/index$/, '/') // Replace `index` by `/`. + .replace(/\/index$/, ''), + }; +}; + +const parseFile = (filename: string) => { + const src = fs.readFileSync(filename, 'utf8'); + return { + src, + shouldSkip: + filename.indexOf('internal') !== -1 || + !!src.match(/@ignore - internal component\./) || + !!src.match(/@ignore - do not document\./), + spread: !src.match(/ = exactProp\(/), + EOL: getLineFeed(src), + inheritedComponent: src.match(/\/\/ @inheritedComponent (.*)/)?.[1], + }; +}; + +export type ComponentInfo = { + /** + * Full path to the file + */ + filename: string; + /** + * Component name + */ + name: string; + /** + * Component name with `Mui` prefix + */ + muiName: string; + apiPathname: string; + readFile: () => { + src: string; + spread: boolean; + shouldSkip: boolean; + EOL: string; + inheritedComponent?: string; + }; + getInheritance: (inheritedComponent?: string) => null | { + /** + * Component name + */ + name: string; + /** + * API pathname + */ + apiPathname: string; + }; + getDemos: () => Array<{ name: string; demoPathname: string }>; + apiPagesDirectory: string; +}; -/** - * Provide information from the filename, can be component or markdown. (will be removed once migration is done) - * component example: /Users/siriwatknp/Personal-Repos/material-ui/packages/mui-material/src/Button/Button.js - * markdown example: /Users/siriwatknp/Personal-Repos/material-ui/docs/src/pages/components/buttons/buttons.md - */ -export const getGeneralPathInfo = (filename: string) => { - filename = normalizeFilePath(filename); - const componentName = filename.match(/.*\/([^/]+)\.(tsx|js)/)?.[1]; +export const getGenericComponentInfo = (filename: string): ComponentInfo => { + const { name } = extractPackageFile(filename); + let srcInfo: null | ReturnType = null; + if (!name) { + throw new Error(`Could not find the component name from: ${filename}`); + } return { - apiUrl: `/api/${kebabCase(componentName)}`, - demoUrl: filename.replace(/^.*\/pages/, '').replace(/\/[^/]+\.(md|js|ts|tsx)/, ''), + filename, + name, + muiName: getMuiName(name), + apiPathname: `/api/${kebabCase(name)}/`, + apiPagesDirectory: path.join(process.cwd(), 'docs/pages/api-docs'), + readFile() { + srcInfo = parseFile(filename); + return srcInfo; + }, + getInheritance(inheritedComponent = srcInfo?.inheritedComponent) { + if (!inheritedComponent) { + return null; + } + return { + name: inheritedComponent, + apiPathname: + inheritedComponent === 'Transition' + ? 'http://reactcommunity.org/react-transition-group/transition/#Transition-props' + : `/api/${kebabCase(inheritedComponent)}/`, + }; + }, + getDemos: () => { + const allMarkdowns = findPagesMarkdown().map((markdown) => ({ + ...markdown, + components: getHeaders(fs.readFileSync(markdown.filename, 'utf8')).components as string[], + })); + return findComponentDemos(name, allMarkdowns); + }, }; }; -export const getMaterialPathInfo = (filename: string) => { - filename = normalizeFilePath(filename); - const componentName = filename.match(/.*\/([^/]+)\.(tsx|js)/)?.[1]; +export const getMaterialComponentInfo = (filename: string): ComponentInfo => { + const { name, muiPackage } = extractPackageFile(filename); + let srcInfo: null | ReturnType = null; + if (!name) { + throw new Error(`Could not find the component name from: ${filename}`); + } + const componentPkg = componentPackageMapping.material?.[name ?? '']; return { - apiUrl: `/material/api/${kebabCase(componentName)}`, - demoUrl: filename.replace(/^.*\/data/, '').replace(/\/[^/]+\.(md|js|ts|tsx)/, ''), + filename, + name, + muiName: getMuiName(name), + apiPathname: `/material/api/${componentPkg}/${kebabCase(name)}/`, + apiPagesDirectory: path.join(process.cwd(), `docs/pages/material/api/${muiPackage}`), + readFile() { + srcInfo = parseFile(filename); + return srcInfo; + }, + getInheritance(inheritedComponent = srcInfo?.inheritedComponent) { + if (!inheritedComponent) { + return null; + } + const inheritedPkg = componentPackageMapping.material?.[inheritedComponent ?? '']; + return { + name: inheritedComponent, + apiPathname: + inheritedComponent === 'Transition' + ? 'http://reactcommunity.org/react-transition-group/transition/#Transition-props' + : `/material/api/${inheritedPkg}/${kebabCase(inheritedComponent)}/`, + }; + }, + getDemos: () => { + const allMarkdowns = findPagesMarkdownNew().map((markdown) => ({ + ...markdown, + components: getHeaders(fs.readFileSync(markdown.filename, 'utf8')).components as string[], + })); + return findComponentDemos(name, allMarkdowns); + }, }; }; -export const getBasePathInfo = (filename: string) => { - filename = normalizeFilePath(filename); - const componentName = filename.match(/.*\/([^/]+)\.(tsx|js)/)?.[1]; +export const getBaseComponentInfo = (filename: string): ComponentInfo => { + const { name, muiPackage } = extractPackageFile(filename); + let srcInfo: null | ReturnType = null; + if (!name) { + throw new Error(`Could not find the component name from: ${filename}`); + } + const componentPkg = componentPackageMapping.base?.[name ?? '']; return { - apiUrl: `/base/api/${kebabCase(componentName)}`, - demoUrl: filename.replace(/^.*\/data/, '').replace(/\/[^/]+\.(md|js|ts|tsx)/, ''), + filename, + name, + muiName: getMuiName(name), + apiPathname: `/base/api/${componentPkg}/${kebabCase(name)}/`, + apiPagesDirectory: path.join(process.cwd(), `docs/pages/base/api/${muiPackage}`), + readFile() { + srcInfo = parseFile(filename); + return srcInfo; + }, + getInheritance(inheritedComponent = srcInfo?.inheritedComponent) { + if (!inheritedComponent) { + return null; + } + const inheritedPkg = componentPackageMapping.base?.[inheritedComponent ?? '']; + return { + name: inheritedComponent, + apiPathname: + inheritedComponent === 'Transition' + ? 'http://reactcommunity.org/react-transition-group/transition/#Transition-props' + : `/base/api/${inheritedPkg}/${kebabCase(inheritedComponent)}/`, + }; + }, + getDemos: () => { + const allMarkdowns = findPagesMarkdownNew().map((markdown) => ({ + ...markdown, + components: getHeaders(fs.readFileSync(markdown.filename, 'utf8')).components as string[], + })); + return findComponentDemos(name, allMarkdowns); + }, }; }; diff --git a/docs/scripts/formattedTSDemos.js b/docs/scripts/formattedTSDemos.js index 8cc123a47d3f8c..f2cea30ac5f638 100644 --- a/docs/scripts/formattedTSDemos.js +++ b/docs/scripts/formattedTSDemos.js @@ -9,7 +9,7 @@ * List of demos to ignore when transpiling * Example: "app-bar/BottomAppBar.tsx" */ -const ignoreList = []; +const ignoreList = ['/pages.ts']; const fse = require('fs-extra'); const path = require('path'); diff --git a/docs/scripts/restructure.ts b/docs/scripts/restructure.ts index 7a1177a3c2ab8f..ad8cd428896f0c 100644 --- a/docs/scripts/restructure.ts +++ b/docs/scripts/restructure.ts @@ -1,9 +1,7 @@ import fs from 'fs'; import path from 'path'; import prettier from 'prettier'; -import pages from 'docs/src/pages'; import { - refactorJsonContent, getNewDataLocation, getNewPageLocation, productPathnames, @@ -29,35 +27,6 @@ function writePrettifiedFile(filename: string, data: string, options: object = { }); } -const prefixSource = (arraySource: string, pathnames: string[], product: string) => { - let target = arraySource; - - // prefix with `/${product}/` - pathnames.forEach((pathname) => { - const replace = `"${pathname}/([-/a-z]*)"`; - target = target.replace(new RegExp(replace, 'g'), `"/${product}${pathname}/$1"`); - target = target.replace( - new RegExp(`"pathname":"${pathname}"`, 'g'), - `"pathname":"/${product}${pathname}"`, - ); - }); - - return target; -}; - -const createProductPagesData = (arraySource: string, product: string) => { - // prepare source code - const source = ` -const pages = ${arraySource} - -export default pages - `; - - // create new folder and add prettified file. - fs.mkdirSync(`${workspaceRoot}/docs/data/${product}`, { recursive: true }); - writePrettifiedFile(`${workspaceRoot}/docs/data/${product}/pages.ts`, source); -}; - const appendSource = (target: string, template: string, source: string) => { const match = source.match(/^(.*)$/m); if (match && target.includes(match[0])) { @@ -109,71 +78,75 @@ function run() { */ (['styles', 'system', 'material'] as const).forEach((product) => { const pathnames = productPathnames[product] as unknown as string[]; - const productPages = pages.filter((item) => pathnames.includes(item.pathname)); - - let arraySource = JSON.stringify(productPages); - - if (product === 'material') { - arraySource = prefixSource(arraySource, [...pathnames, '/api'], 'material'); - } - - createProductPagesData(arraySource, product); // update _app.js to use product pages updateAppToUseProductPagesData(product); pathnames.forEach((pathname) => { - if (pathname !== '/api-docs') { - // clone js/md data to new location - const dataDir = readdirDeep(path.resolve(`docs/src/pages${pathname}`)); - dataDir.forEach((filePath) => { - const info = getNewDataLocation(filePath, product); - // pathname could be a directory - if (info) { - let data = fs.readFileSync(filePath, { encoding: 'utf-8' }); - if (filePath.endsWith('.md')) { - data = markdown.removeDemoRelativePath(data); - data = markdown.addMaterialPrefixToLinks(data); - if (product === 'material') { - data = markdown.addProductFrontmatter(data, 'material'); - } + // clone js/md data to new location + const dataDir = readdirDeep(path.resolve(`docs/src/pages${pathname}`)); + dataDir.forEach((filePath) => { + const info = getNewDataLocation(filePath, product); + // pathname could be a directory + if (info) { + let data = fs.readFileSync(filePath, { encoding: 'utf-8' }); + if (filePath.endsWith('.md')) { + data = markdown.removeDemoRelativePath(data); + if (product === 'material') { + data = markdown.addProductFrontmatter(data, 'material'); } - fs.mkdirSync(info.directory, { recursive: true }); - fs.writeFileSync(info.path, data); // (A) } - }); - } + fs.mkdirSync(info.directory, { recursive: true }); + fs.writeFileSync(info.path, data); // (A) + + fs.rmSync(filePath); + } + }); const pagesDir = readdirDeep(path.resolve(`docs/pages${pathname}`)); pagesDir.forEach((filePath) => { if (product === 'material') { - // clone pages to new location - const info = getNewPageLocation(filePath); - // pathname could be a directory - if (info) { - let data = fs.readFileSync(filePath, { encoding: 'utf-8' }); - - if (filePath.endsWith('.json')) { - data = refactorJsonContent(data); - } + if (!filePath.includes('api-docs')) { + // clone pages to new location + const info = getNewPageLocation(filePath); + // pathname could be a directory + if (info) { + let data = fs.readFileSync(filePath, { encoding: 'utf-8' }); + + if (filePath.endsWith('.js')) { + data = data.replace('/src/pages/', `/data/material/`); // point to data path (A) in new directory + } - if (filePath.endsWith('.js')) { - data = data.replace('/src/pages/', `/products/${product}/`); // point to data path (A) in new directory - } + fs.mkdirSync(info.directory, { recursive: true }); + fs.writeFileSync(info.path, data); - fs.mkdirSync(info.directory, { recursive: true }); - fs.writeFileSync(info.path, data); + fs.writeFileSync(filePath, data); + } } } else { let data = fs.readFileSync(filePath, { encoding: 'utf-8' }); if (filePath.endsWith('.js')) { - data = data.replace('/src/pages/', `/products/`); // point to data path (A) in new directory + data = data.replace('/src/pages/', `/data/${product}`); // point to data path (A) in new directory } fs.writeFileSync(filePath, data); } }); }); }); + + // include `base` pages in `_app.js` + updateAppToUseProductPagesData('base'); + + // Turn feature toggle `enable_product_scope: true` + const featureTogglePath = path.join(process.cwd(), 'docs/src/featureToggle.js'); + let featureToggle = fs.readFileSync(featureTogglePath, { encoding: 'utf8' }); + + featureToggle = featureToggle.replace( + `enable_product_scope: false`, + `enable_product_scope: true`, + ); + + fs.writeFileSync(featureTogglePath, featureToggle); } run(); diff --git a/docs/scripts/restructureUtils.test.ts b/docs/scripts/restructureUtils.test.ts index cc1f2f33474a40..0b84ce8ae56cf7 100644 --- a/docs/scripts/restructureUtils.test.ts +++ b/docs/scripts/restructureUtils.test.ts @@ -1,10 +1,5 @@ import { expect } from 'chai'; -import { - markdown, - refactorJsonContent, - getNewDataLocation, - getNewPageLocation, -} from './restructureUtils'; +import { markdown, getNewDataLocation, getNewPageLocation } from './restructureUtils'; describe('restructure utils', () => { describe('refactorMarkdownContent', () => { @@ -71,24 +66,6 @@ githubLabel: 'component: Avatar' }); }); - describe('refactorJsonContent', () => { - it('add prefix to demos value', () => { - expect( - refactorJsonContent( - `"demos": ""`, - ), - ).to.equal( - `"demos": ""`, - ); - }); - - it('add prefix to pathname value', () => { - expect( - refactorJsonContent(`"inheritance": { "component": "Paper", "pathname": "/api/paper/" },`), - ).to.equal(`"inheritance": { "component": "Paper", "pathname": "/material/api/paper/" },`); - }); - }); - it('getNewDataLocation', () => { expect( getNewDataLocation( @@ -115,5 +92,15 @@ githubLabel: 'component: Avatar' directory: 'material-ui/docs/pages/material/getting-started', path: 'material-ui/docs/pages/material/getting-started/installation.js', }); + + expect(getNewPageLocation('material-ui/docs/pages/components/buttons.js')).to.deep.equal({ + directory: 'material-ui/docs/pages/material', + path: 'material-ui/docs/pages/material/react-buttons.js', + }); + + expect(getNewPageLocation('material-ui/docs/pages/components/about-the-lab.js')).to.deep.equal({ + directory: 'material-ui/docs/pages/material', + path: 'material-ui/docs/pages/material/about-the-lab.js', + }); }); }); diff --git a/docs/scripts/restructureUtils.ts b/docs/scripts/restructureUtils.ts index 6d854fc72def48..424b3d2094e457 100644 --- a/docs/scripts/restructureUtils.ts +++ b/docs/scripts/restructureUtils.ts @@ -1,12 +1,5 @@ export const productPathnames = { - material: [ - '/getting-started', - '/components', - '/api-docs', - '/customization', - '/guides', - '/discover-more', - ], + material: ['/getting-started', '/components', '/customization', '/guides', '/discover-more'], system: ['/system'], styles: ['/styles'], } as const; @@ -16,7 +9,10 @@ export const markdown = { content.replace(/"pages\/[/\-a-zA-Z]*\/([a-zA-Z]*\.js)"/gm, `"$1"`), addMaterialPrefixToLinks: (content: string) => { productPathnames.material.forEach((path) => { - content = content.replace(new RegExp(`\\(${path}`, 'g'), `(/material${path}`); + content = content.replace( + new RegExp(`\\(${path}`, 'g'), + `(/material${path.replace('/components/', '/react-')}`, + ); }); return content; }, @@ -24,23 +20,11 @@ export const markdown = { content.replace('---', `---\nproduct: ${product}`), }; -export const refactorJsonContent = (content: string) => { - let result = content; - - // i. add prefix to "demos" key - result = result.replace(/href=\\"\/components/g, 'href=\\"/material/components'); - - // ii. add prefix to "pathname" value - result = result.replace(/"pathname": "\/api/g, '"pathname": "/material/api'); - - return result; -}; - export const getNewDataLocation = ( filePath: string, product: string, ): { directory: string; path: string } | null => { - const match = filePath.match(/^(.*)\/[^/]+\.(ts|js|tsx|md|json)$/); + const match = filePath.match(/^(.*)\/[^/]+\.(ts|js|tsx|md|json|tsx\.preview)$/); if (!match) { return null; } @@ -50,13 +34,27 @@ export const getNewDataLocation = ( }; }; +const nonComponents = ['about-the-lab']; + export const getNewPageLocation = ( filePath: string, ): { directory: string; path: string } | null => { - const match = filePath.match(/^(.*)\/[^/]+\.(ts|js|tsx|md|json)$/); + const match = filePath.match(/^(.*)\/[^/]+\.(ts|js|tsx|md|json|tsx\.preview)$/); if (!match) { return null; } + if (filePath.includes('components')) { + if (nonComponents.some((path) => filePath.includes(path))) { + return { + directory: match[1].replace('docs/pages/components', 'docs/pages/material'), + path: filePath.replace('docs/pages/components/', 'docs/pages/material/'), + }; + } + return { + directory: match[1].replace('docs/pages/components', 'docs/pages/material'), + path: filePath.replace('docs/pages/components/', 'docs/pages/material/react-'), + }; + } return { directory: match[1].replace('docs/pages', 'docs/pages/material'), path: filePath.replace('docs/pages', 'docs/pages/material'), diff --git a/docs/src/featureToggle.js b/docs/src/featureToggle.js new file mode 100644 index 00000000000000..716f5660c09cf7 --- /dev/null +++ b/docs/src/featureToggle.js @@ -0,0 +1,8 @@ +// need to use commonjs export so that docs/packages/markdown can use +module.exports = { + nav_products: true, + enable_website_banner: false, + // TODO: cleanup once migration is done + enable_product_scope: false, // related to new structure change + enable_redirects: false, // related to new structure change +}; diff --git a/docs/src/featureToggle.ts b/docs/src/featureToggle.ts deleted file mode 100644 index 1d4e86ecfc3cac..00000000000000 --- a/docs/src/featureToggle.ts +++ /dev/null @@ -1,7 +0,0 @@ -const FEATURE_TOGGLE = { - nav_products: true, - enable_product_scope: false, - enable_website_banner: false, -}; - -export default FEATURE_TOGGLE; diff --git a/docs/src/modules/components/ApiPage.js b/docs/src/modules/components/ApiPage.js index 89db4a084d05af..ff198ae460e852 100644 --- a/docs/src/modules/components/ApiPage.js +++ b/docs/src/modules/components/ApiPage.js @@ -1,6 +1,7 @@ /* eslint-disable react/no-danger */ import * as React from 'react'; import PropTypes from 'prop-types'; +import { useRouter } from 'next/router'; import clsx from 'clsx'; import { exactProp } from '@mui/utils'; import { styled } from '@mui/material/styles'; @@ -10,6 +11,7 @@ import { useTranslate, useUserLanguage } from 'docs/src/modules/utils/i18n'; import HighlightedCode from 'docs/src/modules/components/HighlightedCode'; import MarkdownElement from 'docs/src/modules/components/MarkdownElement'; import AppLayoutDocs from 'docs/src/modules/components/AppLayoutDocs'; +import replaceMarkdownLinks from 'docs/src/modules/utils/replaceMarkdownLinks'; const Asterisk = styled('abbr')(({ theme }) => ({ color: theme.palette.error.main })); @@ -180,6 +182,7 @@ Heading.propTypes = { }; function ApiDocs(props) { + const router = useRouter(); const { descriptions, pageContent } = props; const t = useTranslate(); const userLanguage = useUserLanguage(); @@ -345,7 +348,7 @@ import { ${componentName} } from '${source}';`} ) : null} - + diff --git a/docs/src/modules/components/AppNavDrawer.js b/docs/src/modules/components/AppNavDrawer.js index f70acf9c55d886..ab1a1c890e3fa0 100644 --- a/docs/src/modules/components/AppNavDrawer.js +++ b/docs/src/modules/components/AppNavDrawer.js @@ -116,7 +116,7 @@ function reduceChildRoutes(context) { if (page.children && page.children.length > 1) { const title = pageToTitleI18n(page, t); - const topLevel = activePage ? activePage.pathname.indexOf(`${page.pathname}/`) === 0 : false; + const topLevel = activePage ? activePage.pathname.indexOf(`${page.pathname}`) === 0 : false; items.push( {rendered.map((renderedMarkdownOrDemo, index) => { if (typeof renderedMarkdownOrDemo === 'string') { - return ; + return ( + + ); } if (renderedMarkdownOrDemo.component) { diff --git a/docs/src/modules/utils/find.js b/docs/src/modules/utils/find.js index 57c8b548d6ece5..a0ec3160d16a76 100644 --- a/docs/src/modules/utils/find.js +++ b/docs/src/modules/utils/find.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const FEATURE_TOGGLE = require('../../featureToggle'); const markdownRegex = /\.md$/; @@ -10,7 +11,9 @@ const markdownRegex = /\.md$/; * @returns {Array<{ filename: string, pathname: string }>} */ function findPagesMarkdown( - directory = path.resolve(__dirname, '../../../src/pages'), + directory = FEATURE_TOGGLE.enable_product_scope + ? path.resolve(__dirname, '../../../data') + : path.resolve(__dirname, '../../../src/pages'), pagesMarkdown = [], ) { const items = fs.readdirSync(directory); @@ -27,17 +30,68 @@ function findPagesMarkdown( return; } + let pathname = ''; + if (FEATURE_TOGGLE.enable_product_scope) { + pathname = itemPath + .replace(new RegExp(`\\${path.sep}`, 'g'), '/') + .replace(/^.*\/material[^-]/, '/') + .replace('.md', ''); + } else { + pathname = itemPath + .replace(new RegExp(`\\${path.sep}`, 'g'), '/') + .replace(/^.*\/pages/, '') + .replace('.md', ''); + } + + // Remove the last pathname segment. + pathname = pathname.split('/').slice(0, 3).join('/'); + + pagesMarkdown.push({ + // Relative location in the path (URL) system. + pathname, + // Relative location in the file system. + filename: itemPath, + }); + }); + + return pagesMarkdown; +} + +/** + * Returns the markdowns of the documentation in a flat array. + * @param {string} [directory] + * @param {Array<{ filename: string, pathname: string }>} [pagesMarkdown] + * @returns {Array<{ filename: string, pathname: string }>} + */ +function findPagesMarkdownNew( + directory = path.resolve(__dirname, '../../../data'), + pagesMarkdown = [], +) { + const items = fs.readdirSync(directory); + + items.forEach((item) => { + const itemPath = path.resolve(directory, item); + + if (fs.statSync(itemPath).isDirectory()) { + findPagesMarkdownNew(itemPath, pagesMarkdown); + return; + } + + if (!markdownRegex.test(item)) { + return; + } + let pathname = itemPath .replace(new RegExp(`\\${path.sep}`, 'g'), '/') - .replace(/^.*\/pages/, '') + .replace(/^.*\/data/, '') .replace('.md', ''); // Remove the last pathname segment. - pathname = pathname.split('/').slice(0, 3).join('/'); + pathname = pathname.split('/').slice(0, 4).join('/'); pagesMarkdown.push({ // Relative location in the path (URL) system. - pathname, + pathname: pathname.replace('components/', 'react-'), // Relative location in the file system. filename: itemPath, }); @@ -159,5 +213,6 @@ function findPages( module.exports = { findPages, findPagesMarkdown, + findPagesMarkdownNew, findComponents, }; diff --git a/docs/src/modules/utils/findActivePage.test.js b/docs/src/modules/utils/findActivePage.test.js new file mode 100644 index 00000000000000..47819675bd67c3 --- /dev/null +++ b/docs/src/modules/utils/findActivePage.test.js @@ -0,0 +1,96 @@ +import { expect } from 'chai'; +import findActivePage from './findActivePage'; + +describe('findActivePage', () => { + describe('old structure', () => { + const pages = [ + { + pathname: '/getting-started', + icon: 'DescriptionIcon', + children: [{ pathname: '/getting-started/installation' }], + }, + { + pathname: '/components', + icon: 'ToggleOnIcon', + children: [ + { + pathname: '/components', + subheader: '/components/inputs', + children: [ + { pathname: '/components/autocomplete' }, + { pathname: '/components/buttons', title: 'Button' }, + { pathname: '/components/button-group' }, + { pathname: '/components/checkboxes', title: 'Checkbox' }, + { pathname: '/components/floating-action-button' }, + { pathname: '/components/radio-buttons', title: 'Radio button' }, + { pathname: '/components/rating' }, + { pathname: '/components/selects', title: 'Select' }, + { pathname: '/components/slider' }, + { pathname: '/components/switches', title: 'Switch' }, + { pathname: '/components/text-fields', title: 'Text field' }, + { pathname: '/components/transfer-list' }, + { pathname: '/components/toggle-button' }, + ], + }, + ], + }, + ]; + it('return first level page', () => { + expect(findActivePage(pages, '/getting-started')).to.deep.equal({ + pathname: '/getting-started', + icon: 'DescriptionIcon', + children: [{ pathname: '/getting-started/installation' }], + }); + }); + + it('return nested page', () => { + expect(findActivePage(pages, '/getting-started/installation')).to.deep.equal({ + pathname: '/getting-started/installation', + }); + }); + + it('return deep nested page', () => { + expect(findActivePage(pages, '/components/radio-buttons')).to.deep.equal({ + pathname: '/components/radio-buttons', + title: 'Radio button', + }); + }); + }); + + describe('new structure', () => { + const pages = [ + { + pathname: '/material/components', + icon: 'ToggleOnIcon', + children: [ + { + pathname: '/material/components', + subheader: '/components/inputs', + children: [ + { pathname: '/material/react-autocomplete' }, + { pathname: '/material/react-buttons', title: 'Button' }, + { pathname: '/material/react-button-group' }, + { pathname: '/material/react-checkboxes', title: 'Checkbox' }, + { pathname: '/material/react-floating-action-button' }, + { pathname: '/material/react-radio-buttons', title: 'Radio button' }, + { pathname: '/material/react-rating' }, + { pathname: '/material/react-selects', title: 'Select' }, + { pathname: '/material/react-slider' }, + { pathname: '/material/react-switches', title: 'Switch' }, + { pathname: '/material/react-text-fields', title: 'Text field' }, + { pathname: '/material/react-transfer-list' }, + { pathname: '/material/react-toggle-button' }, + ], + }, + ], + }, + ]; + + it('return deep nested page', () => { + expect(findActivePage(pages, '/material/react-radio-buttons')).to.deep.equal({ + pathname: '/material/react-radio-buttons', + title: 'Radio button', + }); + }); + }); +}); diff --git a/docs/src/modules/utils/findActivePage.ts b/docs/src/modules/utils/findActivePage.ts new file mode 100644 index 00000000000000..9d9edd1ef54f3c --- /dev/null +++ b/docs/src/modules/utils/findActivePage.ts @@ -0,0 +1,16 @@ +import { MuiPage } from 'docs/src/pages'; + +export default function findActivePage(currentPages: MuiPage[], pathname: string): MuiPage | null { + const map: Record = {}; + + const traverse = (array: MuiPage[]) => { + array.forEach((item) => { + map[item.pathname] = item; + traverse(item.children || []); + }); + }; + + traverse(currentPages); + + return map[pathname] || null; +} diff --git a/docs/src/modules/utils/helpers.test.js b/docs/src/modules/utils/helpers.test.js index 75595ebc132a88..630c56a56b2deb 100644 --- a/docs/src/modules/utils/helpers.test.js +++ b/docs/src/modules/utils/helpers.test.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { getDependencies } from './helpers'; +import { getDependencies, pageToTitle } from './helpers'; describe('docs getDependencies helpers', () => { before(() => { @@ -10,6 +10,20 @@ describe('docs getDependencies helpers', () => { delete process.env.SOURCE_CODE_REPO; }); + it('should return correct title', () => { + expect(pageToTitle({ pathname: '/docs/src/pages/components/buttons/buttons.md' })).to.equal( + 'Buttons', + ); + expect(pageToTitle({ pathname: '/components' })).to.equal('Components'); + expect(pageToTitle({ pathname: '/customization/how-to-customize' })).to.equal( + 'How To Customize', + ); + }); + + it('should remove `react-` prefix', () => { + expect(pageToTitle({ pathname: '/docs/pages/material/react-buttons.js' })).to.equal('Buttons'); + }); + const s1 = ` import * as React from 'react'; import PropTypes from 'prop-types'; diff --git a/docs/src/modules/utils/helpers.ts b/docs/src/modules/utils/helpers.ts index 4a28d4d55c4b06..3993867bd474e8 100644 --- a/docs/src/modules/utils/helpers.ts +++ b/docs/src/modules/utils/helpers.ts @@ -36,7 +36,7 @@ export function pageToTitle(page: Page): string | null { } const path = page.subheader || page.pathname; - const name = path.replace(/.*\//, ''); + const name = path.replace(/.*\//, '').replace('react-', '').replace(/\..*/, ''); if (path.indexOf('/api') === 0) { return upperFirst(camelCase(name)); diff --git a/docs/src/modules/utils/replaceMarkdownLinks.test.js b/docs/src/modules/utils/replaceMarkdownLinks.test.js index 15b2dda1e1bc15..853ca325c9b1f5 100644 --- a/docs/src/modules/utils/replaceMarkdownLinks.test.js +++ b/docs/src/modules/utils/replaceMarkdownLinks.test.js @@ -105,7 +105,7 @@ describe('replaceMarkdownLinks', () => {

API

  • <Button />
  • <ButtonBase />
  • -
  • <ButtonUnstyled />
  • +
  • <ButtonUnstyled />
  • <IconButton />
  • <LoadingButton />
  • DataGrid
  • @@ -122,7 +122,7 @@ describe('replaceMarkdownLinks', () => {

    API

    @@ -131,7 +131,7 @@ describe('replaceMarkdownLinks', () => {

    API

    @@ -172,7 +172,7 @@ describe('replaceMarkdownLinks', () => {
  • Demo
  • <Button />
  • <ButtonBase />
  • -
  • <ButtonUnstyled />
  • +
  • <ButtonUnstyled />
  • <IconButton />
  • <LoadingButton />
  • DataGrid
  • diff --git a/docs/src/modules/utils/replaceMarkdownLinks.ts b/docs/src/modules/utils/replaceMarkdownLinks.ts index 747b7413382ba4..b066e69e348653 100644 --- a/docs/src/modules/utils/replaceMarkdownLinks.ts +++ b/docs/src/modules/utils/replaceMarkdownLinks.ts @@ -18,15 +18,12 @@ export const replaceAPILinks = (markdown: string) => { /href=(\\*?)"\/api\/(loading-button|tab-list|tab-panel|date-picker|date-time-picker|time-picker|calendar-picker|calendar-picker-skeleton|desktop-picker|mobile-date-picker|month-picker|pickers-day|static-date-picker|year-picker|masonry|timeline|timeline-connector|timeline-content|timeline-dot|timeline-item|timeline-opposite-content|timeline-separator|unstable-trap-focus|tree-item|tree-view)([^"]*)"/gm, 'href=$1"/material/api/mui-lab/$2$3"', ) - .replace( - /href=(\\*?)"\/api\/([^"-]+-unstyled)([^"]*)"/gm, - 'href=$1"/material/api/mui-base/$2$3"', - ) + .replace(/href=(\\*?)"\/api\/([^"-]+-unstyled)([^"]*)"/gm, 'href=$1"/base/api/mui-base/$2$3"') .replace(/href=(\\*?)"\/api\/([^"]*)"/gm, 'href=$1"/material/api/mui-material/$2"'); }; export default function replaceMarkdownLinks(markdown: string, asPath: string) { - if (asPath.startsWith('/material/') || asPath.startsWith('/x/')) { + if (asPath.startsWith('/material/') || asPath.startsWith('/x/') || asPath.startsWith('/base/')) { return replaceMaterialLinks(replaceAPILinks(replaceComponentLinks(markdown))); } return markdown; diff --git a/docs/src/modules/utils/replaceUrl.test.js b/docs/src/modules/utils/replaceUrl.test.js index e72ee08341bc5a..d786f8f1d11191 100644 --- a/docs/src/modules/utils/replaceUrl.test.js +++ b/docs/src/modules/utils/replaceUrl.test.js @@ -55,7 +55,7 @@ describe('replaceUrl', () => { it('replace correct API links', () => { expect(replaceAPILinks(`/api/button/`)).to.equal(`/material/api/mui-material/button/`); expect(replaceAPILinks(`/api/button-unstyled/`)).to.equal( - `/material/api/mui-base/button-unstyled/`, + `/base/api/mui-base/button-unstyled/`, ); expect(replaceAPILinks(`/api/loading-button/`)).to.equal( `/material/api/mui-lab/loading-button/`, @@ -71,8 +71,8 @@ describe('replaceUrl', () => { expect(replaceAPILinks(`/material/api/mui-material/button/`)).to.equal( `/material/api/mui-material/button/`, ); - expect(replaceAPILinks(`/material/api/mui-base/button-unstyled/`)).to.equal( - `/material/api/mui-base/button-unstyled/`, + expect(replaceAPILinks(`/base/api/mui-base/button-unstyled/`)).to.equal( + `/base/api/mui-base/button-unstyled/`, ); expect(replaceAPILinks(`/material/api/mui-lab/loading-button/`)).to.equal( `/material/api/mui-lab/loading-button/`, diff --git a/docs/src/modules/utils/replaceUrl.ts b/docs/src/modules/utils/replaceUrl.ts index ca6de245e2ea50..b22464501797c1 100644 --- a/docs/src/modules/utils/replaceUrl.ts +++ b/docs/src/modules/utils/replaceUrl.ts @@ -16,7 +16,12 @@ export const replaceComponentLinks = (url: string) => { }; export const replaceAPILinks = (url: string) => { - if (url.startsWith('/x') || url.startsWith('/material') || !url.startsWith('/api')) { + if ( + url.startsWith('/x') || + url.startsWith('/material') || + url.startsWith('/base/') || + !url.startsWith('/api') + ) { return url; } url = url @@ -25,16 +30,16 @@ export const replaceAPILinks = (url: string) => { /\/api\/(loading-button|tab-list|tab-panel|date-picker|date-time-picker|time-picker|calendar-picker|calendar-picker-skeleton|desktop-picker|mobile-date-picker|month-picker|pickers-day|static-date-picker|year-picker|masonry|timeline|timeline-connector|timeline-content|timeline-dot|timeline-item|timeline-opposite-content|timeline-separator|unstable-trap-focus|tree-item|tree-view)(.*)/, '/material/api/mui-lab/$1$2', ) - .replace(/\/api\/([^/]+-unstyled)(.*)/, '/material/api/mui-base/$1$2'); + .replace(/\/api\/([^/]+-unstyled)(.*)/, '/base/api/mui-base/$1$2'); - if (url.startsWith('/x') || url.startsWith('/material')) { + if (url.startsWith('/x') || url.startsWith('/material') || url.startsWith('/base/')) { return url; } return url.replace(/\/api\/(.*)/, '/material/api/mui-material/$1'); }; export default function replaceUrl(url: string, asPath: string) { - if (asPath.startsWith('/material/') || asPath.startsWith('/x/')) { + if (asPath.startsWith('/material/') || asPath.startsWith('/x/') || asPath.startsWith('/base/')) { return replaceMaterialLinks(replaceAPILinks(replaceComponentLinks(url))); } return url; diff --git a/docs/tsconfig.json b/docs/tsconfig.json index d541bdd030a46d..b3282c841557ef 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../tsconfig.json", - "include": ["next-env.d.ts", "types", "src", "pages"], + "include": ["next-env.d.ts", "types", "src", "pages", "data"], "compilerOptions": { "allowJs": true, "isolatedModules": true, diff --git a/package.json b/package.json index 8e87e7ed3dc643..bedb956ea28df2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "release:publish": "lerna publish from-package --dist-tag latest --contents build", "release:publish:dry-run": "lerna publish from-package --dist-tag latest --contents build --registry=\"http://localhost:4873/\"", "release:tag": "node scripts/releaseTag", - "docs:api": "rimraf ./docs/pages/**/api-docs && yarn docs:api:build", + "docs:api": "rimraf ./docs/pages/**/api-docs ./docs/pages/**/api && yarn docs:api:build", "docs:api:build": "cross-env BABEL_ENV=development __NEXT_EXPORT_TRAILING_SLASH=true babel-node --extensions \".tsx,.ts,.js\" ./docs/scripts/buildApi.ts", "docs:build": "yarn workspace docs build", "docs:build-sw": "yarn workspace docs build-sw", diff --git a/test/e2e-website/material-current.spec.ts b/test/e2e-website/material-current.spec.ts new file mode 100644 index 00000000000000..60c82fb0e000f5 --- /dev/null +++ b/test/e2e-website/material-current.spec.ts @@ -0,0 +1,124 @@ +import { test as base, expect, Page } from '@playwright/test'; +import kebabCase from 'lodash/kebabCase'; +import { TestFixture } from './playwright.config'; + +const test = base.extend({}); + +test.describe.parallel('Material docs', () => { + test('should have correct link with hash in the TOC', async ({ page }) => { + await page.goto(`/getting-started/installation/`); + + const anchors = page.locator('[aria-label="Page table of contents"] ul a'); + + const anchorTexts = await anchors.allTextContents(); + + await Promise.all( + anchorTexts.map((text, index) => { + return expect(anchors.nth(index)).toHaveAttribute( + 'href', + `/getting-started/installation/#${kebabCase(text)}`, + ); + }), + ); + }); + + test.describe.parallel('Demo page', () => { + test('should have correct link for API section', async ({ page }) => { + await page.goto(`/components/cards/`); + + const anchors = await page.locator('div > h2#heading-api ~ ul a'); + + const anchorTexts = await anchors.allTextContents(); + + await Promise.all( + anchorTexts.map((text, index) => { + return expect(anchors.nth(index)).toHaveAttribute('href', `/api/${kebabCase(text)}/`); + }), + ); + }); + + test('should have correct link for sidebar anchor', async ({ page }) => { + await page.goto(`/components/cards/`); + + const anchor = await page.locator('nav[aria-label="documentation"] ul a:text-is("Card")'); + + await expect(anchor).toHaveAttribute('href', `/components/cards/`); + }); + }); + + test.describe.parallel('API page', () => { + test('should have correct link for sidebar anchor', async ({ page }) => { + await page.goto(`/api/card/`); + + const anchor = await page.locator('nav[aria-label="documentation"] ul a:text-is("Card")'); + + await expect(anchor).toHaveAttribute('app-drawer-active', ''); + await expect(anchor).toHaveAttribute('href', `/api/card/`); + }); + + test('all the links in the main content should have correct prefix', async ({ page }) => { + await page.goto(`/api/card/`); + + const anchors = await page.locator('div#main-content a'); + + const handles = await anchors.elementHandles(); + + const links = await Promise.all(handles.map((elm) => elm.getAttribute('href'))); + + links.forEach((link) => { + expect(link.startsWith('/material')).toBeFalsy(); + }); + }); + }); + + test.describe.parallel('Search', () => { + const retryToggleSearch = async (page: Page, count = 3) => { + try { + await page.keyboard.press('Meta+k'); + await page.waitForSelector('input#docsearch-input', { timeout: 2000 }); + } catch (error) { + if (count === 0) { + throw error; + } + await retryToggleSearch(page, count - 1); + } + }; + test('should have correct link when searching component', async ({ page }) => { + await page.goto(`/getting-started/installation/`, { waitUntil: 'networkidle' }); + + await retryToggleSearch(page); + + await page.type('input#docsearch-input', 'card', { delay: 50 }); + + const anchor = await page.locator('.DocSearch-Hits a:has-text("Card")'); + + await expect(anchor.first()).toHaveAttribute('href', `/components/cards/#main-content`); + }); + + test('should have correct link when searching API', async ({ page }) => { + await page.goto(`/getting-started/installation/`, { waitUntil: 'networkidle' }); + + await retryToggleSearch(page); + + await page.type('input#docsearch-input', 'card api', { delay: 50 }); + + const anchor = await page.locator('.DocSearch-Hits a:has-text("Card API")'); + + await expect(anchor.first()).toHaveAttribute('href', `/api/card/#main-content`); + }); + + test('should have correct link when searching lab API', async ({ page }) => { + await page.goto(`/getting-started/installation/`); + + await page.waitForLoadState('networkidle'); // wait for docsearch + + await retryToggleSearch(page); + + await page.type('input#docsearch-input', 'loading api', { delay: 50 }); + + const anchor = await page.locator('.DocSearch-Hits a:has-text("LoadingButton API")'); + + await expect(anchor.first()).toHaveAttribute('href', `/api/loading-button/#main-content`); + }); + }); +}); diff --git a/test/e2e-website/material-docs.spec.ts b/test/e2e-website/material-new.spec.ts similarity index 56% rename from test/e2e-website/material-docs.spec.ts rename to test/e2e-website/material-new.spec.ts index e73f032deb4c96..451a34499ca541 100644 --- a/test/e2e-website/material-docs.spec.ts +++ b/test/e2e-website/material-new.spec.ts @@ -1,20 +1,17 @@ -import { test as base, expect } from '@playwright/test'; +import { test as base, expect, Page } from '@playwright/test'; import kebabCase from 'lodash/kebabCase'; import FEATURE_TOGGLE from 'docs/src/featureToggle'; import { TestFixture } from './playwright.config'; const test = base.extend({}); -test.beforeEach(async ({ materialUrlPrefix }) => { - test.skip( - !!materialUrlPrefix && !FEATURE_TOGGLE.enable_product_scope, - "Migration haven't started yet", - ); +test.beforeEach(async ({}) => { + test.skip(!FEATURE_TOGGLE.enable_product_scope, "Migration haven't started yet"); }); test.describe.parallel('Material docs', () => { - test('should have correct link with hash in the TOC', async ({ page, materialUrlPrefix }) => { - await page.goto(`${materialUrlPrefix}/getting-started/installation/`); + test('should have correct link with hash in the TOC', async ({ page }) => { + await page.goto(`/material/getting-started/installation/`); const anchors = page.locator('[aria-label="Page table of contents"] ul a'); @@ -24,15 +21,15 @@ test.describe.parallel('Material docs', () => { anchorTexts.map((text, index) => { return expect(anchors.nth(index)).toHaveAttribute( 'href', - `${materialUrlPrefix}/getting-started/installation/#${kebabCase(text)}`, + `/material/getting-started/installation/#${kebabCase(text)}`, ); }), ); }); test.describe.parallel('Demo page', () => { - test('should have correct link for API section', async ({ page, materialUrlPrefix }) => { - await page.goto(`${materialUrlPrefix}/components/cards/`); + test('should have correct link for API section', async ({ page }) => { + await page.goto(`/material/react-cards/`); const anchors = await page.locator('div > h2#heading-api ~ ul a'); @@ -42,36 +39,41 @@ test.describe.parallel('Material docs', () => { anchorTexts.map((text, index) => { return expect(anchors.nth(index)).toHaveAttribute( 'href', - `${materialUrlPrefix}/api/${kebabCase(text)}/`, + `/material/api/mui-material/${kebabCase(text)}/`, ); }), ); }); - test('should have correct link for sidebar anchor', async ({ page, materialUrlPrefix }) => { - await page.goto(`${materialUrlPrefix}/components/cards/`); + test('should have correct API link to mui-base', async ({ page }) => { + await page.goto(`/material/react-buttons/`); + + await expect(page.locator('a[href="/base/api/mui-base/button-unstyled/"]')).toContainText( + '', + ); + }); + + test('should have correct link for sidebar anchor', async ({ page }) => { + await page.goto(`/material/react-cards/`); const anchor = await page.locator('nav[aria-label="documentation"] ul a:text-is("Card")'); - await expect(anchor).toHaveAttribute('href', `${materialUrlPrefix}/components/cards/`); + await expect(anchor).toHaveAttribute('href', `/material/react-cards/`); }); }); test.describe.parallel('API page', () => { - test('should have correct link for sidebar anchor', async ({ page, materialUrlPrefix }) => { - await page.goto(`${materialUrlPrefix}/api/card/`); + test('should have correct link for sidebar anchor', async ({ page }) => { + await page.goto(`/material/api/mui-material/card/`); const anchor = await page.locator('nav[aria-label="documentation"] ul a:text-is("Card")'); await expect(anchor).toHaveAttribute('app-drawer-active', ''); - await expect(anchor).toHaveAttribute('href', `${materialUrlPrefix}/api/card/`); + await expect(anchor).toHaveAttribute('href', `/material/api/mui-material/card/`); }); - test('all the links in the main content should have correct prefix', async ({ - page, - materialUrlPrefix, - }) => { - await page.goto(`${materialUrlPrefix}/api/card/`); + test('all the links in the main content should have correct prefix', async ({ page }) => { + await page.goto(`/material/api/mui-material/card/`); const anchors = await page.locator('div#main-content a'); @@ -81,24 +83,25 @@ test.describe.parallel('Material docs', () => { links.forEach((link) => { if ( - [ - '/getting-started', - '/components', - '/api', - '/customization', - '/guides', - '/discover-more', - ].some((path) => link.replace(materialUrlPrefix, '').startsWith(path)) + ['/getting-started', '/customization', '/guides', '/discover-more'].some((path) => + link.includes(path), + ) ) { - expect(link.startsWith(materialUrlPrefix)).toBeTruthy(); + expect(link.startsWith(`/material`)).toBeTruthy(); } - if (link.replace(materialUrlPrefix, '').startsWith('/system')) { + if (link.startsWith('/material/api/')) { + expect(link).toMatch(/\/material\/api\/mui-(material|lab)\/.*/); + } + + expect(link).not.toMatch(/\/components/); // there should be no `/components` in the url anymore + + if (link.startsWith('/system')) { expect(link.startsWith('/system')).toBeTruthy(); expect(link.match(/\/system{1}/g)).toHaveLength(1); // should not have repeated `/system/system/*` } - if (link.replace(materialUrlPrefix, '').startsWith('/styles')) { + if (link.startsWith('/styles')) { expect(link.startsWith('/styles')).toBeTruthy(); expect(link.match(/\/styles{1}/g)).toHaveLength(1); // should not have repeated `/system/system/*` } @@ -107,40 +110,37 @@ test.describe.parallel('Material docs', () => { }); test.describe.parallel('Search', () => { - test('should have correct link when searching component', async ({ - page, - materialUrlPrefix, - }) => { - await page.goto(`${materialUrlPrefix}/getting-started/installation/`); + const retryToggleSearch = async (page: Page, count = 3) => { + try { + await page.keyboard.press('Meta+k'); + await page.waitForSelector('input#docsearch-input', { timeout: 2000 }); + } catch (error) { + if (count === 0) { + throw error; + } + await retryToggleSearch(page, count - 1); + } + }; + test('should have correct link when searching component', async ({ page }) => { + await page.goto(`/material/getting-started/installation/`); await page.waitForLoadState('networkidle'); // wait for docsearch - await page.keyboard.press('Meta+k'); + await retryToggleSearch(page); await page.type('input#docsearch-input', 'card', { delay: 50 }); const anchor = await page.locator('.DocSearch-Hits a:has-text("Card")'); - if (FEATURE_TOGGLE.enable_product_scope && !materialUrlPrefix) { - // the old url doc should point to the new location - await expect(anchor.first()).toHaveAttribute( - 'href', - `/material/components/cards/#main-content`, - ); - } else { - await expect(anchor.first()).toHaveAttribute( - 'href', - `${materialUrlPrefix}/components/cards/#main-content`, - ); - } + await expect(anchor.first()).toHaveAttribute('href', `/material/react-cards/#main-content`); }); - test('should have correct link when searching API', async ({ page, materialUrlPrefix }) => { - await page.goto(`${materialUrlPrefix}/getting-started/installation/`); + test('should have correct link when searching API', async ({ page }) => { + await page.goto(`/material/getting-started/installation/`); await page.waitForLoadState('networkidle'); // wait for docsearch - await page.keyboard.press('Meta+k'); + await retryToggleSearch(page); await page.type('input#docsearch-input', 'card api', { delay: 50 }); @@ -148,7 +148,7 @@ test.describe.parallel('Material docs', () => { await expect(anchor.first()).toHaveAttribute( 'href', - `${materialUrlPrefix}/api/card/#main-content`, + `/material/api/mui-material/card/#main-content`, ); }); }); diff --git a/test/e2e-website/playwright.config.ts b/test/e2e-website/playwright.config.ts index 3e037ae1ff2e6d..817b0d5975a5e3 100644 --- a/test/e2e-website/playwright.config.ts +++ b/test/e2e-website/playwright.config.ts @@ -1,6 +1,6 @@ import { PlaywrightTestConfig } from '@playwright/test'; -export type TestFixture = { materialUrlPrefix: string }; +export type TestFixture = {}; const config: PlaywrightTestConfig = { reportSlowTests: { @@ -10,20 +10,6 @@ const config: PlaywrightTestConfig = { use: { baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'https://mui.com', }, - projects: [ - { - name: 'current', - use: { - materialUrlPrefix: '', - }, - }, - { - name: 'new', - use: { - materialUrlPrefix: '/material', - }, - }, - ], }; export default config;