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;
}
}