@yolo-labs/yolo-cli 0.23.0
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/README.md +57 -0
- package/dist/artifact-get.js +131 -0
- package/dist/artifact-list.js +133 -0
- package/dist/auth-context.js +60 -0
- package/dist/canonicalizer.js +208 -0
- package/dist/cli.js +1403 -0
- package/dist/context.js +58 -0
- package/dist/deploy-bundle.js +394 -0
- package/dist/deploy-cli.js +1341 -0
- package/dist/deploy-client.js +460 -0
- package/dist/deploy-config.js +301 -0
- package/dist/deploy-detect.js +498 -0
- package/dist/deploy-ship.js +389 -0
- package/dist/lockfile.js +203 -0
- package/dist/plan-diff.js +162 -0
- package/dist/plan-export.js +261 -0
- package/dist/plan-frontmatter.schema.json +184 -0
- package/dist/plan-get.js +244 -0
- package/dist/plan-import.js +420 -0
- package/dist/plan-list.js +153 -0
- package/dist/plan-open.js +100 -0
- package/dist/plan-state.js +228 -0
- package/dist/plan-validate.js +270 -0
- package/dist/revision.js +43 -0
- package/dist/run-get.js +130 -0
- package/dist/run-lifecycle.js +133 -0
- package/dist/run-list.js +148 -0
- package/dist/run-start.js +110 -0
- package/dist/run-transfer.js +128 -0
- package/dist/serve.js +227 -0
- package/dist/tileapp-developer.js +222 -0
- package/dist/tileapp-oci-assembler.js +591 -0
- package/dist/tileapp-personal.js +461 -0
- package/dist/tileapp-publisher.js +134 -0
- package/dist/tileapp-validator.js +239 -0
- package/dist/vendored/flexdb.mjs +1087 -0
- package/dist/webapp-url.js +61 -0
- package/dist/work-client.js +195 -0
- package/package.json +39 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-shape detection for `yolo deploy` (spec
|
|
3
|
+
* `docs/MANAGED_HOSTING_CLI_SPEC.md` §2, `deploy-detect`).
|
|
4
|
+
*
|
|
5
|
+
* Pure DETECTION — running the build command belongs to the ship
|
|
6
|
+
* orchestration; this module only RETURNS `buildCommand` when one
|
|
7
|
+
* applies. First hit wins, in spec order:
|
|
8
|
+
*
|
|
9
|
+
* 1. `.yolo/deploy.json` with `type` → authoritative.
|
|
10
|
+
* 2. `wrangler.jsonc|toml` — read `main` + `assets.directory` via a
|
|
11
|
+
* minimal jsonc comment-strip / toml key-extract (NO new deps).
|
|
12
|
+
* `main` ⇒ worker; assets-only wrangler config ⇒ static.
|
|
13
|
+
* 3. `package.json` with a `build` script (npm/pnpm/yarn/bun chosen
|
|
14
|
+
* via lockfile) → static, output dir resolved as
|
|
15
|
+
* `config build.outputDir` > first of dist/build/out/public/_site
|
|
16
|
+
* containing index.html. No resolvable output dir ⇒ fall through
|
|
17
|
+
* (a TS worker repo with a `build` script must still reach step 5).
|
|
18
|
+
* 4. Plain static: `dist/` or `public/` containing index.html, or the
|
|
19
|
+
* cwd itself when it has a root index.html and no package.json.
|
|
20
|
+
* 5. `src/index.ts|js` matching `export default` + `fetch(` → worker
|
|
21
|
+
* (esbuild handles TS at bundle time).
|
|
22
|
+
* 6. `FAIL [detect-failed]` + the init hint.
|
|
23
|
+
*
|
|
24
|
+
* Everything is driven through the injectable `readFileImpl`
|
|
25
|
+
* (auth-context convention) — existence checks are "can I read the
|
|
26
|
+
* marker file", so unit tests can run against pure in-memory maps and
|
|
27
|
+
* integration tests against tmp-dir fixtures.
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { readDeployConfig } from './deploy-config.js';
|
|
32
|
+
export const DETECT_INIT_HINT = "run 'yolo deploy init --type static|worker'";
|
|
33
|
+
const OUTPUT_DIR_CANDIDATES = ['dist', 'build', 'out', 'public', '_site'];
|
|
34
|
+
const WORKER_ENTRY_CANDIDATES = ['src/index.ts', 'src/index.js'];
|
|
35
|
+
// ─── Public entry ─────────────────────────────────────────────────────────
|
|
36
|
+
export function detectProjectShape(options) {
|
|
37
|
+
const { cwd } = options;
|
|
38
|
+
const read = options.readFileImpl ?? defaultReadFile;
|
|
39
|
+
// Resolve the deploy config (step-1 input AND the outputDir/command
|
|
40
|
+
// overrides steps 3 uses even when `type` is unset).
|
|
41
|
+
let config;
|
|
42
|
+
if (options.config !== undefined) {
|
|
43
|
+
config = options.config;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const result = readDeployConfig(cwd, read);
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
// A malformed/invalid deploy.json is an explicit user artifact —
|
|
49
|
+
// heuristics must not silently override it.
|
|
50
|
+
return fail(result.message);
|
|
51
|
+
}
|
|
52
|
+
config = result.config;
|
|
53
|
+
}
|
|
54
|
+
// Package build command — computed up front so the pinned-static branch can
|
|
55
|
+
// fall back to it (codex P2 r11), same as the untyped package-build path.
|
|
56
|
+
const pkg = readPackageJson(cwd, read);
|
|
57
|
+
const pkgBuildCommand = pkg?.scripts?.build !== undefined ? buildCommandFor(cwd, read) : undefined;
|
|
58
|
+
// ── 1. deploy.json `type` is authoritative ─────────────────────────────
|
|
59
|
+
if (config?.type === 'static') {
|
|
60
|
+
const buildCommand = config.build?.command ?? pkgBuildCommand;
|
|
61
|
+
const assetsDir = config.build?.outputDir ??
|
|
62
|
+
resolveOutputDir(cwd, read) ??
|
|
63
|
+
(read(path.join(cwd, 'index.html')) !== undefined ? '.' : undefined) ??
|
|
64
|
+
// A clean checkout whose build hasn't run yet: with a build command we
|
|
65
|
+
// can default the output dir (build creates it, then bundle reads it) —
|
|
66
|
+
// `yolo deploy init --type static` writes the pin but no build.command.
|
|
67
|
+
(buildCommand !== undefined ? OUTPUT_DIR_CANDIDATES[0] : undefined);
|
|
68
|
+
if (assetsDir === undefined) {
|
|
69
|
+
return fail(".yolo/deploy.json sets type 'static' but no output dir was found: set build.outputDir, add a package.json build script, or build the project so one of " +
|
|
70
|
+
`${OUTPUT_DIR_CANDIDATES.join('/')} contains index.html`);
|
|
71
|
+
}
|
|
72
|
+
return hit({
|
|
73
|
+
type: 'static',
|
|
74
|
+
assetsDir,
|
|
75
|
+
...optional('buildCommand', buildCommand),
|
|
76
|
+
}, 'deploy-json');
|
|
77
|
+
}
|
|
78
|
+
if (config?.type === 'worker') {
|
|
79
|
+
const explicitEntry = config.worker?.entry;
|
|
80
|
+
const entry = explicitEntry ?? findWorkerEntry(cwd, read, /* requireSignature */ false);
|
|
81
|
+
if (entry === undefined) {
|
|
82
|
+
return fail(".yolo/deploy.json sets type 'worker' but no entry was found: set worker.entry (source or a pre-built .js/.mjs module)");
|
|
83
|
+
}
|
|
84
|
+
// Prebuilt skip is OPT-IN via deploy.json worker.prebuilt (codex P2 r9) —
|
|
85
|
+
// the extension is not a reliable signal (a built bundle and a source
|
|
86
|
+
// file can both be .js, and skipping esbuild on source drops its
|
|
87
|
+
// imports). Only an explicitly-set entry can be prebuilt.
|
|
88
|
+
const prebuilt = config.worker?.prebuilt === true && explicitEntry !== undefined;
|
|
89
|
+
return hit({
|
|
90
|
+
type: 'worker',
|
|
91
|
+
entry,
|
|
92
|
+
...(prebuilt && { prebuilt: true }),
|
|
93
|
+
...optional('assetsDir', config.worker?.assetsDir),
|
|
94
|
+
// Symmetric with the static branch + the wrangler branch: fall back to
|
|
95
|
+
// the package.json build script when deploy.json omits an explicit
|
|
96
|
+
// build.command. Without this, an explicit-worker project would NOT
|
|
97
|
+
// auto-build (the asymmetry users hit — static built, worker didn't),
|
|
98
|
+
// and a `prebuilt` worker whose dist is produced by `npm run build`
|
|
99
|
+
// (no explicit build.command) would ship a missing entry.
|
|
100
|
+
...optional('buildCommand', config.build?.command ?? pkgBuildCommand),
|
|
101
|
+
}, 'deploy-json');
|
|
102
|
+
}
|
|
103
|
+
// pkg / pkgBuildCommand are computed above (reused by steps 2–6).
|
|
104
|
+
// ── 2. wrangler.jsonc / wrangler.toml ──────────────────────────────────
|
|
105
|
+
const wrangler = readWranglerConfig(cwd, read);
|
|
106
|
+
if (wrangler !== undefined) {
|
|
107
|
+
const buildCommand = config?.build?.command ?? pkgBuildCommand;
|
|
108
|
+
if (wrangler.main !== undefined) {
|
|
109
|
+
return hit({
|
|
110
|
+
type: 'worker',
|
|
111
|
+
entry: wrangler.main,
|
|
112
|
+
...optional('assetsDir', wrangler.assetsDirectory),
|
|
113
|
+
...optional('buildCommand', buildCommand),
|
|
114
|
+
}, 'wrangler');
|
|
115
|
+
}
|
|
116
|
+
if (wrangler.assetsDirectory !== undefined) {
|
|
117
|
+
// Assets-only wrangler project (no `main`) — nothing to bundle as
|
|
118
|
+
// a module; ship it static and let common-api attach the shim.
|
|
119
|
+
return hit({
|
|
120
|
+
type: 'static',
|
|
121
|
+
assetsDir: wrangler.assetsDirectory,
|
|
122
|
+
...optional('buildCommand', buildCommand),
|
|
123
|
+
}, 'wrangler');
|
|
124
|
+
}
|
|
125
|
+
// wrangler file present but carries neither key — fall through.
|
|
126
|
+
}
|
|
127
|
+
// ── 3. package.json `build` script → static ────────────────────────────
|
|
128
|
+
if (pkgBuildCommand !== undefined) {
|
|
129
|
+
const outputDir = config?.build?.outputDir ?? resolveOutputDir(cwd, read);
|
|
130
|
+
if (outputDir !== undefined) {
|
|
131
|
+
return hit({
|
|
132
|
+
type: 'static',
|
|
133
|
+
assetsDir: outputDir,
|
|
134
|
+
buildCommand: config?.build?.command ?? pkgBuildCommand,
|
|
135
|
+
}, 'package-build');
|
|
136
|
+
}
|
|
137
|
+
// Build script but no resolvable output dir — keep falling: this may
|
|
138
|
+
// be a worker repo whose `build` is typecheck/compile (step 5).
|
|
139
|
+
}
|
|
140
|
+
// ── 4. plain static ─────────────────────────────────────────────────────
|
|
141
|
+
for (const dir of ['dist', 'public']) {
|
|
142
|
+
if (read(path.join(cwd, dir, 'index.html')) !== undefined) {
|
|
143
|
+
return hit({ type: 'static', assetsDir: dir }, 'plain-static');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (read(path.join(cwd, 'index.html')) !== undefined && pkg === undefined) {
|
|
147
|
+
return hit({ type: 'static', assetsDir: '.' }, 'plain-static');
|
|
148
|
+
}
|
|
149
|
+
// ── 5. worker entry signature ───────────────────────────────────────────
|
|
150
|
+
const entry = findWorkerEntry(cwd, read, /* requireSignature */ true);
|
|
151
|
+
if (entry !== undefined) {
|
|
152
|
+
return hit({ type: 'worker', entry }, 'worker-entry');
|
|
153
|
+
}
|
|
154
|
+
// ── 6. build-script static with no output yet (clean checkout) ──────────
|
|
155
|
+
// Detection is offline and runs BEFORE the build, so a fresh static app's
|
|
156
|
+
// output dir doesn't exist yet (codex P2 r8). We've now ruled out a worker
|
|
157
|
+
// entry, so a `build` script means static: default the assets dir to the
|
|
158
|
+
// configured value or `dist`. The orchestrator runs the build, then bundle
|
|
159
|
+
// reads it — a wrong guess fails later with a clear "assets dir not found"
|
|
160
|
+
// pointing the user to set build.outputDir.
|
|
161
|
+
if (pkgBuildCommand !== undefined) {
|
|
162
|
+
return hit({
|
|
163
|
+
type: 'static',
|
|
164
|
+
assetsDir: config?.build?.outputDir ?? OUTPUT_DIR_CANDIDATES[0],
|
|
165
|
+
buildCommand: config?.build?.command ?? pkgBuildCommand,
|
|
166
|
+
}, 'package-build');
|
|
167
|
+
}
|
|
168
|
+
// ── 6. detect-failed ────────────────────────────────────────────────────
|
|
169
|
+
return fail(`could not detect a deployable project in ${cwd}: no .yolo/deploy.json type, ` +
|
|
170
|
+
'wrangler config, build script with an output dir, static dir with index.html, ' +
|
|
171
|
+
'or src/index.ts|js worker entry');
|
|
172
|
+
}
|
|
173
|
+
function readPackageJson(cwd, read) {
|
|
174
|
+
const text = read(path.join(cwd, 'package.json'));
|
|
175
|
+
if (text === undefined)
|
|
176
|
+
return undefined;
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(text);
|
|
179
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
180
|
+
return undefined;
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Package-manager-aware `run build`, chosen via lockfile presence. */
|
|
188
|
+
function buildCommandFor(cwd, read) {
|
|
189
|
+
if (read(path.join(cwd, 'pnpm-lock.yaml')) !== undefined)
|
|
190
|
+
return 'pnpm run build';
|
|
191
|
+
if (read(path.join(cwd, 'yarn.lock')) !== undefined)
|
|
192
|
+
return 'yarn run build';
|
|
193
|
+
if (read(path.join(cwd, 'bun.lockb')) !== undefined || read(path.join(cwd, 'bun.lock')) !== undefined) {
|
|
194
|
+
return 'bun run build';
|
|
195
|
+
}
|
|
196
|
+
return 'npm run build';
|
|
197
|
+
}
|
|
198
|
+
function resolveOutputDir(cwd, read) {
|
|
199
|
+
for (const dir of OUTPUT_DIR_CANDIDATES) {
|
|
200
|
+
if (read(path.join(cwd, dir, 'index.html')) !== undefined)
|
|
201
|
+
return dir;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Find a conventional worker entry. With `requireSignature` (step 5)
|
|
207
|
+
* the file must contain both `export default` and `fetch(`; without it
|
|
208
|
+
* (deploy.json says worker but omitted `entry`) existence is enough.
|
|
209
|
+
*/
|
|
210
|
+
function findWorkerEntry(cwd, read, requireSignature) {
|
|
211
|
+
for (const candidate of WORKER_ENTRY_CANDIDATES) {
|
|
212
|
+
const content = read(path.join(cwd, ...candidate.split('/')));
|
|
213
|
+
if (content === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
if (!requireSignature)
|
|
216
|
+
return candidate;
|
|
217
|
+
if (/export\s+default/.test(content) && content.includes('fetch('))
|
|
218
|
+
return candidate;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
// ─── wrangler config extraction (minimal, no deps) ───────────────────────
|
|
223
|
+
// Wrangler binding stanzas we recognize. We only DETECT their presence (to
|
|
224
|
+
// warn) — we NEVER import them: wrangler bindings reference the user's OWN
|
|
225
|
+
// Cloudflare account resources, which don't exist in YOLO Host's tenancy.
|
|
226
|
+
// D1/KV/R2 are re-provisionable through our deploy.* tools; the rest are
|
|
227
|
+
// unsupported on the platform.
|
|
228
|
+
const PROVISIONABLE_BINDING_KEYS = {
|
|
229
|
+
d1_databases: 'D1',
|
|
230
|
+
kv_namespaces: 'KV',
|
|
231
|
+
r2_buckets: 'R2',
|
|
232
|
+
};
|
|
233
|
+
const UNSUPPORTED_BINDING_KEYS = [
|
|
234
|
+
'durable_objects',
|
|
235
|
+
'queues',
|
|
236
|
+
'services',
|
|
237
|
+
'service', // defensive: the canonical key is `services`, but accept singular too
|
|
238
|
+
'hyperdrive',
|
|
239
|
+
'vectorize',
|
|
240
|
+
'ai',
|
|
241
|
+
'analytics_engine_datasets',
|
|
242
|
+
'dispatch_namespaces',
|
|
243
|
+
'mtls_certificates',
|
|
244
|
+
'send_email',
|
|
245
|
+
'browser',
|
|
246
|
+
'workflows',
|
|
247
|
+
'pipelines',
|
|
248
|
+
'version_metadata',
|
|
249
|
+
'unsafe',
|
|
250
|
+
'wasm_modules',
|
|
251
|
+
'text_blobs',
|
|
252
|
+
'data_blobs',
|
|
253
|
+
];
|
|
254
|
+
const ALL_BINDING_KEYS = [...Object.keys(PROVISIONABLE_BINDING_KEYS), ...UNSUPPORTED_BINDING_KEYS];
|
|
255
|
+
/**
|
|
256
|
+
* Binding stanzas present-and-non-empty in a parsed JSON wrangler config —
|
|
257
|
+
* including per-environment overrides under `env.<name>` (codex P2), since
|
|
258
|
+
* wrangler lets bindings live there with no top-level equivalent.
|
|
259
|
+
*/
|
|
260
|
+
function detectJsonBindingKinds(parsed) {
|
|
261
|
+
const nonEmpty = (v) => Array.isArray(v) ? v.length > 0 : v !== null && typeof v === 'object' ? Object.keys(v).length > 0 : Boolean(v);
|
|
262
|
+
const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
263
|
+
const found = new Set();
|
|
264
|
+
const scan = (obj) => {
|
|
265
|
+
for (const k of ALL_BINDING_KEYS)
|
|
266
|
+
if (nonEmpty(obj[k]))
|
|
267
|
+
found.add(k);
|
|
268
|
+
};
|
|
269
|
+
scan(parsed);
|
|
270
|
+
if (isObj(parsed.env))
|
|
271
|
+
for (const envCfg of Object.values(parsed.env))
|
|
272
|
+
if (isObj(envCfg))
|
|
273
|
+
scan(envCfg);
|
|
274
|
+
return [...found];
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Binding stanzas present in a toml wrangler config — detected by SECTION
|
|
278
|
+
* HEADER (`[[d1_databases]]`, `[queues.producers]`, `[ai]`), not parsed, and
|
|
279
|
+
* including env-scoped sections (`[[env.production.kv_namespaces]]`). The
|
|
280
|
+
* minimal toml reader can't read the values, but it can notice the keys exist:
|
|
281
|
+
* any segment of the dotted section name that is a known binding key counts.
|
|
282
|
+
*/
|
|
283
|
+
function detectTomlBindingKinds(text) {
|
|
284
|
+
const found = new Set();
|
|
285
|
+
for (const raw of text.split('\n')) {
|
|
286
|
+
// Capture the full dotted section name incl. hyphenated env names
|
|
287
|
+
// (`[[env.prod-us.kv_namespaces]]`) before splitting on '.' (codex P3).
|
|
288
|
+
const m = /^\[+([A-Za-z0-9_.-]+)/.exec(raw.trim());
|
|
289
|
+
if (!m)
|
|
290
|
+
continue;
|
|
291
|
+
const segs = m[1].split('.');
|
|
292
|
+
// The binding key is POSITIONAL, not "any segment": top-level `[[key]]` →
|
|
293
|
+
// segs[0]; env-scoped `[[env.<name>.key]]` → segs[2]. Scanning every
|
|
294
|
+
// segment would mis-read an env NAMED after a binding (`[env.ai.vars]`,
|
|
295
|
+
// `[env.queues.vars]`) as a binding (codex P3).
|
|
296
|
+
const key = segs[0] === 'env' ? segs[2] : segs[0];
|
|
297
|
+
if (key && ALL_BINDING_KEYS.includes(key))
|
|
298
|
+
found.add(key);
|
|
299
|
+
}
|
|
300
|
+
return [...found];
|
|
301
|
+
}
|
|
302
|
+
function readWranglerConfig(cwd, read) {
|
|
303
|
+
const jsoncName = read(path.join(cwd, 'wrangler.jsonc')) !== undefined ? 'wrangler.jsonc' : 'wrangler.json';
|
|
304
|
+
const jsoncText = read(path.join(cwd, 'wrangler.jsonc')) ?? read(path.join(cwd, 'wrangler.json'));
|
|
305
|
+
if (jsoncText !== undefined) {
|
|
306
|
+
const parsed = parseJsonc(jsoncText);
|
|
307
|
+
if (parsed !== undefined) {
|
|
308
|
+
const main = typeof parsed.main === 'string' ? parsed.main : undefined;
|
|
309
|
+
const assets = parsed.assets;
|
|
310
|
+
const assetsDirectory = assets !== null && typeof assets === 'object' && !Array.isArray(assets) &&
|
|
311
|
+
typeof assets.directory === 'string'
|
|
312
|
+
? assets.directory
|
|
313
|
+
: undefined;
|
|
314
|
+
const compatibilityFlags = Array.isArray(parsed.compatibility_flags)
|
|
315
|
+
? parsed.compatibility_flags.filter((f) => typeof f === 'string')
|
|
316
|
+
: undefined;
|
|
317
|
+
const bindingKinds = detectJsonBindingKinds(parsed);
|
|
318
|
+
return {
|
|
319
|
+
main,
|
|
320
|
+
assetsDirectory,
|
|
321
|
+
...(compatibilityFlags && compatibilityFlags.length > 0 ? { compatibilityFlags } : {}),
|
|
322
|
+
...(bindingKinds.length > 0 ? { bindingKinds } : {}),
|
|
323
|
+
sourceFile: jsoncName,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Unparseable wrangler file: treat as absent (fall through to toml).
|
|
327
|
+
}
|
|
328
|
+
const tomlText = read(path.join(cwd, 'wrangler.toml'));
|
|
329
|
+
if (tomlText !== undefined) {
|
|
330
|
+
const bindingKinds = detectTomlBindingKinds(tomlText);
|
|
331
|
+
return {
|
|
332
|
+
...extractTomlKeys(tomlText),
|
|
333
|
+
...(bindingKinds.length > 0 ? { bindingKinds } : {}),
|
|
334
|
+
sourceFile: 'wrangler.toml',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Adapt an existing wrangler config into the canonical `.yolo/deploy.json`
|
|
341
|
+
* shape — the migration `yolo deploy init` performs once when a project has a
|
|
342
|
+
* wrangler config but no deploy.json. Returns the deploy.json fields to merge,
|
|
343
|
+
* the source filename, what was migrated, and any honesty warnings (e.g. toml
|
|
344
|
+
* compatibility_flags aren't parsed by our minimal reader). Returns undefined
|
|
345
|
+
* when no wrangler config is present or it carries nothing adaptable.
|
|
346
|
+
*/
|
|
347
|
+
export function adaptWrangler(cwd, readFileImpl) {
|
|
348
|
+
const read = readFileImpl ?? defaultReadFile;
|
|
349
|
+
const w = readWranglerConfig(cwd, read);
|
|
350
|
+
if (w === undefined || w.sourceFile === undefined)
|
|
351
|
+
return undefined;
|
|
352
|
+
const config = {};
|
|
353
|
+
const migrated = [];
|
|
354
|
+
const warnings = [];
|
|
355
|
+
if (w.main !== undefined) {
|
|
356
|
+
config.type = 'worker';
|
|
357
|
+
config.worker = { entry: w.main, ...(w.assetsDirectory !== undefined ? { assetsDir: w.assetsDirectory } : {}) };
|
|
358
|
+
migrated.push(`type=worker`, `worker.entry=${w.main}`);
|
|
359
|
+
if (w.assetsDirectory !== undefined)
|
|
360
|
+
migrated.push(`worker.assetsDir=${w.assetsDirectory}`);
|
|
361
|
+
}
|
|
362
|
+
else if (w.assetsDirectory !== undefined) {
|
|
363
|
+
config.type = 'static';
|
|
364
|
+
config.build = { outputDir: w.assetsDirectory };
|
|
365
|
+
migrated.push(`type=static`, `build.outputDir=${w.assetsDirectory}`);
|
|
366
|
+
}
|
|
367
|
+
if (w.compatibilityFlags && w.compatibilityFlags.length > 0) {
|
|
368
|
+
config.compatibilityFlags = w.compatibilityFlags;
|
|
369
|
+
migrated.push(`compatibilityFlags=[${w.compatibilityFlags.join(', ')}]`);
|
|
370
|
+
}
|
|
371
|
+
else if (w.sourceFile === 'wrangler.toml') {
|
|
372
|
+
// The minimal toml reader extracts only top-level main/[assets].directory,
|
|
373
|
+
// not the compatibility_flags array — be honest rather than silently drop.
|
|
374
|
+
warnings.push('compatibility_flags (if any) were NOT read from wrangler.toml — add them to .yolo/deploy.json `compatibilityFlags` manually if your Worker needs them (e.g. nodejs_compat)');
|
|
375
|
+
}
|
|
376
|
+
// Binding stanzas are NEVER imported (they reference the user's own CF
|
|
377
|
+
// account). Warn so a declared D1/KV/R2 doesn't silently leave env.<NAME>
|
|
378
|
+
// undefined at runtime, and so unsupported bindings aren't assumed to work.
|
|
379
|
+
if (w.bindingKinds && w.bindingKinds.length > 0) {
|
|
380
|
+
const provisionable = w.bindingKinds
|
|
381
|
+
.filter((k) => k in PROVISIONABLE_BINDING_KEYS)
|
|
382
|
+
.map((k) => PROVISIONABLE_BINDING_KEYS[k]);
|
|
383
|
+
const unsupported = w.bindingKinds.filter((k) => !(k in PROVISIONABLE_BINDING_KEYS));
|
|
384
|
+
if (provisionable.length > 0) {
|
|
385
|
+
warnings.push(`wrangler config declares ${provisionable.join(', ')} binding(s) — NOT imported (they reference your own Cloudflare account); ` +
|
|
386
|
+
'provision them on YOLO Host with deploy.db_provision / deploy.kv_create / deploy.bucket_create so env.<NAME> exists at runtime');
|
|
387
|
+
}
|
|
388
|
+
if (unsupported.length > 0) {
|
|
389
|
+
warnings.push(`wrangler config declares ${unsupported.join(', ')} which YOLO Host does not support — dropped`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (migrated.length === 0)
|
|
393
|
+
return undefined;
|
|
394
|
+
return { config, sourceFile: w.sourceFile, migrated, warnings };
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Minimal jsonc: strip line and block comments outside string
|
|
398
|
+
* literals, drop trailing commas, then JSON.parse. NOT a general jsonc
|
|
399
|
+
* parser — exactly enough for wrangler configs, by spec.
|
|
400
|
+
*/
|
|
401
|
+
function parseJsonc(text) {
|
|
402
|
+
let out = '';
|
|
403
|
+
let i = 0;
|
|
404
|
+
let inString = false;
|
|
405
|
+
while (i < text.length) {
|
|
406
|
+
const c = text[i];
|
|
407
|
+
if (inString) {
|
|
408
|
+
out += c;
|
|
409
|
+
if (c === '\\') {
|
|
410
|
+
out += text[i + 1] ?? '';
|
|
411
|
+
i += 2;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (c === '"')
|
|
415
|
+
inString = false;
|
|
416
|
+
i++;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (c === '"') {
|
|
420
|
+
inString = true;
|
|
421
|
+
out += c;
|
|
422
|
+
i++;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
426
|
+
while (i < text.length && text[i] !== '\n')
|
|
427
|
+
i++;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
431
|
+
i += 2;
|
|
432
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
433
|
+
i++;
|
|
434
|
+
i += 2;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
out += c;
|
|
438
|
+
i++;
|
|
439
|
+
}
|
|
440
|
+
out = out.replace(/,\s*([}\]])/g, '$1');
|
|
441
|
+
try {
|
|
442
|
+
const parsed = JSON.parse(out);
|
|
443
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
444
|
+
return undefined;
|
|
445
|
+
return parsed;
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Minimal toml: line-based extraction of top-level `main = "…"` and
|
|
453
|
+
* `[assets] directory = "…"`. NOT a toml parser — exactly the two keys
|
|
454
|
+
* the spec names, by spec.
|
|
455
|
+
*/
|
|
456
|
+
function extractTomlKeys(text) {
|
|
457
|
+
let section = '';
|
|
458
|
+
let main;
|
|
459
|
+
let assetsDirectory;
|
|
460
|
+
for (const raw of text.split('\n')) {
|
|
461
|
+
const line = raw.trim();
|
|
462
|
+
if (line === '' || line.startsWith('#'))
|
|
463
|
+
continue;
|
|
464
|
+
const sectionMatch = /^\[([^\]]+)\]/.exec(line);
|
|
465
|
+
if (sectionMatch) {
|
|
466
|
+
section = sectionMatch[1].trim();
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const kv = /^([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"/.exec(line) ?? /^([A-Za-z0-9_-]+)\s*=\s*'([^']*)'/.exec(line);
|
|
470
|
+
if (!kv)
|
|
471
|
+
continue;
|
|
472
|
+
if (section === '' && kv[1] === 'main' && main === undefined)
|
|
473
|
+
main = kv[2];
|
|
474
|
+
if (section === 'assets' && kv[1] === 'directory' && assetsDirectory === undefined) {
|
|
475
|
+
assetsDirectory = kv[2];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { main, assetsDirectory };
|
|
479
|
+
}
|
|
480
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
481
|
+
function hit(shape, source) {
|
|
482
|
+
return { ok: true, shape, source };
|
|
483
|
+
}
|
|
484
|
+
function fail(message) {
|
|
485
|
+
return { ok: false, kind: 'detect-failed', message: `${message} | hint: ${DETECT_INIT_HINT}` };
|
|
486
|
+
}
|
|
487
|
+
/** Conditional-spread helper so absent optionals are OMITTED, not `undefined`. */
|
|
488
|
+
function optional(key, value) {
|
|
489
|
+
return value === undefined ? {} : { [key]: value };
|
|
490
|
+
}
|
|
491
|
+
function defaultReadFile(filePath) {
|
|
492
|
+
try {
|
|
493
|
+
return readFileSync(filePath, 'utf8');
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
}
|