diff --git a/.github/workflows/docs_migration.yml b/.github/workflows/docs_migration.yml deleted file mode 100644 index 7e631e93c6d..00000000000 --- a/.github/workflows/docs_migration.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Docs migration tracking -on: [push] -jobs: - docs-migration: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: 'npm' - - run: npm ci - - run: node script/docs-migration.js >> $GITHUB_STEP_SUMMARY diff --git a/script/docs-migration.js b/script/docs-migration.js deleted file mode 100644 index ee21625dba3..00000000000 --- a/script/docs-migration.js +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable no-console */ -const glob = require('fast-glob') -const fs = require('node:fs') - -const introduction = `# Docs migration tracking - -We're moving towards a [unified IA](https://github.com/github/primer/issues/1354) for Primer docs that will combine React and Rails component documentation. To implement this unified IA, we need to access Primer React component metadata from outside the primer/react repo. - -The first step to making Primer React component metadata more accessible is to move the prop metadata currently stored in MDX files into structured JSON files per component (example: [TreeView](https://github.com/primer/react/blob/main/src/TreeView/TreeView.docs.json)). These component JSON files are automatically compiled into a file called [generated/components.json](https://github.com/primer/react/blob/main/generated/components.json) that can be imported from outside the primer/react repo. - -This script tracks our progress as we move the source of truth for component metadata from MDX files to JSON files. -` - -console.log(introduction) - -const mdxFiles = glob - .sync('docs/content/**/[A-Z]*.{mdx,md}') - // Ignore deprecated components for now - .filter(file => !file.includes('/deprecated/')) - -const migratedMdxFiles = mdxFiles.filter(file => { - const content = fs.readFileSync(file, 'utf8') - - // We consider an .mdx file "migrated" if it imports a corresponding .docs.json file - return /import .* from ['"].*\.docs\.json['"]/.test(content) -}) - -console.log(`![Progress bar](https://geps.dev/progress/${Math.ceil((migratedMdxFiles.length / mdxFiles.length) * 100)}) -`) - -console.log(`## Migrated MDX files - -${migratedMdxFiles.length} of ${mdxFiles.length} MDX files have corresponding JSON files. - -${migratedMdxFiles.map(file => `- [x] [${file}](https://github.com/primer/react/blob/main/${file})`).join('\n')} -`) - -console.log(`## Not-yet migrated MDX files - -${mdxFiles.length - migratedMdxFiles.length} of ${mdxFiles.length} MDX files do not have corresponding JSON files. - -${mdxFiles - .filter(file => !migratedMdxFiles.includes(file)) - .map(file => `- [ ] [${file}](https://github.com/primer/react/blob/main/${file})`) - .join('\n')} -`) diff --git a/script/props-mdx-to-json.mjs b/script/props-mdx-to-json.mjs deleted file mode 100644 index 04053d22e64..00000000000 --- a/script/props-mdx-to-json.mjs +++ /dev/null @@ -1,267 +0,0 @@ -'use strict' - -// Temporary script to migrate React component metadata -// from .mdx files to .doc.json files. - -// TODO: Remove this script after the migration is complete. - -import glob from 'fast-glob' -import {fromMarkdown} from 'mdast-util-from-markdown' -import {frontmatterFromMarkdown} from 'mdast-util-frontmatter' -import {mdxFromMarkdown} from 'mdast-util-mdx' -import {frontmatter} from 'micromark-extension-frontmatter' -import {mdxjs} from 'micromark-extension-mdxjs' -import fs from 'node:fs' -import find from 'unist-util-find' -import {parse as parseYaml} from 'yaml' -import flatFilter from 'unist-util-flat-filter' -import {toString} from 'mdast-util-to-string' -import {findBefore} from 'unist-util-find-before' -import {snakeCase} from 'change-case' -import path from 'node:path' - -const srcMap = new Map([ - ['docs/content/ActionList.mdx', 'src/ActionList/List.tsx'], - ['docs/content/deprecated/ActionList.mdx', 'src/deprecated/ActionList/List.tsx'], - ['docs/content/deprecated/Buttons.mdx', 'src/deprecated/Button/Button.tsx'], - ['docs/content/drafts/UnderlineNav2.mdx', 'src/UnderlineNav2/UnderlineNav.tsx'], -]) - -// Get all source code files -const srcFiles = glob - .sync('src/**/[A-Z]*.tsx') - // Filter out tests and stories - .filter(filePath => !filePath.includes('.test.') && !filePath.includes('.stories.')) - -// Get all component mdx files -const mdxFiles = glob - .sync('docs/content/**/[A-Z]*.{md,mdx}') - // Filter out deprecated components for now - .filter(file => !file.includes('/deprecated/')) - -const components = mdxFiles.map(mdxPath => { - // Get the component name from the file name - const name = mdxPath.split('/').pop()?.split('.')[0] - - const id = snakeCase(mdxPath.replace('docs/content/', '').replace(/.mdx?/, '')) - - // Find the corresponding source code file - const srcPath = srcFiles.find(srcPath => srcPath.endsWith(`/${name}.tsx`)) - - // Read the contents of the MDX file - const mdxContent = fs.readFileSync(mdxPath, 'utf-8') - - // Parse the MDX file - const ast = fromMarkdown(mdxContent, { - extensions: [mdxjs(), frontmatter(['yaml'])], - mdastExtensions: [mdxFromMarkdown(), frontmatterFromMarkdown(['yaml'])], - }) - - // Get the status and a11yReviewed flag from the frontmatter - const {status, a11yReviewed = false} = parseYaml(find(ast, {type: 'yaml'}).value ?? '') - - // Get prop data by parsing usage of the component - const allComponentProps = flatFilter(ast, {type: 'mdxJsxFlowElement', name: 'PropsTable'})?.children.map(node => { - const name = toString(findBefore(ast, find(ast, node), {type: 'heading', depth: 3})) - - const props = - flatFilter(node, {type: 'mdxJsxFlowElement', name: 'PropsTableRow'})?.children.map(node => { - const name = node.attributes.find(attr => attr.name === 'name')?.value - const type = jsxToMd(node.attributes.find(attr => attr.name === 'type')?.value) - .replace(/`/g, '') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/\[([^)]+)\]\([^)]*\)/, '$1') - const description = jsxToMd(node.attributes.find(attr => attr.name === 'description')?.value) - const defaultValue = jsxToMd(node.attributes.find(attr => attr.name === 'defaultValue')?.value) - const required = node.attributes.find(attr => attr.name === 'required')?.value - const deprecated = node.attributes.find(attr => attr.name === 'deprecated')?.value - - return { - name, - type, - defaultValue, - deprecated: deprecated !== false && deprecated !== undefined ? true : undefined, - required: required !== false && required !== undefined ? true : undefined, - description, - } - }) ?? [] - - let passthrough - - // If the component has an `sx` prop, add it to the list of props - const sxPropRow = find(node, {type: 'mdxJsxFlowElement', name: 'PropsTableSxRow'}) - - if (sxPropRow) { - props.push({name: 'sx', type: 'SystemStyleObject'}) - } - - // If the component has a `ref` prop, add it to the list of props - const refPropRow = find(node, {type: 'mdxJsxFlowElement', name: 'PropsTableRefRow'}) - - if (refPropRow) { - const refType = jsxToMd(refPropRow.attributes.find(attr => attr.name === 'refType')?.value) - - props.push({name: 'ref', type: `React.RefObject<${refType}>`}) - } - - // If the component has am `as` prop, add it to the list of props - const asPropRow = find(node, {type: 'mdxJsxFlowElement', name: 'PropsTableAsRow'}) - - if (asPropRow) { - const defaultElementType = jsxToMd(asPropRow.attributes.find(attr => attr.name === 'defaultElementType')?.value) - const isComponent = defaultElementType[0].toUpperCase() === defaultElementType[0] - props.push({ - name: 'as', - type: 'React.ElementType', - defaultValue: isComponent ? defaultElementType : `"${defaultElementType}"`, - }) - } - - const passthroughRow = find(node, {type: 'mdxJsxFlowElement', name: 'PropsTablePassthroughPropsRow'}) - - if (passthroughRow) { - const element = jsxToMd(passthroughRow.attributes.find(attr => attr.name === 'elementName')?.value) - const url = jsxToMd(passthroughRow.attributes.find(attr => attr.name === 'passthroughPropsLink')?.value) - .replace(/^.*\(/, '') - .replace(/\)$/, '') - - passthrough = {element, url} - } - - const basePropRows = find(node, {type: 'mdxJsxFlowElement', name: 'PropsTableBasePropRows'}) - - if (basePropRows) { - const refType = jsxToMd(basePropRows.attributes.find(attr => attr.name === 'refType')?.value) - const elementType = jsxToMd(basePropRows.attributes.find(attr => attr.name === 'elementType')?.value) - const passthroughPropsLink = jsxToMd( - basePropRows.attributes.find(attr => attr.name === 'passthroughPropsLink')?.value, - ) - .replace(/^.*\(/, '') - .replace(/\)$/, '') - .trim() - const isPolymorphic = jsxToMd(basePropRows.attributes.find(attr => attr.name === 'isPolymorphic')?.value) - - if (refType) { - props.push({name: 'ref', type: `React.RefObject<${refType}>`}) - } - - if (elementType) { - passthrough = {element: elementType, url: passthroughPropsLink} - } - - if (isPolymorphic !== false && isPolymorphic !== undefined ? true : undefined) { - const isComponent = elementType[0].toUpperCase() === elementType[0] - props.push({ - name: 'as', - type: 'React.ElementType', - defaultValue: isComponent ? elementType : `"${elementType}"`, - }) - } - - props.push({name: 'sx', type: 'SystemStyleObject'}) - } - - return {name, props, passthrough} - }) - - // Separate the props for the current component from the props for subcomponents - const props = allComponentProps?.find(component => component.name === name)?.props ?? [] - const passthrough = allComponentProps?.find(component => component.name === name)?.passthrough - const subcomponents = allComponentProps?.filter(component => component.name !== name) ?? [] - - return { - id, - name, - mdxPath, - mdxContent, - srcPath, - status: status.toLowerCase(), - a11yReviewed, - stories: [], - props, - passthrough, - subcomponents, - } -}) - -const componentsWithoutProps = components.filter(component => component.props.length === 0) - -const componentsWithoutSrcPath = components.filter(component => !component.srcPath && !srcMap.has(component.mdxPath)) - -console.log( - 'Components without props', - componentsWithoutProps.map(component => component.name), -) - -console.log( - 'Components wihout props (count)', - `${componentsWithoutProps.length} / ${components.length}`, - `${((componentsWithoutProps.length / components.length) * 100).toFixed(2)}%`, -) - -console.log( - 'Components without srcPath', - componentsWithoutSrcPath.map(component => component.mdxPath), -) - -for (const component of components) { - const docPath = - component.srcPath?.replace('.tsx', '.docs.json') || - `${srcMap.get(component.mdxPath)?.replace(/\/[^/]*$/, '')}/${component.name}.docs.json` - - let existingFile = {} - - if (fs.existsSync(docPath)) { - existingFile = JSON.parse(fs.readFileSync(docPath, 'utf-8')) - } - - const newFile = { - ...existingFile, - id: component.id, - name: component.name, - status: component.status, - a11yReviewed: component.a11yReviewed, - stories: component.stories.length > 0 ? component.stories : existingFile.stories || [], - props: component.props.length > 0 ? component.props : existingFile.props || [], - subcomponents: component.subcomponents.length > 0 ? component.subcomponents : existingFile.subcomponents || [], - } - - fs.writeFileSync(docPath, JSON.stringify(newFile, null, 2)) - - const relativeDocPath = path.relative(component.mdxPath, docPath).replace(/^\.\.\//, '') - - if (component.props.length > 0 && !component.mdxContent.includes(`import data from '${relativeDocPath}'`)) { - let newMdxContent = component.mdxContent.replace(/\n---\n/, `\n---\n\nimport data from '${relativeDocPath}'\n`) - - const propsHeadingIndex = newMdxContent.indexOf('## Props') - const nextHeadingIndex = newMdxContent.indexOf('\n## ', propsHeadingIndex + 1) - - newMdxContent = `${newMdxContent.slice( - 0, - propsHeadingIndex, - )}## Props\n\n\n\n${newMdxContent.slice(nextHeadingIndex)}` - - fs.writeFileSync(component.mdxPath, newMdxContent) - } -} - -// Helper functions - -/** Convert JSX string to markdown string */ -function jsxToMd(node) { - if (typeof node === 'string') return node - - return toString(node) - .replace(//g, '`') - .replace(/<\/InlineCode>/g, '`') - .replace(/<>/g, '') - .replace(/<\/>/g, '') - .replace(/\{' '\}/g, ' ') - .replace(/\s+/g, ' ') - .replace(/(.+?)<\/Link>/g, '[$2]($1)') - .replace(/(.+?)<\/a>/g, '[$2]($1)') - .replace(/</g, '<') - .replace(/>/g, '>') - .trim() -}