Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 10 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"globals": "^16.5.0",
"layerchart": "workspace:*",
"mdsx": "^0.0.7",
"paneforge": "^1.0.2",
"playwright": "^1.57.0",
"posthog-js": "^1.300.0",
"prettier": "^3.7.4",
Expand Down Expand Up @@ -110,6 +111,14 @@
"zod": "^4.1.13"
},
"dependencies": {
"@stackblitz/sdk": "^1.11.0"
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/state": "^6.5.2",
"@stackblitz/sdk": "^1.11.0",
"@uiw/codemirror-theme-github": "^4.25.3",
"@webcontainer/api": "^1.6.1",
"ansi_up": "^6.0.6",
"codemirror": "^6.0.2"
}
}
33 changes: 4 additions & 29 deletions docs/scripts/build-stackblitz-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { readAllFilesFromDirectory } from './stackblitz-utils.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -35,35 +36,9 @@ function readSource(sourcePath: string): string {
return fs.readFileSync(filePath, 'utf-8');
}

/**
* Recursively read all files from a directory and return them as a flat object
* with relative paths as keys and file contents as values
*/
function readAllFilesFromDirectory(dir: string, baseDir: string = dir): Record<string, string> {
const files: Record<string, string> = {};

const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
// Recursively read subdirectories
Object.assign(files, readAllFilesFromDirectory(fullPath, baseDir));
} else if (entry.isFile()) {
// Skip README.md as it's documentation for the templates directory
if (entry.name === 'README.md') {
continue;
}

// Get relative path from base directory
const relativePath = path.relative(baseDir, fullPath);
files[relativePath] = fs.readFileSync(fullPath, 'utf-8');
}
}

return files;
}
// Suppress unused variable warnings - fs and path are used via the imported function
void fs;
void path;

/**
* Generate the base files object by reading from template files
Expand Down
3 changes: 3 additions & 0 deletions docs/scripts/stackblitz-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
"@tailwindcss/vite": "^4.1.16",
"@types/d3-geo": "^3.1.0",
"@types/topojson-client": "^3.1.5",
"d3-array": "^3.2.4",
"d3-geo": "^3.1.1",
"d3-random": "^3.0.1",
"d3-time": "^3.1.0",
"topojson-client": "^3.1.0",
"svelte": "^5.39.13",
"svelte-ux": "2.0.0-next.20",
Expand Down
41 changes: 41 additions & 0 deletions docs/scripts/stackblitz-template/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
<script>
// Intercept console methods and send to parent window
(function() {
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info
};

['log', 'warn', 'error', 'info'].forEach(level => {
console[level] = function(...args) {
// Call original console method
originalConsole[level].apply(console, args);

// Send to parent window if in iframe
if (window.parent !== window) {
try {
window.parent.postMessage({
type: 'console',
level: level,
args: args.map(arg => {
// Handle objects/arrays
if (typeof arg === 'object' && arg !== null) {
try {
return JSON.parse(JSON.stringify(arg));
} catch (e) {
return String(arg);
}
}
return arg;
})
}, '*');
} catch (e) {
// Ignore errors posting message
}
}
};
});
})();
</script>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
Expand Down
84 changes: 84 additions & 0 deletions docs/scripts/stackblitz-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Shared utilities for StackBlitz and WebContainer file operations (Node.js/server-side only)
*/

import type { FileSystemTree } from '@webcontainer/api';

// Node.js imports
import fs from 'fs';
import path from 'path';

/**
* Recursively read all files from a directory and return them as a flat object
* with relative paths as keys and file contents as values
*
* Note: This is only available in Node.js context (build scripts)
*/
export function readAllFilesFromDirectory(dir: string, baseDir: string = dir): Record<string, string> {
const files: Record<string, string> = {};

const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
// Recursively read subdirectories
Object.assign(files, readAllFilesFromDirectory(fullPath, baseDir));
} else if (entry.isFile()) {
// Skip README.md and .gitignore as they're not needed in the project
if (entry.name === 'README.md' || entry.name === '.gitignore') {
continue;
}

// Get relative path from base directory
const relativePath = path.relative(baseDir, fullPath);
files[relativePath] = fs.readFileSync(fullPath, 'utf-8');
}
}

