@smoothbricks/cli 0.10.7 → 0.10.9
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -1
- package/dist/github-ci/index.d.ts +44 -4
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +220 -34
- package/dist/monorepo/ci-workflow.js +16 -6
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +19 -1
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts +5 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts.map +1 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.js +38 -0
- package/dist/monorepo/publish-workflow.js +3 -3
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +85 -5
- package/dist/playwright/index.d.ts +22 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +44 -0
- package/dist/release/bootstrap-npm-packages.d.ts +3 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -1
- package/dist/release/bootstrap-npm-packages.js +21 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +31 -6
- package/dist/wrangler/cloudflare.d.ts +87 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -0
- package/dist/wrangler/cloudflare.js +238 -0
- package/dist/wrangler/deploy-environment.d.ts +48 -0
- package/dist/wrangler/deploy-environment.d.ts.map +1 -0
- package/dist/wrangler/deploy-environment.js +383 -0
- package/dist/wrangler/environment.d.ts +58 -0
- package/dist/wrangler/environment.d.ts.map +1 -0
- package/dist/wrangler/environment.js +297 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +3 -4
- package/package.json +9 -2
- package/src/cli.ts +27 -3
- package/src/github-ci/index.test.ts +175 -2
- package/src/github-ci/index.ts +274 -31
- package/src/monorepo/__tests__/ci-workflow.test.ts +10 -4
- package/src/monorepo/__tests__/pr-preview-cleanup-workflow.test.ts +23 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +7 -5
- package/src/monorepo/ci-workflow.ts +16 -6
- package/src/monorepo/managed-files.test.ts +56 -1
- package/src/monorepo/managed-files.ts +20 -1
- package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
- package/src/monorepo/publish-workflow.ts +8 -5
- package/src/monorepo/tool-validation.test.ts +85 -0
- package/src/monorepo/tool-validation.ts +94 -5
- package/src/playwright/index.test.ts +90 -0
- package/src/playwright/index.ts +73 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +63 -2
- package/src/release/bootstrap-npm-packages.ts +34 -0
- package/src/release/index.ts +30 -4
- package/src/wrangler/cloudflare.test.ts +76 -0
- package/src/wrangler/cloudflare.ts +292 -0
- package/src/wrangler/deploy-environment.test.ts +354 -0
- package/src/wrangler/deploy-environment.ts +445 -0
- package/src/wrangler/environment.test.ts +173 -0
- package/src/wrangler/environment.ts +366 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
|
|
3
|
+
import typia from 'typia';
|
|
4
|
+
import { cloneEnvBlock } from './prepare-env.js';
|
|
5
|
+
|
|
6
|
+
export type EnvironmentToken = 'staging' | 'production' | `pr${number}`;
|
|
7
|
+
|
|
8
|
+
const MAX_PULL_REQUEST_NUMBER = 999_999_999;
|
|
9
|
+
const ENVIRONMENT_PATTERN = /^(?:staging|production|pr[1-9][0-9]{0,8})$/;
|
|
10
|
+
|
|
11
|
+
export function pullRequestEnvironment(prNumber: number): `pr${number}` {
|
|
12
|
+
if (!Number.isInteger(prNumber) || prNumber < 1 || prNumber > MAX_PULL_REQUEST_NUMBER) {
|
|
13
|
+
throw new Error(`Pull request number must be an integer from 1 through ${MAX_PULL_REQUEST_NUMBER}.`);
|
|
14
|
+
}
|
|
15
|
+
return `pr${prNumber}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseEnvironmentToken(value: string): EnvironmentToken {
|
|
19
|
+
if (!ENVIRONMENT_PATTERN.test(value)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'Environment must be exactly staging, production, or pr followed by an integer from 1 through 999999999.',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (value === 'staging' || value === 'production') return value;
|
|
25
|
+
return pullRequestEnvironment(Number(value.slice(2)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isPullRequestEnvironment(environment: EnvironmentToken): environment is `pr${number}` {
|
|
29
|
+
return environment.startsWith('pr');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function environmentDomain(environment: string, zone: string): string {
|
|
33
|
+
const token = parseEnvironmentToken(environment);
|
|
34
|
+
if (!zone || zone.startsWith('.') || zone.endsWith('.')) {
|
|
35
|
+
throw new Error('Zone must be a non-empty DNS name without leading or trailing dots.');
|
|
36
|
+
}
|
|
37
|
+
return token === 'production' ? zone : `${token}.${zone}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function environmentResourceName(base: string, environment: string): string {
|
|
41
|
+
const token = parseEnvironmentToken(environment);
|
|
42
|
+
if (!base) {
|
|
43
|
+
throw new Error('Resource base name must not be empty.');
|
|
44
|
+
}
|
|
45
|
+
return token === 'production' ? base : `${base}-${token}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hasExactEnvironmentSegment(value: string, environment: `pr${number}`): boolean {
|
|
49
|
+
const escaped = environment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
return new RegExp(`(?:^|[-.])${escaped}(?=$|[-.])`).test(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface WranglerRoot {
|
|
54
|
+
env?: Record<string, WranglerEnvironment | undefined>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface WranglerEnvironment {
|
|
58
|
+
name?: unknown;
|
|
59
|
+
routes?: unknown;
|
|
60
|
+
kv_namespaces?: unknown;
|
|
61
|
+
r2_buckets?: unknown;
|
|
62
|
+
ratelimits?: unknown;
|
|
63
|
+
vars?: unknown;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface KvBinding {
|
|
68
|
+
binding: string;
|
|
69
|
+
id: string;
|
|
70
|
+
}
|
|
71
|
+
export interface R2Binding {
|
|
72
|
+
binding: string;
|
|
73
|
+
bucketName: string;
|
|
74
|
+
}
|
|
75
|
+
const isWranglerRoot = typia.createIs<WranglerRoot>();
|
|
76
|
+
const isWranglerEnvironment = typia.createIs<WranglerEnvironment>();
|
|
77
|
+
const isUnknownRecord = typia.createIs<Record<string, unknown>>();
|
|
78
|
+
const isUnknownRows = typia.createIs<Record<string, unknown>[]>();
|
|
79
|
+
|
|
80
|
+
export interface LiveKvNamespace {
|
|
81
|
+
id: string;
|
|
82
|
+
title: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PullRequestKvResource {
|
|
86
|
+
binding: string;
|
|
87
|
+
stagingId: string;
|
|
88
|
+
stagingTitle: string;
|
|
89
|
+
title: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface PullRequestResourcePlan {
|
|
93
|
+
environment: `pr${number}`;
|
|
94
|
+
workerName: string;
|
|
95
|
+
workerBaseName: string;
|
|
96
|
+
kvNamespaces: PullRequestKvResource[];
|
|
97
|
+
r2Buckets: R2Binding[];
|
|
98
|
+
routes: Array<{ pattern: string; zoneName?: string; customDomain: boolean }>;
|
|
99
|
+
}
|
|
100
|
+
export interface ConfiguredEnvironmentResourcePlan {
|
|
101
|
+
environment: EnvironmentToken;
|
|
102
|
+
workerName: string;
|
|
103
|
+
kvNamespaces: KvBinding[];
|
|
104
|
+
r2Buckets: R2Binding[];
|
|
105
|
+
routes: Array<{ pattern: string; zoneName?: string; customDomain: boolean }>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function planConfiguredEnvironmentResources(
|
|
109
|
+
toml: string,
|
|
110
|
+
environment: EnvironmentToken,
|
|
111
|
+
): ConfiguredEnvironmentResourcePlan {
|
|
112
|
+
const block = parseRoot(toml).env?.[environment];
|
|
113
|
+
if (!isWranglerEnvironment(block)) {
|
|
114
|
+
throw new Error(`Wrangler configuration must declare [env.${environment}].`);
|
|
115
|
+
}
|
|
116
|
+
const workerName = requiredString(block, 'name', `[env.${environment}]`);
|
|
117
|
+
return {
|
|
118
|
+
environment,
|
|
119
|
+
workerName,
|
|
120
|
+
kvNamespaces: readKvBindings(block.kv_namespaces),
|
|
121
|
+
r2Buckets: readRows(block.r2_buckets).map((row) => {
|
|
122
|
+
const binding = requiredString(row, 'binding', 'R2 binding');
|
|
123
|
+
return { binding, bucketName: requiredString(row, 'bucket_name', `R2 binding ${binding}`) };
|
|
124
|
+
}),
|
|
125
|
+
routes: readRows(block.routes).map((row) => ({
|
|
126
|
+
pattern: requiredString(row, 'pattern', 'route'),
|
|
127
|
+
...(typeof row.zone_name === 'string' ? { zoneName: row.zone_name } : {}),
|
|
128
|
+
customDomain: row.custom_domain === true,
|
|
129
|
+
})),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseRoot(toml: string): WranglerRoot {
|
|
134
|
+
const value: unknown = getStaticTOMLValue(parseTOML(toml));
|
|
135
|
+
if (!isWranglerRoot(value)) {
|
|
136
|
+
throw new Error('Wrangler configuration is not a valid TOML environment document.');
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function stagingEnvironment(toml: string): WranglerEnvironment {
|
|
142
|
+
const staging = parseRoot(toml).env?.staging;
|
|
143
|
+
if (!isWranglerEnvironment(staging)) {
|
|
144
|
+
throw new Error('Wrangler configuration must declare [env.staging].');
|
|
145
|
+
}
|
|
146
|
+
return staging;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stagingWorkerName(staging: WranglerEnvironment): { workerName: string; workerBaseName: string } {
|
|
150
|
+
if (typeof staging.name !== 'string' || !staging.name.endsWith('-staging')) {
|
|
151
|
+
throw new Error('[env.staging].name must end with the exact suffix -staging.');
|
|
152
|
+
}
|
|
153
|
+
return { workerName: staging.name, workerBaseName: staging.name.slice(0, -'-staging'.length) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function planPullRequestResources(
|
|
157
|
+
toml: string,
|
|
158
|
+
environment: `pr${number}`,
|
|
159
|
+
liveNamespaces: LiveKvNamespace[],
|
|
160
|
+
): PullRequestResourcePlan {
|
|
161
|
+
parseEnvironmentToken(environment);
|
|
162
|
+
const staging = stagingEnvironment(toml);
|
|
163
|
+
const { workerBaseName } = stagingWorkerName(staging);
|
|
164
|
+
const namespaceById = new Map(liveNamespaces.map((namespace) => [namespace.id, namespace]));
|
|
165
|
+
const kvNamespaces = readKvBindings(staging.kv_namespaces).map(({ binding, id }) => {
|
|
166
|
+
const stagingNamespace = namespaceById.get(id);
|
|
167
|
+
if (!stagingNamespace) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Staging KV binding ${binding} references namespace ${id}, which is absent from the account listing.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const title = replaceExactToken(stagingNamespace.title, 'staging', environment);
|
|
173
|
+
if (title === stagingNamespace.title) {
|
|
174
|
+
throw new Error(`Staging KV namespace title ${stagingNamespace.title} has no exact staging segment.`);
|
|
175
|
+
}
|
|
176
|
+
return { binding, stagingId: id, stagingTitle: stagingNamespace.title, title };
|
|
177
|
+
});
|
|
178
|
+
const r2Buckets = readRows(staging.r2_buckets).map((row) => {
|
|
179
|
+
const binding = requiredString(row, 'binding', 'R2 binding');
|
|
180
|
+
const stagingBucket = requiredString(row, 'bucket_name', `R2 binding ${binding}`);
|
|
181
|
+
const bucketName = replaceExactToken(stagingBucket, 'staging', environment);
|
|
182
|
+
if (bucketName === stagingBucket) {
|
|
183
|
+
throw new Error(`Staging R2 bucket ${stagingBucket} has no exact staging segment.`);
|
|
184
|
+
}
|
|
185
|
+
return { binding, bucketName };
|
|
186
|
+
});
|
|
187
|
+
const routes = readRows(staging.routes).map((row) => ({
|
|
188
|
+
pattern: replaceHostnameLabel(requiredString(row, 'pattern', 'route'), environment),
|
|
189
|
+
...(typeof row.zone_name === 'string' ? { zoneName: row.zone_name } : {}),
|
|
190
|
+
customDomain: row.custom_domain === true,
|
|
191
|
+
}));
|
|
192
|
+
return {
|
|
193
|
+
environment,
|
|
194
|
+
workerName: environmentResourceName(workerBaseName, environment),
|
|
195
|
+
workerBaseName,
|
|
196
|
+
kvNamespaces,
|
|
197
|
+
r2Buckets,
|
|
198
|
+
routes,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface DerivePullRequestConfigOptions {
|
|
203
|
+
environment: `pr${number}`;
|
|
204
|
+
accountId: string;
|
|
205
|
+
kvNamespaceIds: ReadonlyMap<string, string>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function derivePullRequestWranglerConfig(toml: string, options: DerivePullRequestConfigOptions): string {
|
|
209
|
+
const environment = options.environment;
|
|
210
|
+
parseEnvironmentToken(environment);
|
|
211
|
+
if (!options.accountId) {
|
|
212
|
+
throw new Error('Cloudflare account id is required to derive rate-limit namespaces.');
|
|
213
|
+
}
|
|
214
|
+
const staging = stagingEnvironment(toml);
|
|
215
|
+
const { workerBaseName } = stagingWorkerName(staging);
|
|
216
|
+
const cloned = cloneEnvBlock(toml, 'staging', environment);
|
|
217
|
+
const program = parseTOML(cloned);
|
|
218
|
+
const rootValue: unknown = getStaticTOMLValue(program);
|
|
219
|
+
if (!isWranglerRoot(rootValue)) {
|
|
220
|
+
throw new Error('Derived Wrangler configuration is not a valid environment document.');
|
|
221
|
+
}
|
|
222
|
+
const root = rootValue;
|
|
223
|
+
const edits: Array<{ start: number; end: number; value: string }> = [];
|
|
224
|
+
|
|
225
|
+
for (const table of program.body[0].body) {
|
|
226
|
+
if (table.type !== 'TOMLTable' || table.resolvedKey[0] !== 'env' || table.resolvedKey[1] !== environment) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const tableValue = valueAtPath(root, table.resolvedKey);
|
|
230
|
+
if (!isUnknownRecord(tableValue)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
for (const keyValue of table.body) {
|
|
234
|
+
const key = cloned.slice(keyValue.key.range[0], keyValue.key.range[1]).trim();
|
|
235
|
+
const current = tableValue[key];
|
|
236
|
+
const next = deriveFieldValue(
|
|
237
|
+
table.resolvedKey.slice(2),
|
|
238
|
+
tableValue,
|
|
239
|
+
key,
|
|
240
|
+
current,
|
|
241
|
+
environment,
|
|
242
|
+
workerBaseName,
|
|
243
|
+
options.accountId,
|
|
244
|
+
options.kvNamespaceIds,
|
|
245
|
+
);
|
|
246
|
+
if (next !== current) {
|
|
247
|
+
edits.push({ start: keyValue.value.range[0], end: keyValue.value.range[1], value: tomlLiteral(next) });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let derived = cloned;
|
|
253
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
254
|
+
derived = derived.slice(0, edit.start) + edit.value + derived.slice(edit.end);
|
|
255
|
+
}
|
|
256
|
+
parseTOML(derived);
|
|
257
|
+
return derived;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function deriveFieldValue(
|
|
261
|
+
path: Array<string | number>,
|
|
262
|
+
table: Record<string, unknown>,
|
|
263
|
+
key: string,
|
|
264
|
+
current: unknown,
|
|
265
|
+
environment: `pr${number}`,
|
|
266
|
+
workerBaseName: string,
|
|
267
|
+
accountId: string,
|
|
268
|
+
kvNamespaceIds: ReadonlyMap<string, string>,
|
|
269
|
+
): unknown {
|
|
270
|
+
const section = path[0];
|
|
271
|
+
if (path.length === 0 && key === 'name') {
|
|
272
|
+
return environmentResourceName(workerBaseName, environment);
|
|
273
|
+
}
|
|
274
|
+
if (section === 'routes' && key === 'pattern' && typeof current === 'string') {
|
|
275
|
+
return replaceHostnameLabel(current, environment);
|
|
276
|
+
}
|
|
277
|
+
if (section === 'vars' && typeof current === 'string') {
|
|
278
|
+
if (key === 'ENVIRONMENT') {
|
|
279
|
+
return environment;
|
|
280
|
+
}
|
|
281
|
+
if (key === 'AUTH_KEYS_INSTANCE_NAME') {
|
|
282
|
+
return replaceExactToken(current, 'staging', environment);
|
|
283
|
+
}
|
|
284
|
+
return replaceHostnameLabel(current, environment);
|
|
285
|
+
}
|
|
286
|
+
if (section === 'send_email' && key === 'allowed_sender_addresses' && Array.isArray(current)) {
|
|
287
|
+
return current.map((value) => (typeof value === 'string' ? replaceHostnameLabel(value, environment) : value));
|
|
288
|
+
}
|
|
289
|
+
if (section === 'kv_namespaces' && key === 'id' && typeof current === 'string') {
|
|
290
|
+
const derived = kvNamespaceIds.get(current);
|
|
291
|
+
if (!derived) {
|
|
292
|
+
throw new Error(`No derived KV namespace id was supplied for staging namespace ${current}.`);
|
|
293
|
+
}
|
|
294
|
+
return derived;
|
|
295
|
+
}
|
|
296
|
+
if (section === 'r2_buckets' && key === 'bucket_name' && typeof current === 'string') {
|
|
297
|
+
return replaceExactToken(current, 'staging', environment);
|
|
298
|
+
}
|
|
299
|
+
if (section === 'ratelimits' && key === 'namespace_id') {
|
|
300
|
+
const bindingName = requiredString(table, 'name', 'Rate-limit binding');
|
|
301
|
+
return rateLimitNamespaceId(accountId, workerBaseName, environment, bindingName);
|
|
302
|
+
}
|
|
303
|
+
return current;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function rateLimitNamespaceId(
|
|
307
|
+
accountId: string,
|
|
308
|
+
workerBaseName: string,
|
|
309
|
+
environment: string,
|
|
310
|
+
bindingName: string,
|
|
311
|
+
): string {
|
|
312
|
+
const token = parseEnvironmentToken(environment);
|
|
313
|
+
const digest = createHash('sha256').update(`${accountId}:${workerBaseName}:${token}:${bindingName}`).digest();
|
|
314
|
+
const value = digest.readUInt32BE(0) & 0x7fff_ffff;
|
|
315
|
+
return String(value === 0 ? 1 : value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function replaceHostnameLabel(value: string, environment: `pr${number}`): string {
|
|
319
|
+
return value.replace(/(^|[.@/])staging(?=\.)/g, `$1${environment}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function replaceExactToken(value: string, from: string, to: string): string {
|
|
323
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
324
|
+
return value.replace(new RegExp(`(^|[-.])${escaped}(?=$|[-.])`, 'g'), `$1${to}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function readKvBindings(value: unknown): KvBinding[] {
|
|
328
|
+
return readRows(value).map((row) => ({
|
|
329
|
+
binding: requiredString(row, 'binding', 'KV namespace'),
|
|
330
|
+
id: requiredString(row, 'id', 'KV namespace'),
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function readRows(value: unknown): Record<string, unknown>[] {
|
|
335
|
+
return isUnknownRows(value) ? value : [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function requiredString(row: Record<string, unknown>, key: string, context: string): string {
|
|
339
|
+
const value = row[key];
|
|
340
|
+
if (typeof value !== 'string' || !value) {
|
|
341
|
+
throw new Error(`${context} must declare a non-empty ${key}.`);
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function valueAtPath(root: unknown, path: Array<string | number>): unknown {
|
|
347
|
+
let value = root;
|
|
348
|
+
for (const segment of path) {
|
|
349
|
+
if (typeof segment === 'number') {
|
|
350
|
+
if (!Array.isArray(value)) return undefined;
|
|
351
|
+
value = value[segment];
|
|
352
|
+
} else {
|
|
353
|
+
if (!isUnknownRecord(value)) return undefined;
|
|
354
|
+
value = value[segment];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function tomlLiteral(value: unknown): string {
|
|
361
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) {
|
|
362
|
+
const literal = JSON.stringify(value);
|
|
363
|
+
if (literal !== undefined) return literal;
|
|
364
|
+
}
|
|
365
|
+
throw new Error(`Cannot materialize Wrangler TOML value of type ${typeof value}.`);
|
|
366
|
+
}
|