From 178fd25efed8eaf0666d2adde62f586f2c0b6918 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 9 Jan 2026 00:08:33 -0500 Subject: [PATCH 1/3] feat(clack): add webcontainer examples --- .gitignore | 1 + astro.config.mjs | 6 ++ package.json | 13 ++- pnpm-lock.yaml | 53 +++++++++++ scripts/snapshot.ts | 45 +++++++++ src/components/WebContainer/Terminal.astro | 59 ++++++++++++ .../WebContainer/WebContainer.astro | 91 +++++++++++++++++++ src/components/WebContainer/theme.ts | 22 +++++ src/content/docs/clack/guides/examples.mdx | 21 ++++- src/pages/test.astro | 15 +++ 10 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 scripts/snapshot.ts create mode 100644 src/components/WebContainer/Terminal.astro create mode 100644 src/components/WebContainer/WebContainer.astro create mode 100644 src/components/WebContainer/theme.ts create mode 100644 src/pages/test.astro diff --git a/.gitignore b/.gitignore index 6240da8..48cfc27 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist/ # generated types .astro/ +public/snapshot # dependencies node_modules/ diff --git a/astro.config.mjs b/astro.config.mjs index 4605f7a..b7e7ab3 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -12,6 +12,12 @@ export default defineConfig({ site: "https://bomb.sh/", base: "/docs", outDir: "./dist/docs/", + server: { + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin' + } + }, integrations: [ starlight({ title: "Bombshell", diff --git a/package.json b/package.json index be532aa..2ab6054 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,13 @@ "version": "0.0.1", "scripts": { "dev": "astro dev", + "dev:snapshot": "", "start": "astro dev", + "prebuild": "node run snapshot", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "snapshot": "node ./scripts/snapshot.ts" }, "dependencies": { "@astrojs/starlight": "^0.37.1", @@ -17,9 +20,17 @@ "@types/node": "^22.19.3", "astro": "^5.16.6", "expressive-code-twoslash": "^0.5.3", + "@webcontainer/api": "^1.6.1", + "@webcontainer/snapshot": "^0.1.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", "sharp": "^0.33.5", "starlight-sidebar-topics": "^0.6.2" }, + "devDependencies": { + "tinyexec": "^1.0.2" + }, "pnpm": { "onlyBuiltDependencies": [ "esbuild", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93c4054..73e0129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,21 @@ importers: '@types/node': specifier: ^22.19.3 version: 22.19.3 + '@webcontainer/api': + specifier: ^1.6.1 + version: 1.6.1 + '@webcontainer/snapshot': + specifier: ^0.1.0 + version: 0.1.0 + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/addon-web-links': + specifier: ^0.12.0 + version: 0.12.0 + '@xterm/xterm': + specifier: ^6.0.0 + version: 6.0.0 astro: specifier: ^5.16.6 version: 5.16.6(@types/node@22.19.3)(rollup@4.55.1)(typescript@5.8.2) @@ -35,6 +50,10 @@ importers: starlight-sidebar-topics: specifier: ^0.6.2 version: 0.6.2(@astrojs/starlight@0.37.1(astro@5.16.6(@types/node@22.19.3)(rollup@4.55.1)(typescript@5.8.2))) + devDependencies: + tinyexec: + specifier: ^1.0.2 + version: 1.0.2 packages: @@ -526,6 +545,10 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + '@msgpack/msgpack@3.1.3': + resolution: {integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==} + engines: {node: '>= 18'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -773,6 +796,22 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@webcontainer/api@1.6.1': + resolution: {integrity: sha512-2RS2KiIw32BY1Icf6M1DvqSmcon9XICZCDgS29QJb2NmF12ZY2V5Ia+949hMKB3Wno+P/Y8W+sPP59PZeXSELg==} + + '@webcontainer/snapshot@0.1.0': + resolution: {integrity: sha512-PTIGQ3osUpTbK/dqB8RYbcZGv8IK+DJACx709z5sFbeIlngB3hUFpTEYFYs1SbUvr/AEQqvd0/bhc4ectrPRRw==} + engines: {node: '>=16'} + + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/addon-web-links@0.12.0': + resolution: {integrity: sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==} + + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2479,6 +2518,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@msgpack/msgpack@3.1.3': {} + '@oslojs/encoding@1.1.0': {} '@pagefind/darwin-arm64@1.4.0': @@ -2676,6 +2717,18 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@webcontainer/api@1.6.1': {} + + '@webcontainer/snapshot@0.1.0': + dependencies: + '@msgpack/msgpack': 3.1.3 + + '@xterm/addon-fit@0.11.0': {} + + '@xterm/addon-web-links@0.12.0': {} + + '@xterm/xterm@6.0.0': {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 diff --git a/scripts/snapshot.ts b/scripts/snapshot.ts new file mode 100644 index 0000000..7acb123 --- /dev/null +++ b/scripts/snapshot.ts @@ -0,0 +1,45 @@ +import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { snapshot } from '@webcontainer/snapshot'; +import {x} from 'tinyexec'; +import { createHash } from "node:crypto"; + +const PACKAGE_JSON = { + name: 'example', + type: 'module', + version: '0.0.0', + dependencies: { + "@bomb.sh/args": "latest", + "@clack/core": "1.0.0-alpha.0", + "@clack/prompts": "1.0.0-alpha.0" + } +} +const IGNORE_FILES = ['*.md', '*.d.*', '*.map', 'LICENSE', 'license']; +const rootDir = new URL('../', import.meta.url); +const snapshotDir = new URL(`./snapshot-${hash()}/`, rootDir); +const outFile = new URL('./public/snapshot', rootDir); + +async function run() { + await fs.mkdir(snapshotDir, { recursive: true }); + await fs.writeFile(new URL('package.json', snapshotDir), JSON.stringify(PACKAGE_JSON)); + await x('npm', ['install'], { + nodeOptions: { + cwd: fileURLToPath(snapshotDir), + } + }) + for await (const file of fs.glob(IGNORE_FILES.map(file => `**/${file}`), { cwd: fileURLToPath(snapshotDir) })) { + await fs.rm(new URL(file, snapshotDir)); + } + const output = await snapshot(fileURLToPath(snapshotDir)); + await fs.writeFile(outFile, output); + await fs.rm(snapshotDir, { recursive: true, force: true }); + console.log('snapshot generated'); +} + +run(); + +function hash() { + return createHash("shake256", { outputLength: 8 }) + .update(Date.now().toString()) + .digest("hex"); +} diff --git a/src/components/WebContainer/Terminal.astro b/src/components/WebContainer/Terminal.astro new file mode 100644 index 0000000..d3cc844 --- /dev/null +++ b/src/components/WebContainer/Terminal.astro @@ -0,0 +1,59 @@ + + + diff --git a/src/components/WebContainer/WebContainer.astro b/src/components/WebContainer/WebContainer.astro new file mode 100644 index 0000000..447a273 --- /dev/null +++ b/src/components/WebContainer/WebContainer.astro @@ -0,0 +1,91 @@ +--- +import Terminal from "./Terminal.astro"; + +interface Props { + file: string; +} +const { file } = Astro.props; +--- + + +
+ +
+ + diff --git a/src/components/WebContainer/theme.ts b/src/components/WebContainer/theme.ts new file mode 100644 index 0000000..eaae1e3 --- /dev/null +++ b/src/components/WebContainer/theme.ts @@ -0,0 +1,22 @@ +export const theme = { + cursor: '#eff0eb', + cursorAccent: '#00000000', + foreground: "#eff0eb", + background: "#16181D", + red: "#ff5c57", + green: "#5af78e", + yellow: "#f3f99d", + blue: "#57c7ff", + magenta: "#ff6ac1", + cyan: "#9aedfe", + white: "#f1f1f0", + brightBlack: "#686868", + brightRed: "#ff5c57", + brightGreen: "#5af78e", + brightYellow: "#f3f99d", + brightBlue: "#57c7ff", + brightMagenta: "#ff6ac1", + brightCyan: "#9aedfe", + brightWhite: "#f1f1f0", + selectionBackground: "#97979b33", +}; diff --git a/src/content/docs/clack/guides/examples.mdx b/src/content/docs/clack/guides/examples.mdx index 1deb6fa..2b31b68 100644 --- a/src/content/docs/clack/guides/examples.mdx +++ b/src/content/docs/clack/guides/examples.mdx @@ -3,12 +3,28 @@ title: Examples description: Learn through practical examples of using Clack --- +import { WebContainer } from '../../../../components/WebContainer/WebContainer.astro'; + This guide provides practical examples of using Clack in different scenarios. ## Basic Examples +### Container + +```ts +import { text, isCancel } from '@clack/prompts'; + +const name = await text({ + message: "What is your name?", + placeholder: "Jane Doe" +}) +``` + ### Simple Text Input + + + ```ts twoslash import { text, isCancel } from '@clack/prompts'; @@ -27,6 +43,9 @@ if (isCancel(name)) { console.log(`Hello, ${name}!`); ``` + + + ### Selection Menu ```ts twoslash @@ -931,4 +950,4 @@ async function racePrompts() { } ``` -For more examples and best practices, check out our [GitHub repository](https://github.com/bombshell-dev/clack/tree/main/examples/basic). \ No newline at end of file +For more examples and best practices, check out our [GitHub repository](https://github.com/bombshell-dev/clack/tree/main/examples/basic). diff --git a/src/pages/test.astro b/src/pages/test.astro new file mode 100644 index 0000000..26c92ba --- /dev/null +++ b/src/pages/test.astro @@ -0,0 +1,15 @@ +--- +import WebContainer from '../components/WebContainer/WebContainer.astro'; + +const code = `import { text } from "@clack/prompts"; + +const name = await text({ + message: "What is your name?", + placeholder: "Jane Doe" +}) +` +--- + +

