Skip to content

[docs-infra] StackBlitz WebContainer demos #45924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/src/modules/sandbox/CodeSandbox.test.js
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ describe('CodeSandbox', () => {
</head>
<body>
<div id="root"></div>
</body>
\n </body>
</html>`,
},
'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(
</head>
<body>
<div id="root"></div>
</body>
\n </body>
</html>`,
},
'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',
});
});

7 changes: 7 additions & 0 deletions docs/src/modules/sandbox/CodeSandbox.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,10 @@ 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 '-'
@@ -71,6 +75,7 @@ function createReactApp(demoData: DemoData) {

const { dependencies, devDependencies } = SandboxDependencies(demoData, {
commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined,
devDeps: CSB_DEV_DEPENDENCIES,
});

files['package.json'] = {
@@ -165,6 +170,7 @@ ReactDOM.createRoot(document.querySelector("#root")${type}).render(
},
{
commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined,
devDeps: CSB_DEV_DEPENDENCIES,
},
);

@@ -254,6 +260,7 @@ ReactDOM.createRoot(document.querySelector("#root")${type}).render(
},
{
commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined,
devDeps: CSB_DEV_DEPENDENCIES,
},
);

3 changes: 3 additions & 0 deletions docs/src/modules/sandbox/CreateReactApp.ts
Original file line number Diff line number Diff line change
@@ -5,11 +5,13 @@ export const getHtml = ({
language,
codeStyling,
raw,
main,
}: {
title: string;
language: string;
codeStyling?: 'Tailwind' | 'MUI System';
raw?: string;
main?: string;
}) => {
return `<!DOCTYPE html>
<html lang="${language}">
@@ -84,6 +86,7 @@ export const getHtml = ({
</head>
<body>
<div id="root"></div>
${main ? `<script type="module" src="${main}"></script>` : ''}
</body>
</html>`;
};
16 changes: 10 additions & 6 deletions docs/src/modules/sandbox/Dependencies.test.js
Original file line number Diff line number Diff line change
@@ -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',
});
});

@@ -533,8 +539,6 @@ export default function EmailExample() {
'@mui/joy': 'latest',
'@mui/material': 'latest',
'@mui/system': 'latest',
'@types/react': 'latest',
'@types/react-dom': 'latest',
typescript: 'latest',
});
});
25 changes: 13 additions & 12 deletions docs/src/modules/sandbox/Dependencies.ts
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ const muiNpmOrgs = ['@mui', '@base_ui', '@pigment-css', '@toolpad'];
*
* @param deps - list of dependency as `name => version`
*/
function addTypeDeps(deps: Record<string, string>): void {
function addTypeDeps(deps: Record<string, string>, devDeps: Record<string, string>): void {
const packagesWithDTPackage = Object.keys(deps)
.filter((name) => !packagesWithBundledTypes.includes(name))
// All the MUI packages come with bundled types
@@ -33,14 +33,17 @@ function addTypeDeps(deps: Record<string, string>): void {
resolvedName = name.slice(1).replace('/', '__');
}

deps[`@types/${resolvedName}`] = 'latest';
devDeps[`@types/${resolvedName}`] = 'latest';
});
}

type Demo = Pick<DemoData, 'productId' | 'raw' | 'codeVariant' | 'relativeModules'>;

