diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md new file mode 100644 index 000000000000..a7f1a686132e --- /dev/null +++ b/.changeset/itchy-news-drive.md @@ -0,0 +1,7 @@ +--- +'@modern-js/doc-plugin-preview': patch +--- + +feat(module-doc): support external demo + +feat(module-doc): 支持外部 demo diff --git a/packages/cli/doc-plugin-preview/src/codeToDemo.ts b/packages/cli/doc-plugin-preview/src/codeToDemo.ts index 5461dbcfb9e4..a6189e382731 100644 --- a/packages/cli/doc-plugin-preview/src/codeToDemo.ts +++ b/packages/cli/doc-plugin-preview/src/codeToDemo.ts @@ -9,6 +9,46 @@ import { injectDemoBlockImport, toValidVarName } from './utils'; import { demoBlockComponentPath } from './constant'; import { demoRoutes } from '.'; +const getExternalDemoContent = (tempVar: string) => ({ + type: 'mdxJsxFlowElement', + name: 'pre', + children: [ + { + type: 'mdxJsxFlowElement', + name: 'code', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'className', + value: 'language-tsx', + }, + { + type: 'mdxJsxAttribute', + name: 'children', + value: { + type: 'mdxJsxExpressionAttribute', + value: tempVar, + data: { + estree: { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: tempVar, + }, + }, + ], + }, + }, + }, + }, + ], + }, + ], +}); + /** * remark plugin to transform code to demo */ @@ -16,9 +56,99 @@ export const remarkCodeToDemo: Plugin< [{ isMobile: boolean; getRouteMeta: () => RouteMeta[] }], Root > = ({ isMobile, getRouteMeta }) => { + const routeMeta = getRouteMeta(); + return (tree, vfile) => { const demos: MdxjsEsm[] = []; let index = 1; + const route = routeMeta.find(meta => meta.absolutePath === vfile.path); + if (!route) { + return; + } + const { pageName } = route; + + function constructDemoNode( + demoId: string, + demoPath: string, + currentNode: any, + isMobileMode: boolean, + // Only for external demo + externalDemoIndex?: number, + ) { + const demoRoute = `/~demo/${demoId}`; + if (isMobileMode) { + // only add demoRoutes in mobile mode + demoRoutes.push({ + path: demoRoute, + }); + } else { + demos.push(getASTNodeImport(`Demo${demoId}`, demoPath)); + } + const tempVar = `externalDemoContent${externalDemoIndex}`; + + if (externalDemoIndex !== undefined) { + demos.push(getASTNodeImport(tempVar, `${demoPath}?raw`)); + } + Object.assign(currentNode, { + type: 'mdxJsxFlowElement', + name: 'Container', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'isMobile', + value: isMobileMode, + }, + { + type: 'mdxJsxAttribute', + name: 'url', + value: demoRoute, + }, + { + type: 'mdxJsxAttribute', + name: 'isExternal', + value: externalDemoIndex !== undefined, + }, + ], + children: [ + externalDemoIndex === undefined + ? { + ...currentNode, + hasVisited: true, + } + : getExternalDemoContent(tempVar), + isMobileMode + ? { + type: 'mdxJsxFlowElement', + name: null, + } + : { + type: 'mdxJsxFlowElement', + name: `Demo${demoId}`, + }, + ], + }); + } + let externalDemoIndex = 0; + // 1. External demo , use to declare demo + tree.children.forEach((node: any) => { + if (node.type === 'mdxJsxFlowElement' && node.name === 'code') { + const src = node.attributes.find( + (attr: { name: string; value: string }) => attr.name === 'src', + )?.value; + const isMobileMode = + node.attributes.find( + (attr: { name: string; value: boolean }) => + attr.name === 'isMobile', + )?.value ?? isMobile; + if (!src) { + return; + } + const id = `${toValidVarName(pageName)}_${index++}`; + constructDemoNode(id, src, node, isMobileMode, externalDemoIndex++); + } + }); + + // 2. Internal demo, use ```j/tsx to declare demo visit(tree, 'code', node => { // hasVisited is a custom property if ('hasVisited' in node) { @@ -40,17 +170,13 @@ export const remarkCodeToDemo: Plugin< node?.meta?.includes('mobile') || (!node?.meta?.includes('web') && isMobile); - const routeMeta = getRouteMeta(); - const { pageName } = routeMeta.find( - meta => meta.absolutePath === vfile.path, - )!; - const id = `${toValidVarName(pageName)}_${index++}`; const demoDir = join( process.cwd(), 'node_modules', '.modern-doc', `virtual-demo`, ); + const id = `${toValidVarName(pageName)}_${index++}`; const virtualModulePath = join(demoDir, `${id}.tsx`); fs.ensureDirSync(join(demoDir)); // Only when the content of the file changes, the file will be written @@ -61,49 +187,7 @@ export const remarkCodeToDemo: Plugin< fs.writeFileSync(virtualModulePath, value); } } - demos.push(getASTNodeImport(`Demo${id}`, virtualModulePath)); - const demoRoute = `/~demo/${id}`; - - if (isMobileMode) { - // only add demoRoutes in mobile mode - demoRoutes.push({ - path: demoRoute, - }); - } else { - demos.push(getASTNodeImport(`Demo${id}`, virtualModulePath)); - } - Object.assign(node, { - type: 'mdxJsxFlowElement', - name: 'Container', - attributes: [ - { - type: 'mdxJsxAttribute', - name: 'isMobile', - value: isMobileMode, - }, - { - type: 'mdxJsxAttribute', - name: 'url', - value: demoRoute, - }, - ], - children: [ - { - // if lang not change, this node will be visited again and again - ...node, - hasVisited: true, - }, - isMobileMode - ? { - type: 'mdxJsxFlowElement', - name: null, - } - : { - type: 'mdxJsxFlowElement', - name: `Demo${id}`, - }, - ], - }); + constructDemoNode(id, virtualModulePath, node, isMobileMode); } }); tree.children.unshift(...demos); diff --git a/packages/cli/doc-plugin-preview/src/index.ts b/packages/cli/doc-plugin-preview/src/index.ts index 1539cfb4ebd4..6b3baf2dfcb4 100644 --- a/packages/cli/doc-plugin-preview/src/index.ts +++ b/packages/cli/doc-plugin-preview/src/index.ts @@ -48,6 +48,49 @@ export function pluginPreview(options?: Options): DocPlugin { const source = await fs.readFile(filepath, 'utf-8'); const ast = processor.parse(source); let index = 1; + const { pageName } = routeMeta.find( + meta => meta.absolutePath === filepath, + )!; + + const registerDemo = ( + demoId: string, + demoPath: string, + isMobileMode: boolean, + ) => { + if (isMobileMode) { + // only add demoMeta in mobile mode + demoMeta[filepath] = demoMeta[filepath] ?? []; + const isExist = demoMeta[filepath].find( + item => item.id === demoId, + ); + if (!isExist) { + demoMeta[filepath].push({ + id: demoId, + virtualModulePath: demoPath, + }); + } + } + }; + + visit(ast, 'mdxJsxFlowElement', (node: any) => { + if (node.name === 'code') { + const src = node.attributes.find( + (attr: { name: string; value: string }) => + attr.name === 'src', + )?.value; + const isMobileMode = + node.attributes.find( + (attr: { name: string; value: boolean }) => + attr.name === 'isMobile', + )?.value ?? isMobile; + if (!src) { + return; + } + const id = `${toValidVarName(pageName)}_${index++}`; + registerDemo(id, src, isMobileMode); + } + }); + visit(ast, 'code', (node: any) => { if (node.lang === 'jsx' || node.lang === 'tsx') { const { value } = node; @@ -76,20 +119,7 @@ export function pluginPreview(options?: Options): DocPlugin { ); const virtualModulePath = join(demoDir, `${id}.tsx`); - - if (isMobileMode) { - // only add demoMeta in mobile mode - demoMeta[filepath] = demoMeta[filepath] ?? []; - const isExist = demoMeta[filepath].find( - item => item.id === id, - ); - if (!isExist) { - demoMeta[filepath].push({ - id, - virtualModulePath, - }); - } - } + registerDemo(id, virtualModulePath, isMobileMode); fs.ensureDirSync(join(demoDir)); fs.writeFileSync( @@ -147,6 +177,9 @@ import Demo from '${demoComponentPath}' }, bundlerChain(chain) { chain.module + .rule('Raw') + .resourceQuery(/raw/) + .type('asset/source') .rule('MDX') .oneOf('MDXMeta') .before('MDXCompile') diff --git a/packages/cli/doc-plugin-preview/static/global-components/Container.tsx b/packages/cli/doc-plugin-preview/static/global-components/Container.tsx index 9e6d7bc15be6..d3d5e8e6c3e0 100644 --- a/packages/cli/doc-plugin-preview/static/global-components/Container.tsx +++ b/packages/cli/doc-plugin-preview/static/global-components/Container.tsx @@ -30,6 +30,7 @@ type ContainerProps = { const Container: React.FC = props => { const { children, isMobile, url } = props; + const [showCode, setShowCode] = useState(false); const lang = useLang() || Object.keys(locales)[0]; const dark = useDark(); @@ -41,6 +42,7 @@ const Container: React.FC = props => { // Do nothing in ssr return ''; }; + const toggleCode = (e: any) => { if (!showCode) { e.target.blur();