@juuno-sdk/cli 4.0.1 → 4.1.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 +40 -0
- package/bin/cli-router.js +90 -0
- package/bin/cli.js +25 -4
- package/dist/cli/src/deploy/index.js +81 -1
- package/dist/cli/src/deploy-backend/context.js +62 -0
- package/dist/cli/src/deploy-backend/index.js +196 -0
- package/dist/cli/src/deploy-backend/outcome.js +85 -0
- package/dist/cli/src/deploy-backend/wrangler-config.js +271 -0
- package/dist/cli/src/utils/bin.js +23 -0
- package/dist/cli/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/deploy/index.d.ts +32 -0
- package/dist/types/src/deploy/index.d.ts.map +1 -1
- package/dist/types/src/deploy-backend/context.d.ts +29 -0
- package/dist/types/src/deploy-backend/context.d.ts.map +1 -0
- package/dist/types/src/deploy-backend/index.d.ts +54 -0
- package/dist/types/src/deploy-backend/index.d.ts.map +1 -0
- package/dist/types/src/deploy-backend/outcome.d.ts +63 -0
- package/dist/types/src/deploy-backend/outcome.d.ts.map +1 -0
- package/dist/types/src/deploy-backend/wrangler-config.d.ts +106 -0
- package/dist/types/src/deploy-backend/wrangler-config.d.ts.map +1 -0
- package/dist/types/src/utils/bin.d.ts +24 -0
- package/dist/types/src/utils/bin.d.ts.map +1 -0
- package/package.json +29 -22
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for a Workers-for-Platforms deploy: building the payload from a
|
|
3
|
+
* worker's wrangler.jsonc, and resolving where to send it.
|
|
4
|
+
*
|
|
5
|
+
* Kept free of I/O so the part that fails silently — the metadata — is directly
|
|
6
|
+
* testable. See wrangler-config.test.ts.
|
|
7
|
+
*/
|
|
8
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
9
|
+
/**
|
|
10
|
+
* Normalise the API base, and refuse to put the developer token on a cleartext
|
|
11
|
+
* connection.
|
|
12
|
+
*
|
|
13
|
+
* Loopback stays exempt so a local Laravel over http still works. Anything else
|
|
14
|
+
* must be https, because the next thing the caller does with this value is send
|
|
15
|
+
* a bearer token to it.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveApiUrl(value) {
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error(`API url must be absolute, got: ${value}`);
|
|
24
|
+
}
|
|
25
|
+
if (url.protocol !== 'https:' && !LOOPBACK_HOSTS.has(url.hostname)) {
|
|
26
|
+
throw new Error(`API url must use https (or a loopback host for local development), got: ${value}`);
|
|
27
|
+
}
|
|
28
|
+
return value.replace(/\/$/, '');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Binding types wrangler does NOT inherit from the top level into `env.<name>`.
|
|
32
|
+
* Everything else (main, compatibility_date, compatibility_flags, …) is
|
|
33
|
+
* inherited. Getting this wrong in the lenient direction is the dangerous one:
|
|
34
|
+
* it would ship a top-level store binding into an environment that deliberately
|
|
35
|
+
* declared none.
|
|
36
|
+
*/
|
|
37
|
+
const NOT_INHERITED = [
|
|
38
|
+
'vars',
|
|
39
|
+
'kv_namespaces',
|
|
40
|
+
'd1_databases',
|
|
41
|
+
'r2_buckets',
|
|
42
|
+
'queues',
|
|
43
|
+
'routes',
|
|
44
|
+
'route',
|
|
45
|
+
'durable_objects',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Binding kinds buildMetadata cannot express.
|
|
49
|
+
*
|
|
50
|
+
* Cloudflare treats the metadata part as the authoritative binding list, so one
|
|
51
|
+
* dropped here deploys a worker whose binding is simply absent: the deploy
|
|
52
|
+
* reports success and the first request that touches it 500s, with nothing in
|
|
53
|
+
* the output connecting the two. Refused instead, until the platform endpoint
|
|
54
|
+
* provisions them the way it already does KV and D1.
|
|
55
|
+
*/
|
|
56
|
+
const UNSUPPORTED_BINDINGS = [
|
|
57
|
+
'r2_buckets',
|
|
58
|
+
'queues',
|
|
59
|
+
'durable_objects',
|
|
60
|
+
'services',
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* An env block by name, own properties only.
|
|
64
|
+
*
|
|
65
|
+
* `constructor` and `toString` are inherited from Object.prototype, so a plain
|
|
66
|
+
* lookup finds a function there and sails through a truthiness check — resolving
|
|
67
|
+
* `--env constructor` to a nonsense config instead of erroring.
|
|
68
|
+
*/
|
|
69
|
+
function ownEnv(config, env) {
|
|
70
|
+
return Object.prototype.hasOwnProperty.call(config.env ?? {}, env)
|
|
71
|
+
? config.env?.[env]
|
|
72
|
+
: undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Strip comments and trailing commas from JSONC so JSON.parse accepts it.
|
|
76
|
+
*
|
|
77
|
+
* String-aware: a `//` or block-comment opener inside a quoted value is left
|
|
78
|
+
* alone, as is an escaped quote. Written by hand rather than pulling in a parser
|
|
79
|
+
* because this is the only JSONC we read and the input is our own.
|
|
80
|
+
*
|
|
81
|
+
* The complexity is suppressed rather than refactored. This is a character
|
|
82
|
+
* scanner: four mutually-exclusive states over one cursor, sharing `out`, `i`
|
|
83
|
+
* and three flags. Extracting the branches means threading all of that through
|
|
84
|
+
* helpers or hoisting it into an object, which trades a shape every reader
|
|
85
|
+
* recognises for one nobody does — and a state machine you have to reassemble
|
|
86
|
+
* across functions is exactly where an off-by-one hides. The 31 cases in
|
|
87
|
+
* `__tests__/wrangler-config.test.ts` are what keeps it honest instead.
|
|
88
|
+
*/
|
|
89
|
+
// fallow-ignore-next-line complexity
|
|
90
|
+
export function stripJsonc(source) {
|
|
91
|
+
let out = '';
|
|
92
|
+
let inString = false;
|
|
93
|
+
let inLine = false;
|
|
94
|
+
let inBlock = false;
|
|
95
|
+
for (let i = 0; i < source.length; i++) {
|
|
96
|
+
const c = source[i];
|
|
97
|
+
const next = source[i + 1];
|
|
98
|
+
if (inLine) {
|
|
99
|
+
if (c === '\n') {
|
|
100
|
+
inLine = false;
|
|
101
|
+
out += c;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (inBlock) {
|
|
106
|
+
if (c === '*' && next === '/') {
|
|
107
|
+
inBlock = false;
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (inString) {
|
|
113
|
+
out += c;
|
|
114
|
+
if (c === '\\') {
|
|
115
|
+
// Copy the escaped character verbatim so \" doesn't end the string.
|
|
116
|
+
out += next ?? '';
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else if (c === '"') {
|
|
120
|
+
inString = false;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (c === '"') {
|
|
125
|
+
inString = true;
|
|
126
|
+
out += c;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (c === '/' && next === '/') {
|
|
130
|
+
inLine = true;
|
|
131
|
+
i++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === '/' && next === '*') {
|
|
135
|
+
inBlock = true;
|
|
136
|
+
i++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Trailing comma before a closing brace/bracket. Done here rather than with
|
|
140
|
+
// a regex over the finished string so a quoted value containing ",}" or
|
|
141
|
+
// ",]" is left alone — at this point we know we are outside a string.
|
|
142
|
+
if (c === '}' || c === ']') {
|
|
143
|
+
out = out.replace(/,\s*$/, '');
|
|
144
|
+
}
|
|
145
|
+
out += c;
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
export function parseWranglerConfig(source) {
|
|
150
|
+
return JSON.parse(stripJsonc(source));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Flatten a wrangler config for one environment.
|
|
154
|
+
*
|
|
155
|
+
* Inherited keys come from the top level unless the environment overrides them;
|
|
156
|
+
* the binding keys in NOT_INHERITED come from the environment alone, matching
|
|
157
|
+
* wrangler's own rule.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveEnvConfig(config, env) {
|
|
160
|
+
const envConfig = ownEnv(config, env);
|
|
161
|
+
if (!envConfig) {
|
|
162
|
+
throw new Error(`wrangler.jsonc declares no env "${env}" (found: ${Object.keys(config.env ?? {}).join(', ') || 'none'}).`);
|
|
163
|
+
}
|
|
164
|
+
const resolved = { ...config, ...envConfig };
|
|
165
|
+
delete resolved.env;
|
|
166
|
+
for (const key of NOT_INHERITED) {
|
|
167
|
+
if (envConfig[key] === undefined) {
|
|
168
|
+
delete resolved[key];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return resolved;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The entry module's name as it appears in the dry-run output. wrangler compiles
|
|
175
|
+
* `./src/index.ts` to `dist/index.js`, so the extension is normalised.
|
|
176
|
+
*/
|
|
177
|
+
export function entryModuleName(main) {
|
|
178
|
+
// Both separators: a wrangler.jsonc authored on Windows can say
|
|
179
|
+
// "src\\index.ts", and the parts bundle() uploads are named by readdirSync —
|
|
180
|
+
// bare basenames. Leaving a backslash in would make main_module name a part
|
|
181
|
+
// that was never sent, and Cloudflare rejects the upload for a missing entry.
|
|
182
|
+
return main.replace(/^.*[/\\]/, '').replace(/\.(ts|tsx|mts|cts)$/, '.js');
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build the `metadata` part for the platform deploy endpoint.
|
|
186
|
+
*
|
|
187
|
+
* Store bindings are emitted **by name only**. The platform provisions the
|
|
188
|
+
* store and fills the id — sending one would make it adopt an existing store
|
|
189
|
+
* instead, and nothing would report that it had. Note the two are not
|
|
190
|
+
* symmetric: KV's id field is `namespace_id`, D1's is `id`, and neither is set
|
|
191
|
+
* here.
|
|
192
|
+
*/
|
|
193
|
+
export function buildMetadata(resolved) {
|
|
194
|
+
if (!resolved.main) {
|
|
195
|
+
throw new Error('wrangler.jsonc has no "main" entry point.');
|
|
196
|
+
}
|
|
197
|
+
if (!resolved.compatibility_date) {
|
|
198
|
+
throw new Error('wrangler.jsonc has no "compatibility_date".');
|
|
199
|
+
}
|
|
200
|
+
const unsupported = UNSUPPORTED_BINDINGS.filter((key) => resolved[key] !== undefined);
|
|
201
|
+
if (unsupported.length) {
|
|
202
|
+
throw new Error(`wrangler.jsonc declares bindings this deploy path cannot send: ${unsupported.join(', ')}.`);
|
|
203
|
+
}
|
|
204
|
+
const bindings = [];
|
|
205
|
+
for (const kv of resolved.kv_namespaces ?? []) {
|
|
206
|
+
bindings.push({ type: 'kv_namespace', name: kv.binding });
|
|
207
|
+
}
|
|
208
|
+
for (const d1 of resolved.d1_databases ?? []) {
|
|
209
|
+
bindings.push({ type: 'd1', name: d1.binding });
|
|
210
|
+
}
|
|
211
|
+
for (const [name, value] of Object.entries(resolved.vars ?? {})) {
|
|
212
|
+
// Every var ships as plain text, so an object or array would arrive as
|
|
213
|
+
// "[object Object]" or a comma-joined list — a binding that looks present in
|
|
214
|
+
// the metadata and is not the value the worker reads. `typeof null` is
|
|
215
|
+
// 'object' too, and "null" is no better. Refused rather than corrupted.
|
|
216
|
+
if (typeof value === 'object') {
|
|
217
|
+
throw new Error(`var "${name}" must be a string, number or boolean — this deploy path sends vars as plain text.`);
|
|
218
|
+
}
|
|
219
|
+
bindings.push({ type: 'plain_text', name, text: String(value) });
|
|
220
|
+
}
|
|
221
|
+
const metadata = {
|
|
222
|
+
main_module: entryModuleName(resolved.main),
|
|
223
|
+
compatibility_date: resolved.compatibility_date,
|
|
224
|
+
};
|
|
225
|
+
if (resolved.compatibility_flags?.length) {
|
|
226
|
+
metadata.compatibility_flags = resolved.compatibility_flags;
|
|
227
|
+
}
|
|
228
|
+
if (bindings.length) {
|
|
229
|
+
metadata.bindings = bindings;
|
|
230
|
+
}
|
|
231
|
+
return metadata;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The tenant script name wrangler will have built for this environment.
|
|
235
|
+
*
|
|
236
|
+
* Read off the raw config rather than the resolved one because the rule is not
|
|
237
|
+
* inheritance: an env that declares its own `name` uses it verbatim, and one
|
|
238
|
+
* that does not gets `<top-level-name>-<env>`. The merged config cannot tell the
|
|
239
|
+
* two apart, so reading it would address the upload to `google-reviews` for a
|
|
240
|
+
* bundle wrangler built as `google-reviews-prod` — refused as unregistered, or
|
|
241
|
+
* worse, landing on whichever tenant already holds that name.
|
|
242
|
+
*/
|
|
243
|
+
export function scriptName(config, env) {
|
|
244
|
+
const envName = ownEnv(config, env)?.name;
|
|
245
|
+
if (envName) {
|
|
246
|
+
return envName;
|
|
247
|
+
}
|
|
248
|
+
if (!config.name) {
|
|
249
|
+
throw new Error('wrangler.jsonc has no "name".');
|
|
250
|
+
}
|
|
251
|
+
return `${config.name}-${env}`;
|
|
252
|
+
}
|
|
253
|
+
/** Files in the dry-run output that belong in the upload. */
|
|
254
|
+
export function isModuleFile(filename) {
|
|
255
|
+
// Anchored, which is what excludes sourcemaps: `index.js.map` ends in `.map`,
|
|
256
|
+
// so it never matches in the first place.
|
|
257
|
+
return /\.(js|mjs|wasm)$/.test(filename);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The Content-Type of a module part, which is how Cloudflare decides what kind
|
|
261
|
+
* of module it is — not a formality. `application/javascript+module` makes it an
|
|
262
|
+
* ES module and `application/wasm` a CompiledWasm one, so labelling a `.wasm`
|
|
263
|
+
* as JavaScript would have Cloudflare try to parse the binary as source and
|
|
264
|
+
* reject the upload. isModuleFile() already accepts `.wasm`, so the two have to
|
|
265
|
+
* agree about what that means.
|
|
266
|
+
*/
|
|
267
|
+
export function moduleContentType(filename) {
|
|
268
|
+
return filename.endsWith('.wasm')
|
|
269
|
+
? 'application/wasm'
|
|
270
|
+
: 'application/javascript+module';
|
|
271
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import crossSpawn from 'cross-spawn';
|
|
2
|
+
/**
|
|
3
|
+
* Spawning a package-manager binary, portably.
|
|
4
|
+
*
|
|
5
|
+
* `npx`, `pnpm` and `yarn` are shell scripts on POSIX and `.cmd` shims on
|
|
6
|
+
* Windows, and neither Node nor libuv will run a `.cmd` from `spawn` without a
|
|
7
|
+
* shell — `CreateProcess` cannot execute one, and since the fix for
|
|
8
|
+
* CVE-2024-27980 Node refuses rather than silently routing through cmd.exe.
|
|
9
|
+
* Naming the shim (`npx.cmd`) fixes PATH resolution and not the spawn.
|
|
10
|
+
*
|
|
11
|
+
* Doing it correctly means invoking `cmd.exe /d /s /c` with the command and
|
|
12
|
+
* every argument escaped for cmd's parser, plus a second round of escaping for
|
|
13
|
+
* shims under `node_modules/.bin`, and `windowsVerbatimArguments` so Node does
|
|
14
|
+
* not re-quote what was just escaped. `shell: true` is the wrong shortcut: it
|
|
15
|
+
* hands the whole line to cmd.exe to re-parse, so a worker directory containing
|
|
16
|
+
* `&` or `^` becomes a command.
|
|
17
|
+
*
|
|
18
|
+
* cross-spawn already does all of it, is the ecosystem's answer to this exact
|
|
19
|
+
* problem, and was already in the tree transitively. Preferred over a local
|
|
20
|
+
* reimplementation, which is where the escaping bugs live.
|
|
21
|
+
*/
|
|
22
|
+
export const spawn = crossSpawn;
|
|
23
|
+
export const spawnSync = crossSpawn.sync;
|