@base44/functions-compiler 0.1.0 → 0.1.4
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 +103 -37
- package/lib/src/assembly.d.ts +43 -0
- package/lib/src/assembly.js +167 -0
- package/lib/src/assembly.js.map +1 -0
- package/lib/src/deno-bundle.js +3 -0
- package/lib/src/deno-bundle.js.map +1 -1
- package/lib/src/esbuild/private-data-sources-virtual.js +1 -0
- package/lib/src/esbuild/private-data-sources-virtual.js.map +1 -1
- package/lib/src/index.d.ts +8 -0
- package/lib/src/index.js +4 -0
- package/lib/src/index.js.map +1 -1
- package/lib/src/invocation-logs.d.ts +6 -0
- package/lib/src/invocation-logs.js +51 -0
- package/lib/src/invocation-logs.js.map +1 -0
- package/lib/src/private-data-sources/node-net-adapter.ts +24 -21
- package/lib/src/private-data-sources/oracle.ts +34 -0
- package/lib/src/shards/build.d.ts +55 -0
- package/lib/src/shards/build.js +156 -0
- package/lib/src/shards/build.js.map +1 -0
- package/lib/src/shards/plan.d.ts +50 -0
- package/lib/src/shards/plan.js +78 -0
- package/lib/src/shards/plan.js.map +1 -0
- package/lib/src/shards/size.d.ts +45 -0
- package/lib/src/shards/size.js +64 -0
- package/lib/src/shards/size.js.map +1 -0
- package/lib/src/version.d.ts +15 -0
- package/lib/src/version.js +16 -0
- package/lib/src/version.js.map +1 -0
- package/lib/src/worker-entry.d.ts +2 -3
- package/lib/src/worker-entry.js +46 -10
- package/lib/src/worker-entry.js.map +1 -1
- package/package.json +6 -1
|
@@ -3,6 +3,7 @@ import nodeNet from "node:net";
|
|
|
3
3
|
import { Duplex } from "node:stream";
|
|
4
4
|
|
|
5
5
|
import { isCloudflareTcpSocket } from "./tcp";
|
|
6
|
+
import type { CloudflareTcpSocket } from "./types";
|
|
6
7
|
|
|
7
8
|
interface NodeNetSocket extends Duplex {
|
|
8
9
|
connecting: boolean;
|
|
@@ -95,10 +96,7 @@ export function buildNodeNetSocket(
|
|
|
95
96
|
label: string,
|
|
96
97
|
options: { deferConnectEvent?: boolean } = {},
|
|
97
98
|
): NodeNetSocket {
|
|
98
|
-
|
|
99
|
-
throw new Error(`${label} did not return a readable/writable TCP socket`);
|
|
100
|
-
}
|
|
101
|
-
const cloudflareSocket = socket;
|
|
99
|
+
let cloudflareSocket: CloudflareTcpSocket | undefined;
|
|
102
100
|
|
|
103
101
|
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
104
102
|
let writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
|
|
@@ -107,6 +105,14 @@ export function buildNodeNetSocket(
|
|
|
107
105
|
let readableEnded = false;
|
|
108
106
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
109
107
|
let writeChain = Promise.resolve();
|
|
108
|
+
const socketReady = Promise.resolve(socket).then((resolved) => {
|
|
109
|
+
if (!isCloudflareTcpSocket(resolved)) {
|
|
110
|
+
throw new Error(`${label} did not return a readable/writable TCP socket`);
|
|
111
|
+
}
|
|
112
|
+
cloudflareSocket = resolved;
|
|
113
|
+
if (closed) resolved.close?.();
|
|
114
|
+
return resolved;
|
|
115
|
+
});
|
|
110
116
|
|
|
111
117
|
function clearTimer() {
|
|
112
118
|
if (!timeout) return;
|
|
@@ -143,8 +149,9 @@ export function buildNodeNetSocket(
|
|
|
143
149
|
const bytes = chunk instanceof Uint8Array ? chunk : Buffer.from(chunk);
|
|
144
150
|
writeChain = writeChain
|
|
145
151
|
.then(async () => {
|
|
152
|
+
const connectedSocket = await socketReady;
|
|
146
153
|
if (closed) throw new Error(`${label} connection is closed`);
|
|
147
|
-
const activeWriter = writer ??
|
|
154
|
+
const activeWriter = writer ?? connectedSocket.writable.getWriter();
|
|
148
155
|
writer = activeWriter;
|
|
149
156
|
await activeWriter.write(bytes);
|
|
150
157
|
})
|
|
@@ -157,7 +164,7 @@ export function buildNodeNetSocket(
|
|
|
157
164
|
writeChain
|
|
158
165
|
.then(() => {
|
|
159
166
|
try {
|
|
160
|
-
cloudflareSocket
|
|
167
|
+
cloudflareSocket?.close?.();
|
|
161
168
|
} finally {
|
|
162
169
|
callback();
|
|
163
170
|
}
|
|
@@ -168,7 +175,7 @@ export function buildNodeNetSocket(
|
|
|
168
175
|
closed = true;
|
|
169
176
|
clearTimer();
|
|
170
177
|
try {
|
|
171
|
-
cloudflareSocket
|
|
178
|
+
cloudflareSocket?.close?.();
|
|
172
179
|
} catch {
|
|
173
180
|
// The stream is already being destroyed; surface the original error below.
|
|
174
181
|
}
|
|
@@ -192,7 +199,7 @@ export function buildNodeNetSocket(
|
|
|
192
199
|
};
|
|
193
200
|
|
|
194
201
|
async function readLoop() {
|
|
195
|
-
const activeReader = reader ?? cloudflareSocket
|
|
202
|
+
const activeReader = reader ?? cloudflareSocket!.readable.getReader();
|
|
196
203
|
reader = activeReader;
|
|
197
204
|
try {
|
|
198
205
|
while (!closed) {
|
|
@@ -226,19 +233,15 @@ export function buildNodeNetSocket(
|
|
|
226
233
|
}
|
|
227
234
|
}
|
|
228
235
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
closed = true;
|
|
239
|
-
clearTimer();
|
|
240
|
-
endReadable();
|
|
241
|
-
releaseLocks();
|
|
236
|
+
socketReady.then(async (connectedSocket) => {
|
|
237
|
+
connectedSocket.closed?.finally(() => {
|
|
238
|
+
closed = true;
|
|
239
|
+
clearTimer();
|
|
240
|
+
endReadable();
|
|
241
|
+
releaseLocks();
|
|
242
|
+
}).catch((error) => stream.destroy(error instanceof Error ? error : new Error(String(error))));
|
|
243
|
+
const info = await connectedSocket.opened;
|
|
244
|
+
scheduleConnected(info);
|
|
242
245
|
}).catch((error) => stream.destroy(error instanceof Error ? error : new Error(String(error))));
|
|
243
246
|
return stream;
|
|
244
247
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { privateDataSourceReference } from "./manifest";
|
|
4
|
+
import { installPrivateNodeNetAdapter, registerPrivateNodeNetRoute } from "./node-net-adapter";
|
|
5
|
+
import { buildTcpPrivateDataSource } from "./tcp";
|
|
6
|
+
|
|
7
|
+
export function oracle(name: unknown) {
|
|
8
|
+
installPrivateNodeNetAdapter();
|
|
9
|
+
const entry = privateDataSourceReference(name, "oracle");
|
|
10
|
+
const source = buildTcpPrivateDataSource(entry);
|
|
11
|
+
// A numeric alias avoids Oracle's client-side DNS lookup of private hostnames.
|
|
12
|
+
const hash = createHash("sha256").update(String(entry.name)).digest("hex").slice(0, 28);
|
|
13
|
+
const driverHost = `fd44:${hash.match(/.{4}/g)!.join(":")}`;
|
|
14
|
+
|
|
15
|
+
const extension = {
|
|
16
|
+
oracledbOptions() {
|
|
17
|
+
const serviceName = source.database;
|
|
18
|
+
if (!serviceName || !/^[a-zA-Z0-9_.$#-]+$/.test(serviceName)) {
|
|
19
|
+
throw new Error("Enter an Oracle service name using letters, numbers, underscores, dots, $, #, or hyphens.");
|
|
20
|
+
}
|
|
21
|
+
const port = source.port!;
|
|
22
|
+
registerPrivateNodeNetRoute(driverHost, port, {
|
|
23
|
+
label: `Oracle private data source "${entry.name}"`,
|
|
24
|
+
connect: () => source.connect(),
|
|
25
|
+
});
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
user: source.username,
|
|
28
|
+
password: source.password,
|
|
29
|
+
connectString: `(DESCRIPTION=(TRANSPORT_CONNECT_TIMEOUT=10)(CONNECT_TIMEOUT=15)(RETRY_COUNT=0)(ADDRESS=(PROTOCOL=TCP)(HOST=${driverHost})(PORT=${port}))(CONNECT_DATA=(SERVICE_NAME=${serviceName})(SERVER=DEDICATED)))`,
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return Object.freeze(Object.defineProperties(extension, Object.getOwnPropertyDescriptors(source)) as typeof source & typeof extension);
|
|
34
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile an app's functions into the final Cloudflare Workers shards.
|
|
3
|
+
*
|
|
4
|
+
* The loop is: plan the partition, compile each shard, measure it, and halve any
|
|
5
|
+
* shard whose module is over a ceiling before anything is uploaded. Ported from
|
|
6
|
+
* `_build_shard_with_split` in apper's `cloudflare_wfp_runtime.py`.
|
|
7
|
+
*
|
|
8
|
+
* That function also splits on two exceptions, and neither is a trigger here.
|
|
9
|
+
* The bundler's 413 is its HTTP request-body cap — a property of the shard's
|
|
10
|
+
* packed SOURCE going over the wire, not of the module coming out, and there is
|
|
11
|
+
* no request to reject locally. Cloudflare's 10027 is a real Worker limit, but
|
|
12
|
+
* it is the same 64 MiB `workerRawSizeBreach` already computes from the bytes in
|
|
13
|
+
* hand, so reaching it from a rejected upload is a failsafe, not the design.
|
|
14
|
+
* Measuring is the design, and it happens here.
|
|
15
|
+
*
|
|
16
|
+
* Unlike the compiler's own `bundleApp`, this refuses a partial result. A module
|
|
17
|
+
* missing a handler is not a smaller success, it is a broken app.
|
|
18
|
+
*/
|
|
19
|
+
import type { AppFunctionInput } from "../contracts.js";
|
|
20
|
+
import type { BundleErrorItem } from "../errors.js";
|
|
21
|
+
import { type ShardPolicy } from "./plan.js";
|
|
22
|
+
export interface CompiledShard {
|
|
23
|
+
/** Position in the emitted list. Not a deployment identity — the caller owns
|
|
24
|
+
* placement, and a split makes more shards than the plan had. */
|
|
25
|
+
index: number;
|
|
26
|
+
/** Function names in this shard, in the order they were compiled. */
|
|
27
|
+
functions: string[];
|
|
28
|
+
module: string;
|
|
29
|
+
mainModule: string;
|
|
30
|
+
rawBytes: number;
|
|
31
|
+
gzipBytes: number;
|
|
32
|
+
}
|
|
33
|
+
export interface ShardBuildFailure {
|
|
34
|
+
/** The function this is about, when it belongs to one. */
|
|
35
|
+
function?: string;
|
|
36
|
+
message: string;
|
|
37
|
+
errors?: BundleErrorItem[];
|
|
38
|
+
}
|
|
39
|
+
export type ShardBuildResult = {
|
|
40
|
+
ok: true;
|
|
41
|
+
shards: CompiledShard[];
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
failures: ShardBuildFailure[];
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Compile every function into shards, or fail the whole build.
|
|
48
|
+
*
|
|
49
|
+
* Succeeds only when each declared function appears exactly once across the
|
|
50
|
+
* shards and every one of them compiled.
|
|
51
|
+
*/
|
|
52
|
+
export declare function compileFunctionShards(functions: AppFunctionInput[], policy: ShardPolicy, options?: {
|
|
53
|
+
postResponseTelemetry?: boolean;
|
|
54
|
+
runtimeSecrets?: boolean;
|
|
55
|
+
}): Promise<ShardBuildResult>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile an app's functions into the final Cloudflare Workers shards.
|
|
3
|
+
*
|
|
4
|
+
* The loop is: plan the partition, compile each shard, measure it, and halve any
|
|
5
|
+
* shard whose module is over a ceiling before anything is uploaded. Ported from
|
|
6
|
+
* `_build_shard_with_split` in apper's `cloudflare_wfp_runtime.py`.
|
|
7
|
+
*
|
|
8
|
+
* That function also splits on two exceptions, and neither is a trigger here.
|
|
9
|
+
* The bundler's 413 is its HTTP request-body cap — a property of the shard's
|
|
10
|
+
* packed SOURCE going over the wire, not of the module coming out, and there is
|
|
11
|
+
* no request to reject locally. Cloudflare's 10027 is a real Worker limit, but
|
|
12
|
+
* it is the same 64 MiB `workerRawSizeBreach` already computes from the bytes in
|
|
13
|
+
* hand, so reaching it from a rejected upload is a failsafe, not the design.
|
|
14
|
+
* Measuring is the design, and it happens here.
|
|
15
|
+
*
|
|
16
|
+
* Unlike the compiler's own `bundleApp`, this refuses a partial result. A module
|
|
17
|
+
* missing a handler is not a smaller success, it is a broken app.
|
|
18
|
+
*/
|
|
19
|
+
import { bundleApp } from "../bundler.js";
|
|
20
|
+
import { planFreshShards } from "./plan.js";
|
|
21
|
+
import { judgeBundleSize } from "./size.js";
|
|
22
|
+
/** Shards compile independently; bound how many esbuild runs are in flight so
|
|
23
|
+
* peak memory does not scale with the app's function count. */
|
|
24
|
+
const MAX_PARALLEL_SHARDS = 4;
|
|
25
|
+
/**
|
|
26
|
+
* Compile every function into shards, or fail the whole build.
|
|
27
|
+
*
|
|
28
|
+
* Succeeds only when each declared function appears exactly once across the
|
|
29
|
+
* shards and every one of them compiled.
|
|
30
|
+
*/
|
|
31
|
+
export async function compileFunctionShards(functions, policy, options = {}) {
|
|
32
|
+
const duplicate = firstDuplicate(functions.map((fn) => fn.name));
|
|
33
|
+
if (duplicate) {
|
|
34
|
+
return fail([{ message: `Duplicate function name "${duplicate}".` }]);
|
|
35
|
+
}
|
|
36
|
+
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
|
37
|
+
const plan = planFreshShards(functions.map((fn) => fn.name), policy);
|
|
38
|
+
const groups = plan.map((names) => names.map((name) => byName.get(name)));
|
|
39
|
+
const outcomes = await mapWithConcurrency(groups, MAX_PARALLEL_SHARDS, (group) => buildWithSplit(group, policy, options));
|
|
40
|
+
const failures = outcomes.flatMap((o) => (o.ok ? [] : o.failures));
|
|
41
|
+
if (failures.length > 0)
|
|
42
|
+
return fail(failures);
|
|
43
|
+
const shards = outcomes
|
|
44
|
+
.flatMap((o) => (o.ok ? o.shards : []))
|
|
45
|
+
.map((shard, index) => ({ ...shard, index }));
|
|
46
|
+
const missing = assertEveryFunctionPlacedOnce(functions, shards);
|
|
47
|
+
if (missing.length > 0)
|
|
48
|
+
return fail(missing);
|
|
49
|
+
return { ok: true, shards };
|
|
50
|
+
}
|
|
51
|
+
/** Compile one planned group, halving it when its module is over a ceiling. The
|
|
52
|
+
* verdict comes from the bytes, so nothing is uploaded to discover it. */
|
|
53
|
+
async function buildWithSplit(group, policy, options) {
|
|
54
|
+
const response = await bundleApp({ functions: group, ...options });
|
|
55
|
+
const compileFailures = response.functions
|
|
56
|
+
.filter((fn) => !fn.ok)
|
|
57
|
+
.map((fn) => ({
|
|
58
|
+
function: fn.name,
|
|
59
|
+
message: `Function "${fn.name}" failed to compile.`,
|
|
60
|
+
errors: fn.ok ? undefined : fn.errors,
|
|
61
|
+
}));
|
|
62
|
+
// A partial module is what the service ships and a whole-app build must not:
|
|
63
|
+
// the peers compiled, but the app is missing a handler.
|
|
64
|
+
if (compileFailures.length > 0)
|
|
65
|
+
return { ok: false, failures: compileFailures };
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
return fail([{ message: "The combined build produced no module." }]);
|
|
68
|
+
}
|
|
69
|
+
const verdict = await judgeBundleSize(response.module, policy.gzipCapBytes);
|
|
70
|
+
if (!verdict.breach) {
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
shards: [shardOf(group, response.module, response.main_module, verdict)],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (group.length === 1) {
|
|
77
|
+
// Splitting is exhausted: one function's module alone is over the ceiling.
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
failures: [{ function: group[0].name, message: verdict.breach }],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const mid = Math.floor(group.length / 2);
|
|
84
|
+
const halves = await Promise.all([
|
|
85
|
+
buildWithSplit(group.slice(0, mid), policy, options),
|
|
86
|
+
buildWithSplit(group.slice(mid), policy, options),
|
|
87
|
+
]);
|
|
88
|
+
const halfFailures = halves.flatMap((h) => (h.ok ? [] : h.failures));
|
|
89
|
+
if (halfFailures.length > 0)
|
|
90
|
+
return { ok: false, failures: halfFailures };
|
|
91
|
+
return { ok: true, shards: halves.flatMap((h) => (h.ok ? h.shards : [])) };
|
|
92
|
+
}
|
|
93
|
+
function shardOf(group, module, mainModule, verdict) {
|
|
94
|
+
return {
|
|
95
|
+
functions: group.map((fn) => fn.name),
|
|
96
|
+
module,
|
|
97
|
+
mainModule,
|
|
98
|
+
rawBytes: verdict.rawBytes,
|
|
99
|
+
gzipBytes: verdict.gzipBytes,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Every declared function must land in exactly one emitted shard. A name that
|
|
103
|
+
* vanished, or turned up twice after a split, would deploy a broken app. */
|
|
104
|
+
function assertEveryFunctionPlacedOnce(functions, shards) {
|
|
105
|
+
const placed = new Map();
|
|
106
|
+
for (const shard of shards) {
|
|
107
|
+
for (const name of shard.functions) {
|
|
108
|
+
placed.set(name, (placed.get(name) ?? 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const failures = [];
|
|
112
|
+
for (const fn of functions) {
|
|
113
|
+
const count = placed.get(fn.name) ?? 0;
|
|
114
|
+
if (count !== 1) {
|
|
115
|
+
failures.push({
|
|
116
|
+
function: fn.name,
|
|
117
|
+
message: `Function "${fn.name}" appears in ${count} shards; expected exactly 1.`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const name of placed.keys()) {
|
|
122
|
+
if (!functions.some((fn) => fn.name === name)) {
|
|
123
|
+
failures.push({
|
|
124
|
+
function: name,
|
|
125
|
+
message: `Shard holds unknown function "${name}".`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return failures;
|
|
130
|
+
}
|
|
131
|
+
function firstDuplicate(names) {
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
if (seen.has(name))
|
|
135
|
+
return name;
|
|
136
|
+
seen.add(name);
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function fail(failures) {
|
|
141
|
+
return { ok: false, failures };
|
|
142
|
+
}
|
|
143
|
+
/** Bounded concurrency, input order preserved. */
|
|
144
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
145
|
+
const results = new Array(items.length);
|
|
146
|
+
let cursor = 0;
|
|
147
|
+
const worker = async () => {
|
|
148
|
+
while (cursor < items.length) {
|
|
149
|
+
const index = cursor++;
|
|
150
|
+
results[index] = await fn(items[index]);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/shards/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which functions go in which shard, for a build with no previous deploy to
|
|
3
|
+
* reuse. Ported from apper's `shard_planning.py` (`target_shard_count`,
|
|
4
|
+
* `full_repartition`) and the capacity refusal in `deploy_app`.
|
|
5
|
+
*
|
|
6
|
+
* Only the fresh path crosses over. Incremental reuse needs the previous
|
|
7
|
+
* deployment map, and the per-app shard-size ratchet is state written across
|
|
8
|
+
* deploys; both stay with the service.
|
|
9
|
+
*/
|
|
10
|
+
/** Everything the planner is allowed to know. It reads no settings and no
|
|
11
|
+
* entitlements — the caller resolves these and passes them in. */
|
|
12
|
+
export interface ShardPolicy {
|
|
13
|
+
/** Functions packed into one shard. May be lower than `globalShardSize` when
|
|
14
|
+
* the caller carries a ratcheted override. */
|
|
15
|
+
shardSize: number;
|
|
16
|
+
/** The size capacity is judged at. A lowered `shardSize` changes packing only
|
|
17
|
+
* and must never lock an app out of deploying. */
|
|
18
|
+
globalShardSize: number;
|
|
19
|
+
maxShards: number;
|
|
20
|
+
/** Compressed ceiling for one shard's module, in bytes. */
|
|
21
|
+
gzipCapBytes: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class ShardCapacityError extends Error {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* How many shards the desired set needs. `maxShards === 1` — sharding off — is
|
|
28
|
+
* always one shard whatever the function count: the whole app bundles into a
|
|
29
|
+
* single Worker, which is the legacy per-app behaviour. Going through the
|
|
30
|
+
* division would trip the capacity check for a flag-off app.
|
|
31
|
+
*/
|
|
32
|
+
export declare function targetShardCount(functionCount: number, shardSize: number, maxShards: number): number;
|
|
33
|
+
/**
|
|
34
|
+
* Refuse a set the product cannot hold. Judged at the GLOBAL shard size: a
|
|
35
|
+
* ratcheted-down `shardSize` affects packing only, so judging at it would lock
|
|
36
|
+
* a recovered app out of deploys. The resulting plan may therefore hold more
|
|
37
|
+
* shards than `maxShards` — bounded by the function count, which is no more
|
|
38
|
+
* Workers than the legacy per-function topology used. Do not "tidy" this into a
|
|
39
|
+
* check on the final shard count.
|
|
40
|
+
*/
|
|
41
|
+
export declare function assertWithinCapacity(functionCount: number, policy: ShardPolicy): void;
|
|
42
|
+
/**
|
|
43
|
+
* The fresh partition, as a list of shards holding function names.
|
|
44
|
+
*
|
|
45
|
+
* A single planned shard keeps the caller's order; a multi-shard plan sorts by
|
|
46
|
+
* name before chunking. The two differ in apper and the difference is load
|
|
47
|
+
* bearing — function order inside a combined module changes the emitted bytes,
|
|
48
|
+
* so normalising them would be a byte change wearing a cleanup's clothes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function planFreshShards(names: string[], policy: ShardPolicy): string[][];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which functions go in which shard, for a build with no previous deploy to
|
|
3
|
+
* reuse. Ported from apper's `shard_planning.py` (`target_shard_count`,
|
|
4
|
+
* `full_repartition`) and the capacity refusal in `deploy_app`.
|
|
5
|
+
*
|
|
6
|
+
* Only the fresh path crosses over. Incremental reuse needs the previous
|
|
7
|
+
* deployment map, and the per-app shard-size ratchet is state written across
|
|
8
|
+
* deploys; both stay with the service.
|
|
9
|
+
*/
|
|
10
|
+
export class ShardCapacityError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ShardCapacityError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** The three counts have to be whole numbers of at least one. A zero or
|
|
17
|
+
* negative `shardSize` made the chunking loop below never advance — it hung
|
|
18
|
+
* rather than refusing, where the Python raises on the same input. */
|
|
19
|
+
function assertUsablePolicy(policy) {
|
|
20
|
+
for (const field of ["shardSize", "globalShardSize", "maxShards"]) {
|
|
21
|
+
const value = policy[field];
|
|
22
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
23
|
+
throw new Error(`ShardPolicy.${field} must be an integer of at least 1, got ${value}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function ceilDiv(a, b) {
|
|
28
|
+
return Math.ceil(a / b);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* How many shards the desired set needs. `maxShards === 1` — sharding off — is
|
|
32
|
+
* always one shard whatever the function count: the whole app bundles into a
|
|
33
|
+
* single Worker, which is the legacy per-app behaviour. Going through the
|
|
34
|
+
* division would trip the capacity check for a flag-off app.
|
|
35
|
+
*/
|
|
36
|
+
export function targetShardCount(functionCount, shardSize, maxShards) {
|
|
37
|
+
if (maxShards === 1)
|
|
38
|
+
return 1;
|
|
39
|
+
return ceilDiv(functionCount, shardSize);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Refuse a set the product cannot hold. Judged at the GLOBAL shard size: a
|
|
43
|
+
* ratcheted-down `shardSize` affects packing only, so judging at it would lock
|
|
44
|
+
* a recovered app out of deploys. The resulting plan may therefore hold more
|
|
45
|
+
* shards than `maxShards` — bounded by the function count, which is no more
|
|
46
|
+
* Workers than the legacy per-function topology used. Do not "tidy" this into a
|
|
47
|
+
* check on the final shard count.
|
|
48
|
+
*/
|
|
49
|
+
export function assertWithinCapacity(functionCount, policy) {
|
|
50
|
+
const needed = targetShardCount(functionCount, policy.globalShardSize, policy.maxShards);
|
|
51
|
+
if (needed > policy.maxShards) {
|
|
52
|
+
throw new ShardCapacityError(`App has ${functionCount} functions — over the per-app Worker capacity of ` +
|
|
53
|
+
`${policy.globalShardSize * policy.maxShards} ` +
|
|
54
|
+
`(${policy.maxShards} shards × ${policy.globalShardSize}).`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The fresh partition, as a list of shards holding function names.
|
|
59
|
+
*
|
|
60
|
+
* A single planned shard keeps the caller's order; a multi-shard plan sorts by
|
|
61
|
+
* name before chunking. The two differ in apper and the difference is load
|
|
62
|
+
* bearing — function order inside a combined module changes the emitted bytes,
|
|
63
|
+
* so normalising them would be a byte change wearing a cleanup's clothes.
|
|
64
|
+
*/
|
|
65
|
+
export function planFreshShards(names, policy) {
|
|
66
|
+
assertUsablePolicy(policy);
|
|
67
|
+
assertWithinCapacity(names.length, policy);
|
|
68
|
+
const target = targetShardCount(names.length, policy.shardSize, policy.maxShards);
|
|
69
|
+
if (target <= 1)
|
|
70
|
+
return [[...names]];
|
|
71
|
+
const sorted = [...names].sort();
|
|
72
|
+
const shards = [];
|
|
73
|
+
for (let i = 0; i < sorted.length; i += policy.shardSize) {
|
|
74
|
+
shards.push(sorted.slice(i, i + policy.shardSize));
|
|
75
|
+
}
|
|
76
|
+
return shards;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../src/shards/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;uEAEuE;AACvE,SAAS,kBAAkB,CAAC,MAAmB;IAC7C,KAAK,MAAM,KAAK,IAAI,CAAC,WAAW,EAAE,iBAAiB,EAAE,WAAW,CAAU,EAAE,CAAC;QAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CACb,eAAe,KAAK,0CAA0C,KAAK,EAAE,CACtE,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,CAAS,EAAE,CAAS;IACnC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,aAAqB,EACrB,SAAiB,EACjB,SAAiB;IAEjB,IAAI,SAAS,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,aAAqB,EACrB,MAAmB;IAEnB,MAAM,MAAM,GAAG,gBAAgB,CAC7B,aAAa,EACb,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,SAAS,CACjB,CAAC;IACF,IAAI,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,kBAAkB,CAC1B,WAAW,aAAa,mDAAmD;YACzE,GAAG,MAAM,CAAC,eAAe,GAAG,MAAM,CAAC,SAAS,GAAG;YAC/C,IAAI,MAAM,CAAC,SAAS,aAAa,MAAM,CAAC,eAAe,IAAI,CAC9D,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAe,EACf,MAAmB;IAEnB,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,oBAAoB,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE3C,MAAM,MAAM,GAAG,gBAAgB,CAC7B,KAAK,CAAC,MAAM,EACZ,MAAM,CAAC,SAAS,EAChB,MAAM,CAAC,SAAS,CACjB,CAAC;IACF,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IAErC,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACzD,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How large a compiled Worker module is, and whether Cloudflare will take it.
|
|
3
|
+
*
|
|
4
|
+
* Ported from apper's `cloudflare_wfp_runtime.py` (`measure_bundle_bytes`,
|
|
5
|
+
* `worker_raw_size_breach`, `worker_gzip_cap_breach`, `judge_bundle_size`).
|
|
6
|
+
* Deciding this before an upload is the whole point: the one real production
|
|
7
|
+
* rejection spent 30 seconds uploading 97 MB to be told the same thing this
|
|
8
|
+
* computes for free.
|
|
9
|
+
*/
|
|
10
|
+
/** zlib's default, and the level `wrangler --dry-run` reports — so the number
|
|
11
|
+
* here means the same thing as the one in Cloudflare's docs, in a support
|
|
12
|
+
* thread and on a developer's terminal. It is not a claim about Cloudflare's
|
|
13
|
+
* own server-side compressor, which is unpublished; the safety margin lives in
|
|
14
|
+
* the cap instead. */
|
|
15
|
+
export declare const BUNDLE_GZIP_LEVEL = 6;
|
|
16
|
+
/** Cloudflare's uncompressed per-Worker ceiling, in the units Cloudflare states
|
|
17
|
+
* it in. Taken from a real 10027 in production, which reads verbatim: "Your
|
|
18
|
+
* Worker exceeded the uncompressed size limit of 64 MiB." Decimal 64_000_000
|
|
19
|
+
* would refuse 3 MiB early for no reason. */
|
|
20
|
+
export declare const WORKER_RAW_SIZE_CEILING_BYTES: number;
|
|
21
|
+
export interface BundleSize {
|
|
22
|
+
rawBytes: number;
|
|
23
|
+
gzipBytes: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SizeVerdict extends BundleSize {
|
|
26
|
+
/** Why the module is over a ceiling, or `null` when it is within both. */
|
|
27
|
+
breach: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** `(raw, gzip)` for a compiled module. Compression runs off the event loop:
|
|
30
|
+
* a multi-megabyte gzip is long enough to matter when several shards are in
|
|
31
|
+
* flight. */
|
|
32
|
+
export declare function measureBundleBytes(module: string): Promise<BundleSize>;
|
|
33
|
+
/** Why Cloudflare will reject this module outright, or `null`. The only verdict
|
|
34
|
+
* taken on a number Cloudflare states itself — no estimate, no compression, no
|
|
35
|
+
* headroom to argue about. */
|
|
36
|
+
export declare function workerRawSizeBreach(rawBytes: number): string | null;
|
|
37
|
+
/** Why the module is over the compressed ceiling, or `null`. Our side of the
|
|
38
|
+
* comparison is the estimate, not Cloudflare's number: it compresses
|
|
39
|
+
* server-side with an unpublished algorithm, so level-6 gzip is a proxy for
|
|
40
|
+
* the figure it measures. The cap carries the margin. */
|
|
41
|
+
export declare function workerGzipCapBreach(gzipBytes: number, gzipCapBytes: number): string | null;
|
|
42
|
+
/** Measure a module and say whether it breaches either ceiling. Never throws:
|
|
43
|
+
* what a breach costs depends on whether the caller can re-partition, and only
|
|
44
|
+
* the caller knows that. */
|
|
45
|
+
export declare function judgeBundleSize(module: string, gzipCapBytes: number): Promise<SizeVerdict>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How large a compiled Worker module is, and whether Cloudflare will take it.
|
|
3
|
+
*
|
|
4
|
+
* Ported from apper's `cloudflare_wfp_runtime.py` (`measure_bundle_bytes`,
|
|
5
|
+
* `worker_raw_size_breach`, `worker_gzip_cap_breach`, `judge_bundle_size`).
|
|
6
|
+
* Deciding this before an upload is the whole point: the one real production
|
|
7
|
+
* rejection spent 30 seconds uploading 97 MB to be told the same thing this
|
|
8
|
+
* computes for free.
|
|
9
|
+
*/
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { gzip } from "node:zlib";
|
|
12
|
+
const gzipAsync = promisify(gzip);
|
|
13
|
+
/** zlib's default, and the level `wrangler --dry-run` reports — so the number
|
|
14
|
+
* here means the same thing as the one in Cloudflare's docs, in a support
|
|
15
|
+
* thread and on a developer's terminal. It is not a claim about Cloudflare's
|
|
16
|
+
* own server-side compressor, which is unpublished; the safety margin lives in
|
|
17
|
+
* the cap instead. */
|
|
18
|
+
export const BUNDLE_GZIP_LEVEL = 6;
|
|
19
|
+
/** Cloudflare's uncompressed per-Worker ceiling, in the units Cloudflare states
|
|
20
|
+
* it in. Taken from a real 10027 in production, which reads verbatim: "Your
|
|
21
|
+
* Worker exceeded the uncompressed size limit of 64 MiB." Decimal 64_000_000
|
|
22
|
+
* would refuse 3 MiB early for no reason. */
|
|
23
|
+
export const WORKER_RAW_SIZE_CEILING_BYTES = 64 * 1024 * 1024;
|
|
24
|
+
/** `(raw, gzip)` for a compiled module. Compression runs off the event loop:
|
|
25
|
+
* a multi-megabyte gzip is long enough to matter when several shards are in
|
|
26
|
+
* flight. */
|
|
27
|
+
export async function measureBundleBytes(module) {
|
|
28
|
+
const raw = Buffer.from(module, "utf8");
|
|
29
|
+
const compressed = await gzipAsync(raw, { level: BUNDLE_GZIP_LEVEL });
|
|
30
|
+
return { rawBytes: raw.byteLength, gzipBytes: compressed.byteLength };
|
|
31
|
+
}
|
|
32
|
+
/** Why Cloudflare will reject this module outright, or `null`. The only verdict
|
|
33
|
+
* taken on a number Cloudflare states itself — no estimate, no compression, no
|
|
34
|
+
* headroom to argue about. */
|
|
35
|
+
export function workerRawSizeBreach(rawBytes) {
|
|
36
|
+
if (rawBytes > WORKER_RAW_SIZE_CEILING_BYTES) {
|
|
37
|
+
return (`bundled module is ${rawBytes} bytes uncompressed, over Cloudflare's ` +
|
|
38
|
+
`${WORKER_RAW_SIZE_CEILING_BYTES}-byte (64 MiB) per-Worker ceiling`);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** Why the module is over the compressed ceiling, or `null`. Our side of the
|
|
43
|
+
* comparison is the estimate, not Cloudflare's number: it compresses
|
|
44
|
+
* server-side with an unpublished algorithm, so level-6 gzip is a proxy for
|
|
45
|
+
* the figure it measures. The cap carries the margin. */
|
|
46
|
+
export function workerGzipCapBreach(gzipBytes, gzipCapBytes) {
|
|
47
|
+
if (gzipBytes > gzipCapBytes) {
|
|
48
|
+
return (`bundled module is ${gzipBytes} bytes gzipped (level ${BUNDLE_GZIP_LEVEL}), ` +
|
|
49
|
+
`over the ${gzipCapBytes}-byte cap for a Cloudflare Worker script`);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/** Measure a module and say whether it breaches either ceiling. Never throws:
|
|
54
|
+
* what a breach costs depends on whether the caller can re-partition, and only
|
|
55
|
+
* the caller knows that. */
|
|
56
|
+
export async function judgeBundleSize(module, gzipCapBytes) {
|
|
57
|
+
const { rawBytes, gzipBytes } = await measureBundleBytes(module);
|
|
58
|
+
// Uncompressed first: it is the exact one, so when both are over it is the
|
|
59
|
+
// verdict worth reporting.
|
|
60
|
+
const breach = workerRawSizeBreach(rawBytes) ??
|
|
61
|
+
workerGzipCapBreach(gzipBytes, gzipCapBytes);
|
|
62
|
+
return { rawBytes, gzipBytes, breach };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=size.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size.js","sourceRoot":"","sources":["../../../src/shards/size.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;AAElC;;;;uBAIuB;AACvB,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAEnC;;;8CAG8C;AAC9C,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAY9D;;cAEc;AACd,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc;IACrD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACtE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC,UAAU,EAAE,CAAC;AACxE,CAAC;AAED;;+BAE+B;AAC/B,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,IAAI,QAAQ,GAAG,6BAA6B,EAAE,CAAC;QAC7C,OAAO,CACL,qBAAqB,QAAQ,yCAAyC;YACtE,GAAG,6BAA6B,mCAAmC,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;0DAG0D;AAC1D,MAAM,UAAU,mBAAmB,CACjC,SAAiB,EACjB,YAAoB;IAEpB,IAAI,SAAS,GAAG,YAAY,EAAE,CAAC;QAC7B,OAAO,CACL,qBAAqB,SAAS,yBAAyB,iBAAiB,KAAK;YAC7E,YAAY,YAAY,0CAA0C,CACnE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;6BAE6B;AAC7B,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,YAAoB;IAEpB,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACjE,2EAA2E;IAC3E,2BAA2B;IAC3B,MAAM,MAAM,GACV,mBAAmB,CAAC,QAAQ,CAAC;QAC7B,mBAAmB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC/C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This package's version, as a literal.
|
|
3
|
+
*
|
|
4
|
+
* It goes into every compiled shard's banner, which means it is part of the
|
|
5
|
+
* emitted bytes and therefore part of a deployed version's identity. Reading it
|
|
6
|
+
* from `package.json` at runtime cannot work: a host that bundles this module
|
|
7
|
+
* into an artifact of its own — which the CLI does — ships no `package.json`
|
|
8
|
+
* beside it, and the read silently degraded to "unknown", so the same functions
|
|
9
|
+
* compiled to different bytes in the CLI and in the service.
|
|
10
|
+
*
|
|
11
|
+
* A literal resolves in every host. `version.test.ts` fails the build if it
|
|
12
|
+
* drifts from `package.json`, so bumping the package still means editing two
|
|
13
|
+
* files but cannot mean forgetting one.
|
|
14
|
+
*/
|
|
15
|
+
export declare const COMPILER_VERSION = "0.1.4";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This package's version, as a literal.
|
|
3
|
+
*
|
|
4
|
+
* It goes into every compiled shard's banner, which means it is part of the
|
|
5
|
+
* emitted bytes and therefore part of a deployed version's identity. Reading it
|
|
6
|
+
* from `package.json` at runtime cannot work: a host that bundles this module
|
|
7
|
+
* into an artifact of its own — which the CLI does — ships no `package.json`
|
|
8
|
+
* beside it, and the read silently degraded to "unknown", so the same functions
|
|
9
|
+
* compiled to different bytes in the CLI and in the service.
|
|
10
|
+
*
|
|
11
|
+
* A literal resolves in every host. `version.test.ts` fails the build if it
|
|
12
|
+
* drifts from `package.json`, so bumping the package still means editing two
|
|
13
|
+
* files but cannot mean forgetting one.
|
|
14
|
+
*/
|
|
15
|
+
export const COMPILER_VERSION = "0.1.4";
|
|
16
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC"}
|