return files;
}

/**
* Convert flat file paths to nested WebContainer directory structure
*
* Example:
* Input: { 'src/app.html': '<html>...</html>' }
* Output: { src: { directory: { 'app.html': { file: { contents: '<html>...</html>' } } } } }
*
* @param files - Object with flat paths as keys (e.g., "src/app.html") and contents as values
* @returns WebContainer-compatible nested FileSystemTree structure
*/
export function buildWebContainerFiles(files: Record<string, string>): FileSystemTree {
const result: FileSystemTree = {};

for (const [path, contents] of Object.entries(files)) {
const parts = path.split('/');
let current: any = result;

for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLastPart = i === parts.length - 1;

if (isLastPart) {
// It's a file
current[part] = {
file: {
contents
}
};
} else {
// It's a directory
if (!current[part]) {
current[part] = {
directory: {}
};
}
current = current[part].directory;
}
}
}

return result;
}
14 changes: 14 additions & 0 deletions docs/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);

// Set Cross-Origin headers for WebContainer support (SharedArrayBuffer requirement)
// Only apply to the playground page to minimize impact
if (event.url.pathname.startsWith('/docs/playground')) {
response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
}

return response;
};
6 changes: 6 additions & 0 deletions docs/src/lib/components/DocsMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import LucideFileCode2 from '~icons/lucide/file-code-2';
import LucideCirclePlay from '~icons/lucide/circle-play';
import LucideParentheses from '~icons/lucide/parentheses';
import SimpleIconsStackblitz from '~icons/simple-icons/stackblitz';

let { onItemClick, class: className }: { onItemClick?: () => void; class?: string } = $props();

Expand Down Expand Up @@ -64,6 +65,11 @@
path: '/docs/examples',
icon: LucideFileCode2
})}
{@render navItem({
label: 'Playground',
path: '/docs/playground',
icon: SimpleIconsStackblitz
})}
{@render navItem({ label: 'Showcase', path: '/docs/showcase', icon: LucideGalleryVertical })}
{@render navItem({
label: 'Releases',
Expand Down
51 changes: 51 additions & 0 deletions docs/src/routes/docs/playground/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { PageServerLoad } from './$types';
import { buildWebContainerFiles } from '../../../../scripts/stackblitz-utils.js';

// Import custom page template
import templatePageSvelte from './template/+page.svelte?raw';

// Import source files for the playground examples
import dataTs from '../../../lib/utils/data.ts?raw';

// Dynamically import all template files using Vite's import.meta.glob
const templateFiles = import.meta.glob('../../../../scripts/stackblitz-template/**/*', {
query: '?raw',
import: 'default',
eager: true
});

// Build flat file structure using imported template files
function generatePlaygroundFiles(): Record<string, string> {
const files: Record<string, string> = {};

// Process all template files
for (const [path, content] of Object.entries(templateFiles)) {
// Extract relative path from the full import path
// Example: "../../../../scripts/stackblitz-template/package.json" -> "package.json"
const relativePath = path.replace('../../../../scripts/stackblitz-template/', '');

// Skip README.md and .gitignore as they're not needed in the project
if (relativePath === 'README.md' || relativePath === '.gitignore') {
continue;
}

files[relativePath] = content as string;
}

// Override/add custom files for the playground
files['src/routes/+page.svelte'] = templatePageSvelte;

// Add utility data file
files['src/lib/utils/data.ts'] = dataTs;

return files;
}

// Convert to WebContainer nested structure
const templateProjectFiles = buildWebContainerFiles(generatePlaygroundFiles());

export const load: PageServerLoad = () => {
return {
templateProjectFiles
};
};
Loading
Loading