diff --git a/.circleci/config.yml b/.circleci/config.yml index a27d822df74..dcc487a9889 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -125,6 +125,11 @@ jobs: steps: - checkout - install_js + - run: + name: '`pnpm docs:typescript:formatted` changes committed?' + command: | + pnpm docs:typescript:formatted --disable-cache + pnpm check-changes - run: name: '`pnpm prettier:all` changes committed?' command: | diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx.preview new file mode 100644 index 00000000000..efad440b376 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx.preview @@ -0,0 +1,12 @@ + + + Dashboard content goes here. + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx.preview new file mode 100644 index 00000000000..efad440b376 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx.preview @@ -0,0 +1,12 @@ + + + Dashboard content goes here. + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx.preview new file mode 100644 index 00000000000..efad440b376 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx.preview @@ -0,0 +1,12 @@ + + + Dashboard content goes here. + + \ No newline at end of file diff --git a/docs/scripts/formattedTSDemos.js b/docs/scripts/formattedTSDemos.js index 909f03e01f3..c90c2b566c0 100644 --- a/docs/scripts/formattedTSDemos.js +++ b/docs/scripts/formattedTSDemos.js @@ -89,7 +89,11 @@ async function transpileFile(tsxPath, project) { transformOptions.plugins = transformOptions.plugins.concat([ [ require.resolve('docs/src/modules/utils/babel-plugin-jsx-preview'), - { maxLines: 16, outputFilename: `${tsxPath}.preview` }, + { + maxLines: 16, + outputFilename: `${tsxPath}.preview`, + wrapperTypes: ['div', 'Box', 'Stack', 'AppProvider'], + }, ], ]); } diff --git a/docs/src/modules/utils/babel-plugin-jsx-preview.js b/docs/src/modules/utils/babel-plugin-jsx-preview.js index f4c96aca513..046190140e0 100644 --- a/docs/src/modules/utils/babel-plugin-jsx-preview.js +++ b/docs/src/modules/utils/babel-plugin-jsx-preview.js @@ -3,8 +3,6 @@ const fs = require('fs'); const pluginName = 'babel-plugin-jsx-preview'; -const wrapperTypes = ['div', 'Box', 'Stack']; - /** * @typedef {import('@babel/core')} babel */ @@ -12,14 +10,16 @@ const wrapperTypes = ['div', 'Box', 'Stack']; /** * * @param {babel.NodePath} path + * @param {babel.PluginPass} state */ -function getPreviewNodes(path) { +function getPreviewNodes(path, state) { + const wrapperTypes = state.opts.wrapperTypes ?? []; /** * @type {(babel.types.JSXElement['children'])} */ - let previewNode = []; + let previewNodes = []; - previewNode = [path.node]; + previewNodes = [path.node]; const name = path.get('openingElement').get('name'); if ( name.isJSXIdentifier() && @@ -37,7 +37,7 @@ function getPreviewNodes(path) { // ^^^^ Blank JSXText including newline // // ) - previewNode = path.node.children.filter((child, index, children) => { + previewNodes = path.node.children.filter((child, index, children) => { const isSurroundingBlankJSXText = (index === 0 || index === children.length - 1) && child.type === 'JSXText' && @@ -45,7 +45,68 @@ function getPreviewNodes(path) { return !isSurroundingBlankJSXText; }); } - return previewNode; + return previewNodes; +} + +/** + * + * @param {string[]} lines + * @returns {string[]} + */ +function trimEmptyLines(lines) { + const start = lines.findIndex((line) => line.trim() !== ''); + const end = lines.findLastIndex((line) => line.trim() !== ''); + return lines.slice(start, end + 1); +} + +/** + * + * @param {string[]} lines + * @returns {string[]} + */ +function dedentLines(lines) { + const trimmedLines = trimEmptyLines(lines); + const indentation = trimmedLines[0]?.match(/^\s*/)?.[0].length ?? 0; + return trimmedLines.map((line) => line.slice(indentation)); +} + +/** @type {(input: string) => boolean} */ +const isPreviewStart = (line) => + ['// preview-start', '{/* preview-start */}'].includes(line.trim()); +/** @type {(input: string) => boolean} */ +const isPreviewEnd = (line) => ['// preview-end', '{/* preview-end */}'].includes(line.trim()); + +/** + * + * @param {string} code + * @returns {string | null} + */ +function extractExplicitPreview(code) { + const lines = code.split(/\n/); + + const ranges = []; + + let start = -1; + for (const [index, line] of lines.entries()) { + if (isPreviewStart(line) && start < 0) { + start = index; + } else if (isPreviewEnd(line) && start >= 0) { + ranges.push([start, index]); + start = -1; + } + } + + const previewSections = ranges.map(([startLine, endLine]) => { + const previewLines = lines.slice(startLine + 1, endLine); + const dedentedPreviewLines = dedentLines(previewLines); + return dedentedPreviewLines.join('\n'); + }); + + if (previewSections.length > 0) { + return previewSections.join('\n\n// ...\n\n'); + } + + return null; } /** @@ -60,18 +121,7 @@ export default function babelPluginJsxPreview() { return { name: pluginName, visitor: { - JSXElement(path) { - const comments = path.node.leadingComments || []; - const hasComment = comments.some((comment) => comment.value.trim() === 'preview'); - if (hasComment) { - previewNodes = getPreviewNodes(path); - } - }, - ExportDefaultDeclaration(path) { - if (previewNodes) { - return; - } - + ExportDefaultDeclaration(path, state) { const declarationPath = path.get('declaration'); if (!declarationPath.isFunctionDeclaration()) { return; @@ -83,17 +133,26 @@ export default function babelPluginJsxPreview() { const returnedJSXPath = lastReturnPath.get('argument'); if (returnedJSXPath.isJSXElement()) { - previewNodes = getPreviewNodes(returnedJSXPath); + previewNodes = getPreviewNodes(returnedJSXPath, state); } }, }, post(state) { - const { maxLines, outputFilename } = state.opts.plugins.find((plugin) => { - return plugin.key === pluginName; - }).options; + const previewPlugin = state.opts.plugins?.find((plugin) => plugin.key === pluginName); + if (!previewPlugin) { + throw new Error(`Can't find the ${pluginName} plugin.`); + } + + const { maxLines, outputFilename } = previewPlugin.options; let hasPreview = false; - if (previewNodes.length > 0) { + + const explicitPreview = extractExplicitPreview(state.code); + + if (explicitPreview) { + fs.writeFileSync(outputFilename, explicitPreview); + hasPreview = true; + } else if (previewNodes.length > 0) { const startNode = previewNodes[0]; const endNode = previewNodes.slice(-1)[0]; const preview = state.code.slice(startNode.start, endNode.end); @@ -101,15 +160,15 @@ export default function babelPluginJsxPreview() { // The first line is already trimmed either due to trimmed blank JSXText or because it's a single node which babel already trims. // The last line is therefore the meassure for indentation const indentation = previewLines.slice(-1)[0].match(/^\s*/)[0].length; - const deindentedPreviewLines = preview.split(/\n/).map((line, index) => { + const dedentedPreviewLines = preview.split(/\n/).map((line, index) => { if (index === 0) { return line; } return line.slice(indentation); }); - if (deindentedPreviewLines.length <= maxLines) { - fs.writeFileSync(outputFilename, deindentedPreviewLines.join('\n')); + if (previewLines.length <= maxLines) { + fs.writeFileSync(outputFilename, dedentedPreviewLines.join('\n')); hasPreview = true; } }