From 4a2ecf2bde491e94c41fb369656846751f70e32c Mon Sep 17 00:00:00 2001 From: Yosi Wizman Date: Mon, 5 Jan 2026 14:08:13 -0500 Subject: [PATCH 01/10] chore: rebrand to X Builder - Update package name and description - Update page title and meta description - Update UI placeholder text and header logo - Update AI system prompt identity - Update favicon and icons - Update README with attribution - Update GitHub issue templates - Add .gitattributes for LF enforcement Co-Authored-By: Warp --- .gitattributes | 25 ++++++++++++ .github/ISSUE_TEMPLATE/bug_report.yml | 6 +-- .github/ISSUE_TEMPLATE/config.yml | 12 ++---- README.md | 56 ++++++++------------------- app/components/chat/BaseChat.tsx | 2 +- app/components/header/Header.tsx | 2 +- app/lib/.server/llm/prompts.ts | 2 +- app/routes/_index.tsx | 2 +- icons/logo.svg | 4 +- package.json | 4 +- public/favicon.svg | 4 +- wrangler.toml | 2 +- 12 files changed, 58 insertions(+), 63 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..dd9fac725f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +# Enforce LF line endings for all text files +* text=auto eol=lf + +# Explicitly mark as text +*.ts text eol=lf +*.tsx text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.json text eol=lf +*.md text eol=lf +*.css text eol=lf +*.scss text eol=lf +*.html text eol=lf +*.svg text eol=lf +*.yml text eol=lf +*.yaml text eol=lf + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 83e293df76..8d706a57b5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -6,8 +6,8 @@ body: value: | Thank you for reporting an issue :pray:. - This issue tracker is for bugs and issues found with [Bolt.new](https://bolt.new). - If you experience issues related to WebContainer, please file an issue in our [WebContainer repo](https://github.com/stackblitz/webcontainer-core), or file an issue in our [StackBlitz core repo](https://github.com/stackblitz/core) for issues with StackBlitz. + This issue tracker is for bugs and issues found with X Builder. + For issues related to the upstream Bolt.new codebase, see [stackblitz/bolt.new](https://github.com/stackblitz/bolt.new). The more information you fill in, the better we can help you. - type: textarea @@ -20,7 +20,7 @@ body: - type: input id: link attributes: - label: Link to the Bolt URL that caused the error + label: Link to the X Builder URL that caused the error description: Please do not delete it after reporting! validations: required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e744c79187..316b41c037 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,5 @@ blank_issues_enabled: false contact_links: - - name: Bolt.new Help Center - url: https://support.bolt.new - about: Official central repository for tips, tricks, tutorials, known issues, and best practices for bolt.new usage. - - name: Billing Issues - url: https://support.bolt.new/Billing-13fd971055d680ebb393cb80973710b6 - about: Instructions for billing and subscription related support - - name: Discord Chat - url: https://discord.gg/stackblitz - about: Build, share, and learn with other Bolters in real time. + - name: Upstream Project (Bolt.new) + url: https://github.com/stackblitz/bolt.new + about: Original Bolt.new project by StackBlitz diff --git a/README.md b/README.md index d3745298ff..055763a238 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,30 @@ -[![Bolt.new: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.new) +# X Builder -# Bolt.new: AI-Powered Full-Stack Web Development in the Browser +AI-powered full-stack web development in the browser. -Bolt.new is an AI-powered web development agent that allows you to prompt, run, edit, and deploy full-stack applications directly from your browser—no local setup required. If you're here to build your own AI-powered web dev agent using the Bolt open source codebase, [click here to get started!](./CONTRIBUTING.md) +> **Based on [Bolt.new](https://github.com/stackblitz/bolt.new)** - the open-source AI web development agent by StackBlitz. -## What Makes Bolt.new Different +## About -Claude, v0, etc are incredible- but you can't install packages, run backends or edit code. That’s where Bolt.new stands out: +X Builder is a white-label fork of Bolt.new that allows you to prompt, run, edit, and deploy full-stack applications directly from your browser. -- **Full-Stack in the Browser**: Bolt.new integrates cutting-edge AI models with an in-browser development environment powered by **StackBlitz’s WebContainers**. This allows you to: - - Install and run npm tools and libraries (like Vite, Next.js, and more) +### Features + +- **Full-Stack in the Browser**: Integrates AI models with an in-browser development environment powered by **StackBlitz's WebContainers** + - Install and run npm tools and libraries (Vite, Next.js, etc.) - Run Node.js servers - Interact with third-party APIs - Deploy to production from chat - - Share your work via a URL - -- **AI with Environment Control**: Unlike traditional dev environments where the AI can only assist in code generation, Bolt.new gives AI models **complete control** over the entire environment including the filesystem, node server, package manager, terminal, and browser console. This empowers AI agents to handle the entire app lifecycle—from creation to deployment. - -Whether you’re an experienced developer, a PM or designer, Bolt.new allows you to build production-grade full-stack applications with ease. -For developers interested in building their own AI-powered development tools with WebContainers, check out the open-source Bolt codebase in this repo! +- **AI with Environment Control**: AI models have complete control over the filesystem, node server, package manager, terminal, and browser console ## Tips and Tricks -Here are some tips to get the most out of Bolt.new: - -- **Be specific about your stack**: If you want to use specific frameworks or libraries (like Astro, Tailwind, ShadCN, or any other popular JavaScript framework), mention them in your initial prompt to ensure Bolt scaffolds the project accordingly. - -- **Use the enhance prompt icon**: Before sending your prompt, try clicking the 'enhance' icon to have the AI model help you refine your prompt, then edit the results before submitting. - -- **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps Bolt understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality. - -- **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask Bolt to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly. - -## FAQs - -**Where do I sign up for a paid plan?** -Bolt.new is free to get started. If you need more AI tokens or want private projects, you can purchase a paid subscription in your [Bolt.new](https://bolt.new) settings, in the lower-left hand corner of the application. - -**What happens if I hit the free usage limit?** -Once your free daily token limit is reached, AI interactions are paused until the next day or until you upgrade your plan. - -**Is Bolt in beta?** -Yes, Bolt.new is in beta, and we are actively improving it based on feedback. - -**How can I report Bolt.new issues?** -Check out the [Issues section](https://github.com/stackblitz/bolt.new/issues) to report an issue or request a new feature. Please use the search feature to check if someone else has already submitted the same issue/request. +- **Be specific about your stack**: Mention frameworks/libraries in your initial prompt +- **Use the enhance prompt icon**: Refine your prompt with AI assistance before submitting +- **Scaffold basics first**: Establish the foundation before adding advanced features +- **Batch simple instructions**: Combine multiple simple tasks in one message -**What frameworks/libraries currently work on Bolt?** -Bolt.new supports most popular JavaScript frameworks and libraries. If it runs on StackBlitz, it will run on Bolt.new as well. +## Attribution -**How can I add make sure my framework/project works well in bolt?** -We are excited to work with the JavaScript ecosystem to improve functionality in Bolt. Reach out to us via [hello@stackblitz.com](mailto:hello@stackblitz.com) to discuss how we can partner! +This project is based on [Bolt.new](https://github.com/stackblitz/bolt.new) by [StackBlitz](https://stackblitz.com/), licensed under MIT. diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index c4f90f43a1..e6dea3a8a2 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -130,7 +130,7 @@ export const BaseChat = React.forwardRef( minHeight: TEXTAREA_MIN_HEIGHT, maxHeight: TEXTAREA_MAX_HEIGHT, }} - placeholder="How can Bolt help you today?" + placeholder="How can X Builder help you today?" translate="no" /> diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx index 15cf4bfbd0..dd3034a3cc 100644 --- a/app/components/header/Header.tsx +++ b/app/components/header/Header.tsx @@ -21,7 +21,7 @@ export function Header() {
diff --git a/app/lib/.server/llm/prompts.ts b/app/lib/.server/llm/prompts.ts index f78b418731..4fbe6c08c0 100644 --- a/app/lib/.server/llm/prompts.ts +++ b/app/lib/.server/llm/prompts.ts @@ -3,7 +3,7 @@ import { allowedHTMLElements } from '~/utils/markdown'; import { stripIndents } from '~/utils/stripIndent'; export const getSystemPrompt = (cwd: string = WORK_DIR) => ` -You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices. +You are X Builder, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices. You are operating in an environment called WebContainer, an in-browser Node.js runtime that emulates a Linux system to some degree. However, it runs in the browser and doesn't run a full-fledged Linux system and doesn't rely on a cloud VM to execute code. All code is executed in the browser. It does come with a shell that emulates zsh. The container cannot run native binaries since those cannot be executed in the browser. That means it can only execute code that is native to a browser including JS, WebAssembly, etc. diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 86d73409c9..944c4b02d6 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -5,7 +5,7 @@ import { Chat } from '~/components/chat/Chat.client'; import { Header } from '~/components/header/Header'; export const meta: MetaFunction = () => { - return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }]; + return [{ title: 'X Builder' }, { name: 'description', content: 'AI-powered full-stack web development' }]; }; export const loader = () => json({}); diff --git a/icons/logo.svg b/icons/logo.svg index c68d62fd45..4262db8672 100644 --- a/icons/logo.svg +++ b/icons/logo.svg @@ -1,4 +1,4 @@ - - + + X diff --git a/package.json b/package.json index 5583455603..afcd1eff64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "bolt", - "description": "StackBlitz AI Agent", + "name": "x-builder", + "description": "AI-powered full-stack web development", "private": true, "license": "MIT", "packageManager": "pnpm@9.4.0", diff --git a/public/favicon.svg b/public/favicon.svg index c68d62fd45..4262db8672 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,4 +1,4 @@ - - + + X diff --git a/wrangler.toml b/wrangler.toml index 09f2e3a88a..0deda9f1f7 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,5 +1,5 @@ #:schema node_modules/wrangler/config-schema.json -name = "bolt" +name = "x-builder" compatibility_flags = ["nodejs_compat"] compatibility_date = "2024-07-01" pages_build_output_dir = "./build/client" From bc388eb202aef43efc498493aa30754b863835b5 Mon Sep 17 00:00:00 2001 From: Yosi Wizman Date: Mon, 5 Jan 2026 14:13:49 -0500 Subject: [PATCH 02/10] ci: add GitHub Actions workflow for lint, typecheck, and tests Co-Authored-By: Warp --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..40b27f10c4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ci: + name: Lint, Typecheck & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.4.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.15.1' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm run lint + + - name: Typecheck + run: pnpm run typecheck + + - name: Test + run: pnpm test From 1ada9e34745bf2e7be78c602a21fdfa9b11045bb Mon Sep 17 00:00:00 2001 From: Yosi Wizman Date: Mon, 5 Jan 2026 14:14:55 -0500 Subject: [PATCH 03/10] fix: remove unused IconButton import Co-Authored-By: Warp --- app/components/sidebar/Menu.client.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index cf6d97812c..e99d5bb4ef 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -2,7 +2,6 @@ import { motion, type Variants } from 'framer-motion'; import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; -import { IconButton } from '~/components/ui/IconButton'; import { ThemeSwitch } from '~/components/ui/ThemeSwitch'; import { db, deleteById, getAll, chatId, type ChatHistoryItem } from '~/lib/persistence'; import { cubicEasingFn } from '~/utils/easings'; From 9e17b796d3ff6a2fb9378c46cb28002bc0465acb Mon Sep 17 00:00:00 2001 From: yosiwizman <135239829+yosiwizman@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:19:36 -0500 Subject: [PATCH 04/10] ci: add staging deployment workflow and update README (#1) - Add deploy-staging.yml for Cloudflare Pages deployment - Add CI status badge to README - Add staging URL and deployment documentation - Add development setup instructions Co-authored-by: Yosi Wizman Co-authored-by: Warp --- .github/workflows/deploy-staging.yml | 45 ++++++++++++++++++++++++++++ README.md | 36 ++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 .github/workflows/deploy-staging.yml diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000000..3830125698 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,45 @@ +name: Deploy Staging + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + name: Deploy to Cloudflare Pages + runs-on: ubuntu-latest + # Only deploy after CI passes + needs: [] + + permissions: + contents: read + deployments: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.4.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.15.1' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy ./build/client --project-name=x-builder-staging diff --git a/README.md b/README.md index 055763a238..a7edafb930 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # X Builder +[![CI](https://github.com/yosiwizman/x-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/yosiwizman/x-builder/actions/workflows/ci.yml) + AI-powered full-stack web development in the browser. > **Based on [Bolt.new](https://github.com/stackblitz/bolt.new)** - the open-source AI web development agent by StackBlitz. +## Staging + +🚀 **Staging URL**: https://x-builder-staging.pages.dev + ## About X Builder is a white-label fork of Bolt.new that allows you to prompt, run, edit, and deploy full-stack applications directly from your browser. @@ -25,6 +31,36 @@ X Builder is a white-label fork of Bolt.new that allows you to prompt, run, edit - **Scaffold basics first**: Establish the foundation before adding advanced features - **Batch simple instructions**: Combine multiple simple tasks in one message +## Development + +### Prerequisites + +- Node.js 20.15.1+ +- pnpm 9.4.0+ + +### Setup + +```bash +pnpm install +pnpm run dev +``` + +### Scripts + +- `pnpm run dev` - Start development server +- `pnpm run build` - Build for production +- `pnpm run lint` - Run ESLint +- `pnpm run typecheck` - Run TypeScript checks +- `pnpm test` - Run tests + +### Deployment + +Staging deploys automatically from `main` branch via GitHub Actions to Cloudflare Pages. + +Required GitHub Secrets: +- `CLOUDFLARE_API_TOKEN` - Cloudflare API token with Pages edit permissions +- `CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID + ## Attribution This project is based on [Bolt.new](https://github.com/stackblitz/bolt.new) by [StackBlitz](https://stackblitz.com/), licensed under MIT. From 4e54ce309d02e0424cf6d4388feb6d32d1f97d2c Mon Sep 17 00:00:00 2001 From: yosiwizman <135239829+yosiwizman@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:14:26 -0500 Subject: [PATCH 05/10] docs: update README with production status and release process (#2) * ci: add staging deployment workflow and update README - Add deploy-staging.yml for Cloudflare Pages deployment - Add CI status badge to README - Add staging URL and deployment documentation - Add development setup instructions Co-Authored-By: Warp * docs: update README with production status and release process - Change staging to production status - Add live URL designation - Add release process documentation - Document safeguards Co-Authored-By: Warp --------- Co-authored-by: Yosi Wizman Co-authored-by: Warp --- README.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a7edafb930..e4ea297a45 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,13 @@ AI-powered full-stack web development in the browser. > **Based on [Bolt.new](https://github.com/stackblitz/bolt.new)** - the open-source AI web development agent by StackBlitz. -## Staging +## Production -🚀 **Staging URL**: https://x-builder-staging.pages.dev +🚀 **Live URL**: https://x-builder-staging.pages.dev + +**Status**: Production (Cloudflare Pages) + +> **Note**: Custom domain can be added later without downtime. ## About @@ -55,12 +59,28 @@ pnpm run dev ### Deployment -Staging deploys automatically from `main` branch via GitHub Actions to Cloudflare Pages. +Production deploys automatically from `main` branch via GitHub Actions to Cloudflare Pages. Required GitHub Secrets: - `CLOUDFLARE_API_TOKEN` - Cloudflare API token with Pages edit permissions - `CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID +### Release Process + +``` +1. Create a Pull Request with your changes +2. CI runs automatically (lint, typecheck, tests) +3. CI must pass before merge is allowed +4. Merge PR to main +5. Auto-deploy to production (Cloudflare Pages) +``` + +**Safeguards**: +- Branch protection requires PR reviews +- All CI checks must pass +- Direct pushes to `main` are blocked +- Linear history enforced + ## Attribution This project is based on [Bolt.new](https://github.com/stackblitz/bolt.new) by [StackBlitz](https://stackblitz.com/), licensed under MIT. From aa97d74bb564c80933c3d1caa77ea097cae707a8 Mon Sep 17 00:00:00 2001 From: yosiwizman <135239829+yosiwizman@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:59:18 -0500 Subject: [PATCH 06/10] fix: enable crossOriginIsolated for WebContainers (COOP/COEP) (#3) - Add public/_headers with COOP/COEP headers for Cloudflare Pages - Update entry.server.tsx to use credentialless COEP - Add COOP/COEP headers to Cloudflare Pages function - Add COOP/COEP headers to Vite dev server - Add crossOriginIsolated verification warning in root.tsx - Document cross-origin isolation in README Headers: - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: credentialless Co-authored-by: Yosi Wizman Co-authored-by: Warp --- README.md | 17 +++++++++++++++++ app/entry.server.tsx | 5 ++++- app/root.tsx | 12 ++++++++++++ functions/[[path]].ts | 16 +++++++++++++++- public/_headers | 3 +++ vite.config.ts | 6 ++++++ 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 public/_headers diff --git a/README.md b/README.md index e4ea297a45..1a323fe62a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,23 @@ X Builder is a white-label fork of Bolt.new that allows you to prompt, run, edit - **Scaffold basics first**: Establish the foundation before adding advanced features - **Batch simple instructions**: Combine multiple simple tasks in one message +## Technical Notes + +### Cross-Origin Isolation + +X Builder requires `crossOriginIsolated` to be enabled for WebContainers (SharedArrayBuffer). This is achieved via HTTP headers: + +- `Cross-Origin-Opener-Policy: same-origin` +- `Cross-Origin-Embedder-Policy: credentialless` + +These headers are set in: +- `public/_headers` - Cloudflare Pages static headers +- `app/entry.server.tsx` - Server-side rendering +- `functions/[[path]].ts` - Cloudflare Pages Functions +- `vite.config.ts` - Development server + +To verify: Open DevTools console and check `self.crossOriginIsolated === true` + ## Development ### Prerequisites diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 4baf07001d..3503a52217 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -68,8 +68,11 @@ export default async function handleRequest( responseHeaders.set('Content-Type', 'text/html'); - responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp'); + /** + * Enable crossOriginIsolated for SharedArrayBuffer (required by WebContainers). + */ responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin'); + responseHeaders.set('Cross-Origin-Embedder-Policy', 'credentialless'); return new Response(body, { headers: responseHeaders, diff --git a/app/root.tsx b/app/root.tsx index 31eb387e03..af45ff3acc 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -69,6 +69,18 @@ export function Layout({ children }: { children: React.ReactNode }) { document.querySelector('html')?.setAttribute('data-theme', theme); }, [theme]); + /** + * Verify crossOriginIsolated is enabled (required for SharedArrayBuffer/WebContainers). + */ + useEffect(() => { + if (typeof window !== 'undefined' && !window.crossOriginIsolated) { + console.warn( + 'crossOriginIsolated is not enabled. SharedArrayBuffer may not work. ' + + 'Ensure COOP/COEP headers are set correctly.', + ); + } + }, []); + return ( <> {children} diff --git a/functions/[[path]].ts b/functions/[[path]].ts index 4f196604d2..9b9f5c0c58 100644 --- a/functions/[[path]].ts +++ b/functions/[[path]].ts @@ -4,6 +4,20 @@ import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages'; // @ts-ignore because the server build file is generated by `remix vite:build` import * as serverBuild from '../build/server'; -export const onRequest = createPagesFunctionHandler({ +const handler = createPagesFunctionHandler({ build: serverBuild as unknown as ServerBuild, }); + +/** + * Wrap handler to add COOP/COEP headers for crossOriginIsolated. + */ +export const onRequest: PagesFunction = async (context) => { + const response = await handler(context); + + // clone response to modify headers + const newResponse = new Response(response.body, response); + newResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin'); + newResponse.headers.set('Cross-Origin-Embedder-Policy', 'credentialless'); + + return newResponse; +}; diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000000..3a270de9d8 --- /dev/null +++ b/public/_headers @@ -0,0 +1,3 @@ +/* + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: credentialless diff --git a/vite.config.ts b/vite.config.ts index 58e76cde5d..2c62ac745f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,6 +10,12 @@ export default defineConfig((config) => { build: { target: 'esnext', }, + server: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'credentialless', + }, + }, plugins: [ nodePolyfills({ include: ['path', 'buffer'], From 98818e9e0fcbb2ed9b15dbb01984061278e3649e Mon Sep 17 00:00:00 2001 From: yosiwizman <135239829+yosiwizman@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:23:46 -0500 Subject: [PATCH 07/10] feat: MVP publish project to Cloudflare Pages (#4) - Add publish.ts store for state management - Add api.publish.ts API endpoint for Cloudflare Pages deployment - Add PublishButton.client.tsx UI component - Update README with publish feature documentation Co-authored-by: Yosi Wizman Co-authored-by: Warp --- README.md | 16 +++ .../workbench/PublishButton.client.tsx | 120 ++++++++++++++++ app/lib/stores/publish.ts | 46 ++++++ app/routes/api.publish.ts | 134 ++++++++++++++++++ 4 files changed, 316 insertions(+) create mode 100644 app/components/workbench/PublishButton.client.tsx create mode 100644 app/lib/stores/publish.ts create mode 100644 app/routes/api.publish.ts diff --git a/README.md b/README.md index 1a323fe62a..ce3c0307b7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,22 @@ X Builder is a white-label fork of Bolt.new that allows you to prompt, run, edit ## Technical Notes +### MVP Publish (Cloudflare Pages) + +X Builder includes an MVP publish feature that deploys projects directly to Cloudflare Pages. + +**Components**: +- `app/lib/stores/publish.ts` - State management for publish status +- `app/routes/api.publish.ts` - API endpoint for Cloudflare Pages deployment +- `app/components/workbench/PublishButton.client.tsx` - UI button component + +**Environment Variables** (for publish to work at runtime): +- `CLOUDFLARE_API_TOKEN` - API token with Pages permissions +- `CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID + +> **TODO**: The Cloudflare Pages Direct Upload API implementation may need adjustment +> based on actual API requirements for production use. + ### Cross-Origin Isolation X Builder requires `crossOriginIsolated` to be enabled for WebContainers (SharedArrayBuffer). This is achieved via HTTP headers: diff --git a/app/components/workbench/PublishButton.client.tsx b/app/components/workbench/PublishButton.client.tsx new file mode 100644 index 0000000000..8b42c0c34e --- /dev/null +++ b/app/components/workbench/PublishButton.client.tsx @@ -0,0 +1,120 @@ +import { useStore } from '@nanostores/react'; +import { memo, useCallback } from 'react'; +import { toast } from 'react-toastify'; +import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton'; +import { + publishState, + setPublishStatus, + setPublishSuccess, + setPublishError, + resetPublishState, +} from '~/lib/stores/publish'; +import { workbenchStore } from '~/lib/stores/workbench'; + +interface PublishButtonProps { + className?: string; +} + +export const PublishButton = memo(({ className }: PublishButtonProps) => { + const { status, url } = useStore(publishState); + const isPublishing = status === 'publishing'; + + const handlePublish = useCallback(async () => { + const files = workbenchStore.files.get(); + + if (!files || Object.keys(files).length === 0) { + toast.error('No files to publish'); + return; + } + + // convert files to publishable format (only file contents, not directories) + const publishableFiles: Record = {}; + + for (const [path, dirent] of Object.entries(files)) { + if (dirent?.type === 'file' && dirent.content) { + publishableFiles[path] = dirent.content; + } + } + + if (Object.keys(publishableFiles).length === 0) { + toast.error('No file content to publish'); + return; + } + + setPublishStatus('publishing'); + toast.info('Publishing project...'); + + try { + const response = await fetch('/api/publish', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ files: publishableFiles }), + }); + + const data = (await response.json()) as { url?: string; error?: string; success?: boolean }; + + if (!response.ok) { + throw new Error(data.error || 'Publish failed'); + } + + const publishUrl = data.url || ''; + setPublishSuccess(publishUrl); + toast.success( +
+ Published successfully! +
+ + {publishUrl} + +
, + { autoClose: false }, + ); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + setPublishError(message); + toast.error(`Publish failed: ${message}`); + } + }, []); + + const handleViewDeployment = useCallback(() => { + if (url) { + window.open(url, '_blank', 'noopener,noreferrer'); + } + }, [url]); + + const handleReset = useCallback(() => { + resetPublishState(); + }, []); + + if (status === 'success' && url) { + return ( +
+ +
+ View Site + + +
+ +
+ ); + } + + return ( + + {isPublishing ? ( + <> +
+ Publishing... + + ) : ( + <> +
+ Publish + + )} + + ); +}); diff --git a/app/lib/stores/publish.ts b/app/lib/stores/publish.ts new file mode 100644 index 0000000000..8140a3fb1d --- /dev/null +++ b/app/lib/stores/publish.ts @@ -0,0 +1,46 @@ +import { atom } from 'nanostores'; + +export type PublishStatus = 'idle' | 'publishing' | 'success' | 'error'; + +export interface PublishState { + status: PublishStatus; + url: string | null; + error: string | null; +} + +export const publishState = atom({ + status: 'idle', + url: null, + error: null, +}); + +export function setPublishStatus(status: PublishStatus) { + publishState.set({ + ...publishState.get(), + status, + }); +} + +export function setPublishSuccess(url: string) { + publishState.set({ + status: 'success', + url, + error: null, + }); +} + +export function setPublishError(error: string) { + publishState.set({ + status: 'error', + url: null, + error, + }); +} + +export function resetPublishState() { + publishState.set({ + status: 'idle', + url: null, + error: null, + }); +} diff --git a/app/routes/api.publish.ts b/app/routes/api.publish.ts new file mode 100644 index 0000000000..1b806a5ec2 --- /dev/null +++ b/app/routes/api.publish.ts @@ -0,0 +1,134 @@ +import { type ActionFunctionArgs, json } from '@remix-run/cloudflare'; + +interface PublishRequest { + files: Record; + projectName?: string; +} + +interface CloudflareEnv { + CLOUDFLARE_API_TOKEN?: string; + CLOUDFLARE_ACCOUNT_ID?: string; +} + +/** + * MVP Publish API endpoint. + * + * Publishes project files to Cloudflare Pages via Direct Upload API. + * + * TODO: The Cloudflare Pages Direct Upload API requires a multi-step process: + * 1. Create upload session + * 2. Upload files as form data + * 3. Create deployment + * This MVP implementation provides the endpoint structure - full implementation + * may need adjustment based on actual API requirements. + */ +export async function action({ context, request }: ActionFunctionArgs) { + const env = context.cloudflare.env as CloudflareEnv; + + const apiToken = env.CLOUDFLARE_API_TOKEN; + const accountId = env.CLOUDFLARE_ACCOUNT_ID; + + if (!apiToken || !accountId) { + return json( + { error: 'Cloudflare credentials not configured. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.' }, + { status: 500 }, + ); + } + + try { + const { files, projectName = 'x-builder-preview' } = await request.json(); + + if (!files || Object.keys(files).length === 0) { + return json({ error: 'No files provided for publishing' }, { status: 400 }); + } + + /** + * Create a deployment using Cloudflare Pages Direct Upload. + * + * Step 1: Create the project if it doesn't exist. + */ + const projectResponse = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects`, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: projectName, + production_branch: 'main', + }), + }); + + // project might already exist (409), which is fine + if (!projectResponse.ok && projectResponse.status !== 409) { + const errorData = await projectResponse.json(); + console.error('Failed to create project:', errorData); + } + + /** + * Step 2: Create a deployment with direct upload. + * + * Note: This uses the simplified deployment endpoint. + * For larger projects, the multi-part upload flow should be used. + */ + const formData = new FormData(); + + // add manifest + const manifest: Record = {}; + + for (const [filePath, content] of Object.entries(files)) { + const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath; + const hash = await hashContent(content); + manifest[normalizedPath] = hash; + + // add file as blob + formData.append(hash, new Blob([content], { type: 'application/octet-stream' }), normalizedPath); + } + + formData.append('manifest', JSON.stringify(manifest)); + + const deployResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + }, + body: formData, + }, + ); + + if (!deployResponse.ok) { + const errorData = await deployResponse.json(); + console.error('Deployment failed:', errorData); + + return json({ error: 'Deployment failed. Check server logs for details.' }, { status: deployResponse.status }); + } + + const deployResult = (await deployResponse.json()) as { + result?: { url?: string; id?: string }; + }; + const deploymentUrl = deployResult.result?.url || `https://${projectName}.pages.dev`; + + return json({ + success: true, + url: deploymentUrl, + deploymentId: deployResult.result?.id, + }); + } catch (error) { + console.error('Publish error:', error); + return json({ error: 'Internal server error during publish' }, { status: 500 }); + } +} + +/** + * Simple hash function for file content (used for manifest). + */ +async function hashContent(content: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(content); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +} From 262f7fa4910b98310e68922b30e6759a2e97bcf8 Mon Sep 17 00:00:00 2001 From: Yosi Wizman Date: Mon, 5 Jan 2026 18:06:18 -0500 Subject: [PATCH 08/10] fix: correct Cloudflare Pages Direct Upload manifest format - Use empty strings as manifest values (per CF API spec) - Use file paths with leading slash as FormData field names - Add getContentType helper for proper MIME types - Remove unused hashContent function Co-Authored-By: Warp --- app/routes/api.publish.ts | 59 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/app/routes/api.publish.ts b/app/routes/api.publish.ts index 1b806a5ec2..0b61b2eb64 100644 --- a/app/routes/api.publish.ts +++ b/app/routes/api.publish.ts @@ -15,12 +15,9 @@ interface CloudflareEnv { * * Publishes project files to Cloudflare Pages via Direct Upload API. * - * TODO: The Cloudflare Pages Direct Upload API requires a multi-step process: - * 1. Create upload session - * 2. Upload files as form data - * 3. Create deployment - * This MVP implementation provides the endpoint structure - full implementation - * may need adjustment based on actual API requirements. + * NOTE: The Cloudflare Pages Direct Upload API uses FormData with: + * - A "manifest" field containing JSON object mapping file paths to empty strings + * - Individual file fields where field name is the file path and value is file content */ export async function action({ context, request }: ActionFunctionArgs) { const env = context.cloudflare.env as CloudflareEnv; @@ -68,21 +65,23 @@ export async function action({ context, request }: ActionFunctionArgs) { /** * Step 2: Create a deployment with direct upload. * - * Note: This uses the simplified deployment endpoint. - * For larger projects, the multi-part upload flow should be used. + * Cloudflare Pages Direct Upload expects: + * - manifest: JSON object with file paths as keys, empty strings as values + * - individual files: FormData parts with file path as field name */ const formData = new FormData(); - // add manifest + // build manifest with empty string values (per CF API spec) const manifest: Record = {}; for (const [filePath, content] of Object.entries(files)) { - const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath; - const hash = await hashContent(content); - manifest[normalizedPath] = hash; + // normalize path to include leading slash (required by CF Pages) + const normalizedPath = filePath.startsWith('/') ? filePath : `/${filePath}`; + manifest[normalizedPath] = ''; - // add file as blob - formData.append(hash, new Blob([content], { type: 'application/octet-stream' }), normalizedPath); + // add file content - use path as field name + const contentType = getContentType(normalizedPath); + formData.append(normalizedPath, new Blob([content], { type: contentType }), normalizedPath); } formData.append('manifest', JSON.stringify(manifest)); @@ -122,13 +121,29 @@ export async function action({ context, request }: ActionFunctionArgs) { } /** - * Simple hash function for file content (used for manifest). + * Get content type based on file extension. */ -async function hashContent(content: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(content); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - - return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +function getContentType(filePath: string): string { + const ext = filePath.split('.').pop()?.toLowerCase() || ''; + const mimeTypes: Record = { + html: 'text/html', + htm: 'text/html', + css: 'text/css', + js: 'application/javascript', + mjs: 'application/javascript', + json: 'application/json', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + svg: 'image/svg+xml', + ico: 'image/x-icon', + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + txt: 'text/plain', + xml: 'application/xml', + }; + + return mimeTypes[ext] || 'application/octet-stream'; } From 1af7839b5160667adacdb3e5c2e297bcdd203772 Mon Sep 17 00:00:00 2001 From: yosiwizman <135239829+yosiwizman@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:08:38 -0500 Subject: [PATCH 09/10] fix: correct Cloudflare Pages Direct Upload manifest format (#5) - Use empty strings as manifest values (per CF API spec) - Use file paths with leading slash as FormData field names - Add getContentType helper for proper MIME types - Remove unused hashContent function Co-authored-by: Yosi Wizman Co-authored-by: Warp --- app/routes/api.publish.ts | 59 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/app/routes/api.publish.ts b/app/routes/api.publish.ts index 1b806a5ec2..0b61b2eb64 100644 --- a/app/routes/api.publish.ts +++ b/app/routes/api.publish.ts @@ -15,12 +15,9 @@ interface CloudflareEnv { * * Publishes project files to Cloudflare Pages via Direct Upload API. * - * TODO: The Cloudflare Pages Direct Upload API requires a multi-step process: - * 1. Create upload session - * 2. Upload files as form data - * 3. Create deployment - * This MVP implementation provides the endpoint structure - full implementation - * may need adjustment based on actual API requirements. + * NOTE: The Cloudflare Pages Direct Upload API uses FormData with: + * - A "manifest" field containing JSON object mapping file paths to empty strings + * - Individual file fields where field name is the file path and value is file content */ export async function action({ context, request }: ActionFunctionArgs) { const env = context.cloudflare.env as CloudflareEnv; @@ -68,21 +65,23 @@ export async function action({ context, request }: ActionFunctionArgs) { /** * Step 2: Create a deployment with direct upload. * - * Note: This uses the simplified deployment endpoint. - * For larger projects, the multi-part upload flow should be used. + * Cloudflare Pages Direct Upload expects: + * - manifest: JSON object with file paths as keys, empty strings as values + * - individual files: FormData parts with file path as field name */ const formData = new FormData(); - // add manifest + // build manifest with empty string values (per CF API spec) const manifest: Record = {}; for (const [filePath, content] of Object.entries(files)) { - const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath; - const hash = await hashContent(content); - manifest[normalizedPath] = hash; + // normalize path to include leading slash (required by CF Pages) + const normalizedPath = filePath.startsWith('/') ? filePath : `/${filePath}`; + manifest[normalizedPath] = ''; - // add file as blob - formData.append(hash, new Blob([content], { type: 'application/octet-stream' }), normalizedPath); + // add file content - use path as field name + const contentType = getContentType(normalizedPath); + formData.append(normalizedPath, new Blob([content], { type: contentType }), normalizedPath); } formData.append('manifest', JSON.stringify(manifest)); @@ -122,13 +121,29 @@ export async function action({ context, request }: ActionFunctionArgs) { } /** - * Simple hash function for file content (used for manifest). + * Get content type based on file extension. */ -async function hashContent(content: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(content); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - - return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +function getContentType(filePath: string): string { + const ext = filePath.split('.').pop()?.toLowerCase() || ''; + const mimeTypes: Record = { + html: 'text/html', + htm: 'text/html', + css: 'text/css', + js: 'application/javascript', + mjs: 'application/javascript', + json: 'application/json', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + svg: 'image/svg+xml', + ico: 'image/x-icon', + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + txt: 'text/plain', + xml: 'application/xml', + }; + + return mimeTypes[ext] || 'application/octet-stream'; } From f70d6769da8e5f6bcd4a55f7efcb0dff91ea45aa Mon Sep 17 00:00:00 2001 From: Yosi Wizman Date: Mon, 5 Jan 2026 20:33:18 -0500 Subject: [PATCH 10/10] chore: add smoke:publish script Co-Authored-By: Warp --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index afcd1eff64..82538a94fc 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "start": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings", "typecheck": "tsc", "typegen": "wrangler types", - "preview": "pnpm run build && pnpm run start" + "preview": "pnpm run build && pnpm run start", + "smoke:publish": "tsx scripts/smoke-test-publish.ts" }, "engines": { "node": ">=18.18.0"