From a707f6a7a17d6f606ab452d8ea20d7164f6850ac Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Sun, 7 Dec 2025 13:37:21 -0500 Subject: [PATCH 01/15] Playground - NEEDS WORK - Took as far as I could - not sure of needed imports - Still Cross-Origin probs, despite [WebContainers Docs](https://webcontainers.io/guides/configuring-headers#sveltekit) - needs app.css too --- docs/package.json | 3 +- docs/src/lib/components/DocsMenu.svelte | 6 + docs/src/routes/docs/playground/+page.md | 52 ++++ .../routes/docs/playground/+page.server.ts | 7 + .../routes/docs/playground/template-data.ts | 250 ++++++++++++++++++ .../docs/playground/template-page.svelte | 26 ++ .../routes/docs/playground/templateProject.ts | 140 ++++++++++ pnpm-lock.yaml | 26 +- 8 files changed, 500 insertions(+), 10 deletions(-) create mode 100644 docs/src/routes/docs/playground/+page.md create mode 100644 docs/src/routes/docs/playground/+page.server.ts create mode 100644 docs/src/routes/docs/playground/template-data.ts create mode 100644 docs/src/routes/docs/playground/template-page.svelte create mode 100644 docs/src/routes/docs/playground/templateProject.ts diff --git a/docs/package.json b/docs/package.json index cceb1a78e..b1ef0176c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -110,6 +110,7 @@ "zod": "^4.1.13" }, "dependencies": { - "@stackblitz/sdk": "^1.11.0" + "@stackblitz/sdk": "^1.11.0", + "@webcontainer/api": "^1.6.1" } } diff --git a/docs/src/lib/components/DocsMenu.svelte b/docs/src/lib/components/DocsMenu.svelte index 88ea9816f..036e23e73 100644 --- a/docs/src/lib/components/DocsMenu.svelte +++ b/docs/src/lib/components/DocsMenu.svelte @@ -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(); @@ -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', diff --git a/docs/src/routes/docs/playground/+page.md b/docs/src/routes/docs/playground/+page.md new file mode 100644 index 000000000..21ba8ad41 --- /dev/null +++ b/docs/src/routes/docs/playground/+page.md @@ -0,0 +1,52 @@ + + +
+ +
\ No newline at end of file diff --git a/docs/src/routes/docs/playground/+page.server.ts b/docs/src/routes/docs/playground/+page.server.ts new file mode 100644 index 000000000..2865bff8c --- /dev/null +++ b/docs/src/routes/docs/playground/+page.server.ts @@ -0,0 +1,7 @@ +/** @type {import('@sveltejs/kit').Load} */ +export const load = ({ setHeaders }) => { + setHeaders({ + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin' + }); +}; diff --git a/docs/src/routes/docs/playground/template-data.ts b/docs/src/routes/docs/playground/template-data.ts new file mode 100644 index 000000000..1e4f427ab --- /dev/null +++ b/docs/src/routes/docs/playground/template-data.ts @@ -0,0 +1,250 @@ +import { timeMinute, timeDay } from 'd3-time'; +import { cumsum } from 'd3-array'; +import { randomNormal } from 'd3-random'; + +import { degreesToRadians, radiansToDegrees } from 'layerchart'; + +/** + * Get random number between min (inclusive) and max (exclusive) + * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_number_between_0_inclusive_and_1_exclusive + */ +export function getRandomNumber(min: number, max: number) { + return Math.random() * (max - min) + min; +} + +/** + * Get random integer between min (inclusive) and max (inclusive by default) + * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive + */ +export function getRandomInteger(min: number, max: number, includeMax = true) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + (includeMax ? 1 : 0)) + min); +} + +/** + * @see: https://observablehq.com/@d3/d3-cumsum + */ +export function randomWalk(options?: { count?: number }) { + const random = randomNormal(); + // @ts-expect-error shh + return Array.from(cumsum({ length: options?.count ?? 100 }, random)); +} + +export function createSeries(options: { + count?: number; + min: number; + max: number; + keys?: TKey[]; + value?: 'number' | 'integer'; +}) { + const count = options.count ?? 10; + const min = options.min; + const max = options.max; + const keys = options.keys ?? ['y']; + + return Array.from({ length: count }).map((_, i) => { + return { + x: options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), + ...Object.fromEntries( + keys.map((key) => { + return [ + key, + options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max) + ]; + }) + ) + } as { x: number } & { [K in TKey]: number }; + }); +} + +export function createDateSeries( + options: { + count?: number; + min?: number; + max?: number; + keys?: TKey[]; + value?: 'number' | 'integer'; + } = {} +) { + const now = timeDay.floor(new Date()); + + const count = options.count ?? 10; + const min = options.min ?? 0; + const max = options.max ?? 100; + const keys = options.keys ?? ['value']; + const valueType = options.value ?? 'number'; + + return Array.from({ length: count }).map((_, i) => { + return { + date: timeDay.offset(now, -count + i), + ...Object.fromEntries( + keys.map((key) => { + return [ + key, + valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max) + ]; + }) + ) + } as { date: Date } & { [K in TKey]: number }; + }); +} + +export function createTimeSeries( + options: { + count?: number; + min?: number; + max?: number; + keys?: TKey[]; + value?: 'number' | 'integer'; + } = {} +) { + const count = options.count ?? 10; + const min = options.min ?? 0; + const max = options.max ?? 100; + const keys = options.keys ?? ['value']; + const valueType = options.value ?? 'number'; + + let lastStartDate = timeDay.floor(new Date()); + + const timeSeries = Array.from({ length: count }).map((_, i) => { + const startDate = timeMinute.offset(lastStartDate, getRandomInteger(0, 60)); + const endDate = timeMinute.offset(startDate, getRandomInteger(5, 60)); + lastStartDate = startDate; + return { + name: `item ${i + 1}`, + startDate, + endDate, + ...Object.fromEntries( + keys.map((key) => { + return [ + key, + valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max) + ]; + }) + ) + } as { name: string; startDate: Date; endDate: Date } & { + [K in TKey]: number; + }; + }); + + return timeSeries; +} + +export const wideData = [ + { year: 2019, apples: 3840, bananas: 1920, cherries: 960, grapes: 400 }, + { year: 2018, apples: 1600, bananas: 1440, cherries: 960, grapes: 400 }, + { year: 2017, apples: 820, bananas: 1000, cherries: 640, grapes: 400 }, + { year: 2016, apples: 820, bananas: 560, cherries: 720, grapes: 400 } +]; + +export const longData = [ + { year: 2019, basket: 1, fruit: 'apples', value: 3840 }, + { year: 2019, basket: 1, fruit: 'bananas', value: 1920 }, + { year: 2019, basket: 2, fruit: 'cherries', value: 960 }, + { year: 2019, basket: 2, fruit: 'grapes', value: 400 }, + + { year: 2018, basket: 1, fruit: 'apples', value: 1600 }, + { year: 2018, basket: 1, fruit: 'bananas', value: 1440 }, + { year: 2018, basket: 2, fruit: 'cherries', value: 960 }, + { year: 2018, basket: 2, fruit: 'grapes', value: 400 }, + + { year: 2017, basket: 1, fruit: 'apples', value: 820 }, + { year: 2017, basket: 1, fruit: 'bananas', value: 1000 }, + { year: 2017, basket: 2, fruit: 'cherries', value: 640 }, + { year: 2017, basket: 2, fruit: 'grapes', value: 400 }, + + { year: 2016, basket: 1, fruit: 'apples', value: 820 }, + { year: 2016, basket: 1, fruit: 'bananas', value: 560 }, + { year: 2016, basket: 2, fruit: 'cherries', value: 720 }, + { year: 2016, basket: 2, fruit: 'grapes', value: 400 } +]; + +export function getPhyllotaxis({ + radius, + count, + width, + height +}: { + radius: number; + count: number; + width: number; + height: number; +}) { + // Phyllotaxis: https://www.youtube.com/watch?v=KWoJgHFYWxY + const rads = Math.PI * (3 - Math.sqrt(5)); // ~2.4 rads or ~137.5 degrees + return getSpiral({ + angle: radiansToDegrees(rads), + radius, + count, + width, + height + }); +} + +export function getSpiral({ + angle, + radius, + count, + width, + height +}: { + angle: number; + radius: number; + count: number; + width: number; + height: number; +}) { + return Array.from({ length: count }, (_, i) => { + const r = radius * Math.sqrt(i); + const a = degreesToRadians(angle * i); + return { + x: width / 2 + r * Math.cos(a), + y: height / 2 + r * Math.sin(a) + }; + }); +} + +interface SineWaveOptions { + numPoints: number; + frequency?: number; + amplitude?: number; + noiseLevel?: number; + phase?: number; + xMin?: number; + xMax?: number; +} + +export function generateSineWave(options: SineWaveOptions) { + const { + numPoints, + frequency = 1, + amplitude = 1, + noiseLevel = 0, + phase = 0, + xMin = 0, + xMax = 2 * Math.PI + } = options; + + if (numPoints <= 0) { + throw new Error('Number of points must be greater than 0'); + } + + const points: { x: number; y: number }[] = []; + const xStep = (xMax - xMin) / (numPoints - 1); + + for (let i = 0; i < numPoints; i++) { + const x = xMin + i * xStep; + + // Generate base sine wave + const sineValue = amplitude * Math.sin(frequency * x + phase); + + // Add random noise if specified + const noise = noiseLevel > 0 ? (Math.random() - 0.5) * 2 * noiseLevel : 0; + const y = sineValue + noise; + + points.push({ x, y }); + } + + return points; +} diff --git a/docs/src/routes/docs/playground/template-page.svelte b/docs/src/routes/docs/playground/template-page.svelte new file mode 100644 index 000000000..c8b3524bb --- /dev/null +++ b/docs/src/routes/docs/playground/template-page.svelte @@ -0,0 +1,26 @@ + + +
+ +
+ + diff --git a/docs/src/routes/docs/playground/templateProject.ts b/docs/src/routes/docs/playground/templateProject.ts new file mode 100644 index 000000000..bd0d86c42 --- /dev/null +++ b/docs/src/routes/docs/playground/templateProject.ts @@ -0,0 +1,140 @@ +import templatePageSvelte from './template-page.svelte?raw'; +import templateDataTs from './template-data.ts?raw'; + +export const templateProjectFiles = { + 'package.json': { + file: { + contents: JSON.stringify( + { + name: 'webcontainer-app', + type: 'module', + scripts: { + dev: 'vite dev', + build: 'vite build', + preview: 'vite preview' + }, + devDependencies: { + tailwindcss: '^4.1.17', + '@layerstack/tailwind': '2.0.0-next.19', + '@layerstack/utils': '2.0.0-next.16', + '@sveltejs/kit': '^2.49.1', + '@sveltejs/vite-plugin-svelte': '^6.2.1', + '@tailwindcss/vite': '^4.1.17', + '@types/d3-array': '^3.2.2', + '@types/d3-color': '^3.1.3', + '@types/d3-dsv': '^3.0.7', + '@types/d3-force': '^3.0.10', + '@types/d3-geo': '^3.1.0', + '@types/d3-hierarchy': '^3.1.7', + '@types/d3-interpolate': '^3.0.4', + '@types/d3-random': '^3.0.3', + '@types/d3-sankey': '^0.12.5', + '@types/d3-scale': '^4.0.9', + '@types/d3-scale-chromatic': '^3.1.0', + '@types/d3-shape': '^3.1.7', + '@types/d3-time': '^3.0.4', + 'd3-array': '^3.2.4', + 'd3-color': '^3.1.0', + 'd3-dsv': '^3.0.1', + 'd3-force': '^3.0.0', + 'd3-geo': '^3.1.1', + 'd3-hierarchy': '^3.1.2', + 'd3-interpolate': '^3.0.1', + 'd3-random': '^3.0.1', + 'd3-sankey': '^0.12.3', + 'd3-scale': '^4.0.2', + 'd3-scale-chromatic': '^3.1.0', + 'd3-shape': '^3.2.0', + 'd3-time': '^3.1.0', + runed: '^0.37.0', + svelte: '5.45.5', + 'svelte-check': '^4.3.4', + 'svelte-ux': '2.0.0-next.20', + tsx: '^4.21.0', + typescript: '^5.9.3' + }, + dependencies: { + vite: '^7.2.6', + layerchart: 'latest', + '@sveltejs/adapter-auto': '^3.0.0' + } + }, + null, + 2 + ) + } + }, + 'vite.config.js': { + file: { + contents: `import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});` + } + }, + 'svelte.config.js': { + file: { + contents: `import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter() + } +}; + +export default config;` + } + }, + 'src/app.d.ts': { + file: { + contents: `// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {};` + } + }, + 'src/app.html': { + file: { + contents: ` + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ +` + } + }, + 'src/routes/+page.svelte': { + file: { + contents: templatePageSvelte + } + }, + 'src/lib/data.ts': { + file: { + contents: templateDataTs + } + }, + 'app.css': { + file: { + contents: `/* Basic app styles */` + } + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69d5186af..b29bb2ebb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@stackblitz/sdk': specifier: ^1.11.0 version: 1.11.0 + '@webcontainer/api': + specifier: ^1.6.1 + version: 1.6.1 devDependencies: '@content-collections/core': specifier: ^0.11.1 @@ -690,7 +693,7 @@ importers: version: 1.2.77 '@rollup/plugin-dsv': specifier: ^3.0.5 - version: 3.0.5(rollup@2.79.2) + version: 3.0.5(rollup@4.53.3) '@sveltejs/adapter-auto': specifier: ^7.0.0 version: 7.0.0(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.5)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.45.5)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1))) @@ -798,7 +801,7 @@ importers: version: 6.0.0 rollup-plugin-visualizer: specifier: ^6.0.5 - version: 6.0.5(rollup@2.79.2) + version: 6.0.5(rollup@4.53.3) shapefile: specifier: ^0.6.6 version: 0.6.6 @@ -2440,6 +2443,9 @@ packages: '@vitest/utils@4.0.15': resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==} + '@webcontainer/api@1.6.1': + resolution: {integrity: sha512-2RS2KiIw32BY1Icf6M1DvqSmcon9XICZCDgS29QJb2NmF12ZY2V5Ia+949hMKB3Wno+P/Y8W+sPP59PZeXSELg==} + '@zag-js/accordion@1.24.2': resolution: {integrity: sha512-sGNhbWR85oAiMyQLk+dliRhNQGP59T56M1gAkQ7bwJJZ7l++hFEQpYcr/FbAHJshXWpvUKm0wV18wHR/56Y30w==} @@ -5875,15 +5881,15 @@ snapshots: dependencies: cross-spawn: 7.0.6 - '@rollup/plugin-dsv@3.0.5(rollup@2.79.2)': + '@rollup/plugin-dsv@3.0.5(rollup@4.53.3)': dependencies: - '@rollup/pluginutils': 5.1.2(rollup@2.79.2) + '@rollup/pluginutils': 5.1.2(rollup@4.53.3) '@types/d3-dsv': 3.0.7 d3-dsv: 2.0.0 strip-bom: 4.0.0 tosource: 2.0.0-alpha.3 optionalDependencies: - rollup: 2.79.2 + rollup: 4.53.3 '@rollup/plugin-node-resolve@13.3.0(rollup@2.79.2)': dependencies: @@ -5907,13 +5913,13 @@ snapshots: estree-walker: 2.0.2 picomatch: 2.3.1 - '@rollup/pluginutils@5.1.2(rollup@2.79.2)': + '@rollup/pluginutils@5.1.2(rollup@4.53.3)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 2.79.2 + rollup: 4.53.3 '@rollup/rollup-android-arm-eabi@4.53.3': optional: true @@ -6602,6 +6608,8 @@ snapshots: '@vitest/pretty-format': 4.0.15 tinyrainbow: 3.0.3 + '@webcontainer/api@1.6.1': {} + '@zag-js/accordion@1.24.2': dependencies: '@zag-js/anatomy': 1.24.2 @@ -8879,14 +8887,14 @@ snapshots: rollup: 2.79.2 svelte: 4.2.20 - rollup-plugin-visualizer@6.0.5(rollup@2.79.2): + rollup-plugin-visualizer@6.0.5(rollup@4.53.3): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.6 yargs: 17.7.2 optionalDependencies: - rollup: 2.79.2 + rollup: 4.53.3 rollup@2.79.2: optionalDependencies: From 7fb695c338656e9c86389d106b9bff612d508684 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 8 Dec 2025 08:17:23 -0500 Subject: [PATCH 02/15] Rename +page.md to +page.svelte --- docs/src/routes/docs/playground/{+page.md => +page.svelte} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/src/routes/docs/playground/{+page.md => +page.svelte} (100%) diff --git a/docs/src/routes/docs/playground/+page.md b/docs/src/routes/docs/playground/+page.svelte similarity index 100% rename from docs/src/routes/docs/playground/+page.md rename to docs/src/routes/docs/playground/+page.svelte From 9dda87b1478adad7c383fc7970055ed7aba54a4c Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 8 Dec 2025 09:10:46 -0500 Subject: [PATCH 03/15] Fix setting up WebContainer --- docs/scripts/build-stackblitz-files.ts | 33 +-- docs/scripts/stackblitz-utils.ts | 84 ++++++++ docs/src/hooks.server.ts | 14 ++ .../routes/docs/playground/+page.server.ts | 47 ++++- docs/src/routes/docs/playground/+page.svelte | 195 ++++++++++++++---- .../routes/docs/playground/templateProject.ts | 140 ------------- 6 files changed, 299 insertions(+), 214 deletions(-) create mode 100644 docs/scripts/stackblitz-utils.ts create mode 100644 docs/src/hooks.server.ts delete mode 100644 docs/src/routes/docs/playground/templateProject.ts diff --git a/docs/scripts/build-stackblitz-files.ts b/docs/scripts/build-stackblitz-files.ts index 38434a1e5..b7b6e4428 100644 --- a/docs/scripts/build-stackblitz-files.ts +++ b/docs/scripts/build-stackblitz-files.ts @@ -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); @@ -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 { - const files: Record = {}; - - 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 diff --git a/docs/scripts/stackblitz-utils.ts b/docs/scripts/stackblitz-utils.ts new file mode 100644 index 000000000..428611bb2 --- /dev/null +++ b/docs/scripts/stackblitz-utils.ts @@ -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 { + const files: Record = {}; + + 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': '...' } + * Output: { src: { directory: { 'app.html': { file: { contents: '...' } } } } } + * + * @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): 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; +} diff --git a/docs/src/hooks.server.ts b/docs/src/hooks.server.ts new file mode 100644 index 000000000..630ccfa01 --- /dev/null +++ b/docs/src/hooks.server.ts @@ -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; +}; diff --git a/docs/src/routes/docs/playground/+page.server.ts b/docs/src/routes/docs/playground/+page.server.ts index 2865bff8c..893d3a7a9 100644 --- a/docs/src/routes/docs/playground/+page.server.ts +++ b/docs/src/routes/docs/playground/+page.server.ts @@ -1,7 +1,42 @@ -/** @type {import('@sveltejs/kit').Load} */ -export const load = ({ setHeaders }) => { - setHeaders({ - 'Cross-Origin-Embedder-Policy': 'require-corp', - 'Cross-Origin-Opener-Policy': 'same-origin' - }); +import type { PageServerLoad } from './$types'; +import { readAllFilesFromDirectory, buildWebContainerFiles } from '../../../../scripts/stackblitz-utils.js'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import fs from 'fs'; + +// Import custom page template +import templatePageSvelte from './template-page.svelte?raw'; +import templateDataTs from './template-data.ts?raw'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const TEMPLATES_DIR = path.resolve(__dirname, '../../../../scripts/stackblitz-template'); + +// Read source files for the playground examples +function readSource(sourcePath: string): string { + const SOURCE_DIR = path.resolve(__dirname, '../../../'); + const filePath = path.join(SOURCE_DIR, sourcePath); + return fs.readFileSync(filePath, 'utf-8'); +} + +// Build flat file structure by reading from stackblitz-template directory +function generatePlaygroundFiles(): Record { + return { + // Read all template files from stackblitz-template directory + ...readAllFilesFromDirectory(TEMPLATES_DIR), + // Override/add custom files for the playground + 'src/routes/+page.svelte': templatePageSvelte, + 'src/lib/data.ts': templateDataTs, + // Add utility data file + 'src/lib/utils/data.ts': readSource('lib/utils/data.ts') + }; +} + +// Convert to WebContainer nested structure +const templateProjectFiles = buildWebContainerFiles(generatePlaygroundFiles()); + +export const load: PageServerLoad = () => { + return { + templateProjectFiles + }; }; diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index 21ba8ad41..dbbb8dc45 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -1,52 +1,169 @@ + + -
- -
\ No newline at end of file +
+ +
+ +
+ +
+ + +
+ {#if isLoadingFile} +
+
Loading...
+
+ {:else} + + {/if} +
+
+ + +
+ +
+
diff --git a/docs/src/routes/docs/playground/templateProject.ts b/docs/src/routes/docs/playground/templateProject.ts deleted file mode 100644 index bd0d86c42..000000000 --- a/docs/src/routes/docs/playground/templateProject.ts +++ /dev/null @@ -1,140 +0,0 @@ -import templatePageSvelte from './template-page.svelte?raw'; -import templateDataTs from './template-data.ts?raw'; - -export const templateProjectFiles = { - 'package.json': { - file: { - contents: JSON.stringify( - { - name: 'webcontainer-app', - type: 'module', - scripts: { - dev: 'vite dev', - build: 'vite build', - preview: 'vite preview' - }, - devDependencies: { - tailwindcss: '^4.1.17', - '@layerstack/tailwind': '2.0.0-next.19', - '@layerstack/utils': '2.0.0-next.16', - '@sveltejs/kit': '^2.49.1', - '@sveltejs/vite-plugin-svelte': '^6.2.1', - '@tailwindcss/vite': '^4.1.17', - '@types/d3-array': '^3.2.2', - '@types/d3-color': '^3.1.3', - '@types/d3-dsv': '^3.0.7', - '@types/d3-force': '^3.0.10', - '@types/d3-geo': '^3.1.0', - '@types/d3-hierarchy': '^3.1.7', - '@types/d3-interpolate': '^3.0.4', - '@types/d3-random': '^3.0.3', - '@types/d3-sankey': '^0.12.5', - '@types/d3-scale': '^4.0.9', - '@types/d3-scale-chromatic': '^3.1.0', - '@types/d3-shape': '^3.1.7', - '@types/d3-time': '^3.0.4', - 'd3-array': '^3.2.4', - 'd3-color': '^3.1.0', - 'd3-dsv': '^3.0.1', - 'd3-force': '^3.0.0', - 'd3-geo': '^3.1.1', - 'd3-hierarchy': '^3.1.2', - 'd3-interpolate': '^3.0.1', - 'd3-random': '^3.0.1', - 'd3-sankey': '^0.12.3', - 'd3-scale': '^4.0.2', - 'd3-scale-chromatic': '^3.1.0', - 'd3-shape': '^3.2.0', - 'd3-time': '^3.1.0', - runed: '^0.37.0', - svelte: '5.45.5', - 'svelte-check': '^4.3.4', - 'svelte-ux': '2.0.0-next.20', - tsx: '^4.21.0', - typescript: '^5.9.3' - }, - dependencies: { - vite: '^7.2.6', - layerchart: 'latest', - '@sveltejs/adapter-auto': '^3.0.0' - } - }, - null, - 2 - ) - } - }, - 'vite.config.js': { - file: { - contents: `import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [sveltekit()] -});` - } - }, - 'svelte.config.js': { - file: { - contents: `import adapter from '@sveltejs/adapter-auto'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter() - } -}; - -export default config;` - } - }, - 'src/app.d.ts': { - file: { - contents: `// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface Platform {} - } -} - -export {};` - } - }, - 'src/app.html': { - file: { - contents: ` - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- -` - } - }, - 'src/routes/+page.svelte': { - file: { - contents: templatePageSvelte - } - }, - 'src/lib/data.ts': { - file: { - contents: templateDataTs - } - }, - 'app.css': { - file: { - contents: `/* Basic app styles */` - } - } -}; From 75e649c93f40c3df767c733331e71ad3e61487b8 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 8 Dec 2025 09:14:27 -0500 Subject: [PATCH 04/15] Setup CodeEditor using codemirror --- docs/package.json | 8 +- docs/src/routes/docs/playground/+page.svelte | 8 +- .../routes/docs/playground/CodeEditor.svelte | 101 +++++++++ pnpm-lock.yaml | 213 ++++++++++++++++++ 4 files changed, 323 insertions(+), 7 deletions(-) create mode 100644 docs/src/routes/docs/playground/CodeEditor.svelte diff --git a/docs/package.json b/docs/package.json index b1ef0176c..1c70704fe 100644 --- a/docs/package.json +++ b/docs/package.json @@ -110,7 +110,13 @@ "zod": "^4.1.13" }, "dependencies": { + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.3", "@stackblitz/sdk": "^1.11.0", - "@webcontainer/api": "^1.6.1" + "@webcontainer/api": "^1.6.1", + "codemirror": "^6.0.2" } } diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index dbbb8dc45..a66485346 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -8,6 +8,7 @@ + +
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b29bb2ebb..e517dbc39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,12 +26,30 @@ importers: docs: dependencies: + '@codemirror/lang-css': + specifier: ^6.3.1 + version: 6.3.1 + '@codemirror/lang-html': + specifier: ^6.4.11 + version: 6.4.11 + '@codemirror/lang-javascript': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/state': + specifier: ^6.5.2 + version: 6.5.2 + '@codemirror/theme-one-dark': + specifier: ^6.1.3 + version: 6.1.3 '@stackblitz/sdk': specifier: ^1.11.0 version: 1.11.0 '@webcontainer/api': specifier: ^1.6.1 version: 1.6.1 + codemirror: + specifier: ^6.0.2 + version: 6.0.2 devDependencies: '@content-collections/core': specifier: ^0.11.1 @@ -980,6 +998,39 @@ packages: '@cloudflare/workers-types@4.20251011.0': resolution: {integrity: sha512-gQpih+pbq3sP4uXltUeCSbPgZxTNp2gQd8639SaIbQMwgA6oJNHLhIART1fWy6DQACngiRzDVULA2x0ohmkGTQ==} + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.0': + resolution: {integrity: sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/language@6.11.3': + resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} + + '@codemirror/lint@6.9.2': + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} + + '@codemirror/search@6.5.11': + resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} + + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.38.8': + resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==} + '@content-collections/core@0.11.1': resolution: {integrity: sha512-4ZbjxOjaDAxnj7mIij1q1vxZgOJQkA20ThoNZ0CsnmhJmUp+IDqIRLUyDyZ+4ZAk+zQy6bKeOzWzyLg32vMgRQ==} peerDependencies: @@ -1750,6 +1801,24 @@ packages: '@layerstack/utils@2.0.0-next.16': resolution: {integrity: sha512-V0aTlVpEylaVgwUOrVhMcEVAN8ysChP1Y4uPCcohl048Ocv+h37eW67pVxj81c+aLAavCd3BXt9IhJfELqTVmw==} + '@lezer/common@1.4.0': + resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} + + '@lezer/css@1.3.0': + resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.12': + resolution: {integrity: sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.4': + resolution: {integrity: sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==} + '@lucide/svelte@0.534.0': resolution: {integrity: sha512-XqlT0ibiEEoEGcmZLdk0RNtq1OW77OjqxXZjJE/23Wabl+2d9Pxg9AAx7+6WFv8BSf8K6im73vz3cZKYEgY9Mw==} peerDependencies: @@ -1776,6 +1845,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2823,6 +2895,9 @@ packages: code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2871,6 +2946,9 @@ packages: core-js@3.47.0: resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4469,6 +4547,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} @@ -4897,6 +4978,9 @@ packages: jsdom: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -5223,6 +5307,89 @@ snapshots: '@cloudflare/workers-types@4.20251011.0': {} + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.4.0 + + '@codemirror/commands@6.10.0': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.4.0 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@lezer/common': 1.4.0 + '@lezer/css': 1.3.0 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.4 + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.4.0 + '@lezer/css': 1.3.0 + '@lezer/html': 1.3.12 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.2 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.4.0 + '@lezer/javascript': 1.5.4 + + '@codemirror/language@6.11.3': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.4 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.2': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + crelt: 1.0.6 + + '@codemirror/search@6.5.11': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + crelt: 1.0.6 + + '@codemirror/state@6.5.2': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.38.8': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@content-collections/core@0.11.1(typescript@5.9.3)': dependencies: '@standard-schema/spec': 1.0.0 @@ -5815,6 +5982,34 @@ snapshots: d3-time-format: 4.1.0 lodash-es: 4.17.21 + '@lezer/common@1.4.0': {} + + '@lezer/css@1.3.0': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.4 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.4.0 + + '@lezer/html@1.3.12': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.4 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.4 + + '@lezer/lr@1.4.4': + dependencies: + '@lezer/common': 1.4.0 + '@lucide/svelte@0.534.0(svelte@5.45.5)': dependencies: svelte: 5.45.5 @@ -5847,6 +6042,8 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@marijn/find-cluster-break@1.0.2': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7238,6 +7435,16 @@ snapshots: estree-walker: 3.0.3 periscopic: 3.1.0 + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.0 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -7274,6 +7481,8 @@ snapshots: core-js@3.47.0: {} + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -9107,6 +9316,8 @@ snapshots: strip-json-comments@3.1.1: {} + style-mod@4.1.3: {} + style-to-object@1.0.14: dependencies: inline-style-parser: 0.2.7 @@ -9594,6 +9805,8 @@ snapshots: - tsx - yaml + w3c-keyname@2.2.8: {} + web-namespaces@2.0.1: {} web-vitals@4.2.4: {} From bfd692a857d8e7c798a33e7bc623f37fcf311b3c Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 8 Dec 2025 09:37:05 -0500 Subject: [PATCH 05/15] Add loading status. Refine template --- .../routes/docs/playground/+page.server.ts | 9 ++- docs/src/routes/docs/playground/+page.svelte | 67 +++++++++++++------ .../docs/playground/template-page.svelte | 26 ------- .../docs/playground/template/+page.svelte | 8 +++ .../{template-data.ts => template/data.ts} | 0 5 files changed, 60 insertions(+), 50 deletions(-) delete mode 100644 docs/src/routes/docs/playground/template-page.svelte create mode 100644 docs/src/routes/docs/playground/template/+page.svelte rename docs/src/routes/docs/playground/{template-data.ts => template/data.ts} (100%) diff --git a/docs/src/routes/docs/playground/+page.server.ts b/docs/src/routes/docs/playground/+page.server.ts index 893d3a7a9..455e8dfda 100644 --- a/docs/src/routes/docs/playground/+page.server.ts +++ b/docs/src/routes/docs/playground/+page.server.ts @@ -1,12 +1,15 @@ import type { PageServerLoad } from './$types'; -import { readAllFilesFromDirectory, buildWebContainerFiles } from '../../../../scripts/stackblitz-utils.js'; +import { + readAllFilesFromDirectory, + buildWebContainerFiles +} from '../../../../scripts/stackblitz-utils.js'; import { fileURLToPath } from 'url'; import path from 'path'; import fs from 'fs'; // Import custom page template -import templatePageSvelte from './template-page.svelte?raw'; -import templateDataTs from './template-data.ts?raw'; +import templatePageSvelte from './template/+page.svelte?raw'; +import templateDataTs from './template/data.ts?raw'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index a66485346..86f968f5b 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -9,6 +9,7 @@ import { onMount } from 'svelte'; import type { PageData } from './$types'; import CodeEditor from './CodeEditor.svelte'; + import { Overlay, ProgressCircle } from 'svelte-ux'; let { data }: { data: PageData } = $props(); @@ -21,6 +22,10 @@ let editableFiles = $state([]); let isLoadingFile = $state(false); + // Loading state + let loadingStatus = $state('Initializing WebContainer...'); + let isReady = $state(false); + async function getWebContainerInstance() { if (!webcontainerPromise) { webcontainerPromise = WebContainer.boot(); @@ -43,11 +48,8 @@ } } traverse(data.templateProjectFiles); - return files.filter(f => - f.endsWith('.svelte') || - f.endsWith('.ts') || - f.endsWith('.js') || - f.endsWith('.css') + return files.filter( + (f) => f.endsWith('.svelte') || f.endsWith('.ts') || f.endsWith('.js') || f.endsWith('.css') ); } @@ -85,26 +87,35 @@ } onMount(async () => { - webcontainerInstance = await getWebContainerInstance(); + try { + loadingStatus = 'Booting WebContainer...'; + webcontainerInstance = await getWebContainerInstance(); - if (!webcontainerInstance) { - throw new Error('Failed to boot WebContainer'); - } - await webcontainerInstance.mount(data.templateProjectFiles); + if (!webcontainerInstance) { + throw new Error('Failed to boot WebContainer'); + } - // Get editable files list - editableFiles = getEditableFiles(); + loadingStatus = 'Mounting project files...'; + await webcontainerInstance.mount(data.templateProjectFiles); - // Load initial file - await loadFileContent(selectedFile); + // Get editable files list + editableFiles = getEditableFiles(); - webcontainerInstance.on('server-ready', (port, url) => { - if (iframeEl) { - iframeEl.src = url; - } - }); + loadingStatus = 'Loading editor...'; + await loadFileContent(selectedFile); + + webcontainerInstance.on('server-ready', (port, url) => { + if (iframeEl) { + iframeEl.src = url; + } + loadingStatus = null; + isReady = true; + }); - await startDevServer(); + await startDevServer(); + } catch (error) { + loadingStatus = 'Error: ' + (error instanceof Error ? error.message : 'Unknown error'); + } }); async function startDevServer() { @@ -113,6 +124,7 @@ } // Install dependencies + loadingStatus = 'Installing dependencies...'; const installProcess = await webcontainerInstance.spawn('npm', ['install']); const installExitCode = await installProcess.exit; @@ -121,6 +133,7 @@ } // Start dev server + loadingStatus = 'Starting dev server...'; await webcontainerInstance.spawn('npm', ['run', 'dev']); } @@ -154,7 +167,19 @@ -
+
+ {#if loadingStatus} + + +
{loadingStatus}
+
+ + {/if}
From ca91fba7b2f418eafa50c98d759a9c17607b0131 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 8 Dec 2025 10:20:15 -0500 Subject: [PATCH 07/15] Remove extra data file --- .../routes/docs/playground/+page.server.ts | 2 - .../docs/playground/template/+page.svelte | 2 +- .../routes/docs/playground/template/data.ts | 250 ------------------ 3 files changed, 1 insertion(+), 253 deletions(-) delete mode 100644 docs/src/routes/docs/playground/template/data.ts diff --git a/docs/src/routes/docs/playground/+page.server.ts b/docs/src/routes/docs/playground/+page.server.ts index 455e8dfda..99381f4b3 100644 --- a/docs/src/routes/docs/playground/+page.server.ts +++ b/docs/src/routes/docs/playground/+page.server.ts @@ -9,7 +9,6 @@ import fs from 'fs'; // Import custom page template import templatePageSvelte from './template/+page.svelte?raw'; -import templateDataTs from './template/data.ts?raw'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -29,7 +28,6 @@ function generatePlaygroundFiles(): Record { ...readAllFilesFromDirectory(TEMPLATES_DIR), // Override/add custom files for the playground 'src/routes/+page.svelte': templatePageSvelte, - 'src/lib/data.ts': templateDataTs, // Add utility data file 'src/lib/utils/data.ts': readSource('lib/utils/data.ts') }; diff --git a/docs/src/routes/docs/playground/template/+page.svelte b/docs/src/routes/docs/playground/template/+page.svelte index 219ff30e4..c2670ec79 100644 --- a/docs/src/routes/docs/playground/template/+page.svelte +++ b/docs/src/routes/docs/playground/template/+page.svelte @@ -1,6 +1,6 @@ diff --git a/docs/src/routes/docs/playground/template/data.ts b/docs/src/routes/docs/playground/template/data.ts deleted file mode 100644 index 1e4f427ab..000000000 --- a/docs/src/routes/docs/playground/template/data.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { timeMinute, timeDay } from 'd3-time'; -import { cumsum } from 'd3-array'; -import { randomNormal } from 'd3-random'; - -import { degreesToRadians, radiansToDegrees } from 'layerchart'; - -/** - * Get random number between min (inclusive) and max (exclusive) - * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_number_between_0_inclusive_and_1_exclusive - */ -export function getRandomNumber(min: number, max: number) { - return Math.random() * (max - min) + min; -} - -/** - * Get random integer between min (inclusive) and max (inclusive by default) - * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive - */ -export function getRandomInteger(min: number, max: number, includeMax = true) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + (includeMax ? 1 : 0)) + min); -} - -/** - * @see: https://observablehq.com/@d3/d3-cumsum - */ -export function randomWalk(options?: { count?: number }) { - const random = randomNormal(); - // @ts-expect-error shh - return Array.from(cumsum({ length: options?.count ?? 100 }, random)); -} - -export function createSeries(options: { - count?: number; - min: number; - max: number; - keys?: TKey[]; - value?: 'number' | 'integer'; -}) { - const count = options.count ?? 10; - const min = options.min; - const max = options.max; - const keys = options.keys ?? ['y']; - - return Array.from({ length: count }).map((_, i) => { - return { - x: options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), - ...Object.fromEntries( - keys.map((key) => { - return [ - key, - options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max) - ]; - }) - ) - } as { x: number } & { [K in TKey]: number }; - }); -} - -export function createDateSeries( - options: { - count?: number; - min?: number; - max?: number; - keys?: TKey[]; - value?: 'number' | 'integer'; - } = {} -) { - const now = timeDay.floor(new Date()); - - const count = options.count ?? 10; - const min = options.min ?? 0; - const max = options.max ?? 100; - const keys = options.keys ?? ['value']; - const valueType = options.value ?? 'number'; - - return Array.from({ length: count }).map((_, i) => { - return { - date: timeDay.offset(now, -count + i), - ...Object.fromEntries( - keys.map((key) => { - return [ - key, - valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max) - ]; - }) - ) - } as { date: Date } & { [K in TKey]: number }; - }); -} - -export function createTimeSeries( - options: { - count?: number; - min?: number; - max?: number; - keys?: TKey[]; - value?: 'number' | 'integer'; - } = {} -) { - const count = options.count ?? 10; - const min = options.min ?? 0; - const max = options.max ?? 100; - const keys = options.keys ?? ['value']; - const valueType = options.value ?? 'number'; - - let lastStartDate = timeDay.floor(new Date()); - - const timeSeries = Array.from({ length: count }).map((_, i) => { - const startDate = timeMinute.offset(lastStartDate, getRandomInteger(0, 60)); - const endDate = timeMinute.offset(startDate, getRandomInteger(5, 60)); - lastStartDate = startDate; - return { - name: `item ${i + 1}`, - startDate, - endDate, - ...Object.fromEntries( - keys.map((key) => { - return [ - key, - valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max) - ]; - }) - ) - } as { name: string; startDate: Date; endDate: Date } & { - [K in TKey]: number; - }; - }); - - return timeSeries; -} - -export const wideData = [ - { year: 2019, apples: 3840, bananas: 1920, cherries: 960, grapes: 400 }, - { year: 2018, apples: 1600, bananas: 1440, cherries: 960, grapes: 400 }, - { year: 2017, apples: 820, bananas: 1000, cherries: 640, grapes: 400 }, - { year: 2016, apples: 820, bananas: 560, cherries: 720, grapes: 400 } -]; - -export const longData = [ - { year: 2019, basket: 1, fruit: 'apples', value: 3840 }, - { year: 2019, basket: 1, fruit: 'bananas', value: 1920 }, - { year: 2019, basket: 2, fruit: 'cherries', value: 960 }, - { year: 2019, basket: 2, fruit: 'grapes', value: 400 }, - - { year: 2018, basket: 1, fruit: 'apples', value: 1600 }, - { year: 2018, basket: 1, fruit: 'bananas', value: 1440 }, - { year: 2018, basket: 2, fruit: 'cherries', value: 960 }, - { year: 2018, basket: 2, fruit: 'grapes', value: 400 }, - - { year: 2017, basket: 1, fruit: 'apples', value: 820 }, - { year: 2017, basket: 1, fruit: 'bananas', value: 1000 }, - { year: 2017, basket: 2, fruit: 'cherries', value: 640 }, - { year: 2017, basket: 2, fruit: 'grapes', value: 400 }, - - { year: 2016, basket: 1, fruit: 'apples', value: 820 }, - { year: 2016, basket: 1, fruit: 'bananas', value: 560 }, - { year: 2016, basket: 2, fruit: 'cherries', value: 720 }, - { year: 2016, basket: 2, fruit: 'grapes', value: 400 } -]; - -export function getPhyllotaxis({ - radius, - count, - width, - height -}: { - radius: number; - count: number; - width: number; - height: number; -}) { - // Phyllotaxis: https://www.youtube.com/watch?v=KWoJgHFYWxY - const rads = Math.PI * (3 - Math.sqrt(5)); // ~2.4 rads or ~137.5 degrees - return getSpiral({ - angle: radiansToDegrees(rads), - radius, - count, - width, - height - }); -} - -export function getSpiral({ - angle, - radius, - count, - width, - height -}: { - angle: number; - radius: number; - count: number; - width: number; - height: number; -}) { - return Array.from({ length: count }, (_, i) => { - const r = radius * Math.sqrt(i); - const a = degreesToRadians(angle * i); - return { - x: width / 2 + r * Math.cos(a), - y: height / 2 + r * Math.sin(a) - }; - }); -} - -interface SineWaveOptions { - numPoints: number; - frequency?: number; - amplitude?: number; - noiseLevel?: number; - phase?: number; - xMin?: number; - xMax?: number; -} - -export function generateSineWave(options: SineWaveOptions) { - const { - numPoints, - frequency = 1, - amplitude = 1, - noiseLevel = 0, - phase = 0, - xMin = 0, - xMax = 2 * Math.PI - } = options; - - if (numPoints <= 0) { - throw new Error('Number of points must be greater than 0'); - } - - const points: { x: number; y: number }[] = []; - const xStep = (xMax - xMin) / (numPoints - 1); - - for (let i = 0; i < numPoints; i++) { - const x = xMin + i * xStep; - - // Generate base sine wave - const sineValue = amplitude * Math.sin(frequency * x + phase); - - // Add random noise if specified - const noise = noiseLevel > 0 ? (Math.random() - 0.5) * 2 * noiseLevel : 0; - const y = sineValue + noise; - - points.push({ x, y }); - } - - return points; -} From ccdab7c62fa092e0736c40761e9701db50e62788 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 8 Dec 2025 11:06:16 -0500 Subject: [PATCH 08/15] Add light/dark CodeEditor theme --- docs/package.json | 2 +- docs/src/routes/docs/playground/+page.svelte | 10 +--- .../routes/docs/playground/CodeEditor.svelte | 38 ++++++++++-- pnpm-lock.yaml | 58 ++++++++++++------- 4 files changed, 73 insertions(+), 35 deletions(-) diff --git a/docs/package.json b/docs/package.json index 1c70704fe..b7f83798d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -114,8 +114,8 @@ "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/state": "^6.5.2", - "@codemirror/theme-one-dark": "^6.1.3", "@stackblitz/sdk": "^1.11.0", + "@uiw/codemirror-theme-github": "^4.25.3", "@webcontainer/api": "^1.6.1", "codemirror": "^6.0.2" } diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index 8514ffb80..c9e797d90 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -181,7 +181,7 @@ } -
+
@@ -210,18 +210,12 @@
-
+
{#if loadingStatus}
{loadingStatus}
- {/if} -
+ + +
+ {#if isLoadingFile} +
+
Loading...
+
+ {:else} + + {/if} +
+
+ + + + + + + + +
+ {#if loadingStatus} + + +
{loadingStatus}
+
+ {:else} + + {/if} + +
+
+ + + + (consoleCollapsed = true)} + onExpand={() => (consoleCollapsed = false)} + > +
+
+
+ +
+ +
+
+ {consoleOutput} +
+
+
+
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aef59285..99ecb56c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: mdsx: specifier: ^0.0.7 version: 0.0.7(svelte@5.45.5) + paneforge: + specifier: ^1.0.2 + version: 1.0.2(svelte@5.45.5) playwright: specifier: ^1.57.0 version: 1.57.0 @@ -711,7 +714,7 @@ importers: version: 1.2.77 '@rollup/plugin-dsv': specifier: ^3.0.5 - version: 3.0.5(rollup@2.79.2) + version: 3.0.5(rollup@4.53.3) '@sveltejs/adapter-auto': specifier: ^7.0.0 version: 7.0.0(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.5)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.45.5)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1))) @@ -819,7 +822,7 @@ importers: version: 6.0.0 rollup-plugin-visualizer: specifier: ^6.0.5 - version: 6.0.5(rollup@2.79.2) + version: 6.0.5(rollup@4.53.3) shapefile: specifier: ^0.6.6 version: 0.6.6 @@ -4012,6 +4015,11 @@ packages: package-manager-detector@1.5.0: resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} + paneforge@1.0.2: + resolution: {integrity: sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA==} + peerDependencies: + svelte: ^5.29.0 + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4397,6 +4405,11 @@ packages: peerDependencies: svelte: ^5.7.0 + runed@0.29.2: + resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} + peerDependencies: + svelte: ^5.7.0 + runed@0.31.1: resolution: {integrity: sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ==} peerDependencies: @@ -4650,6 +4663,12 @@ packages: peerDependencies: svelte: ^5.0.0 + svelte-toolbelt@0.9.3: + resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + svelte-ux@2.0.0-next.17: resolution: {integrity: sha512-jKE/4baK3Q0SsdlHIH7o2kZDIPtktIdddlzFiqumME7hSafzGHW61HU/x2FhfaQoMBC5yyvtA6DcEh5UOylwhw==} peerDependencies: @@ -6078,15 +6097,15 @@ snapshots: dependencies: cross-spawn: 7.0.6 - '@rollup/plugin-dsv@3.0.5(rollup@2.79.2)': + '@rollup/plugin-dsv@3.0.5(rollup@4.53.3)': dependencies: - '@rollup/pluginutils': 5.1.2(rollup@2.79.2) + '@rollup/pluginutils': 5.1.2(rollup@4.53.3) '@types/d3-dsv': 3.0.7 d3-dsv: 2.0.0 strip-bom: 4.0.0 tosource: 2.0.0-alpha.3 optionalDependencies: - rollup: 2.79.2 + rollup: 4.53.3 '@rollup/plugin-node-resolve@13.3.0(rollup@2.79.2)': dependencies: @@ -6110,13 +6129,13 @@ snapshots: estree-walker: 2.0.2 picomatch: 2.3.1 - '@rollup/pluginutils@5.1.2(rollup@2.79.2)': + '@rollup/pluginutils@5.1.2(rollup@4.53.3)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 2.79.2 + rollup: 4.53.3 '@rollup/rollup-android-arm-eabi@4.53.3': optional: true @@ -8821,6 +8840,12 @@ snapshots: package-manager-detector@1.5.0: {} + paneforge@1.0.2(svelte@5.45.5): + dependencies: + runed: 0.23.4(svelte@5.45.5) + svelte: 5.45.5 + svelte-toolbelt: 0.9.3(svelte@5.45.5) + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -9110,14 +9135,14 @@ snapshots: rollup: 2.79.2 svelte: 4.2.20 - rollup-plugin-visualizer@6.0.5(rollup@2.79.2): + rollup-plugin-visualizer@6.0.5(rollup@4.53.3): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.6 yargs: 17.7.2 optionalDependencies: - rollup: 2.79.2 + rollup: 4.53.3 rollup@2.79.2: optionalDependencies: @@ -9165,6 +9190,11 @@ snapshots: esm-env: 1.2.2 svelte: 5.45.5 + runed@0.29.2(svelte@5.45.5): + dependencies: + esm-env: 1.2.2 + svelte: 5.45.5 + runed@0.31.1(svelte@5.45.5): dependencies: esm-env: 1.2.2 @@ -9422,6 +9452,13 @@ snapshots: style-to-object: 1.0.9 svelte: 5.45.5 + svelte-toolbelt@0.9.3(svelte@5.45.5): + dependencies: + clsx: 2.1.1 + runed: 0.29.2(svelte@5.45.5) + style-to-object: 1.0.14 + svelte: 5.45.5 + svelte-ux@2.0.0-next.17(postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.1))(postcss@8.5.6)(svelte@5.45.5): dependencies: '@floating-ui/dom': 1.7.3 From 549e891db943df47b08b45d959212445fb0e0621 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 9 Dec 2025 22:32:18 -0500 Subject: [PATCH 10/15] Fix console pane resizing --- docs/src/routes/docs/playground/+page.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index e1f7e01b3..3f8aa8499 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -300,12 +300,16 @@ >
- + + Date: Tue, 9 Dec 2025 22:43:18 -0500 Subject: [PATCH 11/15] Add minSize to code/preview panes --- docs/src/routes/docs/playground/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index 3f8aa8499..fece6c145 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -211,7 +211,7 @@
- +
@@ -253,7 +253,7 @@ /> - +
From 6ddb1704c5fcd824a6696c1c8a83ac2f6bc40471 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 9 Dec 2025 22:49:07 -0500 Subject: [PATCH 12/15] Fix build by using vite `import.meta.glob` with `?raw` instead of trying to read files using `fs` at runtime --- .../routes/docs/playground/+page.server.ts | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/docs/src/routes/docs/playground/+page.server.ts b/docs/src/routes/docs/playground/+page.server.ts index 99381f4b3..faa6ec759 100644 --- a/docs/src/routes/docs/playground/+page.server.ts +++ b/docs/src/routes/docs/playground/+page.server.ts @@ -1,36 +1,44 @@ import type { PageServerLoad } from './$types'; -import { - readAllFilesFromDirectory, - buildWebContainerFiles -} from '../../../../scripts/stackblitz-utils.js'; -import { fileURLToPath } from 'url'; -import path from 'path'; -import fs from 'fs'; +import { buildWebContainerFiles } from '../../../../scripts/stackblitz-utils.js'; // Import custom page template import templatePageSvelte from './template/+page.svelte?raw'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const TEMPLATES_DIR = path.resolve(__dirname, '../../../../scripts/stackblitz-template'); +// Import source files for the playground examples +import dataTs from '../../../lib/utils/data.ts?raw'; -// Read source files for the playground examples -function readSource(sourcePath: string): string { - const SOURCE_DIR = path.resolve(__dirname, '../../../'); - const filePath = path.join(SOURCE_DIR, sourcePath); - return fs.readFileSync(filePath, 'utf-8'); -} +// 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 by reading from stackblitz-template directory +// Build flat file structure using imported template files function generatePlaygroundFiles(): Record { - return { - // Read all template files from stackblitz-template directory - ...readAllFilesFromDirectory(TEMPLATES_DIR), - // Override/add custom files for the playground - 'src/routes/+page.svelte': templatePageSvelte, - // Add utility data file - 'src/lib/utils/data.ts': readSource('lib/utils/data.ts') - }; + const files: Record = {}; + + // 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 From 69c3a994b31397623d48c97928282575a03cdb5d Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 9 Dec 2025 22:55:30 -0500 Subject: [PATCH 13/15] Speed up playground dep installing using pnpm instead of npm --- docs/scripts/stackblitz-template/package.json | 3 +++ docs/src/routes/docs/playground/+page.svelte | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/scripts/stackblitz-template/package.json b/docs/scripts/stackblitz-template/package.json index 53047e081..b9ee8f2f7 100644 --- a/docs/scripts/stackblitz-template/package.json +++ b/docs/scripts/stackblitz-template/package.json @@ -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", diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index fece6c145..684aaed1b 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -178,16 +178,16 @@ // Install dependencies loadingStatus = 'Installing dependencies...'; - const installProcess = await webcontainerInstance.spawn('npm', ['install']); + const installProcess = await webcontainerInstance.spawn('pnpm', ['install']); const installExitCode = await installProcess.exit; if (installExitCode !== 0) { - throw new Error('Unable to run npm install'); + throw new Error('Unable to run pnpm install'); } // Start dev server loadingStatus = 'Starting dev server...'; - const devProcess = await webcontainerInstance.spawn('npm', ['run', 'dev']); + const devProcess = await webcontainerInstance.spawn('pnpm', ['run', 'dev']); // Listen to output to detect when Vite is building devProcess.output.pipeTo( From e9a3a73d1c4aad91f4a222914f3076e0d31ab774 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 9 Dec 2025 22:57:04 -0500 Subject: [PATCH 14/15] Fix hot reloading playground page (WebContainers) --- docs/src/routes/docs/playground/+page.svelte | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index 684aaed1b..9c4639ed3 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -10,8 +10,14 @@ import RefreshCcwIcon from '~icons/lucide/refresh-ccw'; import TrashIcon from '~icons/lucide/trash'; - // Singleton instance stored outside component lifecycle - let webcontainerPromise: Promise | null = null; + // Singleton instance stored in globalThis to persist across hot reloads + const WEBCONTAINER_KEY = '__webcontainer_instance__'; + + declare global { + interface Window { + [WEBCONTAINER_KEY]?: Promise; + } + }
%sveltekit.body%
diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index 9c4639ed3..2734112df 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -25,15 +25,17 @@ import type { PageData } from './$types'; import CodeEditor from './CodeEditor.svelte'; import { Overlay, ProgressCircle } from 'svelte-ux'; + import { AnsiUp } from 'ansi_up'; let { data }: { data: PageData } = $props(); + const ansiUp = new AnsiUp(); let iframeEl = $state(null); let webcontainerInstance = $state(null); let consolePane: ReturnType; let consoleCollapsed = $state(true); - let consoleOutput = $state('// This needs wired up...'); + let consoleOutput = $state(''); let fileIcon = $derived.by(() => { if (selectedFile.endsWith('.svelte')) { @@ -142,7 +144,7 @@ loadingStatus = 'Loading editor...'; await loadFileContent(selectedFile); - // Listen for messages from the iframe to detect Vite connection + // Listen for messages from the iframe to detect Vite connection and console logs const handleMessage = (event: MessageEvent) => { // Only process messages from our iframe if (event.source !== iframeEl?.contentWindow) return; @@ -150,6 +152,29 @@ // Check if message is from Vite client if (event.data && typeof event.data === 'object') { const data = event.data; + + // Handle console logs from iframe + if (data.type === 'console') { + const logType = data.level || 'log'; + const args = data.args || []; + const logText = args.map((arg: any) => { + if (typeof arg === 'object') { + return JSON.stringify(arg, null, 2); + } + return String(arg); + }).join(' '); + + const colorMap: Record = { + log: '#e5e7eb', + warn: '#fbbf24', + error: '#ef4444', + info: '#3b82f6' + }; + const color = colorMap[logType] || colorMap.log; + + consoleOutput += `[${logType}] ${logText}\n`; + } + // Vite HMR sends various message types - we'll clear loading on any HMR activity if (data.type && (data.type.includes('vite') || data.type === 'connected')) { if (!viteConnected) { @@ -188,6 +213,18 @@ // Install dependencies loadingStatus = 'Installing dependencies...'; const installProcess = await webcontainerInstance.spawn('pnpm', ['install']); + + // Capture install output + installProcess.output.pipeTo( + new WritableStream({ + write(data) { + const text = data.toString(); + console.log('[WebContainer install]:', text); + consoleOutput += ansiUp.ansi_to_html(text); + } + }) + ); + const installExitCode = await installProcess.exit; if (installExitCode !== 0) { @@ -205,6 +242,9 @@ const text = data.toString(); console.log('[WebContainer]:', text); + // Append to console output (converting ANSI codes to HTML) + consoleOutput += ansiUp.ansi_to_html(text); + // Update status based on Vite output if (text.includes('VITE') && text.includes('ready')) { loadingStatus = 'Ready! Loading preview...'; @@ -344,8 +384,8 @@ >Clear
-
- {consoleOutput} +
+ {@html consoleOutput}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99ecb56c1..da12d6c6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@webcontainer/api': specifier: ^1.6.1 version: 1.6.1 + ansi_up: + specifier: ^6.0.6 + version: 6.0.6 codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -2792,6 +2795,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi_up@6.0.6: + resolution: {integrity: sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -7367,6 +7373,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi_up@6.0.6: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3