From 96acb1bb5abdbd32eb7fb9e0d200c37279b49ff7 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:32:36 +0200 Subject: [PATCH 01/17] WIP --- docs/package.json | 1 + docs/src/modules/components/DemoToolbar.js | 15 ++ docs/src/modules/sandbox/CreateReactApp.ts | 3 + docs/src/modules/sandbox/Dependencies.ts | 16 +- docs/src/modules/sandbox/StackBlitzWc.ts | 259 +++++++++++++++++++++ pnpm-lock.yaml | 8 + 6 files changed, 294 insertions(+), 8 deletions(-) create mode 100644 docs/src/modules/sandbox/StackBlitzWc.ts diff --git a/docs/package.json b/docs/package.json index 3f2b5b99445b8b..850751e612475a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -53,6 +53,7 @@ "@mui/x-tree-view": "7.28.1", "@popperjs/core": "^2.11.8", "@react-spring/web": "^9.7.5", + "@stackblitz/sdk": "^1.11.0", "@tailwindcss/postcss": "^4.1.3", "@toolpad/core": "^0.14.0", "autoprefixer": "^10.4.21", diff --git a/docs/src/modules/components/DemoToolbar.js b/docs/src/modules/components/DemoToolbar.js index 62e6579426b8bc..23ca869e0ed233 100644 --- a/docs/src/modules/components/DemoToolbar.js +++ b/docs/src/modules/components/DemoToolbar.js @@ -27,6 +27,7 @@ import { useTranslate } from '@mui/docs/i18n'; import stylingSolutionMapping from 'docs/src/modules/utils/stylingSolutionMapping'; import codeSandbox from '../sandbox/CodeSandbox'; import stackBlitz from '../sandbox/StackBlitz'; +import stackBlitzWc from '../sandbox/StackBlitzWc'; const Root = styled('div')(({ theme }) => [ { @@ -547,6 +548,20 @@ export default function DemoToolbar(props) { + + stackBlitzWc.createReactApp(demoData).openSandbox()} + {...getControlProps(4)} + sx={{ borderRadius: 1 }} + > + + + + + { return ` @@ -84,6 +86,7 @@ export const getHtml = ({
+ ${main ? `` : ''} `; }; diff --git a/docs/src/modules/sandbox/Dependencies.ts b/docs/src/modules/sandbox/Dependencies.ts index 4a1207bbd435e7..7137e0a966b9bd 100644 --- a/docs/src/modules/sandbox/Dependencies.ts +++ b/docs/src/modules/sandbox/Dependencies.ts @@ -19,7 +19,7 @@ const muiNpmOrgs = ['@mui', '@base_ui', '@pigment-css', '@toolpad']; * * @param deps - list of dependency as `name => version` */ -function addTypeDeps(deps: Record): void { +function addTypeDeps(deps: Record, devDeps: Record): void { const packagesWithDTPackage = Object.keys(deps) .filter((name) => !packagesWithBundledTypes.includes(name)) // All the MUI packages come with bundled types @@ -33,7 +33,7 @@ function addTypeDeps(deps: Record): void { resolvedName = name.slice(1).replace('/', '__'); } - deps[`@types/${resolvedName}`] = 'latest'; + devDeps[`@types/${resolvedName}`] = 'latest'; }); } @@ -150,11 +150,6 @@ export default function SandboxDependencies(demo: Demo, options?: { commitRef?: const dependencies = extractDependencies(); - if (demo.codeVariant === CODE_VARIANTS.TS) { - addTypeDeps(dependencies); - dependencies.typescript = 'latest'; - } - if (!demo.productId && !dependencies['@mui/material']) { // The `index.js` imports StyledEngineProvider from '@mui/material', so we need to make sure we have it as a dependency const name = '@mui/material'; @@ -164,9 +159,14 @@ export default function SandboxDependencies(demo: Demo, options?: { commitRef?: dependencies[name] = versions[name] ? versions[name] : 'latest'; } - const devDependencies = { + const devDependencies: Record = { 'react-scripts': 'latest', }; + if (demo.codeVariant === CODE_VARIANTS.TS) { + addTypeDeps(dependencies, devDependencies); + dependencies.typescript = 'latest'; + } + return { dependencies, devDependencies }; } diff --git a/docs/src/modules/sandbox/StackBlitzWc.ts b/docs/src/modules/sandbox/StackBlitzWc.ts new file mode 100644 index 00000000000000..ac9b6fdcc95443 --- /dev/null +++ b/docs/src/modules/sandbox/StackBlitzWc.ts @@ -0,0 +1,259 @@ +import sdk from '@stackblitz/sdk'; +import SandboxDependencies from 'docs/src/modules/sandbox/Dependencies'; +import getFileExtension from 'docs/src/modules/sandbox/FileExtension'; +import flattenRelativeImports from 'docs/src/modules/sandbox/FlattenRelativeImports'; +import { CodeStyling, CodeVariant, DemoData } from 'docs/src/modules/sandbox/types'; +import * as CRA from 'docs/src/modules/sandbox/CreateReactApp'; + +/** + * Open a project in StackBlitz using the SDK + */ +function openStackBlitzSDK({ + title, + description, + files, + initialFile, +}: { + title: string; + description: string; + files: Record; + initialFile: string; +}) { + sdk.openProject( + { + title, + description, + template: 'node', + files, + }, + { openFile: initialFile }, + ); +} + +/** + * Create a Vite project config for StackBlitz + */ +function createViteFiles( + demoData: DemoData, + dependencies: Record = {}, + devDependencies: Record = {}, +): Record { + const ext = getFileExtension(demoData.codeVariant); + return { + [`vite.config.${demoData.codeVariant === 'TS' ? 'ts' : 'js'}`]: ` +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +});`, + 'index.html': CRA.getHtml({ ...demoData, main: `/src/index.${ext}` }), + 'package.json': JSON.stringify( + { + name: 'mui-demo', + private: true, + version: '0.0.0', + type: 'module', + scripts: { + dev: 'vite', + build: 'vite build', + preview: 'vite preview', + }, + dependencies, + devDependencies, + }, + null, + 2, + ), + ...(demoData.codeVariant === 'TS' && { + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +}`, + 'tsconfig.node.json': `{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +}`, + }), + }; +} + +/** + * Create a Material Template for StackBlitz using the SDK and Vite + */ +function createMaterialTemplate(templateData: { + title: string; + files: Record; + githubLocation: string; + codeVariant: CodeVariant; + codeStyling?: CodeStyling; +}) { + const ext = getFileExtension(templateData.codeVariant); + const { title, githubLocation: description } = templateData; + + // Get dependencies + const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies( + { + codeVariant: templateData.codeVariant, + raw: Object.entries(templateData.files ?? {}).reduce((prev, curr) => `${prev}\n${curr}`, ''), + }, + {}, + ); + + // Add Vite specific dependencies + const devDependencies: Record = { + ...baseDevDependencies, + vite: 'latest', + '@vitejs/plugin-react': 'latest', + }; + + delete devDependencies['react-scripts']; + + // Create base Vite files with dependencies + const viteFiles = createViteFiles(templateData, dependencies, devDependencies); + + // Restructure template files to be under src/ + const templateSourceFiles = Object.entries(templateData.files ?? {}).reduce( + (acc, [key, value]) => { + const newKey = key.startsWith('App') ? `src/${key}` : key; + return { ...acc, [newKey]: value }; + }, + {} as Record, + ); + + if (!templateSourceFiles[`src/App.${ext}`]) { + throw new Error(`Missing src/App.${ext} file in template files`); + } + + // Combine all files + const files = { + ...viteFiles, + [`src/index.${ext}`]: CRA.getRootIndex(demoData), + ...templateSourceFiles, + }; + + return { + title, + files, + dependencies, + devDependencies, + replaceContent(updater: (content: string | Record, filePath: string) => string) { + Object.keys(files).forEach((filePath) => { + files[filePath] = updater(files[filePath], filePath); + }); + return this; + }, + openSandbox: (initialFile: string = 'src/App') => { + // Add extension if missing + const normalizedInitialFile = initialFile.endsWith(ext) + ? initialFile + : `${initialFile}.${ext}`; + + openStackBlitzSDK({ + title, + description, + files, + initialFile: normalizedInitialFile, + }); + }, + }; +} + +/** + * Create a React App for StackBlitz using the SDK and Vite + * This maintains similar structure to the original createReactApp but uses Vite + */ +function createReactApp(demoData: DemoData) { + const ext = getFileExtension(demoData.codeVariant); + const { title, githubLocation: description } = demoData; + + // Get dependencies + const { dependencies: baseDependencies, devDependencies: baseDevDependencies } = + SandboxDependencies(demoData, {}); + + const dependencies = { ...baseDependencies }; + + // Add Vite specific dependencies + const devDependencies: Record = { + ...baseDevDependencies, + vite: 'latest', + '@vitejs/plugin-react': 'latest', + }; + + // Remove CRA dependencies + delete devDependencies['react-scripts']; + delete devDependencies['@types/react-scripts']; + + // Create base Vite files with dependencies + const viteFiles = createViteFiles(demoData, dependencies, devDependencies); + + // Create demo files just like in the original implementation + const demoFiles: Record = { + [`src/Demo.${ext}`]: flattenRelativeImports(demoData.raw), + }; + + // Add relative modules if any + const relativeModuleFiles = demoData.relativeModules + ? demoData.relativeModules.reduce( + (acc, curr) => ({ + ...acc, + // Add files to src directory but preserve original names + [`src/${curr.module.replace(/^.*[\\/]/g, '')}`]: flattenRelativeImports(curr.raw), + }), + {}, + ) + : {}; + + // Combine all files + const files = { + ...viteFiles, + [`src/index.${ext}`]: CRA.getRootIndex(demoData), + ...demoFiles, + ...relativeModuleFiles, + }; + + return { + title, + description, + files, + dependencies, + devDependencies, + openSandbox: (initialFile = `src/Demo.${ext}`) => { + openStackBlitzSDK({ + title, + description, + files, + initialFile, + }); + }, + }; +} + +export default { + createReactApp, + createMaterialTemplate, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 492c65fed2e56b..660e8b397fded4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -739,6 +739,9 @@ importers: '@react-spring/web': specifier: ^9.7.5 version: 9.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@stackblitz/sdk': + specifier: ^1.11.0 + version: 1.11.0 '@tailwindcss/postcss': specifier: ^4.1.3 version: 4.1.3 @@ -5474,6 +5477,9 @@ packages: '@socket.io/component-emitter@3.1.0': resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + '@stackblitz/sdk@1.11.0': + resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -17488,6 +17494,8 @@ snapshots: '@socket.io/component-emitter@3.1.0': {} + '@stackblitz/sdk@1.11.0': {} + '@standard-schema/spec@1.0.0': {} '@styled-system/background@5.1.2': From 523001ead1f01995a01b576524c002b407c22d0e Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:38:10 +0200 Subject: [PATCH 02/17] Update StackBlitzWc.ts --- docs/src/modules/sandbox/StackBlitzWc.ts | 42 +++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/docs/src/modules/sandbox/StackBlitzWc.ts b/docs/src/modules/sandbox/StackBlitzWc.ts index ac9b6fdcc95443..53a5c04b2b8c65 100644 --- a/docs/src/modules/sandbox/StackBlitzWc.ts +++ b/docs/src/modules/sandbox/StackBlitzWc.ts @@ -114,15 +114,15 @@ function createMaterialTemplate(templateData: { }) { const ext = getFileExtension(templateData.codeVariant); const { title, githubLocation: description } = templateData; + const raw = Object.entries(templateData.files ?? {}).reduce( + (prev, curr) => `${prev}\n${curr}`, + '', + ); + + const demoData: DemoData = { codeStyling: 'MUI System', ...templateData, raw, language: 'en' }; // Get dependencies - const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies( - { - codeVariant: templateData.codeVariant, - raw: Object.entries(templateData.files ?? {}).reduce((prev, curr) => `${prev}\n${curr}`, ''), - }, - {}, - ); + const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, {}); // Add Vite specific dependencies const devDependencies: Record = { @@ -134,25 +134,45 @@ function createMaterialTemplate(templateData: { delete devDependencies['react-scripts']; // Create base Vite files with dependencies - const viteFiles = createViteFiles(templateData, dependencies, devDependencies); + const viteFiles = createViteFiles(demoData, dependencies, devDependencies); // Restructure template files to be under src/ const templateSourceFiles = Object.entries(templateData.files ?? {}).reduce( (acc, [key, value]) => { - const newKey = key.startsWith('App') ? `src/${key}` : key; + // Move App files to src/ directory, keep configuration files at root + const isConfigFile = /\.(json|yml|md|config|gitignore|rc)$/.test(key); + const newKey = + key.startsWith('App') || (!isConfigFile && !key.includes('/')) ? `src/${key}` : key; return { ...acc, [newKey]: value }; }, {} as Record, ); + // Ensure we have App.js or App.tsx if (!templateSourceFiles[`src/App.${ext}`]) { - throw new Error(`Missing src/App.${ext} file in template files`); + templateSourceFiles[`src/App.${ext}`] = + 'export default function App() { return
MUI Template
; }'; } + // Create a proper React 18 index file for Vite + const indexContent = ` +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { StyledEngineProvider } from '@mui/material/styles'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')${templateData.codeVariant === 'TS' ? '!' : ''}).render( + + + + + +);`; + // Combine all files const files = { ...viteFiles, - [`src/index.${ext}`]: CRA.getRootIndex(demoData), + [`src/index.${ext}`]: indexContent, ...templateSourceFiles, }; From 431bf2012c8e042d9abe44d7e77473dc41340461 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:38:34 +0200 Subject: [PATCH 03/17] Update StackBlitzWc.ts --- docs/src/modules/sandbox/StackBlitzWc.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/modules/sandbox/StackBlitzWc.ts b/docs/src/modules/sandbox/StackBlitzWc.ts index 53a5c04b2b8c65..802a5b4ec1a0e6 100644 --- a/docs/src/modules/sandbox/StackBlitzWc.ts +++ b/docs/src/modules/sandbox/StackBlitzWc.ts @@ -122,7 +122,9 @@ function createMaterialTemplate(templateData: { const demoData: DemoData = { codeStyling: 'MUI System', ...templateData, raw, language: 'en' }; // Get dependencies - const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, {}); + const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + }); // Add Vite specific dependencies const devDependencies: Record = { @@ -213,7 +215,9 @@ function createReactApp(demoData: DemoData) { // Get dependencies const { dependencies: baseDependencies, devDependencies: baseDevDependencies } = - SandboxDependencies(demoData, {}); + SandboxDependencies(demoData, { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + }); const dependencies = { ...baseDependencies }; From 99f38ab3a05807dd609ccbf86c84c981eedc51e8 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:25:20 +0200 Subject: [PATCH 04/17] remove the rest --- docs/src/modules/components/DemoToolbar.js | 60 +--- .../MaterialFreeTemplatesCollection.js | 37 --- docs/src/modules/components/TemplateFrame.js | 41 +-- docs/src/modules/sandbox/CodeSandbox.test.js | 281 ---------------- docs/src/modules/sandbox/CodeSandbox.ts | 297 ----------------- docs/src/modules/sandbox/Dependencies.ts | 4 +- docs/src/modules/sandbox/StackBlitz.ts | 304 ++++++++++++------ docs/src/modules/sandbox/StackBlitzWc.ts | 283 ---------------- 8 files changed, 215 insertions(+), 1092 deletions(-) delete mode 100644 docs/src/modules/sandbox/CodeSandbox.test.js delete mode 100644 docs/src/modules/sandbox/CodeSandbox.ts delete mode 100644 docs/src/modules/sandbox/StackBlitzWc.ts diff --git a/docs/src/modules/components/DemoToolbar.js b/docs/src/modules/components/DemoToolbar.js index 23ca869e0ed233..dcf389bb798a3e 100644 --- a/docs/src/modules/components/DemoToolbar.js +++ b/docs/src/modules/components/DemoToolbar.js @@ -25,9 +25,7 @@ import { useSetCodeVariant } from 'docs/src/modules/utils/codeVariant'; import { useSetCodeStyling, useCodeStyling } from 'docs/src/modules/utils/codeStylingSolution'; import { useTranslate } from '@mui/docs/i18n'; import stylingSolutionMapping from 'docs/src/modules/utils/stylingSolutionMapping'; -import codeSandbox from '../sandbox/CodeSandbox'; import stackBlitz from '../sandbox/StackBlitz'; -import stackBlitzWc from '../sandbox/StackBlitzWc'; const Root = styled('div')(({ theme }) => [ { @@ -533,50 +531,20 @@ export default function DemoToolbar(props) { {showCodeLabel} {demoOptions.hideEditButton ? null : ( - - - stackBlitz.createReactApp(demoData).openSandbox()} - {...getControlProps(4)} - sx={{ borderRadius: 1 }} - > - - - - - - - stackBlitzWc.createReactApp(demoData).openSandbox()} - {...getControlProps(4)} - sx={{ borderRadius: 1 }} - > - - - - - - - codeSandbox.createReactApp(demoData).openSandbox()} - {...getControlProps(5)} - sx={{ borderRadius: 1 }} - > - - - - - - + + stackBlitz.createReactApp(demoData).openSandbox()} + {...getControlProps(4)} + sx={{ borderRadius: 1 }} + > + + + + + )} - - - codeSandbox - .createMaterialTemplate({ - ...item, - files: { ...item.files, ...materialTemplates.sharedTheme?.files }, - title: `${templateName} Template - Material UI`, - githubLocation: `${process.env.SOURCE_CODE_REPO}/blob/v${ - process.env.LIB_VERSION - }/docs/data/material/templates/${templateId}/${templateName}.${ - item.codeVariant === 'TS' ? 'tsx' : 'js' - }`, - }) - .replaceContent((content) => { - if (typeof content === 'string') { - return content - .replace(/\.\.\/shared-theme\//g, './theme/') - .replace('./App', `./${templateName}`); - } - return content; - }) - .openSandbox(`/${templateName}`) - } - > - - - - - diff --git a/docs/src/modules/components/TemplateFrame.js b/docs/src/modules/components/TemplateFrame.js index 2abc7cfd30c6bb..62b9e66004203e 100644 --- a/docs/src/modules/components/TemplateFrame.js +++ b/docs/src/modules/components/TemplateFrame.js @@ -21,8 +21,7 @@ import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import LightModeIcon from '@mui/icons-material/LightModeOutlined'; import DarkModeIcon from '@mui/icons-material/DarkModeOutlined'; import PaletteIcon from '@mui/icons-material/PaletteOutlined'; -import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; -import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; +import stackBlitz from 'docs/src/modules/sandbox/StackBlitzWc'; import sourceMaterialTemplates from 'docs/src/modules/material/sourceMaterialTemplates'; import { pascalCase } from 'docs/src/modules/utils/helpers'; @@ -310,44 +309,6 @@ export default function TemplateFrame({ children }) { - - - codeSandbox - .createMaterialTemplate({ - ...item, - files: { ...item.files, ...materialTemplates.sharedTheme?.files }, - title: `${templateName} Template - Material UI`, - githubLocation: `${process.env.SOURCE_CODE_REPO}/blob/v${ - process.env.LIB_VERSION - }/docs/data/material/templates/${templateId}/${templateName}.${ - item.codeVariant === 'TS' ? 'tsx' : 'js' - }`, - }) - .replaceContent((content) => { - if (typeof content === 'string') { - return content - .replace(/\.\.\/shared-theme\//g, './theme/') - .replace('./App', `./${templateName}`); - } - return content; - }) - .openSandbox(`/${templateName}`) - } - sx={{ alignSelf: 'center', borderRadius: 1 }} - > - - - - - - - - - - ); -} -`; - -describe('CodeSandbox', () => { - it('generate the correct JavaScript result', () => { - const result = CodeSandbox.createReactApp({ - title: 'BasicButtons Material Demo', - githubLocation: - 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.js', - codeVariant: 'JS', - language: 'en', - raw: testCase, - }); - expect(result.files).to.deep.equal({ - 'package.json': { - content: { - description: - 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.js', - dependencies: { - react: 'latest', - // #npm-tag-reference - '@mui/material': 'latest', - 'react-dom': 'latest', - '@emotion/react': 'latest', - '@emotion/styled': 'latest', - }, - devDependencies: { - 'react-scripts': 'latest', - }, - scripts: { - start: 'react-scripts start', - build: 'react-scripts build', - test: 'react-scripts test', - eject: 'react-scripts eject', - }, - }, - }, - 'public/index.html': { - content: ` - - - - BasicButtons Material Demo - - - - - - - - - -
- -`, - }, - 'src/Demo.js': { - content: `import * as React from 'react'; -import Stack from '@mui/material/Stack'; -import Button from '@mui/material/Button'; - -export default function BasicButtons() { - return ( - - - - - - ); -} -`, - }, - 'src/index.js': { - content: `import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { StyledEngineProvider } from '@mui/material/styles'; -import Demo from './Demo'; - -ReactDOM.createRoot(document.querySelector("#root")).render( - - - - - -);`, - }, - }); - }); - - it('generate the correct TypeScript result', () => { - const result = CodeSandbox.createReactApp({ - title: 'BasicButtons Material Demo', - githubLocation: - 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.tsx', - codeVariant: 'TS', - language: 'en', - raw: testCase, - }); - expect(result.files).to.deep.equal({ - 'package.json': { - content: { - description: - 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.tsx', - dependencies: { - react: 'latest', - // #npm-tag-reference - '@mui/material': 'latest', - 'react-dom': 'latest', - '@emotion/react': 'latest', - '@emotion/styled': 'latest', - '@types/react': 'latest', - '@types/react-dom': 'latest', - typescript: 'latest', - }, - devDependencies: { - 'react-scripts': 'latest', - }, - main: 'index.tsx', - scripts: { - build: 'react-scripts build', - eject: 'react-scripts eject', - start: 'react-scripts start', - test: 'react-scripts test', - }, - }, - }, - 'public/index.html': { - content: ` - - - - BasicButtons Material Demo - - - - - - - - - -
- -`, - }, - 'src/Demo.tsx': { - content: `import * as React from 'react'; -import Stack from '@mui/material/Stack'; -import Button from '@mui/material/Button'; - -export default function BasicButtons() { - return ( - - - - - - ); -} -`, - }, - 'src/index.tsx': { - content: `import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { StyledEngineProvider } from '@mui/material/styles'; -import Demo from './Demo'; - -ReactDOM.createRoot(document.querySelector("#root")!).render( - - - - - -);`, - }, - 'tsconfig.json': { - content: `{ - "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react" - }, - "include": [ - "src" - ] -} -`, - }, - }); - expect(result.dependencies).to.deep.equal({ - '@emotion/react': 'latest', - '@emotion/styled': 'latest', - // #npm-tag-reference - '@mui/material': 'latest', - '@types/react': 'latest', - '@types/react-dom': 'latest', - react: 'latest', - 'react-dom': 'latest', - typescript: 'latest', - }); - expect(result.devDependencies).to.deep.equal({ - 'react-scripts': 'latest', - }); - }); - - it('generate the correct index.html result when Tailwind is used', () => { - const result = CodeSandbox.createReactApp({ - title: 'BasicButtons Material Demo', - githubLocation: - 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.js', - codeVariant: 'JS', - language: 'en', - raw: testCase, - codeStyling: 'Tailwind', - }); - expect(result.files['public/index.html'].content).to.contain( - '', - ); - }); - - it('should generate the correct stylesheet font link in index.html for Material Two Tones icons', () => { - const raw = `import * as React from 'react'; - import Icon from '@mui/material/Icon'; - - export default function TwoToneIcons() { - return add_circle; - } - `; - - const result = CodeSandbox.createReactApp({ - raw, - codeVariant: 'JS', - }); - - expect(result.files['public/index.html'].content).to.contain( - 'https://fonts.googleapis.com/icon?family=Material+Icons+Two+Tone', - ); - }); -}); diff --git a/docs/src/modules/sandbox/CodeSandbox.ts b/docs/src/modules/sandbox/CodeSandbox.ts deleted file mode 100644 index 26f51d384a47e2..00000000000000 --- a/docs/src/modules/sandbox/CodeSandbox.ts +++ /dev/null @@ -1,297 +0,0 @@ -// @ts-ignore -import LZString from 'lz-string'; -import addHiddenInput from 'docs/src/modules/utils/addHiddenInput'; -import SandboxDependencies from 'docs/src/modules/sandbox/Dependencies'; -import * as CRA from 'docs/src/modules/sandbox/CreateReactApp'; -import getFileExtension from 'docs/src/modules/sandbox/FileExtension'; -import flattenRelativeImports from 'docs/src/modules/sandbox/FlattenRelativeImports'; -import { DemoData, CodeVariant, CodeStyling } from 'docs/src/modules/sandbox/types'; - -function compress(object: any) { - return LZString.compressToBase64(JSON.stringify(object)) - .replace(/\+/g, '-') // Convert '+' to '-' - .replace(/\//g, '_') // Convert '/' to '_' - .replace(/=+$/, ''); // Remove ending '=' -} - -function openSandbox({ files, codeVariant, initialFile }: any) { - const extension = codeVariant === 'TS' ? '.tsx' : '.js'; - const parameters = compress({ files }); - - // ref: https://codesandbox.io/docs/api/#define-api - const form = document.createElement('form'); - form.method = 'POST'; - form.target = '_blank'; - form.action = 'https://codesandbox.io/api/v1/sandboxes/define'; - addHiddenInput(form, 'parameters', parameters); - addHiddenInput(form, 'embed', '1'); - addHiddenInput( - form, - 'query', - `module=${initialFile}${initialFile.match(/(\.tsx|\.ts|\.js)$/) ? '' : extension}&fontsize=12`, - ); - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); -} - -function createReactApp(demoData: DemoData) { - const ext = getFileExtension(demoData.codeVariant); - const { title, githubLocation: description } = demoData; - - const files: Record = { - 'public/index.html': { - content: CRA.getHtml(demoData), - }, - [`src/index.${ext}`]: { - content: CRA.getRootIndex(demoData), - }, - [`src/Demo.${ext}`]: { - content: flattenRelativeImports(demoData.raw), - }, - // Spread the relative modules - ...(demoData.relativeModules && - // Transform the relative modules array into an object - demoData.relativeModules.reduce( - (acc, curr) => ({ - ...acc, - // Remove the path and keep the filename - [`src/${curr.module.replace(/^.*[\\/]/g, '')}`]: { - content: flattenRelativeImports(curr.raw), - }, - }), - {}, - )), - ...(demoData.codeVariant === 'TS' && { - 'tsconfig.json': { - content: CRA.getTsconfig(), - }, - }), - }; - - const { dependencies, devDependencies } = SandboxDependencies(demoData, { - commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }); - - files['package.json'] = { - content: { - description, - dependencies, - devDependencies, - scripts: { - start: 'react-scripts start', - build: 'react-scripts build', - test: 'react-scripts test', - eject: 'react-scripts eject', - }, - ...(demoData.codeVariant === 'TS' && { - main: 'index.tsx', - }), - }, - }; - - return { - title, - description, - files, - dependencies, - devDependencies, - /** - * @param {string} initialFile - * @description should start with `/`, for example `/Demo.tsx`. If the extension is not provided, - * it will be appended based on the code variant. - */ - openSandbox: (initialFile: string = `/src/Demo.${ext}`) => - openSandbox({ files, codeVariant: demoData.codeVariant, initialFile }), - }; -} - -function createJoyTemplate(templateData: { - title: string; - files: Record; - githubLocation: string; - codeVariant: CodeVariant; - codeStyling?: CodeStyling; -}) { - const ext = getFileExtension(templateData.codeVariant); - const { title, githubLocation: description } = templateData; - - // document.querySelector returns 'Element | null' but createRoot expects 'Element | DocumentFragment'. - const type = templateData.codeVariant === 'TS' ? '!' : ''; - - const files: Record = { - 'public/index.html': { - content: CRA.getHtml({ - title: templateData.title, - language: 'en', - codeStyling: templateData.codeStyling ?? 'MUI System', - }), - }, - [`index.${ext}`]: { - content: `import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { StyledEngineProvider } from '@mui/joy/styles'; -import App from './App'; - -ReactDOM.createRoot(document.querySelector("#root")${type}).render( - - - - - -);`, - }, - ...Object.entries(templateData.files).reduce( - (prev, curr) => ({ - ...prev, - [curr[0]]: { - content: curr[1], - }, - }), - {}, - ), - ...(templateData.codeVariant === 'TS' && { - 'tsconfig.json': { - content: CRA.getTsconfig(), - }, - }), - }; - - const { dependencies, devDependencies } = SandboxDependencies( - { - codeVariant: templateData.codeVariant, - raw: Object.entries(templateData.files).reduce((prev, curr) => `${prev}\n${curr}`, ''), - productId: 'joy-ui', - }, - { - commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }, - ); - - files['package.json'] = { - content: { - description, - dependencies, - devDependencies, - scripts: { - start: 'react-scripts start', - build: 'react-scripts build', - test: 'react-scripts test', - eject: 'react-scripts eject', - }, - ...(templateData.codeVariant === 'TS' && { - main: 'index.tsx', - }), - }, - }; - - return { - title, - files, - dependencies, - devDependencies, - openSandbox: (initialFile: string = '/App') => - openSandbox({ files, codeVariant: templateData.codeVariant, initialFile }), - }; -} - -function createMaterialTemplate(templateData: { - title: string; - files: Record; - githubLocation: string; - codeVariant: CodeVariant; - codeStyling?: CodeStyling; -}) { - const ext = getFileExtension(templateData.codeVariant); - const { title, githubLocation: description } = templateData; - - // document.querySelector returns 'Element | null' but createRoot expects 'Element | DocumentFragment'. - const type = templateData.codeVariant === 'TS' ? '!' : ''; - - const files: Record }> = { - 'public/index.html': { - content: CRA.getHtml({ - title: templateData.title, - language: 'en', - codeStyling: templateData.codeStyling ?? 'MUI System', - }), - }, - [`index.${ext}`]: { - content: `import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { StyledEngineProvider } from '@mui/material/styles'; -import App from './App'; - -ReactDOM.createRoot(document.querySelector("#root")${type}).render( - - - - - -);`, - }, - ...Object.entries(templateData.files).reduce( - (prev, curr) => ({ - ...prev, - [curr[0]]: { - content: curr[1], - }, - }), - {}, - ), - ...(templateData.codeVariant === 'TS' && { - 'tsconfig.json': { - content: CRA.getTsconfig(), - }, - }), - }; - - const { dependencies, devDependencies } = SandboxDependencies( - { - codeVariant: templateData.codeVariant, - raw: Object.entries(templateData.files).reduce((prev, curr) => `${prev}\n${curr}`, ''), - productId: 'material-ui', - }, - { - commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }, - ); - - files['package.json'] = { - content: { - description, - dependencies, - devDependencies, - scripts: { - start: 'react-scripts start', - build: 'react-scripts build', - test: 'react-scripts test', - eject: 'react-scripts eject', - }, - ...(templateData.codeVariant === 'TS' && { - main: 'index.tsx', - }), - }, - }; - - return { - title, - files, - dependencies, - devDependencies, - replaceContent(updater: (content: string | Record, filePath: string) => string) { - Object.keys(files).forEach((filePath) => { - files[filePath].content = updater(files[filePath].content, filePath); - }); - return this; - }, - openSandbox: (initialFile: string = '/App') => - openSandbox({ files, codeVariant: templateData.codeVariant, initialFile }), - }; -} - -export default { - createReactApp, - createJoyTemplate, - createMaterialTemplate, -}; diff --git a/docs/src/modules/sandbox/Dependencies.ts b/docs/src/modules/sandbox/Dependencies.ts index 7137e0a966b9bd..d7ba8cbda88f71 100644 --- a/docs/src/modules/sandbox/Dependencies.ts +++ b/docs/src/modules/sandbox/Dependencies.ts @@ -159,9 +159,7 @@ export default function SandboxDependencies(demo: Demo, options?: { commitRef?: dependencies[name] = versions[name] ? versions[name] : 'latest'; } - const devDependencies: Record = { - 'react-scripts': 'latest', - }; + const devDependencies: Record = {}; if (demo.codeVariant === CODE_VARIANTS.TS) { addTypeDeps(dependencies, devDependencies); diff --git a/docs/src/modules/sandbox/StackBlitz.ts b/docs/src/modules/sandbox/StackBlitz.ts index 5002b015ea93bc..160205b47cf5bd 100644 --- a/docs/src/modules/sandbox/StackBlitz.ts +++ b/docs/src/modules/sandbox/StackBlitz.ts @@ -1,99 +1,110 @@ -import addHiddenInput from 'docs/src/modules/utils/addHiddenInput'; -import { CODE_VARIANTS } from 'docs/src/modules/constants'; +import sdk from '@stackblitz/sdk'; import SandboxDependencies from 'docs/src/modules/sandbox/Dependencies'; -import * as CRA from 'docs/src/modules/sandbox/CreateReactApp'; import getFileExtension from 'docs/src/modules/sandbox/FileExtension'; import flattenRelativeImports from 'docs/src/modules/sandbox/FlattenRelativeImports'; import { CodeStyling, CodeVariant, DemoData } from 'docs/src/modules/sandbox/types'; +import * as CRA from 'docs/src/modules/sandbox/CreateReactApp'; -function openStackBlitz({ +/** + * Open a project in StackBlitz using the SDK + */ +function openStackBlitzSDK({ title, description, - dependencies, - devDependencies, files, - codeVariant, initialFile, }: { title: string; description: string; - dependencies: Record; - devDependencies: Record; files: Record; - codeVariant: string; initialFile: string; }) { - const extension = codeVariant === CODE_VARIANTS.TS ? '.tsx' : '.js'; - // ref: https://developer.stackblitz.com/docs/platform/post-api/ - const form = document.createElement('form'); - form.method = 'POST'; - form.target = '_blank'; - form.action = `https://stackblitz.com/run?file=${initialFile}${ - initialFile.match(/(\.tsx|\.ts|\.js)$/) ? '' : extension - }`; - addHiddenInput(form, 'project[template]', 'create-react-app'); - addHiddenInput(form, 'project[title]', title); - addHiddenInput(form, 'project[description]', `# ${title}\n${description}`); - addHiddenInput(form, 'project[dependencies]', JSON.stringify(dependencies)); - addHiddenInput(form, 'project[devDependencies]', JSON.stringify(devDependencies)); - Object.keys(files).forEach((key) => { - const value = files[key]; - addHiddenInput(form, `project[files][${key}]`, value); - }); - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); + sdk.openProject( + { + title, + description, + template: 'node', + files, + }, + { openFile: initialFile }, + ); } -function createReactApp(demoData: DemoData) { +/** + * Create a Vite project config for StackBlitz + */ +function createViteFiles( + demoData: DemoData, + dependencies: Record = {}, + devDependencies: Record = {}, +): Record { const ext = getFileExtension(demoData.codeVariant); - const { title, githubLocation: description } = demoData; - - const files: Record = { - 'index.html': CRA.getHtml(demoData), - [`index.${ext}`]: CRA.getRootIndex(demoData), - [`Demo.${ext}`]: flattenRelativeImports(demoData.raw), - // Spread the relative modules - ...(demoData.relativeModules && - // Transform the relative modules array into an object - demoData.relativeModules.reduce( - (acc, curr) => ({ - ...acc, - // Remove the path and keep the filename - [`${curr.module.replace(/^.*[\\/]/g, '')}`]: flattenRelativeImports(curr.raw), - }), - {}, - )), - ...(demoData.codeVariant === 'TS' && { - 'tsconfig.json': CRA.getTsconfig(), - }), - }; - - const { dependencies, devDependencies } = SandboxDependencies(demoData, { - // Waiting for https://github.com/stackblitz/core/issues/437 - // commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }); - return { - title, - description, - files, - dependencies, - devDependencies, - openSandbox: (initialFile = `Demo.${ext}`) => { - openStackBlitz({ - title, - description, + [`vite.config.${demoData.codeVariant === 'TS' ? 'ts' : 'js'}`]: ` +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +});`, + 'index.html': CRA.getHtml({ ...demoData, main: `/src/index.${ext}` }), + 'package.json': JSON.stringify( + { + name: 'mui-demo', + private: true, + version: '0.0.0', + type: 'module', + scripts: { + dev: 'vite', + build: 'vite build', + preview: 'vite preview', + }, dependencies, devDependencies, - files, - codeVariant: demoData.codeVariant, - initialFile, - }); - }, + }, + null, + 2, + ), + ...(demoData.codeVariant === 'TS' && { + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +}`, + 'tsconfig.node.json': `{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +}`, + }), }; } +/** + * Create a Material Template for StackBlitz using the SDK and Vite + */ function createMaterialTemplate(templateData: { title: string; files: Record; @@ -103,44 +114,56 @@ function createMaterialTemplate(templateData: { }) { const ext = getFileExtension(templateData.codeVariant); const { title, githubLocation: description } = templateData; + const raw = Object.entries(templateData.files ?? {}).reduce( + (prev, curr) => `${prev}\n${curr}`, + '', + ); - // document.querySelector returns 'Element | null' but createRoot expects 'Element | DocumentFragment'. - const type = templateData.codeVariant === 'TS' ? '!' : ''; + const demoData: DemoData = { codeStyling: 'MUI System', ...templateData, raw, language: 'en' }; - const files: Record = { - 'index.html': CRA.getHtml({ - title: templateData.title, - language: 'en', - codeStyling: templateData.codeStyling ?? 'MUI System', - }), - [`index.${ext}`]: `import * as React from 'react'; + // Get dependencies + const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + }); + + // Add Vite specific dependencies + const devDependencies: Record = { + ...baseDevDependencies, + vite: 'latest', + '@vitejs/plugin-react': 'latest', + }; + + // Create base Vite files with dependencies + const viteFiles = createViteFiles(demoData, dependencies, devDependencies); + + // Restructure template files to be under src/ + const templateSourceFiles = templateData.files + ? Object.fromEntries( + Object.entries(templateData.files).map(([key, value]) => [`src/${key}`, value]), + ) + : {}; + + // Create a proper React 18 index file for Vite + const indexContent = ` +import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { StyledEngineProvider } from '@mui/material/styles'; import App from './App'; -ReactDOM.createRoot(document.querySelector("#root")${type}).render( +ReactDOM.createRoot(document.getElementById('root')${templateData.codeVariant === 'TS' ? '!' : ''}).render( -);`, - ...templateData.files, - ...(templateData.codeVariant === 'TS' && { - 'tsconfig.json': CRA.getTsconfig(), - }), - }; +);`; - const { dependencies, devDependencies } = SandboxDependencies( - { - codeVariant: templateData.codeVariant, - raw: Object.entries(templateData.files).reduce((prev, curr) => `${prev}\n${curr}`, ''), - }, - { - // Waiting for https://github.com/stackblitz/core/issues/437 - // commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }, - ); + // Combine all files + const files = { + ...viteFiles, + [`src/index.${ext}`]: indexContent, + ...templateSourceFiles, + }; return { title, @@ -153,16 +176,87 @@ ReactDOM.createRoot(document.querySelector("#root")${type}).render( }); return this; }, - openStackBlitz: (initialFile: string = '/App') => - openStackBlitz({ - title: templateData.title, + openStackBlitz: (initialFile: string = 'src/App') => { + // Add extension if missing + const normalizedInitialFile = initialFile.endsWith(ext) + ? initialFile + : `${initialFile}.${ext}`; + + openStackBlitzSDK({ + title, + description, files, + initialFile: normalizedInitialFile, + }); + }, + }; +} + +/** + * Create a React App for StackBlitz using the SDK and Vite + * This maintains similar structure to the original createReactApp but uses Vite + */ +function createReactApp(demoData: DemoData) { + const ext = getFileExtension(demoData.codeVariant); + const { title, githubLocation: description } = demoData; + + // Get dependencies + const { dependencies: baseDependencies, devDependencies: baseDevDependencies } = + SandboxDependencies(demoData, { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + }); + + const dependencies = { ...baseDependencies }; + + // Add Vite specific dependencies + const devDependencies: Record = { + ...baseDevDependencies, + vite: 'latest', + '@vitejs/plugin-react': 'latest', + }; + + // Create base Vite files with dependencies + const viteFiles = createViteFiles(demoData, dependencies, devDependencies); + + // Create demo files just like in the original implementation + const demoFiles: Record = { + [`src/Demo.${ext}`]: flattenRelativeImports(demoData.raw), + }; + + // Add relative modules if any + const relativeModuleFiles = demoData.relativeModules + ? demoData.relativeModules.reduce( + (acc, curr) => ({ + ...acc, + // Add files to src directory but preserve original names + [`src/${curr.module.replace(/^.*[\\/]/g, '')}`]: flattenRelativeImports(curr.raw), + }), + {}, + ) + : {}; + + // Combine all files + const files = { + ...viteFiles, + [`src/index.${ext}`]: CRA.getRootIndex(demoData), + ...demoFiles, + ...relativeModuleFiles, + }; + + return { + title, + description, + files, + dependencies, + devDependencies, + openSandbox: (initialFile = `src/Demo.${ext}`) => { + openStackBlitzSDK({ + title, description, - dependencies, - devDependencies, - codeVariant: templateData.codeVariant, + files, initialFile, - }), + }); + }, }; } diff --git a/docs/src/modules/sandbox/StackBlitzWc.ts b/docs/src/modules/sandbox/StackBlitzWc.ts deleted file mode 100644 index 802a5b4ec1a0e6..00000000000000 --- a/docs/src/modules/sandbox/StackBlitzWc.ts +++ /dev/null @@ -1,283 +0,0 @@ -import sdk from '@stackblitz/sdk'; -import SandboxDependencies from 'docs/src/modules/sandbox/Dependencies'; -import getFileExtension from 'docs/src/modules/sandbox/FileExtension'; -import flattenRelativeImports from 'docs/src/modules/sandbox/FlattenRelativeImports'; -import { CodeStyling, CodeVariant, DemoData } from 'docs/src/modules/sandbox/types'; -import * as CRA from 'docs/src/modules/sandbox/CreateReactApp'; - -/** - * Open a project in StackBlitz using the SDK - */ -function openStackBlitzSDK({ - title, - description, - files, - initialFile, -}: { - title: string; - description: string; - files: Record; - initialFile: string; -}) { - sdk.openProject( - { - title, - description, - template: 'node', - files, - }, - { openFile: initialFile }, - ); -} - -/** - * Create a Vite project config for StackBlitz - */ -function createViteFiles( - demoData: DemoData, - dependencies: Record = {}, - devDependencies: Record = {}, -): Record { - const ext = getFileExtension(demoData.codeVariant); - return { - [`vite.config.${demoData.codeVariant === 'TS' ? 'ts' : 'js'}`]: ` -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()] -});`, - 'index.html': CRA.getHtml({ ...demoData, main: `/src/index.${ext}` }), - 'package.json': JSON.stringify( - { - name: 'mui-demo', - private: true, - version: '0.0.0', - type: 'module', - scripts: { - dev: 'vite', - build: 'vite build', - preview: 'vite preview', - }, - dependencies, - devDependencies, - }, - null, - 2, - ), - ...(demoData.codeVariant === 'TS' && { - 'tsconfig.json': `{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -}`, - 'tsconfig.node.json': `{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -}`, - }), - }; -} - -/** - * Create a Material Template for StackBlitz using the SDK and Vite - */ -function createMaterialTemplate(templateData: { - title: string; - files: Record; - githubLocation: string; - codeVariant: CodeVariant; - codeStyling?: CodeStyling; -}) { - const ext = getFileExtension(templateData.codeVariant); - const { title, githubLocation: description } = templateData; - const raw = Object.entries(templateData.files ?? {}).reduce( - (prev, curr) => `${prev}\n${curr}`, - '', - ); - - const demoData: DemoData = { codeStyling: 'MUI System', ...templateData, raw, language: 'en' }; - - // Get dependencies - const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, { - commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }); - - // Add Vite specific dependencies - const devDependencies: Record = { - ...baseDevDependencies, - vite: 'latest', - '@vitejs/plugin-react': 'latest', - }; - - delete devDependencies['react-scripts']; - - // Create base Vite files with dependencies - const viteFiles = createViteFiles(demoData, dependencies, devDependencies); - - // Restructure template files to be under src/ - const templateSourceFiles = Object.entries(templateData.files ?? {}).reduce( - (acc, [key, value]) => { - // Move App files to src/ directory, keep configuration files at root - const isConfigFile = /\.(json|yml|md|config|gitignore|rc)$/.test(key); - const newKey = - key.startsWith('App') || (!isConfigFile && !key.includes('/')) ? `src/${key}` : key; - return { ...acc, [newKey]: value }; - }, - {} as Record, - ); - - // Ensure we have App.js or App.tsx - if (!templateSourceFiles[`src/App.${ext}`]) { - templateSourceFiles[`src/App.${ext}`] = - 'export default function App() { return
MUI Template
; }'; - } - - // Create a proper React 18 index file for Vite - const indexContent = ` -import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { StyledEngineProvider } from '@mui/material/styles'; -import App from './App'; - -ReactDOM.createRoot(document.getElementById('root')${templateData.codeVariant === 'TS' ? '!' : ''}).render( - - - - - -);`; - - // Combine all files - const files = { - ...viteFiles, - [`src/index.${ext}`]: indexContent, - ...templateSourceFiles, - }; - - return { - title, - files, - dependencies, - devDependencies, - replaceContent(updater: (content: string | Record, filePath: string) => string) { - Object.keys(files).forEach((filePath) => { - files[filePath] = updater(files[filePath], filePath); - }); - return this; - }, - openSandbox: (initialFile: string = 'src/App') => { - // Add extension if missing - const normalizedInitialFile = initialFile.endsWith(ext) - ? initialFile - : `${initialFile}.${ext}`; - - openStackBlitzSDK({ - title, - description, - files, - initialFile: normalizedInitialFile, - }); - }, - }; -} - -/** - * Create a React App for StackBlitz using the SDK and Vite - * This maintains similar structure to the original createReactApp but uses Vite - */ -function createReactApp(demoData: DemoData) { - const ext = getFileExtension(demoData.codeVariant); - const { title, githubLocation: description } = demoData; - - // Get dependencies - const { dependencies: baseDependencies, devDependencies: baseDevDependencies } = - SandboxDependencies(demoData, { - commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }); - - const dependencies = { ...baseDependencies }; - - // Add Vite specific dependencies - const devDependencies: Record = { - ...baseDevDependencies, - vite: 'latest', - '@vitejs/plugin-react': 'latest', - }; - - // Remove CRA dependencies - delete devDependencies['react-scripts']; - delete devDependencies['@types/react-scripts']; - - // Create base Vite files with dependencies - const viteFiles = createViteFiles(demoData, dependencies, devDependencies); - - // Create demo files just like in the original implementation - const demoFiles: Record = { - [`src/Demo.${ext}`]: flattenRelativeImports(demoData.raw), - }; - - // Add relative modules if any - const relativeModuleFiles = demoData.relativeModules - ? demoData.relativeModules.reduce( - (acc, curr) => ({ - ...acc, - // Add files to src directory but preserve original names - [`src/${curr.module.replace(/^.*[\\/]/g, '')}`]: flattenRelativeImports(curr.raw), - }), - {}, - ) - : {}; - - // Combine all files - const files = { - ...viteFiles, - [`src/index.${ext}`]: CRA.getRootIndex(demoData), - ...demoFiles, - ...relativeModuleFiles, - }; - - return { - title, - description, - files, - dependencies, - devDependencies, - openSandbox: (initialFile = `src/Demo.${ext}`) => { - openStackBlitzSDK({ - title, - description, - files, - initialFile, - }); - }, - }; -} - -export default { - createReactApp, - createMaterialTemplate, -}; From 17bb0b7b8c5270a50d2ee0616917b96fbffae14a Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:34:28 +0200 Subject: [PATCH 05/17] eslint --- .../templates/TemplateCollection.js | 36 ------------------- docs/src/modules/components/TemplateFrame.js | 2 +- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/docs/data/joy/getting-started/templates/TemplateCollection.js b/docs/data/joy/getting-started/templates/TemplateCollection.js index 94ce2985e85c94..daf069cee4be76 100644 --- a/docs/data/joy/getting-started/templates/TemplateCollection.js +++ b/docs/data/joy/getting-started/templates/TemplateCollection.js @@ -10,11 +10,8 @@ import Link from '@mui/joy/Link'; import List from '@mui/joy/List'; import Button from '@mui/joy/Button'; import Typography from '@mui/joy/Typography'; -import SvgIcon from '@mui/joy/SvgIcon'; import Visibility from '@mui/icons-material/Visibility'; import CodeRoundedIcon from '@mui/icons-material/CodeRounded'; -import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; -import sourceJoyTemplates from 'docs/src/modules/joy/sourceJoyTemplates'; /** * To display a template on the site: @@ -94,8 +91,6 @@ const templates = [ ]; export default function TemplateCollection() { - const joyTemplates = sourceJoyTemplates(); - return ( {templates.map((template) => { - const item = joyTemplates.map.get(template.name); return ( Source - diff --git a/docs/src/modules/components/TemplateFrame.js b/docs/src/modules/components/TemplateFrame.js index 62b9e66004203e..b55a8522b0a343 100644 --- a/docs/src/modules/components/TemplateFrame.js +++ b/docs/src/modules/components/TemplateFrame.js @@ -21,7 +21,7 @@ import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import LightModeIcon from '@mui/icons-material/LightModeOutlined'; import DarkModeIcon from '@mui/icons-material/DarkModeOutlined'; import PaletteIcon from '@mui/icons-material/PaletteOutlined'; -import stackBlitz from 'docs/src/modules/sandbox/StackBlitzWc'; +import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; import sourceMaterialTemplates from 'docs/src/modules/material/sourceMaterialTemplates'; import { pascalCase } from 'docs/src/modules/utils/helpers'; From 0364c164aebafcad384891004163df2a798cb10f Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:46:05 +0200 Subject: [PATCH 06/17] Revert "eslint" This reverts commit 17bb0b7b8c5270a50d2ee0616917b96fbffae14a. --- .../templates/TemplateCollection.js | 36 +++++++++++++++++++ docs/src/modules/components/TemplateFrame.js | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/data/joy/getting-started/templates/TemplateCollection.js b/docs/data/joy/getting-started/templates/TemplateCollection.js index daf069cee4be76..94ce2985e85c94 100644 --- a/docs/data/joy/getting-started/templates/TemplateCollection.js +++ b/docs/data/joy/getting-started/templates/TemplateCollection.js @@ -10,8 +10,11 @@ import Link from '@mui/joy/Link'; import List from '@mui/joy/List'; import Button from '@mui/joy/Button'; import Typography from '@mui/joy/Typography'; +import SvgIcon from '@mui/joy/SvgIcon'; import Visibility from '@mui/icons-material/Visibility'; import CodeRoundedIcon from '@mui/icons-material/CodeRounded'; +import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; +import sourceJoyTemplates from 'docs/src/modules/joy/sourceJoyTemplates'; /** * To display a template on the site: @@ -91,6 +94,8 @@ const templates = [ ]; export default function TemplateCollection() { + const joyTemplates = sourceJoyTemplates(); + return ( {templates.map((template) => { + const item = joyTemplates.map.get(template.name); return ( Source + diff --git a/docs/src/modules/components/TemplateFrame.js b/docs/src/modules/components/TemplateFrame.js index b55a8522b0a343..62b9e66004203e 100644 --- a/docs/src/modules/components/TemplateFrame.js +++ b/docs/src/modules/components/TemplateFrame.js @@ -21,7 +21,7 @@ import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import LightModeIcon from '@mui/icons-material/LightModeOutlined'; import DarkModeIcon from '@mui/icons-material/DarkModeOutlined'; import PaletteIcon from '@mui/icons-material/PaletteOutlined'; -import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; +import stackBlitz from 'docs/src/modules/sandbox/StackBlitzWc'; import sourceMaterialTemplates from 'docs/src/modules/material/sourceMaterialTemplates'; import { pascalCase } from 'docs/src/modules/utils/helpers'; From d413b72fc550f4c54d6292ee5aefda73e70581b7 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 06:57:51 +0200 Subject: [PATCH 07/17] joy --- .../templates/TemplateCollection.js | 16 ++-- .../modules/components/JoyThemeBuilder.tsx | 14 +-- docs/src/modules/components/TemplateFrame.js | 2 +- docs/src/modules/sandbox/StackBlitz.ts | 92 +++++++++++++++++++ 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/docs/data/joy/getting-started/templates/TemplateCollection.js b/docs/data/joy/getting-started/templates/TemplateCollection.js index 94ce2985e85c94..9a10423c44a0fa 100644 --- a/docs/data/joy/getting-started/templates/TemplateCollection.js +++ b/docs/data/joy/getting-started/templates/TemplateCollection.js @@ -13,7 +13,7 @@ import Typography from '@mui/joy/Typography'; import SvgIcon from '@mui/joy/SvgIcon'; import Visibility from '@mui/icons-material/Visibility'; import CodeRoundedIcon from '@mui/icons-material/CodeRounded'; -import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; +import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; import sourceJoyTemplates from 'docs/src/modules/joy/sourceJoyTemplates'; /** @@ -270,16 +270,16 @@ export default function TemplateCollection() { color="neutral" fullWidth startDecorator={ - - + + } - aria-label="CodeSandbox playground" + aria-label="StackBlitz playground" data-ga-event-category="joy-template" data-ga-event-label={template.name} - data-ga-event-action="codesandbox" + data-ga-event-action="stackblitz" onClick={() => - codeSandbox + stackBlitz .createJoyTemplate({ ...item, title: `${startCase(template.name)} Template - Joy UI`, @@ -289,11 +289,11 @@ export default function TemplateCollection() { item.codeVariant === 'TS' ? 'tsx' : 'js' }`, }) - .openSandbox() + .openStackBlitz() } sx={{ fontFamily: 'IBM Plex Sans' }} > - CodeSandbox + StackBlitz diff --git a/docs/src/modules/components/JoyThemeBuilder.tsx b/docs/src/modules/components/JoyThemeBuilder.tsx index 4094509d1235b0..047047d8d77b10 100644 --- a/docs/src/modules/components/JoyThemeBuilder.tsx +++ b/docs/src/modules/components/JoyThemeBuilder.tsx @@ -63,7 +63,7 @@ import DarkMode from '@mui/icons-material/DarkMode'; import LightMode from '@mui/icons-material/LightMode'; import { HighlightedCode } from '@mui/docs/HighlightedCode'; import { BrandingProvider } from '@mui/docs/branding'; -import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; +import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; import sourceJoyTemplates, { TemplateData } from 'docs/src/modules/joy/sourceJoyTemplates'; import extractTemplates from 'docs/src/modules/utils/extractTemplates'; import generateThemeAugmentation from 'docs/src/modules/joy/generateThemeAugmentation'; @@ -1325,7 +1325,7 @@ function TemplatesDialog({ textColor="#fff" overlay onClick={() => { - codeSandbox + stackBlitz .createJoyTemplate({ ...item, files: newFiles, @@ -1333,7 +1333,7 @@ function TemplatesDialog({ title: `Joy UI - Custom theme`, codeVariant: 'TS', }) - .openSandbox(); + .openStackBlitz(); }} endDecorator={} sx={{ fontSize: 'xl', fontWeight: 'xl' }} @@ -1404,14 +1404,14 @@ function TemplatesDialog({ './result/App.tsx': getMinimalJoyTemplate(), './result/theme.ts': generateThemeCode(data), }); - codeSandbox + stackBlitz .createJoyTemplate({ ...result, codeVariant: 'TS', githubLocation: '', title: `Joy UI - Minimal template`, }) - .openSandbox(); + .openStackBlitz(); }} endDecorator={} sx={{ fontSize: 'lg', fontWeight: 'lg' }} @@ -1532,8 +1532,8 @@ export default function JoyThemeBuilder() {
- - + + diff --git a/docs/src/modules/components/TemplateFrame.js b/docs/src/modules/components/TemplateFrame.js index 62b9e66004203e..b55a8522b0a343 100644 --- a/docs/src/modules/components/TemplateFrame.js +++ b/docs/src/modules/components/TemplateFrame.js @@ -21,7 +21,7 @@ import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import LightModeIcon from '@mui/icons-material/LightModeOutlined'; import DarkModeIcon from '@mui/icons-material/DarkModeOutlined'; import PaletteIcon from '@mui/icons-material/PaletteOutlined'; -import stackBlitz from 'docs/src/modules/sandbox/StackBlitzWc'; +import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; import sourceMaterialTemplates from 'docs/src/modules/material/sourceMaterialTemplates'; import { pascalCase } from 'docs/src/modules/utils/helpers'; diff --git a/docs/src/modules/sandbox/StackBlitz.ts b/docs/src/modules/sandbox/StackBlitz.ts index 160205b47cf5bd..d4b3606b260156 100644 --- a/docs/src/modules/sandbox/StackBlitz.ts +++ b/docs/src/modules/sandbox/StackBlitz.ts @@ -102,6 +102,97 @@ export default defineConfig({ }; } +/** + * Create a Material Template for StackBlitz using the SDK and Vite + */ +function createJoyTemplate(templateData: { + title: string; + files: Record; + githubLocation: string; + codeVariant: CodeVariant; + codeStyling?: CodeStyling; +}) { + const ext = getFileExtension(templateData.codeVariant); + const { title, githubLocation: description } = templateData; + const raw = Object.entries(templateData.files ?? {}).reduce( + (prev, curr) => `${prev}\n${curr}`, + '', + ); + + const demoData: DemoData = { codeStyling: 'MUI System', ...templateData, raw, language: 'en' }; + + // Get dependencies + const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + }); + + // Add Vite specific dependencies + const devDependencies: Record = { + ...baseDevDependencies, + vite: 'latest', + '@vitejs/plugin-react': 'latest', + }; + + // Create base Vite files with dependencies + const viteFiles = createViteFiles(demoData, dependencies, devDependencies); + + // Restructure template files to be under src/ + const templateSourceFiles = templateData.files + ? Object.fromEntries( + Object.entries(templateData.files).map(([key, value]) => [`src/${key}`, value]), + ) + : {}; + + // document.querySelector returns 'Element | null' but createRoot expects 'Element | DocumentFragment'. + const type = templateData.codeVariant === 'TS' ? '!' : ''; + + // Create a proper React 18 index file for Vite + const indexContent = `import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { StyledEngineProvider } from '@mui/joy/styles'; +import App from './App'; + +ReactDOM.createRoot(document.querySelector("#root")${type}).render( + + + + + +);`; + + // Combine all files + const files = { + ...viteFiles, + [`src/index.${ext}`]: indexContent, + ...templateSourceFiles, + }; + + return { + title, + files, + dependencies, + devDependencies, + replaceContent(updater: (content: string | Record, filePath: string) => string) { + Object.keys(files).forEach((filePath) => { + files[filePath] = updater(files[filePath], filePath); + }); + return this; + }, + openStackBlitz: (initialFile: string = 'src/App') => { + // Add extension if missing + const normalizedInitialFile = initialFile.endsWith(ext) + ? initialFile + : `${initialFile}.${ext}`; + + openStackBlitzSDK({ + title, + description, + files, + initialFile: normalizedInitialFile, + }); + }, + }; +} /** * Create a Material Template for StackBlitz using the SDK and Vite */ @@ -261,6 +352,7 @@ function createReactApp(demoData: DemoData) { } export default { + createJoyTemplate, createReactApp, createMaterialTemplate, }; From 322456072fe898a52517b261dfa8525b0ecc4016 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 07:11:06 +0200 Subject: [PATCH 08/17] fix tests --- docs/src/modules/sandbox/StackBlitz.test.js | 137 +++++++++++++++----- 1 file changed, 105 insertions(+), 32 deletions(-) diff --git a/docs/src/modules/sandbox/StackBlitz.test.js b/docs/src/modules/sandbox/StackBlitz.test.js index a688a4bb833eee..ed055e34c115a5 100644 --- a/docs/src/modules/sandbox/StackBlitz.test.js +++ b/docs/src/modules/sandbox/StackBlitz.test.js @@ -52,9 +52,32 @@ describe('StackBlitz', () => {
+ `, - 'Demo.js': `import * as React from 'react'; + 'package.json': `{ + "name": "mui-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "latest", + "@mui/material": "latest", + "react-dom": "latest", + "@emotion/react": "latest", + "@emotion/styled": "latest" + }, + "devDependencies": { + "vite": "latest", + "@vitejs/plugin-react": "latest" + } +}`, + 'src/Demo.js': `import * as React from \'react\'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; @@ -68,7 +91,7 @@ export default function BasicButtons() { ); } `, - 'index.js': `import * as React from 'react'; + 'src/index.js': `import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { StyledEngineProvider } from '@mui/material/styles'; import Demo from './Demo'; @@ -80,6 +103,14 @@ ReactDOM.createRoot(document.querySelector("#root")).render( );`, + 'vite.config.js': ` +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +});`, }, dependencies: { react: 'latest', @@ -90,7 +121,8 @@ ReactDOM.createRoot(document.querySelector("#root")).render( '@emotion/styled': 'latest', }, devDependencies: { - 'react-scripts': 'latest', + '@vitejs/plugin-react': 'latest', + vite: 'latest', }, }); }); @@ -130,60 +162,100 @@ ReactDOM.createRoot(document.querySelector("#root")).render(
+ `, - 'Demo.tsx': `import * as React from 'react'; + 'package.json': `{ + "name": "mui-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "latest", + "@mui/material": "latest", + "react-dom": "latest", + "@emotion/react": "latest", + "@emotion/styled": "latest", + "typescript": "latest" + }, + "devDependencies": { + "@types/react": "latest", + "@types/react-dom": "latest", + "vite": "latest", + "@vitejs/plugin-react": "latest" + } +}`, + 'src/Demo.tsx': `import * as React from 'react'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; export default function BasicButtons() { return ( - - - - + + + + ); } `, - 'index.tsx': `import * as React from 'react'; + 'src/index.tsx': `import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { StyledEngineProvider } from '@mui/material/styles'; import Demo from './Demo'; -ReactDOM.createRoot(document.querySelector("#root")!).render( +ReactDOM.createRoot(document.querySelector(\"#root\")!).render( );`, + 'tsconfig.json': `{ "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, - "include": [ - "src" - ] -} -`, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +}`, + 'tsconfig.node.json': `{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +}`, + 'vite.config.ts': ` +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +});`, }, dependencies: { react: 'latest', @@ -192,12 +264,13 @@ ReactDOM.createRoot(document.querySelector("#root")!).render( 'react-dom': 'latest', '@emotion/react': 'latest', '@emotion/styled': 'latest', - '@types/react': 'latest', - '@types/react-dom': 'latest', typescript: 'latest', }, devDependencies: { - 'react-scripts': 'latest', + '@types/react': 'latest', + '@types/react-dom': 'latest', + '@vitejs/plugin-react': 'latest', + vite: 'latest', }, }); }); From 25c862c3db30cb36de34146b8cc0a5db7f499822 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 07:13:26 +0200 Subject: [PATCH 09/17] deps --- docs/src/modules/sandbox/Dependencies.test.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/src/modules/sandbox/Dependencies.test.js b/docs/src/modules/sandbox/Dependencies.test.js index be560a0debfac7..ddad7d938959a9 100644 --- a/docs/src/modules/sandbox/Dependencies.test.js +++ b/docs/src/modules/sandbox/Dependencies.test.js @@ -138,7 +138,7 @@ import 'exceljs'; }); it('can collect required @types packages', () => { - const { dependencies } = SandboxDependencies({ + const { dependencies, devDependencies } = SandboxDependencies({ raw: s1, codeVariant: 'TS', }); @@ -153,16 +153,19 @@ import 'exceljs'; // #npm-tag-reference '@mui/material': 'latest', '@mui/base': 'latest', + typescript: 'latest', + }); + + expect(devDependencies).to.deep.equal({ '@types/foo-bar__bip': 'latest', '@types/prop-types': 'latest', '@types/react-dom': 'latest', '@types/react': 'latest', - typescript: 'latest', }); }); it('should handle @types correctly', () => { - const { dependencies } = SandboxDependencies({ + const { dependencies, devDependencies } = SandboxDependencies({ raw: `import utils from '../utils';`, codeVariant: 'TS', }); @@ -174,9 +177,12 @@ import 'exceljs'; '@emotion/styled': 'latest', // #npm-tag-reference '@mui/material': 'latest', + typescript: 'latest', + }); + + expect(devDependencies).to.deep.equal({ '@types/react-dom': 'latest', '@types/react': 'latest', - typescript: 'latest', }); }); @@ -528,8 +534,6 @@ export default function EmailExample() { '@mui/joy': 'latest', '@mui/material': 'latest', '@mui/system': 'latest', - '@types/react': 'latest', - '@types/react-dom': 'latest', typescript: 'latest', }); }); From 941b2b637e4195e26a320f410707b7c888345d4a Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 07:24:02 +0200 Subject: [PATCH 10/17] Update StackBlitz.test.js --- docs/src/modules/sandbox/StackBlitz.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/modules/sandbox/StackBlitz.test.js b/docs/src/modules/sandbox/StackBlitz.test.js index ed055e34c115a5..a2fb63b79b8649 100644 --- a/docs/src/modules/sandbox/StackBlitz.test.js +++ b/docs/src/modules/sandbox/StackBlitz.test.js @@ -77,7 +77,7 @@ describe('StackBlitz', () => { "@vitejs/plugin-react": "latest" } }`, - 'src/Demo.js': `import * as React from \'react\'; + 'src/Demo.js': `import * as React from 'react'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; @@ -196,10 +196,10 @@ import Button from '@mui/material/Button'; export default function BasicButtons() { return ( - - - - + + + + ); } @@ -209,7 +209,7 @@ import * as ReactDOM from 'react-dom/client'; import { StyledEngineProvider } from '@mui/material/styles'; import Demo from './Demo'; -ReactDOM.createRoot(document.querySelector(\"#root\")!).render( +ReactDOM.createRoot(document.querySelector("#root")!).render( From 203d37b80e0d6405eb2e76465541947e67445c10 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:15:37 +0200 Subject: [PATCH 11/17] Trigger Build From 923e5bd999d3a49027983a745f9715bb8db908fe Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:36:30 +0200 Subject: [PATCH 12/17] Trigger Build From 26a93559ecc50e9e5b6324da54f40291d807902c Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:02:14 +0200 Subject: [PATCH 13/17] Trigger Build From ea6c77308568329e42ab0519d3d0b46b40dbd2ec Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:00:20 +0200 Subject: [PATCH 14/17] Add pkg.pr.new integration for CodeSandbox demos --- docs/src/modules/sandbox/StackBlitz.test.js | 6 ++++-- docs/src/modules/sandbox/StackBlitz.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/src/modules/sandbox/StackBlitz.test.js b/docs/src/modules/sandbox/StackBlitz.test.js index a2fb63b79b8649..0957f02a5bcd99 100644 --- a/docs/src/modules/sandbox/StackBlitz.test.js +++ b/docs/src/modules/sandbox/StackBlitz.test.js @@ -109,7 +109,8 @@ import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] + plugins: [react()], + define: { 'process.env': {} }, });`, }, dependencies: { @@ -254,7 +255,8 @@ import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] + plugins: [react()], + define: { 'process.env': {} }, });`, }, dependencies: { diff --git a/docs/src/modules/sandbox/StackBlitz.ts b/docs/src/modules/sandbox/StackBlitz.ts index d4b3606b260156..bd9a0ca5137833 100644 --- a/docs/src/modules/sandbox/StackBlitz.ts +++ b/docs/src/modules/sandbox/StackBlitz.ts @@ -46,7 +46,8 @@ import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] + plugins: [react()], + define: { 'process.env': {} }, });`, 'index.html': CRA.getHtml({ ...demoData, main: `/src/index.${ext}` }), 'package.json': JSON.stringify( From dd41afaf5a954e2c3480ec38eb0efd9b0a66e1d4 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 17 Apr 2025 11:23:27 +0200 Subject: [PATCH 15/17] update --- .../templates/TemplateCollection.js | 16 +- docs/package.json | 1 - docs/src/modules/components/DemoToolbar.js | 45 ++- .../modules/components/JoyThemeBuilder.tsx | 14 +- .../MaterialFreeTemplatesCollection.js | 37 +++ docs/src/modules/components/TemplateFrame.js | 39 +++ docs/src/modules/sandbox/CodeSandbox.test.js | 281 ++++++++++++++++ docs/src/modules/sandbox/CodeSandbox.ts | 304 ++++++++++++++++++ docs/src/modules/sandbox/Dependencies.ts | 9 +- docs/src/modules/sandbox/StackBlitz.ts | 107 +++--- pnpm-lock.yaml | 8 - 11 files changed, 756 insertions(+), 105 deletions(-) create mode 100644 docs/src/modules/sandbox/CodeSandbox.test.js create mode 100644 docs/src/modules/sandbox/CodeSandbox.ts diff --git a/docs/data/joy/getting-started/templates/TemplateCollection.js b/docs/data/joy/getting-started/templates/TemplateCollection.js index 9a10423c44a0fa..94ce2985e85c94 100644 --- a/docs/data/joy/getting-started/templates/TemplateCollection.js +++ b/docs/data/joy/getting-started/templates/TemplateCollection.js @@ -13,7 +13,7 @@ import Typography from '@mui/joy/Typography'; import SvgIcon from '@mui/joy/SvgIcon'; import Visibility from '@mui/icons-material/Visibility'; import CodeRoundedIcon from '@mui/icons-material/CodeRounded'; -import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; +import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; import sourceJoyTemplates from 'docs/src/modules/joy/sourceJoyTemplates'; /** @@ -270,16 +270,16 @@ export default function TemplateCollection() { color="neutral" fullWidth startDecorator={ - - + + } - aria-label="StackBlitz playground" + aria-label="CodeSandbox playground" data-ga-event-category="joy-template" data-ga-event-label={template.name} - data-ga-event-action="stackblitz" + data-ga-event-action="codesandbox" onClick={() => - stackBlitz + codeSandbox .createJoyTemplate({ ...item, title: `${startCase(template.name)} Template - Joy UI`, @@ -289,11 +289,11 @@ export default function TemplateCollection() { item.codeVariant === 'TS' ? 'tsx' : 'js' }`, }) - .openStackBlitz() + .openSandbox() } sx={{ fontFamily: 'IBM Plex Sans' }} > - StackBlitz + CodeSandbox diff --git a/docs/package.json b/docs/package.json index 850751e612475a..3f2b5b99445b8b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -53,7 +53,6 @@ "@mui/x-tree-view": "7.28.1", "@popperjs/core": "^2.11.8", "@react-spring/web": "^9.7.5", - "@stackblitz/sdk": "^1.11.0", "@tailwindcss/postcss": "^4.1.3", "@toolpad/core": "^0.14.0", "autoprefixer": "^10.4.21", diff --git a/docs/src/modules/components/DemoToolbar.js b/docs/src/modules/components/DemoToolbar.js index dcf389bb798a3e..62e6579426b8bc 100644 --- a/docs/src/modules/components/DemoToolbar.js +++ b/docs/src/modules/components/DemoToolbar.js @@ -25,6 +25,7 @@ import { useSetCodeVariant } from 'docs/src/modules/utils/codeVariant'; import { useSetCodeStyling, useCodeStyling } from 'docs/src/modules/utils/codeStylingSolution'; import { useTranslate } from '@mui/docs/i18n'; import stylingSolutionMapping from 'docs/src/modules/utils/stylingSolutionMapping'; +import codeSandbox from '../sandbox/CodeSandbox'; import stackBlitz from '../sandbox/StackBlitz'; const Root = styled('div')(({ theme }) => [ @@ -531,20 +532,36 @@ export default function DemoToolbar(props) { {showCodeLabel} {demoOptions.hideEditButton ? null : ( - - stackBlitz.createReactApp(demoData).openSandbox()} - {...getControlProps(4)} - sx={{ borderRadius: 1 }} - > - - - - - + + + stackBlitz.createReactApp(demoData).openSandbox()} + {...getControlProps(4)} + sx={{ borderRadius: 1 }} + > + + + + + + + codeSandbox.createReactApp(demoData).openSandbox()} + {...getControlProps(5)} + sx={{ borderRadius: 1 }} + > + + + + + + )} { - stackBlitz + codeSandbox .createJoyTemplate({ ...item, files: newFiles, @@ -1333,7 +1333,7 @@ function TemplatesDialog({ title: `Joy UI - Custom theme`, codeVariant: 'TS', }) - .openStackBlitz(); + .openSandbox(); }} endDecorator={} sx={{ fontSize: 'xl', fontWeight: 'xl' }} @@ -1404,14 +1404,14 @@ function TemplatesDialog({ './result/App.tsx': getMinimalJoyTemplate(), './result/theme.ts': generateThemeCode(data), }); - stackBlitz + codeSandbox .createJoyTemplate({ ...result, codeVariant: 'TS', githubLocation: '', title: `Joy UI - Minimal template`, }) - .openStackBlitz(); + .openSandbox(); }} endDecorator={} sx={{ fontSize: 'lg', fontWeight: 'lg' }} @@ -1532,8 +1532,8 @@ export default function JoyThemeBuilder() { - - + + diff --git a/docs/src/modules/components/MaterialFreeTemplatesCollection.js b/docs/src/modules/components/MaterialFreeTemplatesCollection.js index 119e7aaacd9730..52d29d0134c457 100644 --- a/docs/src/modules/components/MaterialFreeTemplatesCollection.js +++ b/docs/src/modules/components/MaterialFreeTemplatesCollection.js @@ -15,6 +15,7 @@ import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded'; import { useTranslate } from '@mui/docs/i18n'; import { pascalCase } from 'docs/src/modules/utils/helpers'; import sourceMaterialTemplates from 'docs/src/modules/material/sourceMaterialTemplates'; +import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; const sourcePrefix = `${process.env.SOURCE_CODE_REPO}/tree/v${process.env.LIB_VERSION}`; @@ -208,6 +209,42 @@ export default function MaterialFreeTemplatesCollection() { + + + codeSandbox + .createMaterialTemplate({ + ...item, + files: { ...item.files, ...materialTemplates.sharedTheme?.files }, + title: `${templateName} Template - Material UI`, + githubLocation: `${process.env.SOURCE_CODE_REPO}/blob/v${ + process.env.LIB_VERSION + }/docs/data/material/templates/${templateId}/${templateName}.${ + item.codeVariant === 'TS' ? 'tsx' : 'js' + }`, + }) + .replaceContent((content) => { + if (typeof content === 'string') { + return content + .replace(/\.\.\/shared-theme\//g, './theme/') + .replace('./App', `./${templateName}`); + } + return content; + }) + .openSandbox(`/${templateName}`) + } + > + + + + + diff --git a/docs/src/modules/components/TemplateFrame.js b/docs/src/modules/components/TemplateFrame.js index b55a8522b0a343..2abc7cfd30c6bb 100644 --- a/docs/src/modules/components/TemplateFrame.js +++ b/docs/src/modules/components/TemplateFrame.js @@ -21,6 +21,7 @@ import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import LightModeIcon from '@mui/icons-material/LightModeOutlined'; import DarkModeIcon from '@mui/icons-material/DarkModeOutlined'; import PaletteIcon from '@mui/icons-material/PaletteOutlined'; +import codeSandbox from 'docs/src/modules/sandbox/CodeSandbox'; import stackBlitz from 'docs/src/modules/sandbox/StackBlitz'; import sourceMaterialTemplates from 'docs/src/modules/material/sourceMaterialTemplates'; import { pascalCase } from 'docs/src/modules/utils/helpers'; @@ -309,6 +310,44 @@ export default function TemplateFrame({ children }) { + + + codeSandbox + .createMaterialTemplate({ + ...item, + files: { ...item.files, ...materialTemplates.sharedTheme?.files }, + title: `${templateName} Template - Material UI`, + githubLocation: `${process.env.SOURCE_CODE_REPO}/blob/v${ + process.env.LIB_VERSION + }/docs/data/material/templates/${templateId}/${templateName}.${ + item.codeVariant === 'TS' ? 'tsx' : 'js' + }`, + }) + .replaceContent((content) => { + if (typeof content === 'string') { + return content + .replace(/\.\.\/shared-theme\//g, './theme/') + .replace('./App', `./${templateName}`); + } + return content; + }) + .openSandbox(`/${templateName}`) + } + sx={{ alignSelf: 'center', borderRadius: 1 }} + > + + + + + + + + + + ); +} +`; + +describe('CodeSandbox', () => { + it('generate the correct JavaScript result', () => { + const result = CodeSandbox.createReactApp({ + title: 'BasicButtons Material Demo', + githubLocation: + 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.js', + codeVariant: 'JS', + language: 'en', + raw: testCase, + }); + expect(result.files).to.deep.equal({ + 'package.json': { + content: { + description: + 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.js', + dependencies: { + react: 'latest', + // #npm-tag-reference + '@mui/material': 'latest', + 'react-dom': 'latest', + '@emotion/react': 'latest', + '@emotion/styled': 'latest', + }, + devDependencies: { + 'react-scripts': 'latest', + }, + scripts: { + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test', + eject: 'react-scripts eject', + }, + }, + }, + 'public/index.html': { + content: ` + + + + BasicButtons Material Demo + + + + + + + + + +
+ +`, + }, + 'src/Demo.js': { + content: `import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; + +export default function BasicButtons() { + return ( + + + + + + ); +} +`, + }, + 'src/index.js': { + content: `import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { StyledEngineProvider } from '@mui/material/styles'; +import Demo from './Demo'; + +ReactDOM.createRoot(document.querySelector("#root")).render( + + + + + +);`, + }, + }); + }); + + it('generate the correct TypeScript result', () => { + const result = CodeSandbox.createReactApp({ + title: 'BasicButtons Material Demo', + githubLocation: + 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.tsx', + codeVariant: 'TS', + language: 'en', + raw: testCase, + }); + expect(result.files).to.deep.equal({ + 'package.json': { + content: { + description: + 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.tsx', + dependencies: { + react: 'latest', + // #npm-tag-reference + '@mui/material': 'latest', + 'react-dom': 'latest', + '@emotion/react': 'latest', + '@emotion/styled': 'latest', + '@types/react': 'latest', + '@types/react-dom': 'latest', + typescript: 'latest', + }, + devDependencies: { + 'react-scripts': 'latest', + }, + main: 'index.tsx', + scripts: { + build: 'react-scripts build', + eject: 'react-scripts eject', + start: 'react-scripts start', + test: 'react-scripts test', + }, + }, + }, + 'public/index.html': { + content: ` + + + + BasicButtons Material Demo + + + + + + + + + +
+ +`, + }, + 'src/Demo.tsx': { + content: `import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; + +export default function BasicButtons() { + return ( + + + + + + ); +} +`, + }, + 'src/index.tsx': { + content: `import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { StyledEngineProvider } from '@mui/material/styles'; +import Demo from './Demo'; + +ReactDOM.createRoot(document.querySelector("#root")!).render( + + + + + +);`, + }, + 'tsconfig.json': { + content: `{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": [ + "src" + ] +} +`, + }, + }); + expect(result.dependencies).to.deep.equal({ + '@emotion/react': 'latest', + '@emotion/styled': 'latest', + // #npm-tag-reference + '@mui/material': 'latest', + '@types/react': 'latest', + '@types/react-dom': 'latest', + react: 'latest', + 'react-dom': 'latest', + typescript: 'latest', + }); + expect(result.devDependencies).to.deep.equal({ + 'react-scripts': 'latest', + }); + }); + + it('generate the correct index.html result when Tailwind is used', () => { + const result = CodeSandbox.createReactApp({ + title: 'BasicButtons Material Demo', + githubLocation: + 'https://github.com/mui/material-ui/blob/v5.7.0/docs/data/material/components/buttons/BasicButtons.js', + codeVariant: 'JS', + language: 'en', + raw: testCase, + codeStyling: 'Tailwind', + }); + expect(result.files['public/index.html'].content).to.contain( + '', + ); + }); + + it('should generate the correct stylesheet font link in index.html for Material Two Tones icons', () => { + const raw = `import * as React from 'react'; + import Icon from '@mui/material/Icon'; + + export default function TwoToneIcons() { + return add_circle; + } + `; + + const result = CodeSandbox.createReactApp({ + raw, + codeVariant: 'JS', + }); + + expect(result.files['public/index.html'].content).to.contain( + 'https://fonts.googleapis.com/icon?family=Material+Icons+Two+Tone', + ); + }); +}); diff --git a/docs/src/modules/sandbox/CodeSandbox.ts b/docs/src/modules/sandbox/CodeSandbox.ts new file mode 100644 index 00000000000000..cfa766e4e269cb --- /dev/null +++ b/docs/src/modules/sandbox/CodeSandbox.ts @@ -0,0 +1,304 @@ +// @ts-ignore +import LZString from 'lz-string'; +import addHiddenInput from 'docs/src/modules/utils/addHiddenInput'; +import SandboxDependencies from 'docs/src/modules/sandbox/Dependencies'; +import * as CRA from 'docs/src/modules/sandbox/CreateReactApp'; +import getFileExtension from 'docs/src/modules/sandbox/FileExtension'; +import flattenRelativeImports from 'docs/src/modules/sandbox/FlattenRelativeImports'; +import { DemoData, CodeVariant, CodeStyling } from 'docs/src/modules/sandbox/types'; + +const CSB_DEV_DEPENDENCIES = { + 'react-scripts': 'latest', +}; + +function compress(object: any) { + return LZString.compressToBase64(JSON.stringify(object)) + .replace(/\+/g, '-') // Convert '+' to '-' + .replace(/\//g, '_') // Convert '/' to '_' + .replace(/=+$/, ''); // Remove ending '=' +} + +function openSandbox({ files, codeVariant, initialFile }: any) { + const extension = codeVariant === 'TS' ? '.tsx' : '.js'; + const parameters = compress({ files }); + + // ref: https://codesandbox.io/docs/api/#define-api + const form = document.createElement('form'); + form.method = 'POST'; + form.target = '_blank'; + form.action = 'https://codesandbox.io/api/v1/sandboxes/define'; + addHiddenInput(form, 'parameters', parameters); + addHiddenInput(form, 'embed', '1'); + addHiddenInput( + form, + 'query', + `module=${initialFile}${initialFile.match(/(\.tsx|\.ts|\.js)$/) ? '' : extension}&fontsize=12`, + ); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); +} + +function createReactApp(demoData: DemoData) { + const ext = getFileExtension(demoData.codeVariant); + const { title, githubLocation: description } = demoData; + + const files: Record = { + 'public/index.html': { + content: CRA.getHtml(demoData), + }, + [`src/index.${ext}`]: { + content: CRA.getRootIndex(demoData), + }, + [`src/Demo.${ext}`]: { + content: flattenRelativeImports(demoData.raw), + }, + // Spread the relative modules + ...(demoData.relativeModules && + // Transform the relative modules array into an object + demoData.relativeModules.reduce( + (acc, curr) => ({ + ...acc, + // Remove the path and keep the filename + [`src/${curr.module.replace(/^.*[\\/]/g, '')}`]: { + content: flattenRelativeImports(curr.raw), + }, + }), + {}, + )), + ...(demoData.codeVariant === 'TS' && { + 'tsconfig.json': { + content: CRA.getTsconfig(), + }, + }), + }; + + const { dependencies, devDependencies } = SandboxDependencies(demoData, { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + devDeps: CSB_DEV_DEPENDENCIES, + }); + + files['package.json'] = { + content: { + description, + dependencies, + devDependencies, + scripts: { + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test', + eject: 'react-scripts eject', + }, + ...(demoData.codeVariant === 'TS' && { + main: 'index.tsx', + }), + }, + }; + + return { + title, + description, + files, + dependencies, + devDependencies, + /** + * @param {string} initialFile + * @description should start with `/`, for example `/Demo.tsx`. If the extension is not provided, + * it will be appended based on the code variant. + */ + openSandbox: (initialFile: string = `/src/Demo.${ext}`) => + openSandbox({ files, codeVariant: demoData.codeVariant, initialFile }), + }; +} + +function createJoyTemplate(templateData: { + title: string; + files: Record; + githubLocation: string; + codeVariant: CodeVariant; + codeStyling?: CodeStyling; +}) { + const ext = getFileExtension(templateData.codeVariant); + const { title, githubLocation: description } = templateData; + + // document.querySelector returns 'Element | null' but createRoot expects 'Element | DocumentFragment'. + const type = templateData.codeVariant === 'TS' ? '!' : ''; + + const files: Record = { + 'public/index.html': { + content: CRA.getHtml({ + title: templateData.title, + language: 'en', + codeStyling: templateData.codeStyling ?? 'MUI System', + }), + }, + [`index.${ext}`]: { + content: `import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { StyledEngineProvider } from '@mui/joy/styles'; +import App from './App'; + +ReactDOM.createRoot(document.querySelector("#root")${type}).render( + + + + + +);`, + }, + ...Object.entries(templateData.files).reduce( + (prev, curr) => ({ + ...prev, + [curr[0]]: { + content: curr[1], + }, + }), + {}, + ), + ...(templateData.codeVariant === 'TS' && { + 'tsconfig.json': { + content: CRA.getTsconfig(), + }, + }), + }; + + const { dependencies, devDependencies } = SandboxDependencies( + { + codeVariant: templateData.codeVariant, + raw: Object.entries(templateData.files).reduce((prev, curr) => `${prev}\n${curr}`, ''), + productId: 'joy-ui', + }, + { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + devDeps: CSB_DEV_DEPENDENCIES, + }, + ); + + files['package.json'] = { + content: { + description, + dependencies, + devDependencies, + scripts: { + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test', + eject: 'react-scripts eject', + }, + ...(templateData.codeVariant === 'TS' && { + main: 'index.tsx', + }), + }, + }; + + return { + title, + files, + dependencies, + devDependencies, + openSandbox: (initialFile: string = '/App') => + openSandbox({ files, codeVariant: templateData.codeVariant, initialFile }), + }; +} + +function createMaterialTemplate(templateData: { + title: string; + files: Record; + githubLocation: string; + codeVariant: CodeVariant; + codeStyling?: CodeStyling; +}) { + const ext = getFileExtension(templateData.codeVariant); + const { title, githubLocation: description } = templateData; + + // document.querySelector returns 'Element | null' but createRoot expects 'Element | DocumentFragment'. + const type = templateData.codeVariant === 'TS' ? '!' : ''; + + const files: Record }> = { + 'public/index.html': { + content: CRA.getHtml({ + title: templateData.title, + language: 'en', + codeStyling: templateData.codeStyling ?? 'MUI System', + }), + }, + [`index.${ext}`]: { + content: `import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { StyledEngineProvider } from '@mui/material/styles'; +import App from './App'; + +ReactDOM.createRoot(document.querySelector("#root")${type}).render( + + + + + +);`, + }, + ...Object.entries(templateData.files).reduce( + (prev, curr) => ({ + ...prev, + [curr[0]]: { + content: curr[1], + }, + }), + {}, + ), + ...(templateData.codeVariant === 'TS' && { + 'tsconfig.json': { + content: CRA.getTsconfig(), + }, + }), + }; + + const { dependencies, devDependencies } = SandboxDependencies( + { + codeVariant: templateData.codeVariant, + raw: Object.entries(templateData.files).reduce((prev, curr) => `${prev}\n${curr}`, ''), + productId: 'material-ui', + }, + { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + devDeps: CSB_DEV_DEPENDENCIES, + }, + ); + + files['package.json'] = { + content: { + description, + dependencies, + devDependencies, + scripts: { + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test', + eject: 'react-scripts eject', + }, + ...(templateData.codeVariant === 'TS' && { + main: 'index.tsx', + }), + }, + }; + + return { + title, + files, + dependencies, + devDependencies, + replaceContent(updater: (content: string | Record, filePath: string) => string) { + Object.keys(files).forEach((filePath) => { + files[filePath].content = updater(files[filePath].content, filePath); + }); + return this; + }, + openSandbox: (initialFile: string = '/App') => + openSandbox({ files, codeVariant: templateData.codeVariant, initialFile }), + }; +} + +export default { + createReactApp, + createJoyTemplate, + createMaterialTemplate, +}; diff --git a/docs/src/modules/sandbox/Dependencies.ts b/docs/src/modules/sandbox/Dependencies.ts index 93b4dd5e988931..e3b76448c9520a 100644 --- a/docs/src/modules/sandbox/Dependencies.ts +++ b/docs/src/modules/sandbox/Dependencies.ts @@ -39,8 +39,11 @@ function addTypeDeps(deps: Record, devDeps: Record; -export default function SandboxDependencies(demo: Demo, options?: { commitRef?: string }) { - const { commitRef } = options || {}; +export default function SandboxDependencies( + demo: Demo, + options?: { commitRef?: string; devDeps?: Record }, +) { + const { commitRef, devDeps = {} } = options || {}; /** * @param packageName - The name of a package living inside this repository. @@ -158,7 +161,7 @@ export default function SandboxDependencies(demo: Demo, options?: { commitRef?: dependencies[name] = versions[name] ? versions[name] : 'latest'; } - const devDependencies: Record = {}; + const devDependencies: Record = { ...devDeps }; if (demo.codeVariant === CODE_VARIANTS.TS) { addTypeDeps(dependencies, devDependencies); diff --git a/docs/src/modules/sandbox/StackBlitz.ts b/docs/src/modules/sandbox/StackBlitz.ts index bd9a0ca5137833..aaf5664e70869f 100644 --- a/docs/src/modules/sandbox/StackBlitz.ts +++ b/docs/src/modules/sandbox/StackBlitz.ts @@ -1,14 +1,20 @@ -import sdk from '@stackblitz/sdk'; +import addHiddenInput from 'docs/src/modules/utils/addHiddenInput'; import SandboxDependencies from 'docs/src/modules/sandbox/Dependencies'; import getFileExtension from 'docs/src/modules/sandbox/FileExtension'; import flattenRelativeImports from 'docs/src/modules/sandbox/FlattenRelativeImports'; import { CodeStyling, CodeVariant, DemoData } from 'docs/src/modules/sandbox/types'; import * as CRA from 'docs/src/modules/sandbox/CreateReactApp'; -/** - * Open a project in StackBlitz using the SDK - */ -function openStackBlitzSDK({ +function ensureExtension(file: string, extension: string): string { + return file.endsWith(`.${extension}`) ? file : `${file}.${extension}`; +} + +const VITE_DEV_DEPENDENCIES = { + '@vitejs/plugin-react': 'latest', + vite: 'latest', +}; + +function openStackBlitz({ title, description, files, @@ -19,15 +25,21 @@ function openStackBlitzSDK({ files: Record; initialFile: string; }) { - sdk.openProject( - { - title, - description, - template: 'node', - files, - }, - { openFile: initialFile }, - ); + // ref: https://developer.stackblitz.com/docs/platform/post-api/ + const form = document.createElement('form'); + form.method = 'POST'; + form.target = '_blank'; + form.action = `https://stackblitz.com/run?file=${initialFile}`; + addHiddenInput(form, 'project[template]', 'node'); + addHiddenInput(form, 'project[title]', title); + addHiddenInput(form, 'project[description]', `# ${title}\n${description}`); + Object.keys(files).forEach((key) => { + const value = files[key]; + addHiddenInput(form, `project[files][${key}]`, value); + }); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); } /** @@ -123,17 +135,11 @@ function createJoyTemplate(templateData: { const demoData: DemoData = { codeStyling: 'MUI System', ...templateData, raw, language: 'en' }; // Get dependencies - const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, { + const { dependencies, devDependencies } = SandboxDependencies(demoData, { commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + devDeps: VITE_DEV_DEPENDENCIES, }); - // Add Vite specific dependencies - const devDependencies: Record = { - ...baseDevDependencies, - vite: 'latest', - '@vitejs/plugin-react': 'latest', - }; - // Create base Vite files with dependencies const viteFiles = createViteFiles(demoData, dependencies, devDependencies); @@ -179,17 +185,12 @@ ReactDOM.createRoot(document.querySelector("#root")${type}).render( }); return this; }, - openStackBlitz: (initialFile: string = 'src/App') => { - // Add extension if missing - const normalizedInitialFile = initialFile.endsWith(ext) - ? initialFile - : `${initialFile}.${ext}`; - - openStackBlitzSDK({ + openStackBlitz: (initialFile: string = `src/App`) => { + openStackBlitz({ title, description, files, - initialFile: normalizedInitialFile, + initialFile: ensureExtension(initialFile, ext), }); }, }; @@ -214,17 +215,11 @@ function createMaterialTemplate(templateData: { const demoData: DemoData = { codeStyling: 'MUI System', ...templateData, raw, language: 'en' }; // Get dependencies - const { dependencies, devDependencies: baseDevDependencies } = SandboxDependencies(demoData, { + const { dependencies, devDependencies } = SandboxDependencies(demoData, { commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + devDeps: VITE_DEV_DEPENDENCIES, }); - // Add Vite specific dependencies - const devDependencies: Record = { - ...baseDevDependencies, - vite: 'latest', - '@vitejs/plugin-react': 'latest', - }; - // Create base Vite files with dependencies const viteFiles = createViteFiles(demoData, dependencies, devDependencies); @@ -268,17 +263,12 @@ ReactDOM.createRoot(document.getElementById('root')${templateData.codeVariant == }); return this; }, - openStackBlitz: (initialFile: string = 'src/App') => { - // Add extension if missing - const normalizedInitialFile = initialFile.endsWith(ext) - ? initialFile - : `${initialFile}.${ext}`; - - openStackBlitzSDK({ + openStackBlitz: (initialFile: string = `src/App`) => { + openStackBlitz({ title, description, files, - initialFile: normalizedInitialFile, + initialFile: ensureExtension(initialFile, ext), }); }, }; @@ -293,24 +283,13 @@ function createReactApp(demoData: DemoData) { const { title, githubLocation: description } = demoData; // Get dependencies - const { dependencies: baseDependencies, devDependencies: baseDevDependencies } = - SandboxDependencies(demoData, { - commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, - }); - - const dependencies = { ...baseDependencies }; - - // Add Vite specific dependencies - const devDependencies: Record = { - ...baseDevDependencies, - vite: 'latest', - '@vitejs/plugin-react': 'latest', - }; + const { dependencies, devDependencies } = SandboxDependencies(demoData, { + commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined, + devDeps: VITE_DEV_DEPENDENCIES, + }); - // Create base Vite files with dependencies const viteFiles = createViteFiles(demoData, dependencies, devDependencies); - // Create demo files just like in the original implementation const demoFiles: Record = { [`src/Demo.${ext}`]: flattenRelativeImports(demoData.raw), }; @@ -341,12 +320,12 @@ function createReactApp(demoData: DemoData) { files, dependencies, devDependencies, - openSandbox: (initialFile = `src/Demo.${ext}`) => { - openStackBlitzSDK({ + openSandbox: (initialFile = 'src/Demo') => { + openStackBlitz({ title, description, files, - initialFile, + initialFile: ensureExtension(initialFile, ext), }); }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3320c2fb28833c..2a26df4465f4d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -739,9 +739,6 @@ importers: '@react-spring/web': specifier: ^9.7.5 version: 9.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@stackblitz/sdk': - specifier: ^1.11.0 - version: 1.11.0 '@tailwindcss/postcss': specifier: ^4.1.3 version: 4.1.3 @@ -5488,9 +5485,6 @@ packages: '@socket.io/component-emitter@3.1.0': resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} - '@stackblitz/sdk@1.11.0': - resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} - '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -17505,8 +17499,6 @@ snapshots: '@socket.io/component-emitter@3.1.0': {} - '@stackblitz/sdk@1.11.0': {} - '@standard-schema/spec@1.0.0': {} '@styled-system/background@5.1.2': From a74041347b99a983bf22c57256621a089bf9b6f4 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 17 Apr 2025 19:46:22 +0200 Subject: [PATCH 16/17] Update StackBlitz.test.js --- docs/src/modules/sandbox/StackBlitz.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/modules/sandbox/StackBlitz.test.js b/docs/src/modules/sandbox/StackBlitz.test.js index 0957f02a5bcd99..d313dd40c7da7c 100644 --- a/docs/src/modules/sandbox/StackBlitz.test.js +++ b/docs/src/modules/sandbox/StackBlitz.test.js @@ -73,8 +73,8 @@ describe('StackBlitz', () => { "@emotion/styled": "latest" }, "devDependencies": { - "vite": "latest", - "@vitejs/plugin-react": "latest" + "@vitejs/plugin-react": "latest", + "vite": "latest" } }`, 'src/Demo.js': `import * as React from 'react'; @@ -185,10 +185,10 @@ export default defineConfig({ "typescript": "latest" }, "devDependencies": { - "@types/react": "latest", - "@types/react-dom": "latest", + "@vitejs/plugin-react": "latest", "vite": "latest", - "@vitejs/plugin-react": "latest" + "@types/react": "latest", + "@types/react-dom": "latest" } }`, 'src/Demo.tsx': `import * as React from 'react'; From ed758b3c3ad2fd0bda6be16a515f80352af48844 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:48:59 +0200 Subject: [PATCH 17/17] Update CodeSandbox.test.js --- docs/src/modules/sandbox/CodeSandbox.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/modules/sandbox/CodeSandbox.test.js b/docs/src/modules/sandbox/CodeSandbox.test.js index 03a05904cf719d..e03269b5604239 100644 --- a/docs/src/modules/sandbox/CodeSandbox.test.js +++ b/docs/src/modules/sandbox/CodeSandbox.test.js @@ -72,7 +72,7 @@ describe('CodeSandbox', () => {
- + \n `, }, 'src/Demo.js': { @@ -129,12 +129,12 @@ ReactDOM.createRoot(document.querySelector("#root")).render( 'react-dom': 'latest', '@emotion/react': 'latest', '@emotion/styled': 'latest', - '@types/react': 'latest', - '@types/react-dom': 'latest', typescript: 'latest', }, devDependencies: { 'react-scripts': 'latest', + '@types/react': 'latest', + '@types/react-dom': 'latest', }, main: 'index.tsx', scripts: { @@ -167,7 +167,7 @@ ReactDOM.createRoot(document.querySelector("#root")).render(
- + \n `, }, 'src/Demo.tsx': { @@ -234,14 +234,14 @@ ReactDOM.createRoot(document.querySelector("#root")!).render( '@emotion/styled': 'latest', // #npm-tag-reference '@mui/material': 'latest', - '@types/react': 'latest', - '@types/react-dom': 'latest', react: 'latest', 'react-dom': 'latest', typescript: 'latest', }); expect(result.devDependencies).to.deep.equal({ 'react-scripts': 'latest', + '@types/react': 'latest', + '@types/react-dom': 'latest', }); });