Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1 | //! Builds one commit of a repository and hands the result to Cloudflare, as |
| 2 | //! a preview of a pull request or as the repository's production. | |
| 3 | //! | |
| 4 | //! The sandbox never holds a Cloudflare credential that could touch anything | |
| 5 | //! else. The deployments service opens an upload for exactly the files | |
| 6 | //! this build produced and gives back a key that can only upload those; | |
| 7 | //! the sandbox uploads them with it, and sends the Worker's code to the | |
| 8 | //! service, which puts the app in place. | |
| 9 | //! | |
| 10 | //! What gets built: | |
| 11 | //! | |
| 12 | //! - A Workers project (a `wrangler.jsonc`, `wrangler.json` or | |
| 13 | //! `wrangler.toml`): bundled by `wrangler deploy --dry-run`, with its | |
| 14 | //! static assets, compatibility settings and `vars`. Other bindings (D1, | |
| 15 | //! KV, R2, Durable Objects…) are not provisioned yet; the deployment says | |
| 16 | //! which were left out. | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 17 | //! - A framework g1t knows (deploy/frameworks.rs: Vite, Astro, Next.js, |
| 18 | //! SvelteKit, Nuxt, Hugo, Jekyll and the rest), found by the package's | |
| 19 | //! dependencies and config files: built with its own command and served | |
| 20 | //! from the folder it writes. A Cloudflare adapter's Worker (Astro's | |
| 21 | //! `dist/_worker.js`, SvelteKit's `.svelte-kit/cloudflare/_worker.js`) is | |
| 22 | //! bundled as a Workers project's is, without a Wrangler config of its | |
| 23 | //! own. A framework set up to need a server fails before anything is | |
| 24 | //! installed, saying what to change. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 25 | //! - Anything else: a static site. Its `build` script runs, and the first |
| 26 | //! of `dist`, `build`, `out`, `public`, `_site` or `.output/public` that | |
| 27 | //! exists is served, or the repository itself if it has an `index.html`. | |
| 28 | //! | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 29 | //! The project's own **Build command** and **Output directory** win over |
| 30 | //! what is detected. | |
| 31 | //! | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 32 | //! Configuration comes from the environment: |
| 33 | //! | |
| 34 | //! - `G1T_API`, `DEPLOY_ID`, `DEPLOY_TOKEN`: where and how to report. | |
| 35 | //! - `GIT_REMOTE`, `GIT_COMMIT`, `G1T_USER`, `G1T_TOKEN`: what to check out. | |
| 36 | //! - `ROOT_DIR`: where in the repository the project lives; empty for all | |
| 37 | //! of it. Everything below is relative to it. | |
| 38 | //! - `BUILD_COMMAND`, `OUTPUT_DIR`: the project's own choices, if any. | |
| 39 | //! - `BUILD_ENV`, `BUILD_SECRETS`: JSON objects of the repository's | |
| 40 | //! variables and secrets for deploy builds. Both are set for the build; | |
| 41 | //! secrets' values are redacted from its log. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 42 | //! |
| 43 | //! From a workflow step, `g1t-runner deploy` runs the same build in the | |
| 44 | //! job's own checkout: without `GIT_REMOTE` nothing is cloned, and the | |
| 45 | //! project is the current directory (`ROOT_DIR` below it). The build's id | |
| 46 | //! and token come from `POST /repos/{owner}/{repo}/deployments/builds`, | |
| 47 | //! which the `g1t/deploy` action calls (actions/deploy). Its output goes | |
| 48 | //! to the step's log as well as the deployment's. `g1t-runner detect` | |
| 49 | //! says what the build would find, as `key=value` lines for | |
| 50 | //! `$GITHUB_OUTPUT`, without building anything. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 51 | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 52 | use std::collections::{BTreeMap, BTreeSet}; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 53 | use std::path::{Path, PathBuf}; |
| 54 | use std::time::Instant; | |
| 55 | ||
| 56 | use anyhow::{Context, Result, bail}; | |
| 57 | use base64::Engine; | |
| 58 | use base64::engine::general_purpose::STANDARD; | |
| 59 | use serde::{Deserialize, Serialize}; | |
| 60 | use serde_json::{Value, json}; | |
| 61 | use sha2::{Digest, Sha256}; | |
| 62 | ||
| 63 | use crate::checks::{redact, run_command}; | |
| 64 | use crate::{WORKDIR, auth_option, env, git}; | |
| 65 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 66 | mod frameworks; |
| 67 | ||
| 68 | use frameworks::{Adapter, Framework}; | |
| 69 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 70 | /// Where `wrangler deploy --dry-run` writes the bundle in a sandbox. |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 71 | const BUNDLE_DIR: &str = "/work/g1t-bundle"; |
| 72 | /// Cloudflare's limits on a Worker's static assets. | |
| 73 | const MAX_FILES: usize = 20_000; | |
| 74 | const MAX_FILE_BYTES: u64 = 25 * 1024 * 1024; | |
| 75 | /// How much of the build's output is kept for the deployment's log. | |
| 76 | const MAX_LOG_CHARS: usize = 20_000; | |
| 77 | /// Directories a static build usually writes to, in the order they are tried. | |
| 78 | const OUTPUT_DIRS: [&str; 6] = ["dist", "build", "out", "public", "_site", ".output/public"]; | |
| 79 | /// Bindings a Workers project may declare that are not provisioned yet. | |
| 80 | const UNSUPPORTED_BINDINGS: [&str; 10] = [ | |
| 81 | "kv_namespaces", | |
| 82 | "d1_databases", | |
| 83 | "r2_buckets", | |
| 84 | "durable_objects", | |
| 85 | "services", | |
| 86 | "queues", | |
| 87 | "vectorize", | |
| 88 | "hyperdrive", | |
| 89 | "ai", | |
| 90 | "workflows", | |
| 91 | ]; | |
| 92 | ||
| 93 | struct Reporter { | |
| 94 | base: String, | |
| 95 | token: String, | |
| 96 | } | |
| 97 | ||
| 98 | impl Reporter { | |
| 99 | fn send(&self, step: &str, mut body: Value) -> Result<Value> { | |
| 100 | body["token"] = self.token.clone().into(); | |
| 101 | let response = ureq::post(&format!("{}/{step}", self.base)) | |
| 102 | .send_json(body) | |
| 103 | .with_context(|| format!("could not report `{step}` to g1t"))?; | |
| 104 | Ok(response.into_json().unwrap_or(Value::Null)) | |
| 105 | } | |
| 106 | } | |
| 107 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 108 | /// The build's log, kept to its end; also printed, in a workflow step. |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 109 | #[derive(Default)] |
| 110 | struct Log { | |
| 111 | text: String, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 112 | /// Each line to standard output as well, redacted. |
| 113 | echo: Option<Vec<String>>, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 114 | } |
| 115 | ||
| 116 | impl Log { | |
| 117 | fn line(&mut self, line: &str) { | |
| 118 | self.text.push_str(line); | |
| 119 | self.text.push('\n'); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 120 | if let Some(secrets) = &self.echo { |
| 121 | println!("{}", redact(line, secrets)); | |
| 122 | } | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 123 | } |
| 124 | ||
| 125 | fn tail(&self) -> String { | |
| 126 | let length = self.text.chars().count(); | |
| 127 | if length <= MAX_LOG_CHARS { | |
| 128 | return self.text.clone(); | |
| 129 | } | |
| 130 | let kept: String = self.text.chars().skip(length - MAX_LOG_CHARS).collect(); | |
| 131 | format!("… (earlier output not shown)\n{kept}") | |
| 132 | } | |
| 133 | } | |
| 134 | ||
| 135 | /// Runs a command in the checkout, logging it; fails if it fails. | |
| 136 | fn step(log: &mut Log, command: &str, secrets: &[String]) -> Result<()> { | |
| 137 | log.line(&format!("$ {command}")); | |
| 138 | let result = run_command(command, &project_dir(), secrets); | |
| 139 | if !result.output_text().is_empty() { | |
| 140 | log.line(result.output_text()); | |
| 141 | } | |
| 142 | if !result.passed { | |
| 143 | bail!("`{command}` failed"); | |
| 144 | } | |
| 145 | Ok(()) | |
| 146 | } | |
| 147 | ||
| 148 | /// A Workers project's settings, from whichever config file it has. | |
| 149 | #[derive(Debug, Default, Deserialize)] | |
| 150 | struct WranglerConfig { | |
| 151 | main: Option<String>, | |
| 152 | compatibility_date: Option<String>, | |
| 153 | #[serde(default)] | |
| 154 | compatibility_flags: Vec<String>, | |
| 155 | assets: Option<AssetsConfig>, | |
| 156 | #[serde(default)] | |
| 157 | vars: BTreeMap<String, Value>, | |
| 158 | #[serde(flatten)] | |
| 159 | rest: BTreeMap<String, Value>, | |
| 160 | } | |
| 161 | ||
| 162 | #[derive(Debug, Default, Deserialize)] | |
| 163 | struct AssetsConfig { | |
| 164 | directory: Option<String>, | |
| 165 | binding: Option<String>, | |
| 166 | html_handling: Option<String>, | |
| 167 | not_found_handling: Option<String>, | |
| 168 | } | |
| 169 | ||
| 170 | /// JSON with comments and trailing commas, as `wrangler.jsonc` allows. | |
| 171 | fn strip_jsonc(text: &str) -> String { | |
| 172 | let mut out = String::with_capacity(text.len()); | |
| 173 | let mut chars = text.chars().peekable(); | |
| 174 | let mut in_string = false; | |
| 175 | while let Some(c) = chars.next() { | |
| 176 | if in_string { | |
| 177 | out.push(c); | |
| 178 | if c == '\\' { | |
| 179 | if let Some(next) = chars.next() { | |
| 180 | out.push(next); | |
| 181 | } | |
| 182 | } else if c == '"' { | |
| 183 | in_string = false; | |
| 184 | } | |
| 185 | continue; | |
| 186 | } | |
| 187 | match (c, chars.peek()) { | |
| 188 | ('"', _) => { | |
| 189 | in_string = true; | |
| 190 | out.push(c); | |
| 191 | } | |
| 192 | ('/', Some('/')) => { | |
| 193 | for c in chars.by_ref() { | |
| 194 | if c == '\n' { | |
| 195 | out.push('\n'); | |
| 196 | break; | |
| 197 | } | |
| 198 | } | |
| 199 | } | |
| 200 | ('/', Some('*')) => { | |
| 201 | chars.next(); | |
| 202 | let mut last = ' '; | |
| 203 | for c in chars.by_ref() { | |
| 204 | if last == '*' && c == '/' { | |
| 205 | break; | |
| 206 | } | |
| 207 | last = c; | |
| 208 | } | |
| 209 | } | |
| 210 | _ => out.push(c), | |
| 211 | } | |
| 212 | } | |
| 213 | // Trailing commas before a closing bracket. | |
| 214 | let mut cleaned = String::with_capacity(out.len()); | |
| 215 | let chars: Vec<char> = out.chars().collect(); | |
| 216 | let mut in_string = false; | |
| 217 | let mut i = 0; | |
| 218 | while i < chars.len() { | |
| 219 | let c = chars[i]; | |
| 220 | if c == '"' && (i == 0 || chars[i - 1] != '\\') { | |
| 221 | in_string = !in_string; | |
| 222 | } | |
| 223 | if c == ',' && !in_string { | |
| 224 | let next = chars[i + 1..].iter().find(|c| !c.is_whitespace()); | |
| 225 | if matches!(next, Some('}') | Some(']')) { | |
| 226 | i += 1; | |
| 227 | continue; | |
| 228 | } | |
| 229 | } | |
| 230 | cleaned.push(c); | |
| 231 | i += 1; | |
| 232 | } | |
| 233 | cleaned | |
| 234 | } | |
| 235 | ||
| 236 | fn read_wrangler(dir: &Path) -> Result<Option<WranglerConfig>> { | |
| 237 | for name in ["wrangler.jsonc", "wrangler.json"] { | |
| 238 | let path = dir.join(name); | |
| 239 | if path.exists() { | |
| 240 | let text = std::fs::read_to_string(&path)?; | |
| 241 | return Ok(Some( | |
| 242 | serde_json::from_str(&strip_jsonc(&text)).with_context(|| format!("could not read {name}"))?, | |
| 243 | )); | |
| 244 | } | |
| 245 | } | |
| 246 | let path = dir.join("wrangler.toml"); | |
| 247 | if path.exists() { | |
| 248 | let text = std::fs::read_to_string(&path)?; | |
| 249 | return Ok(Some(toml::from_str(&text).context("could not read wrangler.toml")?)); | |
| 250 | } | |
| 251 | Ok(None) | |
| 252 | } | |
| 253 | ||
| 254 | /// How to install the project's dependencies, judged by its lockfile. | |
| 255 | fn install_command(dir: &Path) -> Option<&'static str> { | |
| 256 | if !dir.join("package.json").exists() { | |
| 257 | return None; | |
| 258 | } | |
| 259 | Some(if dir.join("pnpm-lock.yaml").exists() { | |
| 260 | "corepack enable && pnpm install --frozen-lockfile" | |
| 261 | } else if dir.join("yarn.lock").exists() { | |
| 262 | "corepack enable && yarn install" | |
| 263 | } else if dir.join("bun.lockb").exists() || dir.join("bun.lock").exists() { | |
| 264 | "npx --yes bun install" | |
| 265 | } else if dir.join("package-lock.json").exists() { | |
| 266 | "npm ci" | |
| 267 | } else { | |
| 268 | "npm install" | |
| 269 | }) | |
| 270 | } | |
| 271 | ||
| 272 | fn has_build_script(dir: &Path) -> bool { | |
| 273 | std::fs::read_to_string(dir.join("package.json")) | |
| 274 | .ok() | |
| 275 | .and_then(|text| serde_json::from_str::<Value>(&text).ok()) | |
| 276 | .is_some_and(|package| package["scripts"]["build"].is_string()) | |
| 277 | } | |
| 278 | ||
| 279 | /// One file of the site, as Cloudflare's asset upload names it. | |
| 280 | struct Asset { | |
| 281 | path: String, | |
| 282 | hash: String, | |
| 283 | size: u64, | |
| 284 | file: PathBuf, | |
| 285 | } | |
| 286 | ||
| 287 | fn content_type(path: &str) -> &'static str { | |
| 288 | let extension = path.rsplit('.').next().unwrap_or("").to_ascii_lowercase(); | |
| 289 | match extension.as_str() { | |
| 290 | "html" | "htm" => "text/html", | |
| 291 | "css" => "text/css", | |
| 292 | "js" | "mjs" => "application/javascript", | |
| 293 | "json" | "map" => "application/json", | |
| 294 | "svg" => "image/svg+xml", | |
| 295 | "png" => "image/png", | |
| 296 | "jpg" | "jpeg" => "image/jpeg", | |
| 297 | "gif" => "image/gif", | |
| 298 | "webp" => "image/webp", | |
| 299 | "avif" => "image/avif", | |
| 300 | "ico" => "image/x-icon", | |
| 301 | "woff" => "font/woff", | |
| 302 | "woff2" => "font/woff2", | |
| 303 | "ttf" => "font/ttf", | |
| 304 | "txt" => "text/plain", | |
| 305 | "xml" => "application/xml", | |
| 306 | "wasm" => "application/wasm", | |
| 307 | "pdf" => "application/pdf", | |
| 308 | "mp4" => "video/mp4", | |
| 309 | "webm" => "video/webm", | |
| 310 | _ => "application/octet-stream", | |
| 311 | } | |
| 312 | } | |
| 313 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 314 | /// What is never served from the top of a site: a Pages-style Worker |
| 315 | /// (`_worker.js`, its server code, which a Cloudflare adapter writes beside | |
| 316 | /// the site's files), `_routes.json`, and `.assetsignore`, with each name | |
| 317 | /// that `.assetsignore` lists, as wrangler leaves them out. | |
| 318 | fn ignored_at_root(root: &Path) -> BTreeSet<String> { | |
| 319 | let mut ignored: BTreeSet<String> = ["_worker.js", "_routes.json", ".assetsignore"].map(str::to_owned).into(); | |
| 320 | if let Ok(text) = std::fs::read_to_string(root.join(".assetsignore")) { | |
| 321 | ignored.extend( | |
| 322 | text.lines() | |
| 323 | .map(|line| line.trim().trim_matches('/')) | |
| 324 | .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.contains(['*', '/', '!'])) | |
| 325 | .map(str::to_owned), | |
| 326 | ); | |
| 327 | } | |
| 328 | ignored | |
| 329 | } | |
| 330 | ||
| 331 | /// Every file of the site at `root`, but for what never belongs in one. | |
| 332 | fn collect_site(root: &Path, skip_project: bool) -> Result<Vec<Asset>> { | |
| 333 | let mut assets = Vec::new(); | |
| 334 | collect(root, root, skip_project, &ignored_at_root(root), &mut assets)?; | |
| 335 | Ok(assets) | |
| 336 | } | |
| 337 | ||
| 338 | fn collect(root: &Path, dir: &Path, skip_project: bool, ignored: &BTreeSet<String>, out: &mut Vec<Asset>) -> Result<()> { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 339 | for entry in std::fs::read_dir(dir)? { |
| 340 | let entry = entry?; | |
| 341 | let name = entry.file_name().to_string_lossy().into_owned(); | |
| 342 | let path = entry.path(); | |
| 343 | let kind = entry.file_type()?; | |
| 344 | if name == ".git" || (skip_project && (name == "node_modules" || name.starts_with(".g1t"))) { | |
| 345 | continue; | |
| 346 | } | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 347 | if dir == root && ignored.contains(&name) { |
| 348 | continue; | |
| 349 | } | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 350 | if kind.is_dir() { |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 351 | collect(root, &path, skip_project, ignored, out)?; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 352 | continue; |
| 353 | } | |
| 354 | if !kind.is_file() || name == "_headers" || name == "_redirects" { | |
| 355 | continue; | |
| 356 | } | |
| 357 | let size = entry.metadata()?.len(); | |
| 358 | let relative = path | |
| 359 | .strip_prefix(root)? | |
| 360 | .to_string_lossy() | |
| 361 | .replace('\\', "/"); | |
| 362 | if size > MAX_FILE_BYTES { | |
| 363 | bail!("{relative} is larger than Cloudflare's 25 MiB limit for one file"); | |
| 364 | } | |
| 365 | let bytes = std::fs::read(&path)?; | |
| 366 | let digest = hex::encode(Sha256::digest(&bytes)); | |
| 367 | out.push(Asset { | |
| 368 | path: format!("/{relative}"), | |
| 369 | hash: digest[..32].to_owned(), | |
| 370 | size, | |
| 371 | file: path, | |
| 372 | }); | |
| 373 | if out.len() > MAX_FILES { | |
| 374 | bail!("the site has more than {MAX_FILES} files, Cloudflare's limit"); | |
| 375 | } | |
| 376 | } | |
| 377 | Ok(()) | |
| 378 | } | |
| 379 | ||
| 380 | /// The upload, as the API sends it: `snake_case`, or `uploadUrl` from | |
| 381 | /// before its bodies were. | |
| 382 | #[derive(Deserialize)] | |
| 383 | struct UploadSession { | |
| 384 | jwt: String, | |
| 385 | #[serde(default)] | |
| 386 | buckets: Vec<Vec<String>>, | |
| 387 | #[serde(alias = "uploadUrl")] | |
| 388 | upload_url: String, | |
| 389 | } | |
| 390 | ||
| 391 | /// Sends one bucket of files with the upload's key. The last bucket's | |
| 392 | /// answer carries the key that completes the upload. | |
| 393 | fn upload_bucket(session: &UploadSession, bucket: &[String], by_hash: &BTreeMap<&str, &Asset>) -> Result<Option<String>> { | |
| 394 | let boundary = format!("g1t-{}", hex::encode(Sha256::digest(bucket.join(",").as_bytes()))[..24].to_owned()); | |
| 395 | let mut body: Vec<u8> = Vec::new(); | |
| 396 | for hash in bucket { | |
| 397 | let asset = by_hash | |
| 398 | .get(hash.as_str()) | |
| 399 | .with_context(|| format!("Cloudflare asked for a file this build does not have ({hash})"))?; | |
| 400 | let bytes = std::fs::read(&asset.file)?; | |
| 401 | body.extend_from_slice( | |
| 402 | format!( | |
| 403 | "--{boundary}\r\nContent-Disposition: form-data; name=\"{hash}\"; filename=\"{hash}\"\r\nContent-Type: {}\r\n\r\n", | |
| 404 | content_type(&asset.path) | |
| 405 | ) | |
| 406 | .as_bytes(), | |
| 407 | ); | |
| 408 | body.extend_from_slice(STANDARD.encode(bytes).as_bytes()); | |
| 409 | body.extend_from_slice(b"\r\n"); | |
| 410 | } | |
| 411 | body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes()); | |
| 412 | let response = ureq::post(&session.upload_url) | |
| 413 | .set("authorization", &format!("Bearer {}", session.jwt)) | |
| 414 | .set("content-type", &format!("multipart/form-data; boundary={boundary}")) | |
| 415 | .send_bytes(&body); | |
| 416 | let response = match response { | |
| 417 | Ok(response) => response, | |
| 418 | Err(ureq::Error::Status(code, response)) => { | |
| 419 | bail!("Cloudflare refused the upload ({code}): {}", response.into_string().unwrap_or_default()) | |
| 420 | } | |
| 421 | Err(error) => bail!("could not upload to Cloudflare: {error}"), | |
| 422 | }; | |
| 423 | let answer: Value = response.into_json().unwrap_or(Value::Null); | |
| 424 | Ok(answer["result"]["jwt"].as_str().map(str::to_owned)) | |
| 425 | } | |
| 426 | ||
| 427 | #[derive(Serialize)] | |
| 428 | #[serde(rename_all = "camelCase")] | |
| 429 | struct Module { | |
| 430 | name: String, | |
| 431 | content_base64: String, | |
| 432 | content_type: String, | |
| 433 | } | |
| 434 | ||
| 435 | /// The bundle `wrangler deploy --dry-run` wrote, main module first. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 436 | fn bundle_modules(main: &str, bundle: &Path) -> Result<(String, Vec<Module>)> { |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 437 | let stem = Path::new(main) |
| 438 | .file_stem() | |
| 439 | .map(|stem| stem.to_string_lossy().into_owned()) | |
| 440 | .unwrap_or_else(|| "index".to_owned()); | |
| 441 | let mut modules = Vec::new(); | |
| 442 | let mut files = Vec::new(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 443 | collect_files(bundle, &mut files)?; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 444 | for file in files { |
| 445 | let name = file | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 446 | .strip_prefix(bundle)? |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 447 | .to_string_lossy() |
| 448 | .replace('\\', "/"); | |
| 449 | let kind = match name.rsplit('.').next().unwrap_or("") { | |
| 450 | "js" | "mjs" => "application/javascript+module", | |
| 451 | "wasm" => "application/wasm", | |
| 452 | "map" | "md" => continue, | |
| 453 | _ => "text/plain", | |
| 454 | }; | |
| 455 | modules.push(Module { | |
| 456 | content_base64: STANDARD.encode(std::fs::read(&file)?), | |
| 457 | content_type: kind.to_owned(), | |
| 458 | name, | |
| 459 | }); | |
| 460 | } | |
| 461 | let main_name = modules | |
| 462 | .iter() | |
| 463 | .map(|module| module.name.clone()) | |
| 464 | .find(|name| *name == format!("{stem}.js") || *name == format!("{stem}.mjs")) | |
| 465 | .or_else(|| { | |
| 466 | modules | |
| 467 | .iter() | |
| 468 | .find(|module| module.content_type == "application/javascript+module") | |
| 469 | .map(|module| module.name.clone()) | |
| 470 | }) | |
| 471 | .context("wrangler wrote no JavaScript module")?; | |
| 472 | Ok((main_name, modules)) | |
| 473 | } | |
| 474 | ||
| 475 | fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> { | |
| 476 | for entry in std::fs::read_dir(dir)? { | |
| 477 | let entry = entry?; | |
| 478 | if entry.file_type()?.is_dir() { | |
| 479 | collect_files(&entry.path(), out)?; | |
| 480 | } else { | |
| 481 | out.push(entry.path()); | |
| 482 | } | |
| 483 | } | |
| 484 | Ok(()) | |
| 485 | } | |
| 486 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 487 | /// What a Workers config asks for that g1t.page does not provision, said |
| 488 | /// as the deployment's warnings: bindings it runs without, and scheduled | |
| 489 | /// (`triggers.crons`) runs that never fire. | |
| 490 | fn config_warnings(config: &WranglerConfig) -> Vec<String> { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 491 | // An empty list, as a config a framework's build writes has for each |
| 492 | // kind of binding (`"d1_databases": []`), declares nothing. | |
| 493 | fn declares(value: &Value) -> bool { | |
| 494 | match value { | |
| 495 | Value::Null => false, | |
| 496 | Value::Array(items) => !items.is_empty(), | |
| 497 | Value::Object(fields) => fields.values().any(declares), | |
| 498 | _ => true, | |
| 499 | } | |
| 500 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 501 | let mut warnings = Vec::new(); |
| 502 | for binding in UNSUPPORTED_BINDINGS { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 503 | if config.rest.get(binding).is_some_and(declares) { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 504 | warnings.push(format!("`{binding}` is not provisioned on g1t.page yet, so the app runs without it.")); |
| 505 | } | |
| 506 | } | |
| 507 | let crons = config | |
| 508 | .rest | |
| 509 | .get("triggers") | |
| 510 | .and_then(|triggers| triggers.get("crons")) | |
| 511 | .and_then(Value::as_array) | |
| 512 | .map_or(0, |crons| crons.iter().filter(|cron| cron.as_str().is_some_and(|c| !c.trim().is_empty())).count()); | |
| 513 | if crons > 0 { | |
| 514 | let schedules = if crons == 1 { "schedule" } else { "schedules" }; | |
| 515 | warnings.push(format!( | |
| 516 | "`triggers.crons` ({crons} {schedules}) is not set up on g1t.page yet, so the Worker's `scheduled` handler never runs." | |
| 517 | )); | |
| 518 | } | |
| 519 | warnings | |
| 520 | } | |
| 521 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 522 | /// What was built: the Worker's code and settings, and where its site is. |
| 523 | struct Built { | |
| 524 | worker: Value, | |
| 525 | assets_dir: Option<PathBuf>, | |
| 526 | warnings: Vec<String>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 527 | /// What kind of project it was found to be, for its settings page: |
| 528 | /// `workers`, `static` (a build's output) or `html` (the files as they are). | |
| 529 | detected: &'static str, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 530 | /// The framework it was found to be built with, if any (`astro`…). |
| 531 | framework: Option<&'static str>, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 532 | } |
| 533 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 534 | /// What a project without a Workers config is: a site it built, or its own |
| 535 | /// files served as they are. | |
| 536 | fn static_kind(project: &Path, assets_dir: &Path, built: bool) -> &'static str { | |
| 537 | if !built && assets_dir == project { "html" } else { "static" } | |
| 538 | } | |
| 539 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 540 | /// The command that bundles a Workers project, without deploying it, |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 541 | /// into `bundle` when given, by the config at `config` when given. |
| 542 | fn wrangler_build(bundle: Option<&Path>, config: Option<&Path>) -> String { | |
| 543 | let mut command = "npx --yes wrangler@4 deploy --dry-run".to_owned(); | |
| 544 | if let Some(config) = config { | |
| 545 | command.push_str(&format!(" --config {}", config.display())); | |
| 546 | } | |
| 547 | if let Some(bundle) = bundle { | |
| 548 | command.push_str(&format!(" --outdir {}", bundle.display())); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 549 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 550 | command |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 551 | } |
| 552 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 553 | /// The config a framework's build wrote for wrangler, which wrangler reads |
| 554 | /// in place of the project's own (`.wrangler/deploy/config.json`, as | |
| 555 | /// `@cloudflare/vite-plugin` writes it), with the folder its paths are | |
| 556 | /// relative to. | |
| 557 | fn redirected_config(dir: &Path) -> Result<Option<(WranglerConfig, PathBuf)>> { | |
| 558 | let pointer = dir.join(".wrangler/deploy/config.json"); | |
| 559 | let Ok(text) = std::fs::read_to_string(&pointer) else { return Ok(None) }; | |
| 560 | let Some(path) = serde_json::from_str::<Value>(&text).ok().and_then(|v| v["configPath"].as_str().map(str::to_owned)) else { | |
| 561 | return Ok(None); | |
| 562 | }; | |
| 563 | let path = dir.join(".wrangler/deploy").join(path); | |
| 564 | let path = path.canonicalize().unwrap_or(path); | |
| 565 | let text = std::fs::read_to_string(&path).with_context(|| format!("could not read {}", path.display()))?; | |
| 566 | let config = serde_json::from_str(&strip_jsonc(&text)).with_context(|| format!("could not read {}", path.display()))?; | |
| 567 | Ok(Some((config, path.parent().map_or_else(|| dir.to_path_buf(), Path::to_path_buf)))) | |
| 568 | } | |
| 569 | ||
| 570 | /// The entry of the Worker a framework's adapter wrote, once it has built: | |
| 571 | /// the file, or a directory's `index.js`. | |
| 572 | fn adapter_entry(dir: &Path, adapter: &Adapter) -> Option<PathBuf> { | |
| 573 | let entry = dir.join(adapter.entry); | |
| 574 | if entry.is_file() { | |
| 575 | return Some(entry); | |
| 576 | } | |
| 577 | ["index.js", "index.mjs"].iter().map(|name| entry.join(name)).find(|index| index.is_file()) | |
| 578 | } | |
| 579 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 580 | /// The first of the usual output directories a static build wrote (or the |
| 581 | /// project's own `index.html`), relative to the project: `.` for itself. | |
| 582 | fn find_output(dir: &Path) -> Option<String> { | |
| 583 | OUTPUT_DIRS | |
| 584 | .iter() | |
| 585 | .find(|name| { | |
| 586 | let path = dir.join(name); | |
| 587 | path.join("index.html").exists() || (path.is_dir() && **name != "public") | |
| 588 | }) | |
| 589 | .map(|name| (*name).to_owned()) | |
| 590 | .or_else(|| dir.join("index.html").exists().then(|| ".".to_owned())) | |
| 591 | } | |
| 592 | ||
| 593 | /// What a build of the project at `dir` does, found before anything runs: | |
| 594 | /// what it is, how its dependencies are installed, what builds it, and | |
| 595 | /// where what it serves is. For a site with a build of its own, the output | |
| 596 | /// directory may only exist once it has run; then `output_dir` is `None` | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 597 | /// unless one was chosen or the framework says where it writes. |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 598 | #[derive(Debug, PartialEq, Eq)] |
| 599 | pub(crate) struct Detected { | |
| 600 | /// `workers`, `static`, `html`, or `none` when there is nothing to serve. | |
| 601 | pub kind: &'static str, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 602 | /// The framework it is built with, if g1t knows it. |
| 603 | pub framework: Option<Framework>, | |
| 604 | pub install: Option<String>, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 605 | pub build: Option<String>, |
| 606 | pub output_dir: Option<String>, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 607 | /// Why it cannot be served as it is set up, and what to change: the |
| 608 | /// kind is then `none`. | |
| 609 | pub problem: Option<String>, | |
| 610 | /// What runs before wrangler bundles a Workers project; for anything | |
| 611 | /// else, `build`. | |
| 612 | pre_build: Option<String>, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 613 | } |
| 614 | ||
| 615 | pub(crate) fn detect(dir: &Path, custom_build: Option<&str>, chosen_output: Option<&str>) -> Result<Detected> { | |
| 616 | let custom_build = custom_build.map(str::trim).filter(|c| !c.is_empty()); | |
| 617 | let chosen_output = chosen_output.map(|d| d.trim().trim_matches('/')).filter(|d| !d.is_empty()); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 618 | let config = read_wrangler(dir)?; |
| 619 | let framework = frameworks::detect(dir, config.is_some()); | |
| 620 | let installs: Vec<&str> = [install_command(dir), framework.as_ref().and_then(|f| f.install)].into_iter().flatten().collect(); | |
| 621 | let install = (!installs.is_empty()).then(|| installs.join(" && ")); | |
| 622 | let framework_build = framework.as_ref().and_then(|f| f.build.clone()); | |
| 623 | if let Some(config) = config { | |
| 624 | // A framework built for Workers builds before wrangler bundles what | |
| 625 | // it wrote; any other project's config builds it, as it always has. | |
| 626 | let pre_build = custom_build | |
| 627 | .map(str::to_owned) | |
| 628 | .or_else(|| framework.as_ref().filter(|f| f.workers).and(framework_build)); | |
| 629 | let mut steps: Vec<String> = pre_build.clone().into_iter().collect(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 630 | if config.main.is_some() { |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 631 | steps.push(wrangler_build(None, None)); |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 632 | } |
| 633 | return Ok(Detected { | |
| 634 | kind: "workers", | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 635 | framework, |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 636 | install, |
| 637 | build: (!steps.is_empty()).then(|| steps.join(" && ")), | |
| 638 | output_dir: config.assets.and_then(|assets| assets.directory), | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 639 | problem: None, |
| 640 | pre_build, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 641 | }); |
| 642 | } | |
| 643 | let build = custom_build | |
| 644 | .map(str::to_owned) | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 645 | .or(framework_build) |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 646 | .or_else(|| has_build_script(dir).then(|| "npm run build".to_owned())); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 647 | // An adapter's Worker, unless the project chose a folder to serve. |
| 648 | if chosen_output.is_none() && framework.as_ref().is_some_and(|f| f.adapter.is_some()) { | |
| 649 | // Where its files are is known once it has built: the adapter's | |
| 650 | // own folder, or where the config it writes says. | |
| 651 | return Ok(Detected { | |
| 652 | kind: "workers", | |
| 653 | output_dir: None, | |
| 654 | framework, | |
| 655 | install, | |
| 656 | pre_build: build.clone(), | |
| 657 | build, | |
| 658 | problem: None, | |
| 659 | }); | |
| 660 | } | |
| 661 | // A command or a folder of the project's own says it knows better. | |
| 662 | let problem = framework | |
| 663 | .as_ref() | |
| 664 | .and_then(|f| f.problem.clone()) | |
| 665 | .filter(|_| custom_build.is_none() && chosen_output.is_none()); | |
| 666 | if problem.is_some() { | |
| 667 | return Ok(Detected { kind: "none", framework, install, build: None, output_dir: None, problem, pre_build: None }); | |
| 668 | } | |
| 669 | let output_dir = chosen_output | |
| 670 | .map(str::to_owned) | |
| 671 | .or_else(|| framework.as_ref().and_then(|f| f.output.clone())) | |
| 672 | .or_else(|| find_output(dir)); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 673 | // `.` is the project itself: its own files, served as they are. |
| 674 | let kind = match (&build, output_dir.as_deref()) { | |
| 675 | (None, Some(".")) => "html", | |
| 676 | (None, None) => "none", | |
| 677 | _ => "static", | |
| 678 | }; | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 679 | Ok(Detected { kind, framework, install, pre_build: build.clone(), build, output_dir, problem: None }) |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 680 | } |
| 681 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 682 | /// The compatibility date a Worker gets when its config names none. |
| 683 | const COMPATIBILITY_DATE: &str = "2026-09-26"; | |
| 684 | ||
| 685 | /// A Workers project bundled by wrangler: `config` with its paths relative | |
| 686 | /// to `base`, read by wrangler from `config_file` when given (else the | |
| 687 | /// project's own, or the one its build wrote). | |
| 688 | fn build_worker( | |
| 689 | log: &mut Log, | |
| 690 | secrets: &[String], | |
| 691 | config: WranglerConfig, | |
| 692 | base: &Path, | |
| 693 | config_file: Option<&Path>, | |
| 694 | ) -> Result<(Value, Option<PathBuf>, Vec<String>)> { | |
| 695 | let bundle = bundle_dir(); | |
| 696 | let warnings = config_warnings(&config); | |
| 697 | let mut worker = json!({ | |
| 698 | "compatibilityDate": config.compatibility_date.clone().unwrap_or_else(|| COMPATIBILITY_DATE.to_owned()), | |
| 699 | "compatibilityFlags": config.compatibility_flags, | |
| 700 | "vars": config.vars, | |
| 701 | }); | |
| 702 | if let Some(main) = &config.main { | |
| 703 | // `--dry-run` runs the config's own build and bundles it, without | |
| 704 | // deploying anywhere. | |
| 705 | let _ = std::fs::remove_dir_all(&bundle); | |
| 706 | step(log, &wrangler_build(Some(&bundle), config_file), secrets)?; | |
| 707 | let (main_module, modules) = bundle_modules(main, &bundle)?; | |
| 708 | worker["mainModule"] = main_module.into(); | |
| 709 | worker["modules"] = serde_json::to_value(modules)?; | |
| 710 | } | |
| 711 | let assets = config.assets.unwrap_or_default(); | |
| 712 | let assets_dir = assets.directory.as_ref().map(|directory| base.join(directory)); | |
| 713 | worker["assetsBinding"] = assets.binding.into(); | |
| 714 | worker["htmlHandling"] = assets.html_handling.into(); | |
| 715 | worker["notFoundHandling"] = assets.not_found_handling.into(); | |
| 716 | Ok((worker, assets_dir, warnings)) | |
| 717 | } | |
| 718 | ||
| 719 | /// Where the config for an adapter's Worker is written: beside the | |
| 720 | /// bundle, never in the checkout. | |
| 721 | fn adapter_config_path() -> PathBuf { | |
| 722 | let bundle = bundle_dir(); | |
| 723 | bundle.with_file_name(format!("{}.wrangler.json", bundle.file_name().unwrap_or_default().to_string_lossy())) | |
| 724 | } | |
| 725 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 726 | fn build(log: &mut Log, secrets: &[String]) -> Result<Built> { |
| 727 | let project = project_dir(); | |
| 728 | let dir = project.as_path(); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 729 | let custom_build = std::env::var("BUILD_COMMAND").ok(); |
| 730 | let chosen = std::env::var("OUTPUT_DIR").ok().filter(|d| !d.trim().is_empty()); | |
| 731 | let plan = detect(dir, custom_build.as_deref(), chosen.as_deref())?; | |
| 732 | let framework_id = plan.framework.as_ref().map(|f| f.id); | |
| 733 | if let Some(framework) = &plan.framework { | |
| 734 | log.line(&format!("Detected {}: framework={}.", framework.name, framework.id)); | |
| 735 | } | |
| 736 | // Said before anything is installed: it would fail after, less clearly. | |
| 737 | if let Some(problem) = &plan.problem { | |
| 738 | bail!("{problem}"); | |
| 739 | } | |
| 740 | if let Some(install) = &plan.install { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 741 | step(log, install, secrets)?; |
| 742 | } | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 743 | if let Some(command) = &plan.pre_build { |
| 744 | step(log, command, secrets)?; | |
| 745 | } | |
| 746 | let own_config = read_wrangler(dir)?; | |
| 747 | // A framework built for Workers may write the config wrangler reads | |
| 748 | // (`@cloudflare/vite-plugin` does, and Astro's adapter with it), with | |
| 749 | // or without one of the project's own. | |
| 750 | let written = if own_config.is_some() || plan.framework.as_ref().is_some_and(|f| f.workers) { | |
| 751 | redirected_config(dir)? | |
| 752 | } else { | |
| 753 | None | |
| 754 | }; | |
| 755 | if let Some((config, base)) = written { | |
| 756 | log.line(&format!("Using the Workers config the build wrote, in {}.", base.display())); | |
| 757 | let (worker, assets_dir, warnings) = build_worker(log, secrets, config, &base, None)?; | |
| 758 | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id }); | |
| 759 | } | |
| 760 | if let Some(config) = own_config { | |
| 761 | let (worker, assets_dir, warnings) = build_worker(log, secrets, config, dir, None)?; | |
| 762 | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id }); | |
| 763 | } | |
| 764 | let adapter = plan.framework.as_ref().and_then(|f| f.adapter.as_ref()).filter(|_| chosen.is_none()); | |
| 765 | if let Some(adapter) = adapter | |
| 766 | && let Some(entry) = adapter_entry(dir, adapter) | |
| 767 | { | |
| 768 | // The adapter's Worker, bundled as a Workers project's is. Its | |
| 769 | // static files are uploaded by g1t, so wrangler is given none. | |
| 770 | let file = adapter_config_path(); | |
| 771 | let flags = vec!["nodejs_compat".to_owned()]; | |
| 772 | std::fs::write( | |
| 773 | &file, | |
| 774 | serde_json::to_string_pretty(&json!({ | |
| 775 | "name": "g1t-app", | |
| 776 | "main": entry, | |
| 777 | "compatibility_date": COMPATIBILITY_DATE, | |
| 778 | "compatibility_flags": flags, | |
| 779 | }))?, | |
| 780 | )?; | |
| 781 | log.line(&format!("Bundling the Worker the adapter wrote, {}.", adapter.entry)); | |
| 782 | let config = WranglerConfig { | |
| 783 | main: Some(entry.to_string_lossy().into_owned()), | |
| 784 | compatibility_flags: flags, | |
| 785 | assets: Some(AssetsConfig { | |
| 786 | directory: Some(adapter.assets.to_owned()), | |
| 787 | binding: Some("ASSETS".to_owned()), | |
| 788 | ..AssetsConfig::default() | |
| 789 | }), | |
| 790 | ..WranglerConfig::default() | |
| 791 | }; | |
| 792 | let built = build_worker(log, secrets, config, dir, Some(&file)); | |
| 793 | let _ = std::fs::remove_file(&file); | |
| 794 | let (worker, assets_dir, warnings) = built?; | |
| 795 | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id }); | |
| 796 | } | |
| 797 | if let (Some(adapter), Some(framework)) = (adapter, &plan.framework) | |
| 798 | && framework.output.is_none() | |
| 799 | { | |
| 800 | bail!("{}'s build did not write its Worker, `{}`", framework.name, adapter.entry); | |
| 801 | } | |
| 802 | let assets_dir = match &chosen { | |
| 803 | Some(chosen) => { | |
| 804 | let path = dir.join(chosen.trim().trim_matches('/')); | |
| 805 | if !path.is_dir() { | |
| 806 | bail!("the output directory `{chosen}` does not exist after the build"); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 807 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 808 | path |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 809 | } |
| 810 | None => { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 811 | let expected = plan.framework.as_ref().and_then(|f| f.output.as_deref()); |
| 812 | match expected.filter(|output| dir.join(output).is_dir()) { | |
| 813 | Some(output) => dir.join(output), | |
| 814 | None => { | |
| 815 | let found = find_output(dir).with_context(|| match (&plan.framework, expected) { | |
| 816 | (Some(framework), Some(expected)) => format!( | |
| 817 | "found nothing to serve: {} writes its site to `{expected}`, which the build did not write. \ | |
| 818 | Set **Output directory** if it writes somewhere else.", | |
| 819 | framework.name | |
| 820 | ), | |
| 821 | (Some(framework), None) if framework.problem.is_some() => { | |
| 822 | format!("found nothing to serve. {}", framework.problem.as_deref().unwrap_or_default()) | |
| 823 | } | |
| 824 | _ => "found nothing to serve: no Workers config, no index.html, and none of dist, build, out, public, _site or .output/public".to_owned(), | |
| 825 | })?; | |
| 826 | if let Some(expected) = expected { | |
| 827 | log.line(&format!("`{expected}` was not written; serving `{found}`.")); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 828 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 829 | if found == "." { dir.to_path_buf() } else { dir.join(found) } |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 830 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 831 | } |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 832 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 833 | }; |
| 834 | let spa = !assets_dir.join("404.html").exists(); | |
| 835 | Ok(Built { | |
| 836 | worker: json!({ | |
| 837 | "compatibilityDate": COMPATIBILITY_DATE, | |
| 838 | "compatibilityFlags": [], | |
| 839 | "vars": {}, | |
| 840 | "notFoundHandling": if spa { "single-page-application" } else { "404-page" }, | |
| 841 | }), | |
| 842 | detected: static_kind(dir, &assets_dir, plan.build.is_some()), | |
| 843 | assets_dir: Some(assets_dir), | |
| 844 | warnings: Vec::new(), | |
| 845 | framework: framework_id, | |
| 846 | }) | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 847 | } |
| 848 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 849 | /// Whether the build checks out its own commit (a sandbox, given |
| 850 | /// `GIT_REMOTE`) rather than building the checkout it runs in. | |
| 851 | fn clones() -> bool { | |
| 852 | std::env::var("GIT_REMOTE").is_ok_and(|remote| !remote.trim().is_empty()) | |
| 853 | } | |
| 854 | ||
| 855 | /// The checkout the build runs in: the sandbox's clone, or, from a | |
| 856 | /// workflow step, the current directory. | |
| 857 | fn checkout_dir() -> PathBuf { | |
| 858 | if clones() { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 859 | PathBuf::from(WORKDIR) |
| 860 | } else { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 861 | std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) |
| 862 | } | |
| 863 | } | |
| 864 | ||
| 865 | /// Where `wrangler deploy --dry-run` writes the bundle: in a workflow | |
| 866 | /// step, a directory of the job's temporary space, never the checkout. | |
| 867 | fn bundle_dir() -> PathBuf { | |
| 868 | if clones() { | |
| 869 | return PathBuf::from(BUNDLE_DIR); | |
| 870 | } | |
| 871 | let base = std::env::var_os("RUNNER_TEMP").map(PathBuf::from).unwrap_or_else(std::env::temp_dir); | |
| 872 | base.join(format!("g1t-bundle-{}", std::process::id())) | |
| 873 | } | |
| 874 | ||
| 875 | /// Where the project lives in the checkout `base`: its root directory, | |
| 876 | /// never above the checkout. | |
| 877 | fn project_dir_in(base: &Path, root: &str) -> PathBuf { | |
| 878 | let root = root.trim().trim_matches('/'); | |
| 879 | if root.is_empty() || root == "." || root.split('/').any(|part| part == "..") { | |
| 880 | base.to_path_buf() | |
| 881 | } else { | |
| 882 | base.join(root) | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 883 | } |
| 884 | } | |
| 885 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 886 | /// Where the project lives: `ROOT_DIR` in the checkout. |
| 887 | fn project_dir() -> PathBuf { | |
| 888 | project_dir_in(&checkout_dir(), &std::env::var("ROOT_DIR").unwrap_or_default()) | |
| 889 | } | |
| 890 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 891 | fn check_out(secrets: &[String]) -> Result<()> { |
| 892 | let remote = env("GIT_REMOTE")?; | |
| 893 | let commit = env("GIT_COMMIT")?; | |
| 894 | let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?); | |
| 895 | std::fs::create_dir_all("/work")?; | |
| Previews build from any branch; Agent in place of Ask AI; pin from the sidebar | 896 | let workdir = Path::new(WORKDIR); |
| 897 | // The clone is shallow and of the default branch; a preview's commit is | |
| 898 | // often on another one, so it is fetched before checking out. | |
| 899 | let cloned = crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR) | |
| 900 | .and_then(|_| git(workdir, &["rev-parse", "--abbrev-ref", "HEAD"])) | |
| 901 | .and_then(|branch| crate::clone::ensure(workdir, &auth, "origin", &branch, &commit)) | |
| 902 | .and_then(|_| git(workdir, &["-c", "advice.detachedHead=false", "checkout", "--quiet", &commit])); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 903 | if let Err(error) = cloned { |
| 904 | bail!("{}", redact(&format!("{error:#}"), secrets)); | |
| 905 | } | |
| 906 | Ok(()) | |
| 907 | } | |
| 908 | ||
| 909 | fn deploy(reporter: &Reporter, log: &mut Log, secrets: &[String]) -> Result<Value> { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 910 | if clones() { |
| 911 | check_out(secrets).context("the commit could not be checked out")?; | |
| 912 | } else { | |
| 913 | let dir = project_dir(); | |
| 914 | if !dir.is_dir() { | |
| 915 | bail!("the project's directory {} does not exist in this checkout", dir.display()); | |
| 916 | } | |
| 917 | log.line(&format!("Building {} as it is checked out.", dir.display())); | |
| 918 | } | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 919 | // The repository's variables and secrets for deploy builds. |
| 920 | for source in ["BUILD_ENV", "BUILD_SECRETS"] { | |
| 921 | if let Ok(vars) = std::env::var(source) | |
| 922 | && let Ok(Value::Object(vars)) = serde_json::from_str::<Value>(&vars) | |
| 923 | { | |
| 924 | for (name, value) in vars { | |
| 925 | if let Some(value) = value.as_str() { | |
| 926 | // SAFETY: single-threaded; set before any command runs. | |
| 927 | unsafe { std::env::set_var(name, value) }; | |
| 928 | } | |
| 929 | } | |
| 930 | } | |
| 931 | } | |
| 932 | let built = build(log, secrets)?; | |
| 933 | let mut finish = json!({ | |
| 934 | "worker": built.worker, | |
| 935 | "warnings": built.warnings, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 936 | "detected": built.detected, |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 937 | // Not stored by the deployments service yet, which ignores fields |
| 938 | // it does not know; the build's log names it too. | |
| 939 | "framework": built.framework, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 940 | }); |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 941 | for warning in &built.warnings { |
| 942 | log.line(&format!("Warning: {warning}")); | |
| 943 | } | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 944 | if let Some(dir) = &built.assets_dir { |
| 945 | let skip_project = *dir == project_dir(); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 946 | let assets = collect_site(dir, skip_project)?; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 947 | if assets.is_empty() { |
| 948 | bail!("the site to serve is empty"); | |
| 949 | } | |
| 950 | log.line(&format!("Uploading {} files.", assets.len())); | |
| 951 | for special in ["_headers", "_redirects"] { | |
| 952 | if let Ok(text) = std::fs::read_to_string(dir.join(special)) { | |
| 953 | finish["worker"][special] = text.into(); | |
| 954 | } | |
| 955 | } | |
| 956 | let manifest: BTreeMap<&str, Value> = assets | |
| 957 | .iter() | |
| 958 | .map(|asset| (asset.path.as_str(), json!({ "hash": asset.hash, "size": asset.size }))) | |
| 959 | .collect(); | |
| 960 | let answer = reporter.send("session", json!({ "manifest": manifest }))?; | |
| 961 | if answer["ok"] == false { | |
| 962 | bail!("{}", answer["error"]["message"].as_str().unwrap_or("g1t refused the upload")); | |
| 963 | } | |
| 964 | let session: UploadSession = | |
| 965 | serde_json::from_value(answer["value"].clone()).context("g1t's answer to the upload was not understood")?; | |
| 966 | let by_hash: BTreeMap<&str, &Asset> = assets.iter().map(|asset| (asset.hash.as_str(), asset)).collect(); | |
| 967 | let mut completion = session.jwt.clone(); | |
| 968 | for bucket in &session.buckets { | |
| 969 | if let Some(jwt) = upload_bucket(&session, bucket, &by_hash)? { | |
| 970 | completion = jwt; | |
| 971 | } | |
| 972 | } | |
| 973 | finish["completionJwt"] = completion.into(); | |
| 974 | } | |
| 975 | Ok(finish) | |
| 976 | } | |
| 977 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 978 | /// A sandbox's build (`MODE=deploy`). |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 979 | pub fn main() -> i32 { |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 980 | run(false) |
| 981 | } | |
| 982 | ||
| 983 | /// `g1t-runner deploy`, from a workflow step: the build in the job's own | |
| 984 | /// checkout, its log in the step's as well. Exits 1 when the build fails, | |
| 985 | /// after reporting it, so the step fails too. | |
| 986 | pub fn cli(args: &[String]) -> i32 { | |
| 987 | if args.iter().any(|arg| arg == "--help" || arg == "-h") { | |
| 988 | println!("{CLI_HELP}"); | |
| 989 | return 0; | |
| 990 | } | |
| 991 | run(true) | |
| 992 | } | |
| 993 | ||
| 994 | const CLI_HELP: &str = "g1t-runner deploy: builds the project in this checkout and puts it on g1t.page. | |
| 995 | ||
| 996 | Reads DEPLOY_ID and DEPLOY_TOKEN (from POST /repos/{owner}/{repo}/deployments/builds), | |
| 997 | G1T_API (https://api.g1t.sh), and optionally ROOT_DIR, BUILD_COMMAND, OUTPUT_DIR, | |
| 998 | BUILD_ENV and BUILD_SECRETS (JSON objects). The g1t/deploy action sets them all. | |
| 999 | ||
| 1000 | g1t-runner detect prints what the build would find, as key=value lines."; | |
| 1001 | ||
| 1002 | fn run(step_log: bool) -> i32 { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1003 | let reporter = match (env("G1T_API"), env("DEPLOY_ID"), env("DEPLOY_TOKEN")) { |
| 1004 | (Ok(api), Ok(id), Ok(token)) => Reporter { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1005 | base: format!("{}/deployments/jobs/{id}", api.trim_end_matches('/')), |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1006 | token, |
| 1007 | }, | |
| 1008 | _ => { | |
| 1009 | eprintln!("g1t-runner: G1T_API, DEPLOY_ID and DEPLOY_TOKEN must be set"); | |
| 1010 | return 2; | |
| 1011 | } | |
| 1012 | }; | |
| 1013 | let mut secrets: Vec<String> = ["G1T_TOKEN", "DEPLOY_TOKEN"] | |
| 1014 | .iter() | |
| 1015 | .filter_map(|name| std::env::var(name).ok()) | |
| 1016 | .filter(|secret| !secret.is_empty()) | |
| 1017 | .collect(); | |
| 1018 | // The repository's build secrets never appear in the log. | |
| 1019 | if let Ok(Value::Object(build)) = serde_json::from_str::<Value>(&std::env::var("BUILD_SECRETS").unwrap_or_default()) { | |
| 1020 | secrets.extend(build.values().filter_map(Value::as_str).filter(|v| v.len() >= 4).map(str::to_owned)); | |
| 1021 | } | |
| 1022 | if let Err(error) = reporter.send("started", json!({})) { | |
| 1023 | eprintln!("g1t-runner: {error:#}"); | |
| 1024 | return 1; | |
| 1025 | } | |
| 1026 | let started = Instant::now(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1027 | let mut log = Log { echo: step_log.then(|| secrets.clone()), ..Log::default() }; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1028 | let outcome = deploy(&reporter, &mut log, &secrets); |
| 1029 | let seconds = started.elapsed().as_secs(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1030 | if step_log { |
| 1031 | let _ = std::fs::remove_dir_all(bundle_dir()); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1032 | let _ = std::fs::remove_file(adapter_config_path()); |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1033 | } |
| 1034 | let (sent, failed) = match outcome { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1035 | Ok(mut finish) => { |
| 1036 | finish["log"] = redact(&log.tail(), &secrets).into(); | |
| 1037 | finish["buildSeconds"] = seconds.into(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1038 | (reporter.send("finish", finish), false) |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1039 | } |
| 1040 | Err(error) => { | |
| 1041 | let message = redact(&format!("{error:#}"), &secrets); | |
| 1042 | log.line(&format!("The build failed: {message}")); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1043 | let sent = reporter.send( |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1044 | "fail", |
| 1045 | json!({ "message": message, "log": redact(&log.tail(), &secrets), "buildSeconds": seconds }), | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1046 | ); |
| 1047 | (sent, true) | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1048 | } |
| 1049 | }; | |
| 1050 | match sent { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1051 | // A sandbox's build has said how it ended; a step fails with its build. |
| 1052 | Ok(answer) if step_log && (failed || answer["ok"] == false || answer["value"] == false) => { | |
| 1053 | if !failed { | |
| 1054 | eprintln!("g1t-runner: g1t did not take the build; its deployment says why"); | |
| 1055 | } | |
| 1056 | 1 | |
| 1057 | } | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1058 | Ok(_) => 0, |
| 1059 | Err(error) => { | |
| 1060 | eprintln!("g1t-runner: {error:#}"); | |
| 1061 | 1 | |
| 1062 | } | |
| 1063 | } | |
| 1064 | } | |
| 1065 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1066 | /// `g1t-runner detect`: what a build of the project would find, as |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1067 | /// `key=value` lines for `$GITHUB_OUTPUT`: `kind`, `install`, `build`, |
| 1068 | /// `output-dir`, and `framework` when one is found. Reads `ROOT_DIR`, | |
| 1069 | /// `BUILD_COMMAND` and `OUTPUT_DIR` as the build does; `--root-dir DIR` is | |
| 1070 | /// the same as `ROOT_DIR`. A framework set up to need a server is `none`, | |
| 1071 | /// with what to change on standard error. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1072 | pub fn detect_cli(args: &[String]) -> i32 { |
| 1073 | let mut root = std::env::var("ROOT_DIR").unwrap_or_default(); | |
| 1074 | let mut rest = args.iter(); | |
| 1075 | while let Some(arg) = rest.next() { | |
| 1076 | match arg.as_str() { | |
| 1077 | "--root-dir" => root = rest.next().cloned().unwrap_or_default(), | |
| 1078 | "--help" | "-h" => { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1079 | println!( |
| 1080 | "g1t-runner detect [--root-dir DIR]: prints kind, install, build, output-dir and framework (when one is found) as key=value lines." | |
| 1081 | ); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1082 | return 0; |
| 1083 | } | |
| 1084 | other => { | |
| 1085 | eprintln!("g1t-runner detect: unknown argument {other}"); | |
| 1086 | return 2; | |
| 1087 | } | |
| 1088 | } | |
| 1089 | } | |
| 1090 | let dir = project_dir_in(&checkout_dir(), &root); | |
| 1091 | let custom = std::env::var("BUILD_COMMAND").ok(); | |
| 1092 | let output = std::env::var("OUTPUT_DIR").ok(); | |
| 1093 | match detect(&dir, custom.as_deref(), output.as_deref()) { | |
| 1094 | Ok(found) => { | |
| 1095 | print!("{}", output_lines(&found)); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1096 | if let Some(problem) = &found.problem { |
| 1097 | eprintln!("g1t-runner detect: {problem}"); | |
| 1098 | } | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1099 | 0 |
| 1100 | } | |
| 1101 | Err(error) => { | |
| 1102 | eprintln!("g1t-runner detect: {error:#}"); | |
| 1103 | 1 | |
| 1104 | } | |
| 1105 | } | |
| 1106 | } | |
| 1107 | ||
| 1108 | /// `Detected` as `$GITHUB_OUTPUT` lines; a value on one line, empty when | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1109 | /// there is none. `framework` is left out when none was found, which a |
| 1110 | /// step's outputs read as empty all the same. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1111 | fn output_lines(found: &Detected) -> String { |
| 1112 | let one_line = |value: Option<&str>| value.unwrap_or_default().replace(['\r', '\n'], " "); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1113 | let mut lines = format!( |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1114 | "kind={}\ninstall={}\nbuild={}\noutput-dir={}\n", |
| 1115 | found.kind, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1116 | one_line(found.install.as_deref()), |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1117 | one_line(found.build.as_deref()), |
| 1118 | one_line(found.output_dir.as_deref()), | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1119 | ); |
| 1120 | if let Some(framework) = &found.framework { | |
| 1121 | lines.push_str(&format!("framework={}\n", framework.id)); | |
| 1122 | } | |
| 1123 | lines | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1124 | } |
| 1125 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1126 | #[cfg(test)] |
| 1127 | mod tests { | |
| 1128 | use super::*; | |
| 1129 | ||
| 1130 | #[test] | |
| 1131 | fn the_upload_session_is_read_in_either_spelling() { | |
| 1132 | for answer in [ | |
| 1133 | json!({ "jwt": "j", "buckets": [["a"]], "upload_url": "https://u" }), | |
| 1134 | json!({ "jwt": "j", "uploadUrl": "https://u" }), | |
| 1135 | ] { | |
| 1136 | let session: UploadSession = serde_json::from_value(answer).unwrap(); | |
| 1137 | assert_eq!(session.upload_url, "https://u"); | |
| 1138 | } | |
| 1139 | } | |
| 1140 | ||
| 1141 | #[test] | |
| 1142 | fn jsonc_comments_and_trailing_commas_are_dropped() { | |
| 1143 | let text = r#"{ | |
| 1144 | // a comment | |
| 1145 | "main": "src/index.ts", /* another */ | |
| 1146 | "vars": { "URL": "https://x.dev//not-a-comment", }, | |
| 1147 | }"#; | |
| 1148 | let config: WranglerConfig = serde_json::from_str(&strip_jsonc(text)).unwrap(); | |
| 1149 | assert_eq!(config.main.as_deref(), Some("src/index.ts")); | |
| 1150 | assert_eq!(config.vars["URL"], "https://x.dev//not-a-comment"); | |
| 1151 | } | |
| 1152 | ||
| 1153 | #[test] | |
| 1154 | fn unsupported_bindings_are_noticed() { | |
| 1155 | let config: WranglerConfig = | |
| 1156 | serde_json::from_str(r#"{ "main": "a.js", "d1_databases": [{ "binding": "DB" }] }"#).unwrap(); | |
| 1157 | assert!(config.rest.contains_key("d1_databases")); | |
| 1158 | } | |
| 1159 | ||
| 1160 | #[test] | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 1161 | fn crons_g1t_does_not_run_are_warned_about() { |
| 1162 | let config: WranglerConfig = | |
| 1163 | serde_json::from_str(r#"{ "main": "a.js", "triggers": { "crons": ["*/5 * * * *", "0 3 * * *"] }, "kv_namespaces": [{ "binding": "KV" }] }"#) | |
| 1164 | .unwrap(); | |
| 1165 | let warnings = config_warnings(&config); | |
| 1166 | assert_eq!(warnings.len(), 2); | |
| 1167 | assert!(warnings.iter().any(|w| w.contains("kv_namespaces"))); | |
| 1168 | assert!(warnings.iter().any(|w| w.contains("triggers.crons") && w.contains("2 schedules"))); | |
| 1169 | let none: WranglerConfig = serde_json::from_str(r#"{ "main": "a.js", "triggers": { "crons": [] } }"#).unwrap(); | |
| 1170 | assert!(config_warnings(&none).is_empty()); | |
| 1171 | let toml: WranglerConfig = toml::from_str("main = \"a.js\" | |
| 1172 | [triggers] | |
| 1173 | crons = [\"0 * * * *\"] | |
| 1174 | ").unwrap(); | |
| 1175 | assert!(config_warnings(&toml)[0].contains("1 schedule)")); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1176 | // A config a build wrote lists every kind of binding, mostly empty. |
| 1177 | let written: WranglerConfig = serde_json::from_str( | |
| 1178 | r#"{ "main": "entry.mjs", "d1_databases": [], "durable_objects": { "bindings": [] }, "triggers": {}, "kv_namespaces": [{ "binding": "SESSION" }] }"#, | |
| 1179 | ) | |
| 1180 | .unwrap(); | |
| 1181 | assert_eq!(config_warnings(&written).len(), 1); | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 1182 | } |
| 1183 | ||
| 1184 | #[test] | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1185 | fn a_site_is_plain_html_only_when_its_own_files_are_served_unbuilt() { |
| 1186 | let project = Path::new("/w/site"); | |
| 1187 | assert_eq!(static_kind(project, project, false), "html"); | |
| 1188 | assert_eq!(static_kind(project, project, true), "static"); | |
| 1189 | assert_eq!(static_kind(project, &project.join("dist"), false), "static"); | |
| 1190 | } | |
| 1191 | ||
| 1192 | #[test] | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1193 | fn files_are_typed_by_extension() { |
| 1194 | assert_eq!(content_type("/index.HTML"), "text/html"); | |
| 1195 | assert_eq!(content_type("/a/b.woff2"), "font/woff2"); | |
| 1196 | assert_eq!(content_type("/LICENSE"), "application/octet-stream"); | |
| 1197 | } | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1198 | |
| 1199 | /// A fresh directory with `files` (path, contents) in it. | |
| 1200 | fn project(name: &str, files: &[(&str, &str)]) -> PathBuf { | |
| 1201 | let dir = std::env::temp_dir().join(format!("g1t-detect-{name}-{}", std::process::id())); | |
| 1202 | let _ = std::fs::remove_dir_all(&dir); | |
| 1203 | for (path, text) in files { | |
| 1204 | let path = dir.join(path); | |
| 1205 | std::fs::create_dir_all(path.parent().unwrap()).unwrap(); | |
| 1206 | std::fs::write(path, text).unwrap(); | |
| 1207 | } | |
| 1208 | std::fs::create_dir_all(&dir).unwrap(); | |
| 1209 | dir | |
| 1210 | } | |
| 1211 | ||
| 1212 | #[test] | |
| 1213 | fn detect_finds_a_workers_project_and_how_it_builds() { | |
| 1214 | let dir = project( | |
| 1215 | "workers", | |
| 1216 | &[ | |
| 1217 | ("wrangler.jsonc", r#"{ "main": "src/index.ts", "assets": { "directory": "./public" }, }"#), | |
| 1218 | ("package.json", "{}"), | |
| 1219 | ("pnpm-lock.yaml", ""), | |
| 1220 | ], | |
| 1221 | ); | |
| 1222 | let found = detect(&dir, None, None).unwrap(); | |
| 1223 | assert_eq!(found.kind, "workers"); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1224 | assert_eq!(found.install.as_deref(), Some("corepack enable && pnpm install --frozen-lockfile")); |
| 1225 | assert_eq!(found.framework, None); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1226 | assert_eq!(found.build.as_deref(), Some("npx --yes wrangler@4 deploy --dry-run")); |
| 1227 | assert_eq!(found.output_dir.as_deref(), Some("./public")); | |
| 1228 | // The project's own build command runs first. | |
| 1229 | let custom = detect(&dir, Some("npm run codegen"), None).unwrap(); | |
| 1230 | assert_eq!(custom.build.as_deref(), Some("npm run codegen && npx --yes wrangler@4 deploy --dry-run")); | |
| 1231 | } | |
| 1232 | ||
| 1233 | #[test] | |
| 1234 | fn detect_finds_a_static_build_and_a_plain_site() { | |
| 1235 | let built = project("static", &[("package.json", r#"{ "scripts": { "build": "vite build" } }"#), ("package-lock.json", "{}")]); | |
| 1236 | let found = detect(&built, None, None).unwrap(); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1237 | assert_eq!(found.kind, "static"); |
| 1238 | assert_eq!(found.install.as_deref(), Some("npm ci")); | |
| 1239 | assert_eq!(found.build.as_deref(), Some("npm run build")); | |
| 1240 | assert_eq!(found.output_dir, None, "its output is found once it has built"); | |
| 1241 | assert_eq!(found.framework, None, "a build script alone names no framework"); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1242 | std::fs::create_dir_all(built.join("dist")).unwrap(); |
| 1243 | std::fs::write(built.join("dist/index.html"), "<h1>hi</h1>").unwrap(); | |
| 1244 | assert_eq!(detect(&built, None, None).unwrap().output_dir.as_deref(), Some("dist")); | |
| 1245 | assert_eq!(detect(&built, None, Some("/site/")).unwrap().output_dir.as_deref(), Some("site")); | |
| 1246 | ||
| 1247 | let html = project("html", &[("index.html", "<h1>hi</h1>")]); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1248 | let found = detect(&html, None, None).unwrap(); |
| 1249 | assert_eq!((found.kind, found.install, found.build, found.output_dir.as_deref()), ("html", None, None, Some("."))); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1250 | // A `public` folder alone, without an index.html, is not a site. |
| 1251 | let empty = project("empty", &[("public/robots.txt", ""), ("README.md", "")]); | |
| 1252 | assert_eq!(detect(&empty, None, None).unwrap().kind, "none"); | |
| 1253 | } | |
| 1254 | ||
| 1255 | #[test] | |
| 1256 | fn detect_prints_lines_for_github_output() { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1257 | let mut found = Detected { |
| 1258 | kind: "static", | |
| 1259 | framework: None, | |
| 1260 | install: Some("npm ci".into()), | |
| 1261 | build: Some("a\nb".into()), | |
| 1262 | output_dir: None, | |
| 1263 | problem: None, | |
| 1264 | pre_build: None, | |
| 1265 | }; | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1266 | assert_eq!(output_lines(&found), "kind=static\ninstall=npm ci\nbuild=a b\noutput-dir=\n"); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1267 | let vite = project("lines", &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#)]); |
| 1268 | found.framework = frameworks::detect(&vite, false); | |
| 1269 | assert!(output_lines(&found).ends_with("output-dir=\nframework=vite\n")); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1270 | } |
| 1271 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1272 | /// One project of the table below: its files, the project's own |
| 1273 | /// settings, and what detect should say. | |
| 1274 | struct Case { | |
| 1275 | name: &'static str, | |
| 1276 | files: &'static [(&'static str, &'static str)], | |
| 1277 | custom_build: Option<&'static str>, | |
| 1278 | chosen_output: Option<&'static str>, | |
| 1279 | kind: &'static str, | |
| 1280 | framework: Option<&'static str>, | |
| 1281 | build: Option<&'static str>, | |
| 1282 | output_dir: Option<&'static str>, | |
| 1283 | /// A phrase the problem says, if there is one. | |
| 1284 | problem: Option<&'static str>, | |
| 1285 | } | |
| 1286 | ||
| 1287 | const CASE: Case = Case { | |
| 1288 | name: "", | |
| 1289 | files: &[], | |
| 1290 | custom_build: None, | |
| 1291 | chosen_output: None, | |
| 1292 | kind: "static", | |
| 1293 | framework: None, | |
| 1294 | build: Some("npm run build"), | |
| 1295 | output_dir: None, | |
| 1296 | problem: None, | |
| 1297 | }; | |
| 1298 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1299 | #[test] |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1300 | fn detect_knows_each_framework() { |
| 1301 | let cases = [ | |
| 1302 | Case { | |
| 1303 | name: "vite", | |
| 1304 | files: &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#)], | |
| 1305 | framework: Some("vite"), | |
| 1306 | build: Some("npx vite build"), | |
| 1307 | output_dir: Some("dist"), | |
| 1308 | ..CASE | |
| 1309 | }, | |
| 1310 | Case { | |
| 1311 | name: "vite-react", | |
| 1312 | files: &[ | |
| 1313 | ("package.json", r#"{ "scripts": { "build": "tsc && vite build" }, "dependencies": { "react": "19" }, "devDependencies": { "vite": "7", "@vitejs/plugin-react": "5" } }"#), | |
| 1314 | ("vite.config.ts", "export default defineConfig({ plugins: [react()], build: { outDir: 'site' } })"), | |
| 1315 | ], | |
| 1316 | framework: Some("vite-react"), | |
| 1317 | output_dir: Some("site"), | |
| 1318 | ..CASE | |
| 1319 | }, | |
| 1320 | Case { | |
| 1321 | name: "vite-vue", | |
| 1322 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7", "@vitejs/plugin-vue": "6" } }"#)], | |
| 1323 | framework: Some("vite-vue"), | |
| 1324 | output_dir: Some("dist"), | |
| 1325 | ..CASE | |
| 1326 | }, | |
| 1327 | Case { | |
| 1328 | name: "vite-svelte", | |
| 1329 | files: &[("package.json", r#"{ "devDependencies": { "vite": "7", "@sveltejs/vite-plugin-svelte": "6" } }"#)], | |
| 1330 | framework: Some("vite-svelte"), | |
| 1331 | build: Some("npx vite build"), | |
| 1332 | output_dir: Some("dist"), | |
| 1333 | ..CASE | |
| 1334 | }, | |
| 1335 | Case { | |
| 1336 | name: "vite-solid", | |
| 1337 | files: &[("package.json", r#"{ "devDependencies": { "vite": "7", "vite-plugin-solid": "2" } }"#)], | |
| 1338 | framework: Some("vite-solid"), | |
| 1339 | build: Some("npx vite build"), | |
| 1340 | output_dir: Some("dist"), | |
| 1341 | ..CASE | |
| 1342 | }, | |
| 1343 | Case { | |
| 1344 | name: "vite-cloudflare", | |
| 1345 | files: &[ | |
| 1346 | ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7", "@cloudflare/vite-plugin": "1" } }"#), | |
| 1347 | ("wrangler.jsonc", r#"{ "main": "worker/index.ts" }"#), | |
| 1348 | ], | |
| 1349 | kind: "workers", | |
| 1350 | framework: Some("vite"), | |
| 1351 | build: Some("npm run build && npx --yes wrangler@4 deploy --dry-run"), | |
| 1352 | ..CASE | |
| 1353 | }, | |
| 1354 | Case { | |
| 1355 | name: "astro", | |
| 1356 | files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5" } }"#), ("astro.config.mjs", "export default defineConfig({ site: 'https://x.dev' })")], | |
| 1357 | framework: Some("astro"), | |
| 1358 | output_dir: Some("dist"), | |
| 1359 | ..CASE | |
| 1360 | }, | |
| 1361 | Case { | |
| 1362 | name: "astro-server", | |
| 1363 | files: &[("package.json", r#"{ "dependencies": { "astro": "5", "@astrojs/node": "9" } }"#), ("astro.config.mjs", "export default { output: \"server\" }")], | |
| 1364 | kind: "none", | |
| 1365 | framework: Some("astro"), | |
| 1366 | build: None, | |
| 1367 | problem: Some("`output: 'static'`"), | |
| 1368 | ..CASE | |
| 1369 | }, | |
| 1370 | Case { | |
| 1371 | name: "astro-cloudflare", | |
| 1372 | files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5", "@astrojs/cloudflare": "12" } }"#)], | |
| 1373 | kind: "workers", | |
| 1374 | framework: Some("astro"), | |
| 1375 | ..CASE | |
| 1376 | }, | |
| 1377 | Case { | |
| 1378 | name: "astro-cloudflare-folder", | |
| 1379 | files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5", "@astrojs/cloudflare": "12" } }"#)], | |
| 1380 | chosen_output: Some("dist"), | |
| 1381 | framework: Some("astro"), | |
| 1382 | output_dir: Some("dist"), | |
| 1383 | ..CASE | |
| 1384 | }, | |
| 1385 | Case { | |
| 1386 | name: "next-export", | |
| 1387 | files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#), ("next.config.ts", "const config: NextConfig = {\n output: 'export',\n};")], | |
| 1388 | framework: Some("nextjs"), | |
| 1389 | output_dir: Some("out"), | |
| 1390 | ..CASE | |
| 1391 | }, | |
| 1392 | Case { | |
| 1393 | name: "next-server", | |
| 1394 | files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#), ("next.config.mjs", "export default {}")], | |
| 1395 | kind: "none", | |
| 1396 | framework: Some("nextjs"), | |
| 1397 | build: None, | |
| 1398 | problem: Some("`@opennextjs/cloudflare`"), | |
| 1399 | ..CASE | |
| 1400 | }, | |
| 1401 | Case { | |
| 1402 | name: "next-server-own-folder", | |
| 1403 | files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#)], | |
| 1404 | chosen_output: Some("out"), | |
| 1405 | framework: Some("nextjs"), | |
| 1406 | output_dir: Some("out"), | |
| 1407 | ..CASE | |
| 1408 | }, | |
| 1409 | Case { | |
| 1410 | name: "opennext", | |
| 1411 | files: &[ | |
| 1412 | ("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#), | |
| 1413 | ("wrangler.jsonc", r#"{ "main": ".open-next/worker.js", "assets": { "directory": ".open-next/assets", "binding": "ASSETS" } }"#), | |
| 1414 | ], | |
| 1415 | kind: "workers", | |
| 1416 | framework: Some("nextjs"), | |
| 1417 | build: Some("npx opennextjs-cloudflare build && npx --yes wrangler@4 deploy --dry-run"), | |
| 1418 | output_dir: Some(".open-next/assets"), | |
| 1419 | ..CASE | |
| 1420 | }, | |
| 1421 | Case { | |
| 1422 | name: "opennext-no-config", | |
| 1423 | files: &[("package.json", r#"{ "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#)], | |
| 1424 | kind: "none", | |
| 1425 | framework: Some("nextjs"), | |
| 1426 | build: None, | |
| 1427 | problem: Some("`.open-next/worker.js`"), | |
| 1428 | ..CASE | |
| 1429 | }, | |
| 1430 | Case { | |
| 1431 | name: "sveltekit-static", | |
| 1432 | files: &[ | |
| 1433 | ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-static": "3", "vite": "7" } }"#), | |
| 1434 | ("svelte.config.js", "import adapter from '@sveltejs/adapter-static';\nexport default { kit: { adapter: adapter() } };"), | |
| 1435 | ], | |
| 1436 | framework: Some("sveltekit"), | |
| 1437 | output_dir: Some("build"), | |
| 1438 | ..CASE | |
| 1439 | }, | |
| 1440 | Case { | |
| 1441 | name: "sveltekit-3-static", | |
| 1442 | files: &[ | |
| 1443 | ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "3", "@sveltejs/adapter-static": "4", "@sveltejs/adapter-cloudflare": "8" } }"#), | |
| 1444 | ("vite.config.js", "import adapter from '@sveltejs/adapter-static';\nexport default defineConfig({ plugins: [sveltekit({ adapter: adapter({ pages: 'www' }) })] });"), | |
| 1445 | ], | |
| 1446 | framework: Some("sveltekit"), | |
| 1447 | output_dir: Some("www"), | |
| 1448 | ..CASE | |
| 1449 | }, | |
| 1450 | Case { | |
| 1451 | name: "sveltekit-cloudflare", | |
| 1452 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-cloudflare": "7" } }"#)], | |
| 1453 | kind: "workers", | |
| 1454 | framework: Some("sveltekit"), | |
| 1455 | ..CASE | |
| 1456 | }, | |
| 1457 | Case { | |
| 1458 | name: "sveltekit-auto", | |
| 1459 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-auto": "6" } }"#)], | |
| 1460 | kind: "none", | |
| 1461 | framework: Some("sveltekit"), | |
| 1462 | build: None, | |
| 1463 | problem: Some("`@sveltejs/adapter-static`"), | |
| 1464 | ..CASE | |
| 1465 | }, | |
| 1466 | Case { | |
| 1467 | name: "nuxt", | |
| 1468 | files: &[("package.json", r#"{ "scripts": { "build": "nuxt build", "generate": "nuxt generate" }, "dependencies": { "nuxt": "4" } }"#)], | |
| 1469 | framework: Some("nuxt"), | |
| 1470 | build: Some("npm run generate"), | |
| 1471 | output_dir: Some(".output/public"), | |
| 1472 | ..CASE | |
| 1473 | }, | |
| 1474 | Case { | |
| 1475 | name: "nuxt-no-generate", | |
| 1476 | files: &[("package.json", r#"{ "scripts": { "build": "nuxt build" }, "dependencies": { "nuxt": "4" } }"#)], | |
| 1477 | framework: Some("nuxt"), | |
| 1478 | build: Some("npx nuxi generate"), | |
| 1479 | output_dir: Some(".output/public"), | |
| 1480 | ..CASE | |
| 1481 | }, | |
| 1482 | Case { | |
| 1483 | name: "react-router-spa", | |
| 1484 | files: &[ | |
| 1485 | ("package.json", r#"{ "scripts": { "build": "react-router build" }, "devDependencies": { "@react-router/dev": "7", "vite": "7" } }"#), | |
| 1486 | ("react-router.config.ts", "export default {\n // Server-side render by default\n ssr: false,\n} satisfies Config;"), | |
| 1487 | ], | |
| 1488 | framework: Some("react-router"), | |
| 1489 | output_dir: Some("build/client"), | |
| 1490 | ..CASE | |
| 1491 | }, | |
| 1492 | Case { | |
| 1493 | name: "react-router-ssr", | |
| 1494 | files: &[("package.json", r#"{ "devDependencies": { "@react-router/dev": "7" } }"#), ("react-router.config.ts", "export default { ssr: true }")], | |
| 1495 | kind: "none", | |
| 1496 | framework: Some("react-router"), | |
| 1497 | build: None, | |
| 1498 | problem: Some("`ssr: false`"), | |
| 1499 | ..CASE | |
| 1500 | }, | |
| 1501 | Case { | |
| 1502 | name: "remix-spa", | |
| 1503 | files: &[ | |
| 1504 | ("package.json", r#"{ "scripts": { "build": "remix vite:build" }, "devDependencies": { "@remix-run/dev": "2", "vite": "5" } }"#), | |
| 1505 | ("vite.config.ts", "export default defineConfig({ plugins: [remix({ ssr: false })] })"), | |
| 1506 | ], | |
| 1507 | framework: Some("remix"), | |
| 1508 | output_dir: Some("build/client"), | |
| 1509 | ..CASE | |
| 1510 | }, | |
| 1511 | Case { | |
| 1512 | name: "gatsby", | |
| 1513 | files: &[("package.json", r#"{ "scripts": { "build": "gatsby build" }, "dependencies": { "gatsby": "5" } }"#)], | |
| 1514 | framework: Some("gatsby"), | |
| 1515 | output_dir: Some("public"), | |
| 1516 | ..CASE | |
| 1517 | }, | |
| 1518 | Case { | |
| 1519 | name: "docusaurus", | |
| 1520 | files: &[("package.json", r#"{ "scripts": { "build": "docusaurus build" }, "dependencies": { "@docusaurus/core": "3" } }"#)], | |
| 1521 | framework: Some("docusaurus"), | |
| 1522 | output_dir: Some("build"), | |
| 1523 | ..CASE | |
| 1524 | }, | |
| 1525 | Case { | |
| 1526 | name: "eleventy", | |
| 1527 | files: &[("package.json", r#"{ "devDependencies": { "@11ty/eleventy": "3" } }"#)], | |
| 1528 | framework: Some("eleventy"), | |
| 1529 | build: Some("npx @11ty/eleventy"), | |
| 1530 | output_dir: Some("_site"), | |
| 1531 | ..CASE | |
| 1532 | }, | |
| 1533 | Case { | |
| 1534 | name: "angular", | |
| 1535 | files: &[ | |
| 1536 | ("package.json", r#"{ "scripts": { "build": "ng build" }, "dependencies": { "@angular/core": "20" } }"#), | |
| 1537 | ("angular.json", r#"{ "projects": { "shop": { "projectType": "application", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/shop" } } } } } }"#), | |
| 1538 | ], | |
| 1539 | framework: Some("angular"), | |
| 1540 | output_dir: Some("dist/shop/browser"), | |
| 1541 | ..CASE | |
| 1542 | }, | |
| 1543 | Case { | |
| 1544 | name: "angular-default", | |
| 1545 | files: &[ | |
| 1546 | ("package.json", r#"{ "dependencies": { "@angular/core": "20" } }"#), | |
| 1547 | ("angular.json", r#"{ "projects": { "app": { "projectType": "application", "architect": { "build": { "builder": "@angular/build:application", "options": {} } } } } }"#), | |
| 1548 | ], | |
| 1549 | framework: Some("angular"), | |
| 1550 | build: Some("npx ng build"), | |
| 1551 | output_dir: Some("dist/app/browser"), | |
| 1552 | ..CASE | |
| 1553 | }, | |
| 1554 | Case { | |
| 1555 | name: "hugo", | |
| 1556 | files: &[("hugo.toml", "baseURL = 'https://example.org/'\ntitle = 'Site'\n")], | |
| 1557 | framework: Some("hugo"), | |
| 1558 | build: Some(frameworks_hugo()), | |
| 1559 | output_dir: Some("public"), | |
| 1560 | ..CASE | |
| 1561 | }, | |
| 1562 | Case { | |
| 1563 | name: "hugo-config-toml", | |
| 1564 | files: &[("config.toml", "baseURL = \"https://example.org/\"\npublishDir = \"site\"\n")], | |
| 1565 | framework: Some("hugo"), | |
| 1566 | build: Some(frameworks_hugo()), | |
| 1567 | output_dir: Some("site"), | |
| 1568 | ..CASE | |
| 1569 | }, | |
| 1570 | Case { | |
| 1571 | name: "not-hugo", | |
| 1572 | files: &[("config.toml", "base_url = \"https://example.org\"\n"), ("index.html", "")], | |
| 1573 | kind: "html", | |
| 1574 | build: None, | |
| 1575 | output_dir: Some("."), | |
| 1576 | ..CASE | |
| 1577 | }, | |
| 1578 | Case { | |
| 1579 | name: "jekyll", | |
| 1580 | files: &[("_config.yml", "title: Blog\n"), ("Gemfile", "gem 'jekyll'\n")], | |
| 1581 | framework: Some("jekyll"), | |
| 1582 | build: Some("bundle exec jekyll build"), | |
| 1583 | output_dir: Some("_site"), | |
| 1584 | ..CASE | |
| 1585 | }, | |
| 1586 | Case { | |
| 1587 | name: "mdbook", | |
| 1588 | files: &[("book.toml", "[book]\ntitle = \"Guide\"\n")], | |
| 1589 | framework: Some("mdbook"), | |
| 1590 | build: Some(frameworks_mdbook()), | |
| 1591 | output_dir: Some("book"), | |
| 1592 | ..CASE | |
| 1593 | }, | |
| 1594 | Case { | |
| 1595 | name: "mdbook-build-dir", | |
| 1596 | files: &[("book.toml", "[book]\ntitle = \"Guide\"\n[build]\nbuild-dir = \"site\"\n")], | |
| 1597 | framework: Some("mdbook"), | |
| 1598 | build: Some(frameworks_mdbook()), | |
| 1599 | output_dir: Some("site"), | |
| 1600 | ..CASE | |
| 1601 | }, | |
| 1602 | // The project's own settings win. | |
| 1603 | Case { | |
| 1604 | name: "own-settings", | |
| 1605 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7" } }"#)], | |
| 1606 | custom_build: Some("make site"), | |
| 1607 | chosen_output: Some("www"), | |
| 1608 | framework: Some("vite"), | |
| 1609 | build: Some("make site"), | |
| 1610 | output_dir: Some("www"), | |
| 1611 | ..CASE | |
| 1612 | }, | |
| 1613 | ]; | |
| 1614 | for case in cases { | |
| 1615 | let dir = project(&format!("fw-{}", case.name), case.files); | |
| 1616 | let found = detect(&dir, case.custom_build, case.chosen_output).unwrap(); | |
| 1617 | let name = case.name; | |
| 1618 | assert_eq!(found.kind, case.kind, "{name}: kind"); | |
| 1619 | assert_eq!(found.framework.as_ref().map(|f| f.id), case.framework, "{name}: framework"); | |
| 1620 | assert_eq!(found.build.as_deref(), case.build, "{name}: build"); | |
| 1621 | assert_eq!(found.output_dir.as_deref(), case.output_dir, "{name}: output-dir"); | |
| 1622 | match case.problem { | |
| 1623 | Some(phrase) => assert!(found.problem.as_deref().is_some_and(|p| p.contains(phrase)), "{name}: {:?}", found.problem), | |
| 1624 | None => assert_eq!(found.problem, None, "{name}: problem"), | |
| 1625 | } | |
| 1626 | let _ = std::fs::remove_dir_all(&dir); | |
| 1627 | } | |
| 1628 | } | |
| 1629 | ||
| 1630 | fn frameworks_hugo() -> &'static str { | |
| 1631 | "if command -v hugo >/dev/null 2>&1; then hugo --minify; else npx --yes hugo-extended@0.166.0 --minify; fi" | |
| 1632 | } | |
| 1633 | ||
| 1634 | fn frameworks_mdbook() -> &'static str { | |
| 1635 | "if ! command -v mdbook >/dev/null 2>&1; then cargo install mdbook --version 0.5.4 --locked --quiet; fi && mdbook build" | |
| 1636 | } | |
| 1637 | ||
| 1638 | #[test] | |
| 1639 | fn jekyll_installs_its_gems_after_the_packages() { | |
| 1640 | let dir = project("jekyll-install", &[("_config.yml", ""), ("Gemfile", ""), ("package.json", "{}")]); | |
| 1641 | let found = detect(&dir, None, None).unwrap(); | |
| 1642 | assert_eq!(found.install.as_deref(), Some("npm install && bundle config set --local path vendor/bundle && bundle install")); | |
| 1643 | } | |
| 1644 | ||
| 1645 | #[test] | |
| 1646 | fn an_adapters_worker_is_found_once_built_and_never_served() { | |
| 1647 | let dir = project( | |
| 1648 | "adapter", | |
| 1649 | &[ | |
| 1650 | ("dist/_worker.js/index.js", "export default {}"), | |
| 1651 | ("dist/_routes.json", "{}"), | |
| 1652 | ("dist/.assetsignore", "_worker.js\nsecret.txt\n"), | |
| 1653 | ("dist/secret.txt", "no"), | |
| 1654 | ("dist/index.html", "<h1>hi</h1>"), | |
| 1655 | ("dist/about/_worker.js", "a page's own file"), | |
| 1656 | ("kit/.svelte-kit/cloudflare/_worker.js", "export default {}"), | |
| 1657 | ], | |
| 1658 | ); | |
| 1659 | let astro = Adapter { entry: "dist/_worker.js", assets: "dist" }; | |
| 1660 | assert_eq!(adapter_entry(&dir, &astro), Some(dir.join("dist/_worker.js/index.js"))); | |
| 1661 | let kit = Adapter { entry: ".svelte-kit/cloudflare/_worker.js", assets: ".svelte-kit/cloudflare" }; | |
| 1662 | assert_eq!(adapter_entry(&dir.join("kit"), &kit), Some(dir.join("kit/.svelte-kit/cloudflare/_worker.js"))); | |
| 1663 | assert_eq!(adapter_entry(&dir.join("missing"), &kit), None); | |
| 1664 | let mut served: Vec<String> = collect_site(&dir.join("dist"), false).unwrap().into_iter().map(|a| a.path).collect(); | |
| 1665 | served.sort(); | |
| 1666 | assert_eq!(served, ["/about/_worker.js", "/index.html"]); | |
| 1667 | } | |
| 1668 | ||
| 1669 | #[test] | |
| 1670 | fn a_config_the_build_wrote_is_read_in_place_of_the_projects() { | |
| 1671 | let dir = project( | |
| 1672 | "redirected", | |
| 1673 | &[ | |
| 1674 | ("wrangler.jsonc", r#"{ "main": "worker/index.ts" }"#), | |
| 1675 | (".wrangler/deploy/config.json", r#"{ "configPath": "../../dist/app/wrangler.json" }"#), | |
| 1676 | ("dist/app/wrangler.json", r#"{ "main": "index.js", "assets": { "directory": "../client" } }"#), | |
| 1677 | ], | |
| 1678 | ); | |
| 1679 | let (config, base) = redirected_config(&dir).unwrap().unwrap(); | |
| 1680 | assert_eq!(config.main.as_deref(), Some("index.js")); | |
| 1681 | assert_eq!(base, dir.join("dist/app").canonicalize().unwrap()); | |
| 1682 | assert!(base.join(config.assets.unwrap().directory.unwrap()).ends_with("dist/app/../client")); | |
| 1683 | assert!(redirected_config(&project("not-redirected", &[])).unwrap().is_none()); | |
| 1684 | } | |
| 1685 | ||
| 1686 | #[test] | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1687 | fn the_root_directory_stays_inside_the_checkout() { |
| 1688 | let base = Path::new("/w/repo"); | |
| 1689 | assert_eq!(project_dir_in(base, ""), base); | |
| 1690 | assert_eq!(project_dir_in(base, "."), base); | |
| 1691 | assert_eq!(project_dir_in(base, "/apps/site/"), base.join("apps/site")); | |
| 1692 | assert_eq!(project_dir_in(base, "../etc"), base); | |
| 1693 | assert_eq!(project_dir_in(base, "apps/../../x"), base); | |
| 1694 | } | |
| 1695 | ||
| 1696 | #[test] | |
| 1697 | fn a_step_prints_the_log_redacted() { | |
| 1698 | let mut log = Log { echo: Some(vec!["hunter22".to_owned()]), ..Log::default() }; | |
| 1699 | log.line("token hunter22 used"); | |
| 1700 | assert!(log.text.contains("hunter22"), "the deployment's log is redacted when it is sent"); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1701 | assert_eq!(wrangler_build(Some(Path::new("/tmp/b")), None), "npx --yes wrangler@4 deploy --dry-run --outdir /tmp/b"); |
| 1702 | assert_eq!( | |
| 1703 | wrangler_build(Some(Path::new("/tmp/b")), Some(Path::new("/tmp/w.json"))), | |
| 1704 | "npx --yes wrangler@4 deploy --dry-run --config /tmp/w.json --outdir /tmp/b" | |
| 1705 | ); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1706 | } |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1707 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.