@let.com/cli 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +154 -2
- package/dist/THIRD_PARTY_LICENSES.txt +25 -0
- package/dist/bin.js +622 -0
- package/dist/build-local.mjs +12 -0
- package/dist/edge/edge-build.mjs +250 -0
- package/dist/edge/pack.mjs +875 -0
- package/package.json +50 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import{spawn,spawnSync}from"node:child_process";import{copyFileSync,cpSync,existsSync,lstatSync,mkdirSync,mkdtempSync,readFileSync,readdirSync,rmSync}from"node:fs";import{homedir,tmpdir}from"node:os";import{dirname,join,relative,resolve,sep}from"node:path";import{fileURLToPath}from"node:url";var FRAMEWORK_LABELS={nextjs:"Next.js",astro:"Astro",sveltekit:"SvelteKit","react-router":"React Router",remix:"Remix",nuxt:"Nuxt",static:"Static site",unknown:"Unknown"},EDGE_ADAPTER_PINS={opennext:"1.20.12",wrangler:"4.151.3",sveltekitCloudflare:"7.2.9"},SVELTEKIT3_CLOUDFLARE_ADAPTER_PIN="8.0.1";function svelteKitCloudflarePin(kitRange){let major=majorOf(kitRange);return major!==null&&major<3?EDGE_ADAPTER_PINS.sveltekitCloudflare:SVELTEKIT3_CLOUDFLARE_ADAPTER_PIN}var IGNORED_DIR=/(^|\/)(node_modules|\.git|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify|\.wrangler|\.open-next|\.astro|\.turbo|\.cache|dist|build|out|coverage|vendor)(\/|$)/,CONFIG_FILES=/^(next|astro|svelte|react-router|remix|nuxt|vite|open-next)\.config\.(js|cjs|mjs|ts|cts|mts)$/,ROOT_FILES=new Set(["package.json","create.json","wrangler.json","wrangler.jsonc","wrangler.toml","server.js","server.mjs","server.cjs","server.ts","index.html","prisma/schema.prisma"]),MIDDLEWARE=/^(src\/)?(middleware|proxy)\.(js|mjs|ts|mts)$/,SOURCE=/\.(js|jsx|mjs|cjs|ts|tsx|mts|cts|vue|svelte|astro)$/,BUILD_TIME=/(^|\/)(scripts|test|tests|__tests__|e2e|cypress|playwright|\.storybook|stories|migrations|seed)(\/|$)|\.(test|spec|stories|config)\.[a-z]+$|^(postcss|tailwind|eslint|prettier|vitest|jest|playwright|babel|webpack|rollup|tsup|drizzle)\.config\./,DETECTOR_CAPTURE_LIMITS={maxFileBytes:256*1024,maxTotalBytes:4*1024*1024,maxSourceFiles:2e3},DETECTOR_SCAN_LIMITS={maxScanChars:64*1024,budgetMs:250};function detectorWantsContent(path){let p=path.replace(/^\.\//,"");return IGNORED_DIR.test(p)?null:ROOT_FILES.has(p)||CONFIG_FILES.test(p)||MIDDLEWARE.test(p)?"manifest":SOURCE.test(p)&&!p.endsWith(".d.ts")?"source":null}var NATIVE_DEPENDENCIES={sharp:"sharp (a native image library)",bcrypt:"bcrypt (a native addon; bcryptjs works at the edge)",argon2:"argon2 (a native addon)",canvas:"canvas (a native addon)","@napi-rs/canvas":"@napi-rs/canvas (a native addon)","better-sqlite3":"better-sqlite3 (a native SQLite binding)",sqlite3:"sqlite3 (a native SQLite binding)","node-pty":"node-pty (a native addon)",re2:"re2 (a native addon)",puppeteer:"puppeteer (drives a local Chromium)",playwright:"playwright (drives local browsers)","playwright-core":"playwright-core (drives local browsers)","ffmpeg-static":"ffmpeg-static (a native binary)"},LONG_RUNNING_DEPENDENCIES={"socket.io":"socket.io (a WebSocket server)",bullmq:"BullMQ (background job workers)",bull:"Bull (background job workers)",agenda:"Agenda (background job scheduler)","node-cron":"node-cron (an in-process scheduler)","node-schedule":"node-schedule (an in-process scheduler)"},NEXT_ROUTE_SOURCE=/^(src\/)?(app|pages)\//,NEXT_APP_EDGE_RUNTIME=/\bexport\s+const\s+runtime\s*=\s*['"`](?:experimental-)?edge['"`]/,NEXT_PAGES_EDGE_RUNTIME=/\bruntime\s*:\s*['"`](?:experimental-)?edge['"`]/,DETECTOR_SOURCE_RULES=[{code:"child_process",pattern:/(?:from\s+|require\(\s*|import\(\s*)['"](?:node:)?child_process['"]/,message:p=>`${p} starts processes (child_process), which a Worker cannot do`},{code:"filesystem_write",pattern:/\b(?:writeFileSync|writeFile|appendFileSync|appendFile|createWriteStream|mkdirSync|renameSync|unlinkSync|rmSync)\s*\(/,message:p=>`${p} writes to the local filesystem, which a Worker does not have`},{code:"websocket_server",pattern:/new\s+(?:WebSocketServer|WebSocket\.Server)\s*\(|\bcreateServer\s*\([^)]{0,200}\)\s*\.listen\s*\(|\.listen\s*\(\s*(?:process\.env\.PORT|\d{2,5})\b/,message:p=>`${p} runs its own long-lived server (listen / WebSocket server), which needs a container`},{code:"native_addon_import",pattern:/(?:from\s+|require\(\s*)['"](?:sharp|bcrypt|argon2|canvas|better-sqlite3|sqlite3)['"]/,message:p=>`${p} imports a native addon`}];function parseJson(text){if(text===null)return null;try{return JSON.parse(text)}catch{return null}}function stripComments(text){let src=text.length>DETECTOR_SCAN_LIMITS.maxScanChars?text.slice(0,DETECTOR_SCAN_LIMITS.maxScanChars):text,out="",i=0;for(;i<src.length;){let slash=src.indexOf("/",i);if(slash===-1){out+=src.slice(i);break}let next=src[slash+1];if(next==="*"){out+=src.slice(i,slash);let end=src.indexOf("*/",slash+2);if(end===-1)break;i=end+2}else if(next==="/"&&src[slash-1]!==":"&&src[slash-1]!=="\\"){out+=src.slice(i,slash);let nl=src.indexOf(`
|
|
4
|
+
`,slash+2);if(nl===-1)break;i=nl}else out+=src.slice(i,slash+1),i=slash+1}return out}function rangeCanReach(range,min){let r=range.trim();if(/^(latest|canary|next|rc|beta|\*|x)$/i.test(r)||r==="")return!0;let m=/^(\^|~|>=|>|=)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?/.exec(r);if(!m)return null;let op=m[1]??"=",major=Number(m[2]),minor=m[3]===void 0||m[3]==="x"||m[3]==="*"?null:Number(m[3]),patch=m[4]===void 0||m[4]==="x"||m[4]==="*"?null:Number(m[4]),cmp=(a,b)=>a[0]-b[0]||a[1]-b[1]||a[2]-b[2],INF=Number.MAX_SAFE_INTEGER,max;return op===">="||op===">"?max=[INF,INF,INF]:op==="^"?max=major>0?[major,INF,INF]:[0,minor??INF,INF]:op==="~"?max=[major,minor??INF,INF]:max=[major,minor??INF,patch??INF],cmp(max,min)>=0}function majorOf(range){if(!range)return null;let m=/(\d+)/.exec(range);return m?Number(m[1]):null}var NEXT_MIN_15=[15,5,28],NEXT_MIN_16=[16,4,1],NEXT_EDGE_MIN_VERSIONS={v15:NEXT_MIN_15.join("."),v16:NEXT_MIN_16.join(".")};function nextSupported(range){let major=majorOf(range);return major===null?rangeCanReach(range,NEXT_MIN_16):/^(latest|canary|rc)$/i.test(range.trim())?!0:major<=14?!1:major===15?rangeCanReach(range,NEXT_MIN_15):major===16?rangeCanReach(range,NEXT_MIN_16):!0}function packageManagerOf(paths){return paths.has("pnpm-lock.yaml")?"pnpm":paths.has("yarn.lock")?"yarn":paths.has("bun.lock")||paths.has("bun.lockb")?"bun":paths.has("package-lock.json")||paths.has("npm-shrinkwrap.json")||paths.has("package.json")?"npm":null}function firstConfig(paths,base){for(let ext of["ts","mts","js","mjs","cjs","cts"])if(paths.has(`${base}.config.${ext}`))return`${base}.config.${ext}`;return null}function detectFramework(input,options={}){let now=options.now??(()=>Date.now()),deadline=now()+(options.budgetMs??DETECTOR_SCAN_LIMITS.budgetMs),all=input.paths.map(p=>p.replace(/^\.\//,"")).filter(p=>p!==""),paths=new Set(all),reasons=[],block=(code,message,path)=>reasons.push({code,message,blocking:!0,...path?{path}:{}}),note=(code,message,path)=>reasons.push({code,message,blocking:!1,...path?{path}:{}}),read=p=>paths.has(p)?input.read(p):null,pkgText=read("package.json"),pkg=parseJson(pkgText);pkgText!==null&&pkg===null&&block("package_json_invalid","package.json is not valid JSON","package.json");let deps={...pkg?.optionalDependencies??{},...pkg?.devDependencies??{},...pkg?.dependencies??{}},has=name=>Object.prototype.hasOwnProperty.call(deps,name),packageManager=packageManagerOf(paths),createJson=parseJson(read("create.json")),requested=null;createJson?.target==="server"||createJson?.target==="container"||createJson?.edge===!1?requested="container":(createJson?.target==="edge"||createJson?.edge===!0)&&(requested="edge");let found=[],nextConfig=firstConfig(paths,"next");(has("next")||nextConfig)&&found.push({framework:"nextjs",version:deps.next??null});let nuxtConfig=firstConfig(paths,"nuxt");(has("nuxt")||nuxtConfig)&&found.push({framework:"nuxt",version:deps.nuxt??null});let svelteConfig=firstConfig(paths,"svelte");has("@sveltejs/kit")&&found.push({framework:"sveltekit",version:deps["@sveltejs/kit"]??null});let astroConfig=firstConfig(paths,"astro");(has("astro")||astroConfig)&&found.push({framework:"astro",version:deps.astro??null});let rrConfig=firstConfig(paths,"react-router");(has("@react-router/dev")||rrConfig)&&found.push({framework:"react-router",version:deps["@react-router/dev"]??deps["react-router"]??null});let remixConfig=firstConfig(paths,"remix");!found.some(f=>f.framework==="react-router")&&(Object.keys(deps).some(d=>d.startsWith("@remix-run/"))||remixConfig)&&found.push({framework:"remix",version:deps["@remix-run/react"]??deps["@remix-run/dev"]??null});let base=(framework,version)=>({version:1,framework,frameworkVersion:version,target:"container",reason:"",reasons,adapter:null,packageManager,outputDir:null,spa:!1,isr:!1,requested});found.length>1&&block("mixed_frameworks",`The folder uses more than one framework (${found.map(f=>FRAMEWORK_LABELS[f.framework]).join(", ")}); deploy each app from its own folder to run it at the edge`),pkg&&pkg.workspaces!==void 0&&block("monorepo_root","package.json declares workspaces: this is a monorepo root; deploy the app\u2019s own folder to run it at the edge","package.json"),paths.has("pnpm-workspace.yaml")&&block("monorepo_root","pnpm-workspace.yaml marks a monorepo root; deploy the app\u2019s own folder to run it at the edge","pnpm-workspace.yaml");let detection,primary=found[0];if(!primary)detection=detectStatic(base,paths,pkg,deps,has,note,block);else switch(detection=base(primary.framework,primary.version),primary.framework){case"nextjs":applyNext(detection,{read,has,deps,paths,pkg,nextConfig,note,block});break;case"astro":applyAstro(detection,{read,has,astroConfig,note,block});break;case"sveltekit":applySvelteKit(detection,{read,has,svelteConfig,note,block});break;case"react-router":case"remix":applyReactRouter(detection,{read,has,rrConfig,note,block});break;case"nuxt":applyNuxt(detection,{read,deps,nuxtConfig,note,block});break;default:break}if(detection.adapter?.mode==="worker"){for(let[dep,label]of Object.entries(NATIVE_DEPENDENCIES))if(has(dep)){if(dep==="sharp"&&detection.framework==="nextjs"){note("sharp_unused_at_edge","sharp is not used at the edge: next/image serves images unoptimized there","package.json");continue}block("native_dependency",`package.json depends on ${label}, which cannot run in a Worker`,"package.json")}has("@prisma/client")&&!Object.keys(deps).some(d=>d.startsWith("@prisma/adapter-"))&&block("prisma_engine","Prisma without a driver adapter uses its native query engine, which cannot run in a Worker (add @prisma/adapter-d1 / -neon / -pg for the edge)","package.json");for(let[dep,label]of Object.entries(LONG_RUNNING_DEPENDENCIES))has(dep)&&block("long_running",`package.json depends on ${label}, which needs a process that keeps running`,"package.json");for(let p of all)if(!IGNORED_DIR.test(p)&&p.endsWith(".node")){block("native_addon_file",`${p} is a compiled native Node addon`,p);break}let scanned=0,seen=new Set;for(let p of all){if(scanned>=DETECTOR_CAPTURE_LIMITS.maxSourceFiles)break;if(now()>deadline){block("detection_timed_out",'The source could not be inspected in time; it builds as a server (deploy with target "edge" to override)');break}if(detectorWantsContent(p)!=="source"||BUILD_TIME.test(p)||CONFIG_FILES.test(p))continue;let text=input.read(p);if(text===null)continue;scanned+=1;let clean=stripComments(text);detection.framework==="nextjs"&&!seen.has("next_edge_runtime")&&NEXT_ROUTE_SOURCE.test(p)&&(NEXT_APP_EDGE_RUNTIME.test(clean)||/^(src\/)?pages\//.test(p)&&NEXT_PAGES_EDGE_RUNTIME.test(clean))&&(seen.add("next_edge_runtime"),note("next_edge_runtime",`${p} asks for the Edge runtime: Let.com builds it for the Node.js runtime, which runs in the same edge Worker (OpenNext bundles only Next's Node.js server)`,p));for(let rule of DETECTOR_SOURCE_RULES)seen.has(rule.code)||!rule.pattern.test(clean)||(seen.add(rule.code),block(rule.code,rule.message(p),p))}}return requested==="container"&&block("customer_chose_server","create.json chooses the server (container) tier","create.json"),finish(detection)}function applyNext(d,c){let config=c.nextConfig?c.read(c.nextConfig):null,clean=config?stripComments(config):"";d.frameworkVersion&&nextSupported(d.frameworkVersion)===!1&&c.block("framework_version",`Next.js ${d.frameworkVersion} is older than the edge adapter supports (OpenNext needs ${NEXT_EDGE_MIN_VERSIONS.v15}+ or ${NEXT_EDGE_MIN_VERSIONS.v16}+); upgrade Next.js to run at the edge`,"package.json");for(let f of["server.js","server.mjs","server.cjs","server.ts"]){let text=c.read(f);if(text!==null&&/(?:require\(\s*['"]next['"]\s*\)|from\s+['"]next['"]|\bnext\s*\(\s*\{)/.test(text)){c.block("custom_server",`${f} is a custom Next.js server; a custom server needs a container`,f);break}}let start=c.pkg?.scripts?.start??"";start&&!/\bnext\s+start\b/.test(start)&&/\bnode\b|\btsx\b|\bts-node\b/.test(start)&&c.block("custom_server",`The start script ("${start}") runs a custom server instead of next start`,"package.json");for(let f of["middleware.ts","middleware.js","middleware.mjs","middleware.mts","src/middleware.ts","src/middleware.js","src/middleware.mjs","src/middleware.mts"]){let text=c.read(f);text!==null&&/runtime\s*:\s*['"]nodejs['"]/.test(text)&&c.note("next_node_middleware",`${f} runs on the Node.js runtime: Let.com runs it inside the edge Worker (OpenNext's Node.js middleware support; set target "container" in create.json to run it on a server instead)`,f)}for(let f of["proxy.ts","proxy.js","proxy.mjs","proxy.mts","src/proxy.ts","src/proxy.js","src/proxy.mjs","src/proxy.mts"])if(c.paths.has(f)){c.note("next_node_middleware",`${f} is Next.js proxy (the Node.js runtime): Let.com runs it inside the edge Worker (OpenNext's Node.js middleware support; set target "container" in create.json to run it on a server instead)`,f);break}if(/output\s*:\s*['"]export['"]/.test(clean)){d.adapter={name:"static",package:null,version:null,mode:"static"},d.outputDir="out",c.note("static_export",'next.config sets output: "export" \u2014 a static site, served from static files only',c.nextConfig??void 0);return}/output\s*:\s*['"]standalone['"]/.test(clean)&&c.note("standalone_output",'next.config sets output: "standalone"; the edge build ignores it',c.nextConfig??void 0),d.adapter={name:"opennext",package:"@opennextjs/cloudflare",version:c.deps["@opennextjs/cloudflare"]??EDGE_ADAPTER_PINS.opennext,mode:"worker"},d.isr=!0}function applyAstro(d,c){let config=c.astroConfig?stripComments(c.read(c.astroConfig)??""):"";if(c.has("@astrojs/cloudflare")){d.adapter={name:"astro-cloudflare",package:"@astrojs/cloudflare",version:null,mode:"worker"};return}for(let other of["@astrojs/node","@astrojs/vercel","@astrojs/netlify","@deno/astro-adapter"])if(c.has(other)){c.block("node_adapter",`Astro is configured with ${other}, a server adapter for another platform; add @astrojs/cloudflare to run it at the edge`,"package.json");return}if(/output\s*:\s*['"](server|hybrid)['"]/.test(config)){c.block("server_output_without_adapter",`${c.astroConfig} renders on a server but names no adapter; add @astrojs/cloudflare to run it at the edge`,c.astroConfig??void 0);return}d.adapter={name:"static",package:null,version:null,mode:"static"},d.outputDir="dist"}function applySvelteKit(d,c){let config=c.svelteConfig?stripComments(c.read(c.svelteConfig)??""):"",uses=pkg=>c.has(pkg)||config.includes(`'${pkg}'`)||config.includes(`"${pkg}"`);if(uses("@sveltejs/adapter-cloudflare")){d.adapter={name:"sveltekit-cloudflare",package:"@sveltejs/adapter-cloudflare",version:null,mode:"worker"};return}if(uses("@sveltejs/adapter-static")){d.adapter={name:"static",package:null,version:null,mode:"static"},d.outputDir="build";return}if(uses("@sveltejs/adapter-auto")||!c.svelteConfig){d.adapter={name:"sveltekit-cloudflare",package:"@sveltejs/adapter-cloudflare",version:svelteKitCloudflarePin(d.frameworkVersion),mode:"worker"},c.note("adapter_auto","svelte.config uses adapter-auto; the edge build uses @sveltejs/adapter-cloudflare",c.svelteConfig??void 0);return}let other=/@sveltejs\/adapter-[a-z-]+/.exec(config)?.[0]??"another adapter";c.block("node_adapter",`svelte.config uses ${other}; switch to @sveltejs/adapter-cloudflare (or adapter-auto) to run it at the edge`,c.svelteConfig)}function applyReactRouter(d,c){let config=c.rrConfig?stripComments(c.read(c.rrConfig)??""):"";if(/ssr\s*:\s*false/.test(config)){d.adapter={name:"static",package:null,version:null,mode:"static"},d.outputDir="build/client",d.spa=!0,c.note("spa_mode",`${c.rrConfig} sets ssr: false \u2014 a single-page app served from static files`,c.rrConfig??void 0);return}if(c.has("@cloudflare/vite-plugin")||c.has("@react-router/cloudflare")||c.has("@remix-run/cloudflare")||c.has("@remix-run/cloudflare-workers")){d.adapter={name:"cloudflare-vite",package:c.has("@cloudflare/vite-plugin")?"@cloudflare/vite-plugin":null,version:null,mode:"worker"};return}let server=["@react-router/node","@react-router/serve","@react-router/express","@remix-run/node","@remix-run/serve","@remix-run/express"].find(p=>c.has(p));c.block("node_adapter",`The app is set up for a Node.js server${server?` (${server})`:""}; start from the Cloudflare template (@cloudflare/vite-plugin) to run it at the edge`,"package.json")}function applyNuxt(d,c){let major=majorOf(d.frameworkVersion);if(major!==null&&major<3){c.block("framework_version",`Nuxt ${d.frameworkVersion} predates Nitro; Nuxt 3+ runs at the edge`,"package.json");return}let config=c.nuxtConfig?stripComments(c.read(c.nuxtConfig)??""):"",preset=/preset\s*:\s*['"]([a-z0-9_-]+)['"]/.exec(config)?.[1];if(preset&&!/^cloudflare/.test(preset)){c.block("node_adapter",`${c.nuxtConfig} sets the Nitro preset "${preset}"; remove it (or use cloudflare_module) to run it at the edge`,c.nuxtConfig??void 0);return}d.adapter={name:"nitro-cloudflare",package:null,version:null,mode:"worker"}}function detectStatic(base,paths,pkg,deps,has,note,block){let server=["express","fastify","koa","hono","@hapi/hapi","@nestjs/core","restify","polka","socket.io","ws"].find(has),start=pkg?.scripts?.start??"",build=pkg?.scripts?.build??"";if(server||/\bnode\b|\btsx\b|\bts-node\b/.test(start)&&!/\b(serve|http-server|vite preview)\b/.test(start)){let d2=base("unknown",null);return block("server_app",server?`package.json depends on ${server}: this is a server app`:`The start script ("${start}") runs a Node.js server`,"package.json"),d2}let hasIndex=paths.has("index.html")||paths.has("public/index.html"),vite=has("vite"),cra=has("react-scripts");if(vite||cra||build&&hasIndex||!pkg&&hasIndex){let d2=base("static",vite?deps.vite??null:cra?deps["react-scripts"]??null:null);return d2.adapter={name:"static",package:null,version:null,mode:"static"},d2.outputDir=pkg?cra?"build":vite?"dist":null:".",d2.spa=vite||cra,pkg||note("plain_files","No package.json: the files are served as they are"),d2}if(pkg&&Object.keys(deps).length===0&&!build&&hasIndex){let d2=base("static",null);return d2.adapter={name:"static",package:null,version:null,mode:"static"},d2.outputDir=".",d2}let d=base("unknown",null);return block("no_frontend_framework",pkg?"No frontend framework or static site was found; it builds as a server":"No package.json or index.html was found; it builds as a server"),d}function finish(d){let blocking=d.reasons.filter(r=>r.blocking),label=FRAMEWORK_LABELS[d.framework],version=d.frameworkVersion?` ${d.frameworkVersion}`:"";if(blocking.length>0||!d.adapter){d.target="container",d.adapter=null,d.isr=!1;let first=blocking[0]?.message??"No edge adapter applies";return d.reason=`${d.framework==="unknown"?"Not an edge frontend":`${label}${version}`} \u2192 server (container): ${first}${blocking.length>1?` (and ${blocking.length-1} more reason${blocking.length>2?"s":""})`:""}.`,d}d.target="edge";let how=d.adapter.mode==="static"?"static files on the edge network":`a Worker on the edge network (${d.adapter.package??(d.adapter.name==="nitro-cloudflare"?"Nitro cloudflare_module preset":d.adapter.name)})`;return d.reason=`${label}${version} \u2192 edge: builds to ${how}${d.isr?", with its incremental cache in the frontend\u2019s own R2 bucket and D1 tag cache":""}.`,d}function forcedEdgeAdapter(framework,frameworkVersion=null){switch(framework){case"nextjs":return{name:"opennext",package:"@opennextjs/cloudflare",version:EDGE_ADAPTER_PINS.opennext,mode:"worker"};case"astro":return{name:"astro-cloudflare",package:"@astrojs/cloudflare",version:null,mode:"worker"};case"sveltekit":return{name:"sveltekit-cloudflare",package:"@sveltejs/adapter-cloudflare",version:svelteKitCloudflarePin(frameworkVersion),mode:"worker"};case"react-router":case"remix":return{name:"cloudflare-vite",package:"@cloudflare/vite-plugin",version:null,mode:"worker"};case"nuxt":return{name:"nitro-cloudflare",package:null,version:null,mode:"worker"};case"static":return{name:"static",package:null,version:null,mode:"static"};default:return null}}var HERE=dirname(fileURLToPath(import.meta.url)),PACKAGED_EDGE_BUILD_SCRIPT=resolve(HERE,"edge/edge-build.mjs"),EDGE_BUILD_SCRIPT=existsSync(PACKAGED_EDGE_BUILD_SCRIPT)?PACKAGED_EDGE_BUILD_SCRIPT:resolve(HERE,"../../../infra/app-cluster/builds/image/edge/edge-build.mjs"),MAX_SCRIPT_BYTES=64*1024*1024,SKIP=new Set(["node_modules",".git",".next",".open-next",".wrangler",".create-edge",".vercel",".netlify",".turbo",".cache",".svelte-kit",".nuxt",".output",".astro"]);function isSecretFile(name){return name===".env.example"?!1:/^\.env(\..*)?$/.test(name)||/^\.dev\.vars(\..*)?$/.test(name)||/\.(pem|key|p12|pfx)$/i.test(name)}function buildEnv(source,extra=[]){let exact=new Set(["PATH","HOME","USER","LOGNAME","SHELL","TMPDIR","TMP","TEMP","LANG","TZ","TERM","CI","HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy","SystemRoot","ComSpec","APPDATA","LOCALAPPDATA",...extra]),prefixes=["LC_","NODE_","COREPACK_","npm_config_","NPM_CONFIG_","YARN_","PNPM_","NEXT_PUBLIC_","PUBLIC_","VITE_"],out={};for(let[k,v]of Object.entries(source))v!==void 0&&(exact.has(k)||prefixes.some(p=>k.startsWith(p)))&&(out[k]=v);return out}var MAX_FILES=2e4;function listFiles(root){let out=[],walk=abs=>{for(let name of readdirSync(abs)){let full=join(abs,name),st=lstatSync(full,{throwIfNoEntry:!1});if(!(!st||st.isSymbolicLink())){if(st.isDirectory())SKIP.has(name)||walk(full);else if(st.isFile()&&(out.push(relative(root,full).split(sep).join("/")),out.length>MAX_FILES))throw new Error(`more than ${MAX_FILES} files under ${root}; is this the project folder?`)}}};return walk(root),out.sort()}function detectLocal(root,include=null){let paths=include?[...include].sort():listFiles(root);return detectFramework({paths,read:p=>{if(!detectorWantsContent(p))return null;try{return readFileSync(join(root,p),"utf8").slice(0,DETECTOR_SCAN_LIMITS.maxScanChars)}catch{return null}}},{budgetMs:1e4})}function localEdgePlan(detection,{forceEdge=!1}={}){if(detection.target!=="edge"&&!forceEdge){let why=detection.reasons.map(r=>` - ${r.code}: ${r.message}`).join(`
|
|
5
|
+
`);throw Object.assign(new Error(`Create would run this app as a server, not at the edge: ${detection.reason}
|
|
6
|
+
${why}
|
|
7
|
+
Fix what is listed, or pass --edge to build for the edge anyway.`),{code:"not_edge"})}let adapter=detection.adapter??forcedEdgeAdapter(detection.framework,detection.frameworkVersion??null);if(!adapter)throw Object.assign(new Error(`There is no edge adapter for ${detection.framework}`),{code:"edge_not_supported"});return{framework:detection.framework,adapter,packageManager:detection.packageManager??"npm",outputDir:detection.outputDir??null,spa:detection.spa??!1,maxScriptBytes:MAX_SCRIPT_BYTES,pins:EDGE_ADAPTER_PINS}}function parseArgs(argv){let args={dir:process.cwd(),out:null,edge:!1,keep:!1,env:[]};for(let i=0;i<argv.length;i+=1){let a=argv[i];if(a==="--dir")args.dir=argv[++i];else if(a==="--out")args.out=argv[++i];else if(a==="--edge")args.edge=!0;else if(a==="--keep")args.keep=!0;else if(a==="--env"){let name=argv[++i]??"";if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))throw new Error("--env takes a variable NAME (its value comes from this shell)");args.env.push(name)}else throw new Error(`unknown argument ${a} (usage: build-local.mjs --dir <project> --out <folder> [--edge] [--keep] [--env NAME \u2026])`)}if(!args.out)throw new Error("--out <folder> is required: the unpacked bundle goes there");return{...args,dir:resolve(args.dir),out:resolve(args.out)}}function prepareOut(out){if(!existsSync(out))return!1;let entries=readdirSync(out),LINK_DIRS=[".let",".create"];if(entries.every(name=>LINK_DIRS.includes(name)))return!1;if(!entries.includes("create-edge.json"))throw new Error(`${out} is not empty and is not an earlier edge bundle; pass a new folder`);for(let name of entries)LINK_DIRS.includes(name)||rmSync(join(out,name),{recursive:!0,force:!0});return!0}var LIVE_BUILDS=new Set,exitHook=!1;function killLiveBuilds(){for(let pid of LIVE_BUILDS)try{process.platform!=="win32"?process.kill(-pid,"SIGKILL"):process.kill(pid,"SIGKILL")}catch{}LIVE_BUILDS.clear()}async function buildEdgeBundle(options){let dir=resolve(options.dir),out=resolve(options.out),log=options.log??(line=>console.log(line)),onOutput=options.onOutput??(chunk=>process.stdout.write(chunk));if(!existsSync(join(dir,"package.json"))&&!existsSync(join(dir,"index.html")))throw Object.assign(new Error(`${dir} has no package.json (and no index.html)`),{code:"not_a_project"});if(out===dir||out.startsWith(`${dir}${sep}`))throw new Error("--out must be outside the project folder");let previous=prepareOut(out),detection=detectLocal(dir,options.include??null);log(`detector: ${detection.reason}`);let plan=localEdgePlan(detection,{forceEdge:options.forceEdge===!0});log(`plan: ${plan.framework} via ${plan.adapter.name} (${plan.adapter.package??"no package"}), ${plan.packageManager}`);let work=mkdtempSync(join(tmpdir(),"create-edge-local-")),src=join(work,"app"),bundleOut=join(work,"out");try{if(options.include)for(let rel of options.include){let parts=rel.split("/");if(parts.some(p=>p===".."||p===""||SKIP.has(p))||isSecretFile(parts[parts.length-1]))continue;let from=join(dir,...parts),st=lstatSync(from,{throwIfNoEntry:!1});!st||!st.isFile()||(mkdirSync(dirname(join(src,...parts)),{recursive:!0}),copyFileSync(from,join(src,...parts)))}else cpSync(dir,src,{recursive:!0,verbatimSymlinks:!0,filter:from=>{let name=from.split(sep).pop()??"";return from===dir?!0:!(lstatSync(from,{throwIfNoEntry:!1})?.isSymbolicLink()||SKIP.has(name)||isSecretFile(name))}});mkdirSync(bundleOut,{recursive:!0});let isolate=options.isolateHome!==!1,home=join(work,"home");isolate&&mkdirSync(home,{recursive:!0});let env=buildEnv(options.sourceEnv??process.env,options.env??[]);if(isolate&&(env.HOME=home,env.USERPROFILE=home,!env.npm_config_cache&&!env.NPM_CONFIG_CACHE&&(env.npm_config_cache=options.npmCache??join(homedir(),".cache","create-edge-build","npm"))),options.signal?.aborted)throw Object.assign(new Error("the build was cancelled"),{code:"cancelled"});let tail=[],status=await new Promise((resolveExit,reject)=>{let child=spawn(process.execPath,[EDGE_BUILD_SCRIPT],{cwd:src,stdio:["ignore","pipe","pipe"],detached:process.platform!=="win32",env:{...env,CREATE_EDGE_PLAN:JSON.stringify(plan),CREATE_EDGE_OUT:bundleOut,NEXT_TELEMETRY_DISABLED:"1",WRANGLER_SEND_METRICS:"false"}}),kill=()=>{let signal=sig=>{try{process.platform!=="win32"&&child.pid?process.kill(-child.pid,sig):child.kill(sig)}catch{}};signal("SIGTERM"),setTimeout(()=>signal("SIGKILL"),5e3).unref()};options.signal?.addEventListener("abort",kill,{once:!0}),child.pid&&LIVE_BUILDS.add(child.pid),exitHook||(exitHook=!0,process.once("exit",killLiveBuilds));let take=chunk=>{let text=String(chunk);onOutput(text);for(let line of text.split(`
|
|
8
|
+
`))line.trim()&&tail.push(line.trimEnd());tail.length>40&&tail.splice(0,tail.length-40)};child.stdout.on("data",take),child.stderr.on("data",take),child.on("error",reject),child.on("close",code=>{child.pid&&LIVE_BUILDS.delete(child.pid),options.signal?.removeEventListener("abort",kill),resolveExit(code)})});if(options.signal?.aborted)throw Object.assign(new Error("the build was cancelled"),{code:"cancelled"});if(status!==0){let marker=[...tail].reverse().find(l=>l.includes("::create-edge-error::")),before=tail.filter(l=>!l.includes("::create-edge-error::")&&!/^\s+at /.test(l)).slice(-12);throw Object.assign(new Error(`the edge build failed (exit ${status})${marker?`: ${marker.replace(/^.*::create-edge-error::\s*/,"")}`:""}; its last lines:
|
|
9
|
+
${before.join(`
|
|
10
|
+
`)}`),{code:"edge_build_failed",tail})}if(mkdirSync(out,{recursive:!0}),spawnSync("tar",["-xzf",join(bundleOut,"bundle.tar.gz"),"-C",out],{stdio:["ignore","ignore","pipe"]}).status!==0)throw new Error("could not unpack bundle.tar.gz");if(!existsSync(join(out,"create-edge.json")))throw new Error("the unpacked bundle has no create-edge.json")}finally{options.keep?log(`work copy kept at ${work}`):rmSync(work,{recursive:!0,force:!0})}return{out,previous,plan,detection}}async function main(){let args=parseArgs(process.argv.slice(2)),{previous}=await buildEdgeBundle({dir:args.dir,out:args.out,forceEdge:args.edge,env:args.env,keep:args.keep});console.log(`
|
|
11
|
+
Edge bundle ${previous?"rebuilt":"ready"} in ${args.out}. Deploy it with:
|
|
12
|
+
create deploy --dir ${args.out}${existsSync(join(args.out,".let","project.json"))||existsSync(join(args.out,".create","project.json"))?"":" --project <project>"}`)}process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url)&&main().catch(error=>{console.error(`error: ${error instanceof Error?error.message:String(error)}`),process.exit(1)});export{EDGE_BUILD_SCRIPT,MAX_SCRIPT_BYTES,buildEdgeBundle,buildEnv,detectLocal,isSecretFile,listFiles,localEdgePlan,prepareOut};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The Create edge build (app hosting spec §7.4, slice E2): one framework app → one Create edge bundle.
|
|
3
|
+
//
|
|
4
|
+
// Runs as the only RUN step of a generated Dockerfile stage (see ../create-build.sh, "edge"), inside
|
|
5
|
+
// the build pod's rootless BuildKit, in the Node image pinned by EDGE_BUILD_NODE_IMAGE
|
|
6
|
+
// (packages/shared/src/apps-frameworks.ts). Nothing here talks to Cloudflare or holds a Cloudflare
|
|
7
|
+
// credential: the bundle it writes to /out is uploaded by the pod to the CONTROL PLANE, which alone
|
|
8
|
+
// provisions resources and uploads Workers (spec §7.4 "only the control plane holds the Cloudflare API
|
|
9
|
+
// token and performs uploads").
|
|
10
|
+
//
|
|
11
|
+
// Input: CREATE_EDGE_PLAN (JSON): { framework, adapter: { name, package, version, mode },
|
|
12
|
+
// packageManager, outputDir, spa, maxScriptBytes, pins: { opennext, wrangler, sveltekitCloudflare },
|
|
13
|
+
// installCommand, buildCommand } — the last two from the service's build settings (null =
|
|
14
|
+
// the package manager's install / the package's build script); run with `sh -c`.
|
|
15
|
+
// The build runs in the service's root directory (the generated Dockerfile's WORKDIR).
|
|
16
|
+
// The project is in the working directory (/app); build-time variables are in the environment.
|
|
17
|
+
// Output: /out/bundle.tar.gz and /out/result.json; on failure a line
|
|
18
|
+
// `::create-edge-error:: code=<code> <message>` (the control plane's build summary reads it).
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { EdgeBuildError, NEXT_OPEN_NEXT_CONFIG, applyNextStageTimers, assertNoEdgeFunctions, bundleCommand, nextBuildFindings, nextCacheComponentsEnabled, nextEdgeRuntimeToNode, collectModules, nextImagesConfig, nextStaticHeaders, satisfiesSimpleRange, packBundle, resolveOutput, writeBundle } from './pack.mjs';
|
|
24
|
+
|
|
25
|
+
const root = process.cwd();
|
|
26
|
+
const outDir = process.env.CREATE_EDGE_OUT ?? '/out';
|
|
27
|
+
|
|
28
|
+
function say(line) {
|
|
29
|
+
process.stdout.write(`${line}\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function run(command, args, env = {}) {
|
|
33
|
+
say(`$ ${[command, ...args].join(' ')}`);
|
|
34
|
+
const r = spawnSync(command, args, { cwd: root, stdio: 'inherit', env: { ...process.env, ...env } });
|
|
35
|
+
if (r.status !== 0) throw new EdgeBuildError('edge_build_command_failed', `${command} ${args.join(' ')} exited with code ${r.status ?? 'unknown'}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pkgJson() {
|
|
39
|
+
const p = join(root, 'package.json');
|
|
40
|
+
return existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasDep(pkg, name) {
|
|
44
|
+
return Boolean(pkg && (pkg.dependencies?.[name] || pkg.devDependencies?.[name]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isYarnClassic() {
|
|
48
|
+
const lock = join(root, 'yarn.lock');
|
|
49
|
+
return existsSync(lock) && readFileSync(lock, 'utf8').includes('# yarn lockfile v1');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function install(pm) {
|
|
53
|
+
if (!existsSync(join(root, 'package.json'))) return;
|
|
54
|
+
switch (pm) {
|
|
55
|
+
case 'pnpm':
|
|
56
|
+
run('corepack', ['enable']);
|
|
57
|
+
run('pnpm', ['install', '--frozen-lockfile']);
|
|
58
|
+
return;
|
|
59
|
+
case 'yarn':
|
|
60
|
+
run('corepack', ['enable']);
|
|
61
|
+
run('yarn', isYarnClassic() ? ['install', '--frozen-lockfile'] : ['install', '--immutable']);
|
|
62
|
+
return;
|
|
63
|
+
default:
|
|
64
|
+
if (pm === 'bun') say('bun is not in the edge build image; installing with npm');
|
|
65
|
+
run('npm', existsSync(join(root, 'package-lock.json')) || existsSync(join(root, 'npm-shrinkwrap.json')) ? ['ci', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund']);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Adds a pinned dev dependency when the project does not already have the package. Saved into the
|
|
71
|
+
* build's own copy of package.json for every package manager: npm's `--no-save` (used before
|
|
72
|
+
* 2026-10-03) pruned the previously added pin on the next install — OpenNext, then wrangler — so a
|
|
73
|
+
* Next.js app on npm that declared neither failed with "could not determine executable to run"
|
|
74
|
+
* (found by the Next.js compatibility run, docs/apps/nextjs-compatibility.md).
|
|
75
|
+
*/
|
|
76
|
+
function ensure(pm, name, version) {
|
|
77
|
+
if (hasDep(pkgJson(), name)) return;
|
|
78
|
+
const spec = `${name}@${version}`;
|
|
79
|
+
if (pm === 'pnpm') run('pnpm', ['add', '-D', spec]);
|
|
80
|
+
else if (pm === 'yarn') run('yarn', ['add', '-D', spec]);
|
|
81
|
+
else {
|
|
82
|
+
const args = ['install', '--save-dev', '--save-exact', '--no-audit', '--no-fund', spec];
|
|
83
|
+
say(`$ npm ${args.join(' ')}`);
|
|
84
|
+
const r = spawnSync('npm', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: process.env });
|
|
85
|
+
process.stdout.write(`${r.stdout ?? ''}${r.stderr ?? ''}`);
|
|
86
|
+
if (r.status === 0) return;
|
|
87
|
+
// A lockfile can hold a Next.js older than the adapter's peer range (a security floor) although
|
|
88
|
+
// package.json's range allows a newer one: the adapter still builds it; the note tells the
|
|
89
|
+
// customer to upgrade (see nextAdapterRange).
|
|
90
|
+
if (!/ERESOLVE/.test(`${r.stdout}${r.stderr}`)) throw new EdgeBuildError('edge_build_command_failed', `npm ${args.join(' ')} exited with code ${r.status ?? 'unknown'}`);
|
|
91
|
+
run('npm', [...args, '--legacy-peer-deps']);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The installed Next.js against the installed adapter's peer range: a note when it is below (the security floor). */
|
|
96
|
+
function nextAdapterRange() {
|
|
97
|
+
try {
|
|
98
|
+
const next = JSON.parse(readFileSync(join(root, 'node_modules', 'next', 'package.json'), 'utf8')).version;
|
|
99
|
+
const adapter = JSON.parse(readFileSync(join(root, 'node_modules', '@opennextjs', 'cloudflare', 'package.json'), 'utf8'));
|
|
100
|
+
const range = adapter.peerDependencies?.next;
|
|
101
|
+
if (next && range && !satisfiesSimpleRange(next, range)) {
|
|
102
|
+
note('next_below_adapter_range', `Next.js ${next} (from the lockfile) is older than @opennextjs/cloudflare ${adapter.version} supports (next ${range}, its security floor); it was built anyway — upgrade Next.js`);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// no node_modules (pnpm/yarn layouts differ): nothing to say
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A command from the service's build settings, through the shell (it is the customer's own build). */
|
|
110
|
+
function shell(cmd, env = {}) {
|
|
111
|
+
run('sh', ['-c', cmd], env);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The build step: the settings' buildCommand when set, else the package's `name` script. */
|
|
115
|
+
function buildStep(plan, pm, env, name = 'build') {
|
|
116
|
+
if (plan.buildCommand) {
|
|
117
|
+
shell(plan.buildCommand, env);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return script(pm, name, env);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function script(pm, name, env = {}) {
|
|
124
|
+
const pkg = pkgJson();
|
|
125
|
+
if (!pkg?.scripts?.[name]) return false;
|
|
126
|
+
run(pm === 'pnpm' ? 'pnpm' : pm === 'yarn' ? 'yarn' : 'npm', ['run', name], env);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildNext(plan, pm) {
|
|
131
|
+
ensure(pm, '@opennextjs/cloudflare', plan.pins.opennext);
|
|
132
|
+
ensure(pm, 'wrangler', plan.pins.wrangler);
|
|
133
|
+
nextAdapterRange();
|
|
134
|
+
// next.config is optional for Next.js (many public starters ship without one), but OpenNext refuses
|
|
135
|
+
// to build without it: an empty config is exactly what Next.js assumes when the file is missing.
|
|
136
|
+
const nextConfigs = ['next.config.js', 'next.config.mjs', 'next.config.cjs', 'next.config.ts', 'next.config.mts', 'next.config.cts'];
|
|
137
|
+
if (!nextConfigs.some((c) => existsSync(join(root, c)))) {
|
|
138
|
+
writeFileSync(join(root, 'next.config.mjs'), '/** Written by the Let.com edge build: this app had no next.config (Next.js defaults). */\nexport default {};\n');
|
|
139
|
+
say('wrote next.config.mjs (the app has none: Next.js defaults)');
|
|
140
|
+
}
|
|
141
|
+
const configs = ['open-next.config.ts', 'open-next.config.mjs', 'open-next.config.js'];
|
|
142
|
+
if (!configs.some((c) => existsSync(join(root, c)))) {
|
|
143
|
+
writeFileSync(join(root, 'open-next.config.ts'), NEXT_OPEN_NEXT_CONFIG);
|
|
144
|
+
say('wrote open-next.config.ts (R2 incremental cache, D1 tag cache, memory queue)');
|
|
145
|
+
}
|
|
146
|
+
if (!['wrangler.jsonc', 'wrangler.json', 'wrangler.toml'].some((c) => existsSync(join(root, c)))) {
|
|
147
|
+
// OpenNext reads a wrangler config during its build; the bindings here are placeholders for its
|
|
148
|
+
// checks only — the deployed Worker is bound by the control plane to the frontend's own resources.
|
|
149
|
+
writeFileSync(
|
|
150
|
+
join(root, 'wrangler.jsonc'),
|
|
151
|
+
JSON.stringify(
|
|
152
|
+
{
|
|
153
|
+
name: 'create-edge-build',
|
|
154
|
+
main: '.open-next/worker.js',
|
|
155
|
+
compatibility_date: '2026-09-01',
|
|
156
|
+
compatibility_flags: ['nodejs_compat', 'global_fetch_strictly_public'],
|
|
157
|
+
r2_buckets: [{ binding: 'NEXT_INC_CACHE_R2_BUCKET', bucket_name: 'create-build-placeholder' }],
|
|
158
|
+
d1_databases: [{ binding: 'NEXT_TAG_CACHE_D1', database_name: 'create-build-placeholder', database_id: '00000000-0000-0000-0000-000000000000' }],
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
2,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
// Edge-runtime routes are built for the Node.js runtime, which runs in the same Worker (pack.mjs).
|
|
166
|
+
const edgeRuntime = nextEdgeRuntimeToNode(root);
|
|
167
|
+
if (edgeRuntime.length > 0) {
|
|
168
|
+
note('next_edge_runtime', `${edgeRuntime.join(', ')} ${edgeRuntime.length === 1 ? 'asks' : 'ask'} for the Edge runtime; built for the Node.js runtime, which runs in the same edge Worker`);
|
|
169
|
+
}
|
|
170
|
+
if (plan.buildCommand) {
|
|
171
|
+
// The settings' build command builds Next.js; OpenNext then only adapts its output.
|
|
172
|
+
shell(plan.buildCommand, { NEXT_TELEMETRY_DISABLED: '1' });
|
|
173
|
+
run('npx', ['--no-install', 'opennextjs-cloudflare', 'build', '--skipNextBuild'], { NEXT_TELEMETRY_DISABLED: '1' });
|
|
174
|
+
} else {
|
|
175
|
+
run('npx', ['--no-install', 'opennextjs-cloudflare', 'build'], { NEXT_TELEMETRY_DISABLED: '1' });
|
|
176
|
+
}
|
|
177
|
+
assertNoEdgeFunctions(root);
|
|
178
|
+
if (nextBuildFindings(root).nodeMiddleware) note('next_node_middleware', 'Node.js middleware (or proxy) runs inside the edge Worker (nodejs_compat)');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A line the build summary shows on a successful build: `::create-edge-note:: code=<code> <message>`. */
|
|
182
|
+
function note(code, message) {
|
|
183
|
+
say(`::create-edge-note:: code=${code} ${message}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function frameworkBuild(plan, pm) {
|
|
187
|
+
const env = { NEXT_TELEMETRY_DISABLED: '1', ASTRO_TELEMETRY_DISABLED: '1', NUXT_TELEMETRY_DISABLED: '1', WRANGLER_SEND_METRICS: 'false' };
|
|
188
|
+
switch (plan.adapter.name) {
|
|
189
|
+
case 'opennext':
|
|
190
|
+
return buildNext(plan, pm);
|
|
191
|
+
case 'sveltekit-cloudflare':
|
|
192
|
+
// The detector's per-Kit-major choice (adapter 8.x for SvelteKit 3), else the plan's Kit 2 pin.
|
|
193
|
+
ensure(pm, '@sveltejs/adapter-cloudflare', plan.adapter.version ?? plan.pins.sveltekitCloudflare);
|
|
194
|
+
ensure(pm, 'wrangler', plan.pins.wrangler);
|
|
195
|
+
// adapter-auto picks adapter-cloudflare in Cloudflare's build environment.
|
|
196
|
+
buildStep(plan, pm, { ...env, CF_PAGES: '1' });
|
|
197
|
+
return;
|
|
198
|
+
case 'nitro-cloudflare':
|
|
199
|
+
ensure(pm, 'wrangler', plan.pins.wrangler);
|
|
200
|
+
if (!buildStep(plan, pm, { ...env, NITRO_PRESET: 'cloudflare_module' })) run('npx', ['--no-install', 'nuxi', 'build'], { ...env, NITRO_PRESET: 'cloudflare_module' });
|
|
201
|
+
return;
|
|
202
|
+
case 'static':
|
|
203
|
+
buildStep(plan, pm, env);
|
|
204
|
+
return;
|
|
205
|
+
default:
|
|
206
|
+
ensure(pm, 'wrangler', plan.pins.wrangler);
|
|
207
|
+
if (!buildStep(plan, pm, env)) throw new EdgeBuildError('edge_build_command_failed', 'package.json has no build script');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function main() {
|
|
212
|
+
const plan = JSON.parse(process.env.CREATE_EDGE_PLAN ?? '{}');
|
|
213
|
+
if (!plan.adapter) throw new EdgeBuildError('edge_plan_missing', 'The build has no edge plan');
|
|
214
|
+
const pm = plan.packageManager ?? 'npm';
|
|
215
|
+
say(`::create-edge:: framework=${plan.framework} adapter=${plan.adapter.name} mode=${plan.adapter.mode}`);
|
|
216
|
+
if (plan.installCommand) shell(plan.installCommand);
|
|
217
|
+
else install(pm);
|
|
218
|
+
frameworkBuild(plan, pm);
|
|
219
|
+
const output = resolveOutput(root, plan);
|
|
220
|
+
let modules = [];
|
|
221
|
+
if (output.main) {
|
|
222
|
+
const bundled = join(root, '.create-edge', 'worker');
|
|
223
|
+
mkdirSync(bundled, { recursive: true });
|
|
224
|
+
const cmd = bundleCommand(output, bundled, join(root, '.create-edge', 'wrangler.json'));
|
|
225
|
+
ensure(pm, 'wrangler', plan.pins.wrangler);
|
|
226
|
+
run(cmd.command, cmd.args, cmd.env);
|
|
227
|
+
if (plan.adapter.name === 'opennext') {
|
|
228
|
+
// Next.js Cache Components stage timers on workerd (pack.mjs, patchNextStageTimers).
|
|
229
|
+
const timers = applyNextStageTimers(bundled, nextCacheComponentsEnabled(root));
|
|
230
|
+
if (timers.patched > 0) say(`::create-edge:: next stage timers adjusted for workerd (${timers.patched})`);
|
|
231
|
+
}
|
|
232
|
+
modules = collectModules(bundled, output.main);
|
|
233
|
+
}
|
|
234
|
+
// E5: Next.js's image sources; the dispatch Worker serves /_next/image as /_create/image.
|
|
235
|
+
const images = plan.adapter.name === 'opennext' ? nextImagesConfig(root) : null;
|
|
236
|
+
// next.config headers() for the static files the dispatch Worker serves before the Worker.
|
|
237
|
+
const staticHeaders = plan.adapter.name === 'opennext' ? nextStaticHeaders(root) : null;
|
|
238
|
+
const packed = packBundle({ output, modules, plan, images, staticHeaders });
|
|
239
|
+
writeBundle(outDir, packed);
|
|
240
|
+
say(`::create-edge:: bundle sha256=${packed.sha256} bytes=${packed.tarball.length} worker=${packed.stats.scriptBytes} assets=${packed.stats.assetCount} cache=${packed.stats.cacheCount}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
main();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const code = error instanceof EdgeBuildError ? error.code : 'edge_build_failed';
|
|
247
|
+
say(`::create-edge-error:: code=${code} ${error instanceof Error ? error.message : String(error)}`);
|
|
248
|
+
if (error instanceof EdgeBuildError && error.fix) say(`fix: ${error.fix}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|