Hello test!

+ + From 8afde86fe93ec775d6456a55304ea13decb47c84 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 9 Jan 2026 01:18:32 -0500 Subject: [PATCH 2/3] feat(clack): improve terminal styles --- src/components/WebContainer/Terminal.astro | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/WebContainer/Terminal.astro b/src/components/WebContainer/Terminal.astro index d3cc844..8758b3c 100644 --- a/src/components/WebContainer/Terminal.astro +++ b/src/components/WebContainer/Terminal.astro @@ -2,7 +2,6 @@ + + From 1a834fc519dc307bf097c2136f55de15e0aa6325 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 9 Jan 2026 01:18:43 -0500 Subject: [PATCH 3/3] feat(clack): webcontainer runs --- .../WebContainer/WebContainer.astro | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/components/WebContainer/WebContainer.astro b/src/components/WebContainer/WebContainer.astro index 447a273..63cbc89 100644 --- a/src/components/WebContainer/WebContainer.astro +++ b/src/components/WebContainer/WebContainer.astro @@ -8,7 +8,7 @@ const { file } = Astro.props; --- -
+
@@ -17,7 +17,7 @@ const { file } = Astro.props; import { WebContainer } from "@webcontainer/api"; let host: WebContainer; - host = await WebContainer.boot(); + host = await WebContainer.boot({ workdirName: 'demo' }); const snapshotResponse = await fetch(`/docs/snapshot`); const snapshot = await snapshotResponse.arrayBuffer(); await host.mount(snapshot, { mountPoint: "/" }); @@ -25,8 +25,15 @@ const { file } = Astro.props; customElements.define( "web-container", class extends HTMLElement { + get dir() { + return `${this.name}/` + } + get file() { + return `${this.dir}index.js`; + } get fileContent() { - return this.querySelector("[data-content]")!.textContent; + const text = this.querySelector("[data-content]")!.textContent; + return `import { intro, outro } from "@clack/prompts";console.clear();intro("\\x1b[46m\\x1b[30m ${this.name} \\x1b[0m");\n${text};process.on('exit', () => console.log('EOF'));`; } get name() { return this.getAttribute("name")!; @@ -35,9 +42,9 @@ const { file } = Astro.props; return (this.querySelector("docs-terminal") as any)?.instance; } async connectedCallback() { - await host.fs.mkdir(`/examples/${this.name}`, { recursive: true }); + await host.fs.mkdir(this.dir, { recursive: true }); await host.fs.writeFile( - `/examples/${this.name}/index.js`, + this.file, this.fileContent, { encoding: "utf-8" } ); @@ -45,16 +52,15 @@ const { file } = Astro.props; } async render() { - const { terminal } = this; + const { terminal, name, render } = this; terminal?.reset(); const main = async () => { - const jshReady = Promise.withResolvers(); - let process; - let isJSHReady = false; - // we set an infinite loop so that when the user runs the `exit` command, we restart while (true) { - process = await host.spawn("jsh", { + const jsh = Promise.withResolvers(); + let isJSHReady = false; + const process = await host.spawn("jsh", { + cwd: name, terminal: { rows: terminal?.rows!, cols: terminal?.cols! }, }); process.output.pipeTo( @@ -62,20 +68,26 @@ const { file } = Astro.props; write(data) { if (data.includes("❯") && !isJSHReady) { isJSHReady = true; - jshReady.resolve(undefined); + jsh.resolve(undefined); + } + if (data.includes("❯") || data.includes('~/demo')) { + return; + } + if (data.includes("EOF")) { + process.kill(); + render(); + return; } terminal?.write(data); - }, + } }) ); - const shellWriter = process.input.getWriter(); - await jshReady.promise; - await shellWriter.write( - `cd examples/${this.name} && clear && node index.js\n` - ); + const shell = process.input.getWriter(); + await jsh.promise; + await shell.write(`node index.js\n`); // write the terminal input to the process const terminalWriter = terminal?.onData((data) => { - shellWriter.write(data); + shell.write(data); }); // wait for the process to finish await process.exit;