export default function SandboxDependencies(demo: Demo, options?: { commitRef?: string }) {
const { commitRef } = options || {};
export default function SandboxDependencies(
demo: Demo,
options?: { commitRef?: string; devDeps?: Record<string, string> },
) {
const { commitRef, devDeps = {} } = options || {};

/**
* @param packageName - The name of a package living inside this repository.
@@ -149,11 +152,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';
@@ -163,9 +161,12 @@ export default function SandboxDependencies(demo: Demo, options?: { commitRef?:
dependencies[name] = versions[name] ? versions[name] : 'latest';
}

const devDependencies = {
'react-scripts': 'latest',
};
const devDependencies: Record<string, string> = { ...devDeps };

if (demo.codeVariant === CODE_VARIANTS.TS) {
addTypeDeps(dependencies, devDependencies);
dependencies.typescript = 'latest';
}

return { dependencies, devDependencies };
}
129 changes: 102 additions & 27 deletions docs/src/modules/sandbox/StackBlitz.test.js
Original file line number Diff line number Diff line change
@@ -52,9 +52,32 @@ describe('StackBlitz', () => {
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.js"></script>
</body>
</html>`,
'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": {
"@vitejs/plugin-react": "latest",
"vite": "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,15 @@ ReactDOM.createRoot(document.querySelector("#root")).render(
</StyledEngineProvider>
</React.StrictMode>
);`,
'vite.config.js': `
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: { 'process.env': {} },
});`,
},
dependencies: {
react: 'latest',
@@ -90,7 +122,8 @@ ReactDOM.createRoot(document.querySelector("#root")).render(
'@emotion/styled': 'latest',
},
devDependencies: {
'react-scripts': 'latest',
'@vitejs/plugin-react': 'latest',
vite: 'latest',
},
});
});
@@ -130,9 +163,35 @@ ReactDOM.createRoot(document.querySelector("#root")).render(
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>`,
'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": {
"@vitejs/plugin-react": "latest",
"vite": "latest",
"@types/react": "latest",
"@types/react-dom": "latest"
}
}`,
'src/Demo.tsx': `import * as React from 'react';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
@@ -146,7 +205,7 @@ export default function BasicButtons() {
);
}
`,
'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';
@@ -158,32 +217,47 @@ ReactDOM.createRoot(document.querySelector("#root")!).render(
</StyledEngineProvider>
</React.StrictMode>
);`,

'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()],
define: { 'process.env': {} },
});`,
},
dependencies: {
react: 'latest',
@@ -192,12 +266,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',
},
});
});
316 changes: 241 additions & 75 deletions docs/src/modules/sandbox/StackBlitz.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
import addHiddenInput from 'docs/src/modules/utils/addHiddenInput';
import { CODE_VARIANTS } from 'docs/src/modules/constants';
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 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,
dependencies,
devDependencies,
files,
codeVariant,
initialFile,
}: {
title: string;
description: string;
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
files: Record<string, string>;
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');
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}`);
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);
@@ -45,55 +42,162 @@ function openStackBlitz({
document.body.removeChild(form);
}

function createReactApp(demoData: DemoData) {
/**
* Create a Vite project config for StackBlitz
*/
function createViteFiles(
demoData: DemoData,
dependencies: Record<string, string> = {},
devDependencies: Record<string, string> = {},
): Record<string, string> {
const ext = getFileExtension(demoData.codeVariant);
const { title, githubLocation: description } = demoData;
return {
[`vite.config.${demoData.codeVariant === 'TS' ? 'ts' : 'js'}`]: `
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
const files: Record<string, string> = {
'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),
}),
{},
)),
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: { 'process.env': {} },
});`,
'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': CRA.getTsconfig(),
'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 createJoyTemplate(templateData: {
title: string;
files: Record<string, string>;
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 } = SandboxDependencies(demoData, {
// Waiting for https://github.com/stackblitz/core/issues/437
// commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined,
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);

// 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(
<React.StrictMode>
<StyledEngineProvider injectFirst>
<App />
</StyledEngineProvider>
</React.StrictMode>
);`;

// Combine all files
const files = {
...viteFiles,
[`src/index.${ext}`]: indexContent,
...templateSourceFiles,
};

return {
title,
description,
files,
dependencies,
devDependencies,
openSandbox: (initialFile = `Demo.${ext}`) => {
replaceContent(updater: (content: string | Record<string, any>, filePath: string) => string) {
Object.keys(files).forEach((filePath) => {
files[filePath] = updater(files[filePath], filePath);
});
return this;
},
openStackBlitz: (initialFile: string = `src/App`) => {
openStackBlitz({
title,
description,
dependencies,
devDependencies,
files,
codeVariant: demoData.codeVariant,
initialFile,
initialFile: ensureExtension(initialFile, ext),
});
},
};
}

/**
* Create a Material Template for StackBlitz using the SDK and Vite
*/
function createMaterialTemplate(templateData: {
title: string;
files: Record<string, string>;
@@ -103,44 +207,50 @@ 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<string, string> = {
'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 } = 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);

// 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(
<React.StrictMode>
<StyledEngineProvider injectFirst>
<App />
</StyledEngineProvider>
</React.StrictMode>
);`,
...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,20 +263,76 @@ ReactDOM.createRoot(document.querySelector("#root")${type}).render(
});
return this;
},
openStackBlitz: (initialFile: string = '/App') =>
openStackBlitz: (initialFile: string = `src/App`) => {
openStackBlitz({
title: templateData.title,
title,
description,
files,
initialFile: ensureExtension(initialFile, ext),
});
},
};
}

/**
* 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, devDependencies } = SandboxDependencies(demoData, {
commitRef: process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined,
devDeps: VITE_DEV_DEPENDENCIES,
});

const viteFiles = createViteFiles(demoData, dependencies, devDependencies);

const demoFiles: Record<string, string> = {
[`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') => {
openStackBlitz({
title,
description,
dependencies,
devDependencies,
codeVariant: templateData.codeVariant,
initialFile,
}),
files,
initialFile: ensureExtension(initialFile, ext),
});
},
};
}

export default {
createJoyTemplate,
createReactApp,
createMaterialTemplate,
};