@vibes.diy/vibe-runtime 12.1.1 → 12.1.2

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.
@@ -1,13 +1,13 @@
1
1
  export const MIN_INTERVAL_MS = 5_000;
2
2
  export const MAX_INTERVAL_MS = 3_600_000;
3
3
  const HANDLER_NAMES = ["fetch", "scheduled", "onChange"];
4
- function exportsName(source, name) {
4
+ function exportsName(code, name) {
5
5
  const decl = new RegExp(`export\\s+(?:async\\s+)?(?:function\\s+${name}\\b|(?:const|let|var)\\s+${name}\\b)`);
6
- if (decl.test(source))
6
+ if (decl.test(code))
7
7
  return true;
8
8
  const listPattern = /export\s*\{([^}]*)\}/g;
9
9
  let m;
10
- while ((m = listPattern.exec(source)) !== null) {
10
+ while ((m = listPattern.exec(code)) !== null) {
11
11
  const inner = m[1];
12
12
  const bareOrAliased = new RegExp(`(?:\\bas\\s+${name}\\b|(?:^|,)\\s*${name}\\s*(?:,|$))`);
13
13
  if (bareOrAliased.test(inner))
@@ -25,6 +25,10 @@ function parseDurationMs(raw) {
25
25
  const mult = m[2] === "s" ? 1_000 : m[2] === "m" ? 60_000 : 3_600_000;
26
26
  return n * mult;
27
27
  }
28
+ const sliceAligned = (a, from, to) => ({
29
+ code: a.code.slice(from, to),
30
+ text: a.text.slice(from, to),
31
+ });
28
32
  function skipQuoted(src, start, quote) {
29
33
  let i = start + 1;
30
34
  while (i < src.length) {
@@ -124,23 +128,6 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
124
128
  "await",
125
129
  "throw",
126
130
  ]);
127
- function regexAllowedAt(chars, idx) {
128
- let i = idx - 1;
129
- while (i >= 0 && /\s/.test(chars[i]))
130
- i--;
131
- if (i < 0)
132
- return true;
133
- const ch = chars[i];
134
- if (ch === ")" || ch === "]")
135
- return false;
136
- if (/[A-Za-z0-9_$]/.test(ch)) {
137
- let j = i;
138
- while (j >= 0 && /[A-Za-z0-9_$]/.test(chars[j]))
139
- j--;
140
- return REGEX_PRECEDING_KEYWORDS.has(chars.slice(j + 1, i + 1).join(""));
141
- }
142
- return true;
143
- }
144
131
  function maskNonCode(source) {
145
132
  const chars = source.split("");
146
133
  const blank = (from, to) => {
@@ -149,6 +136,8 @@ function maskNonCode(source) {
149
136
  chars[k] = " ";
150
137
  }
151
138
  };
139
+ let prevIsValue = false;
140
+ let prevIsDot = false;
152
141
  let i = 0;
153
142
  while (i < source.length) {
154
143
  const ch = source[i];
@@ -171,82 +160,149 @@ function maskNonCode(source) {
171
160
  }
172
161
  if (ch === '"' || ch === "'") {
173
162
  i = skipQuoted(source, i, ch);
163
+ prevIsValue = true;
164
+ prevIsDot = false;
174
165
  continue;
175
166
  }
176
167
  if (ch === "`") {
177
168
  const end = skipTemplate(source, i);
178
169
  blank(i, end);
179
170
  i = end;
171
+ prevIsValue = true;
172
+ prevIsDot = false;
180
173
  continue;
181
174
  }
182
- if (ch === "/" && regexAllowedAt(chars, i)) {
183
- const end = skipRegex(source, i);
184
- if (end !== undefined) {
185
- blank(i, end);
186
- i = end;
187
- continue;
175
+ if (ch === "/") {
176
+ if (!prevIsValue) {
177
+ const end = skipRegex(source, i);
178
+ if (end !== undefined) {
179
+ blank(i, end);
180
+ i = end;
181
+ prevIsValue = true;
182
+ prevIsDot = false;
183
+ continue;
184
+ }
188
185
  }
186
+ i++;
187
+ prevIsValue = false;
188
+ prevIsDot = false;
189
+ continue;
190
+ }
191
+ if (/[A-Za-z0-9_$]/.test(ch)) {
192
+ let j = i;
193
+ while (j < source.length && /[A-Za-z0-9_$]/.test(source[j]))
194
+ j++;
195
+ prevIsValue = prevIsDot || !REGEX_PRECEDING_KEYWORDS.has(source.slice(i, j));
196
+ prevIsDot = false;
197
+ i = j;
198
+ continue;
189
199
  }
200
+ if (ch === ")" || ch === "]") {
201
+ prevIsValue = true;
202
+ prevIsDot = false;
203
+ i++;
204
+ continue;
205
+ }
206
+ if ((ch === "+" && source[i + 1] === "+") || (ch === "-" && source[i + 1] === "-")) {
207
+ prevIsValue = true;
208
+ prevIsDot = false;
209
+ i += 2;
210
+ continue;
211
+ }
212
+ if (/\s/.test(ch)) {
213
+ i++;
214
+ continue;
215
+ }
216
+ prevIsValue = false;
217
+ prevIsDot = ch === ".";
190
218
  i++;
191
219
  }
192
220
  return chars.join("");
193
221
  }
194
- function hasLiveKey(masked, key) {
195
- const at = new RegExp(`\\b${key}\\s*:`, "y");
196
- for (let i = 0; i < masked.length; i++) {
222
+ function blankStrings(masked) {
223
+ const chars = masked.split("");
224
+ let i = 0;
225
+ while (i < masked.length) {
197
226
  const ch = masked[i];
198
227
  if (ch === '"' || ch === "'") {
199
- i = skipQuoted(masked, i, ch) - 1;
228
+ const end = skipQuoted(masked, i, ch);
229
+ const interiorEnd = masked[end - 1] === ch ? end - 1 : end;
230
+ for (let k = i + 1; k < interiorEnd && k < chars.length; k++) {
231
+ if (chars[k] !== "\n")
232
+ chars[k] = " ";
233
+ }
234
+ i = end;
200
235
  continue;
201
236
  }
202
- at.lastIndex = i;
203
- if (at.exec(masked) !== null)
237
+ i++;
238
+ }
239
+ return chars.join("");
240
+ }
241
+ function viewsOf(source) {
242
+ const text = maskNonCode(source);
243
+ return { text, code: blankStrings(text) };
244
+ }
245
+ function skipSpace(code, from) {
246
+ let i = from;
247
+ while (i < code.length && /\s/.test(code[i]))
248
+ i++;
249
+ return i;
250
+ }
251
+ function quotedSpan(code, start) {
252
+ const quote = code[start];
253
+ let i = start + 1;
254
+ while (i < code.length && code[i] !== quote) {
255
+ if (code[i] === "\n")
256
+ return undefined;
257
+ i++;
258
+ }
259
+ return i < code.length ? { end: i + 1 } : undefined;
260
+ }
261
+ function mentionsKey(view, key) {
262
+ if (new RegExp(`\\b${key}\\s*:`).test(view.code))
263
+ return true;
264
+ const code = view.code;
265
+ for (let i = 0; i < code.length; i++) {
266
+ if (code[i] !== '"' && code[i] !== "'")
267
+ continue;
268
+ const span = quotedSpan(code, i);
269
+ if (span === undefined)
270
+ continue;
271
+ if (view.text.slice(i + 1, span.end - 1) === key && code[skipSpace(code, span.end)] === ":")
204
272
  return true;
273
+ i = span.end - 1;
205
274
  }
206
275
  return false;
207
276
  }
208
- function sliceBalancedObject(source, openIdx) {
277
+ function balancedObjectEnd(code, openIdx) {
209
278
  let depth = 0;
210
- let quote = null;
211
- for (let i = openIdx; i < source.length; i++) {
212
- const ch = source[i];
213
- if (quote !== null) {
214
- if (ch === "\\") {
215
- i++;
216
- continue;
217
- }
218
- if (ch === quote)
219
- quote = null;
220
- continue;
221
- }
222
- if (ch === '"' || ch === "'" || ch === "`") {
223
- quote = ch;
224
- }
225
- else if (ch === "{") {
279
+ for (let i = openIdx; i < code.length; i++) {
280
+ const ch = code[i];
281
+ if (ch === "{")
226
282
  depth++;
227
- }
228
283
  else if (ch === "}") {
229
284
  depth--;
230
285
  if (depth === 0)
231
- return source.slice(openIdx, i + 1);
286
+ return i + 1;
232
287
  }
233
288
  }
234
289
  return undefined;
235
290
  }
236
- function extractConfigObject(masked) {
237
- const decl = /(?:export\s+)?(?:const|let|var)\s+config\s*=\s*\{/.exec(masked);
291
+ function extractConfigObject(src) {
292
+ const decl = /(?:export\s+)?(?:const|let|var)\s+config\s*=\s*\{/.exec(src.code);
238
293
  if (decl === null)
239
294
  return undefined;
240
295
  const openIdx = decl.index + decl[0].length - 1;
241
- return sliceBalancedObject(masked, openIdx);
296
+ const end = balancedObjectEnd(src.code, openIdx);
297
+ return end === undefined ? undefined : sliceAligned(src, openIdx, end);
242
298
  }
243
- function exportedConfigLocalNames(masked) {
299
+ function exportedConfigLocalNames(code) {
244
300
  const names = new Set();
245
- if (/export\s+(?:const|let|var)\s+config\s*=/.test(masked))
301
+ if (/export\s+(?:const|let|var)\s+config\s*=/.test(code))
246
302
  names.add("config");
247
303
  const listPattern = /export\s*\{([^}]*)\}/g;
248
304
  let m;
249
- while ((m = listPattern.exec(masked)) !== null) {
305
+ while ((m = listPattern.exec(code)) !== null) {
250
306
  for (const entry of m[1].split(",")) {
251
307
  const aliased = /^\s*([A-Za-z_$][\w$]*)\s+as\s+config\s*$/.exec(entry);
252
308
  if (aliased !== null) {
@@ -259,16 +315,12 @@ function exportedConfigLocalNames(masked) {
259
315
  }
260
316
  return [...names];
261
317
  }
262
- function topLevelObjectBindings(masked) {
318
+ function topLevelObjectBindings(code) {
263
319
  const at = /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*\{/y;
264
320
  const found = [];
265
321
  let depth = 0;
266
- for (let i = 0; i < masked.length; i++) {
267
- const ch = masked[i];
268
- if (ch === '"' || ch === "'") {
269
- i = skipQuoted(masked, i, ch) - 1;
270
- continue;
271
- }
322
+ for (let i = 0; i < code.length; i++) {
323
+ const ch = code[i];
272
324
  if (ch === "{" || ch === "(" || ch === "[") {
273
325
  depth++;
274
326
  continue;
@@ -279,10 +331,10 @@ function topLevelObjectBindings(masked) {
279
331
  }
280
332
  if (depth !== 0)
281
333
  continue;
282
- if (i > 0 && /[\w$]/.test(masked[i - 1]))
334
+ if (i > 0 && /[\w$]/.test(code[i - 1]))
283
335
  continue;
284
336
  at.lastIndex = i;
285
- const m = at.exec(masked);
337
+ const m = at.exec(code);
286
338
  if (m !== null) {
287
339
  const openIdx = i + m[0].length - 1;
288
340
  found.push({ name: m[1], openIdx });
@@ -292,119 +344,144 @@ function topLevelObjectBindings(masked) {
292
344
  }
293
345
  return found;
294
346
  }
295
- function extractExportedConfigObject(masked) {
296
- const localNames = exportedConfigLocalNames(masked);
347
+ function extractExportedConfigObject(src) {
348
+ const localNames = exportedConfigLocalNames(src.code);
297
349
  if (localNames.length === 0)
298
350
  return undefined;
299
- const bindings = topLevelObjectBindings(masked).filter((b) => localNames.includes(b.name));
351
+ const bindings = topLevelObjectBindings(src.code).filter((b) => localNames.includes(b.name));
300
352
  if (bindings.length !== 1)
301
353
  return undefined;
302
- return sliceBalancedObject(masked, bindings[0].openIdx);
354
+ const end = balancedObjectEnd(src.code, bindings[0].openIdx);
355
+ return end === undefined ? undefined : sliceAligned(src, bindings[0].openIdx, end);
303
356
  }
304
- function extractIntervalLiteral(masked) {
305
- const configObj = extractConfigObject(masked);
357
+ function extractIntervalLiteral(src) {
358
+ const configObj = extractConfigObject(src);
306
359
  if (configObj === undefined)
307
360
  return undefined;
308
- const m = /scheduled\s*:\s*\{[^}]*?\binterval\s*:\s*["']([^"']+)["']/s.exec(configObj);
361
+ const m = /scheduled\s*:\s*\{[^}]*?\binterval\s*:\s*["']([^"']+)["']/s.exec(configObj.text);
309
362
  return m?.[1];
310
363
  }
311
- function extractTopLevelObject(objSource, key) {
312
- const at = new RegExp(`\\b${key}\\s*:\\s*\\{`, "y");
313
- let depth = 0;
314
- let quote = null;
315
- for (let i = 0; i < objSource.length; i++) {
316
- const ch = objSource[i];
317
- if (quote !== null) {
318
- if (ch === "\\") {
319
- i++;
320
- continue;
321
- }
322
- if (ch === quote)
323
- quote = null;
324
- continue;
364
+ const OPEN_BRACKETS = "{[(";
365
+ const CLOSE_BRACKETS = "}])";
366
+ function readTopLevelKey(obj, start) {
367
+ const code = obj.code;
368
+ const ch = code[start];
369
+ const ambiguous = { key: { ambiguous: true }, next: start };
370
+ if (ch === "[")
371
+ return ambiguous;
372
+ if (ch === ".")
373
+ return ambiguous;
374
+ if (ch === "*")
375
+ return ambiguous;
376
+ let name;
377
+ let nameEnd;
378
+ if (ch === '"' || ch === "'") {
379
+ const span = quotedSpan(code, start);
380
+ if (span === undefined)
381
+ return ambiguous;
382
+ const body = obj.text.slice(start + 1, span.end - 1);
383
+ if (body.includes("\\"))
384
+ return { key: { ambiguous: true }, next: span.end };
385
+ name = body;
386
+ nameEnd = span.end;
387
+ }
388
+ else if (/[A-Za-z_$]/.test(ch)) {
389
+ let j = start;
390
+ while (j < code.length && /[\w$]/.test(code[j]))
391
+ j++;
392
+ name = code.slice(start, j);
393
+ nameEnd = j;
394
+ if (name === "get" || name === "set" || name === "async") {
395
+ const after = skipSpace(code, nameEnd);
396
+ if (after < code.length && /["'[*A-Za-z_$]/.test(code[after]))
397
+ return ambiguous;
325
398
  }
326
- if (ch === '"' || ch === "'" || ch === "`") {
327
- quote = ch;
399
+ }
400
+ else if (/[0-9]/.test(ch)) {
401
+ let j = start;
402
+ while (j < code.length && /[\w$.]/.test(code[j]))
403
+ j++;
404
+ name = code.slice(start, j);
405
+ nameEnd = j;
406
+ }
407
+ else {
408
+ return ambiguous;
409
+ }
410
+ const afterName = skipSpace(code, nameEnd);
411
+ if (code[afterName] === ":") {
412
+ const valueStart = skipSpace(code, afterName + 1);
413
+ return { key: { name, valueStart, ambiguous: false }, next: valueStart };
414
+ }
415
+ return { key: { name, ambiguous: false }, next: nameEnd };
416
+ }
417
+ function topLevelKeys(obj) {
418
+ const code = obj.code;
419
+ const keys = [];
420
+ let depth = 0;
421
+ let expectKey = false;
422
+ let i = 0;
423
+ while (i < code.length) {
424
+ const ch = code[i];
425
+ if (depth === 1 && expectKey && !/\s/.test(ch) && ch !== "}") {
426
+ const read = readTopLevelKey(obj, i);
427
+ keys.push(read.key);
428
+ expectKey = false;
429
+ i = read.next;
328
430
  continue;
329
431
  }
330
- if (ch === "{") {
432
+ if (OPEN_BRACKETS.includes(ch)) {
331
433
  depth++;
434
+ if (depth === 1 && ch === "{")
435
+ expectKey = true;
436
+ i++;
332
437
  continue;
333
438
  }
334
- if (ch === "}") {
439
+ if (CLOSE_BRACKETS.includes(ch)) {
335
440
  depth--;
441
+ i++;
336
442
  continue;
337
443
  }
338
- if (depth !== 1)
339
- continue;
340
- at.lastIndex = i;
341
- const m = at.exec(objSource);
342
- if (m !== null)
343
- return sliceBalancedObject(objSource, i + m[0].length - 1);
444
+ if (depth === 1 && ch === ",")
445
+ expectKey = true;
446
+ i++;
344
447
  }
345
- return undefined;
448
+ return keys;
449
+ }
450
+ const named = (keys, key) => keys.filter((k) => k.name === key);
451
+ function objectAt(obj, valueStart) {
452
+ if (obj.code[valueStart] !== "{")
453
+ return undefined;
454
+ const end = balancedObjectEnd(obj.code, valueStart);
455
+ return end === undefined ? undefined : sliceAligned(obj, valueStart, end);
346
456
  }
347
- function extractTopLevelValue(objSource, key) {
348
- const at = new RegExp(`\\b${key}\\s*:\\s*`, "y");
349
- const OPEN = "{[(";
350
- const CLOSE = "}])";
457
+ function valueTextAt(obj, valueStart) {
458
+ const OPEN = OPEN_BRACKETS;
459
+ const CLOSE = CLOSE_BRACKETS;
351
460
  let depth = 0;
352
- for (let i = 0; i < objSource.length; i++) {
353
- const ch = objSource[i];
354
- if (ch === '"' || ch === "'") {
355
- i = skipQuoted(objSource, i, ch) - 1;
356
- continue;
357
- }
358
- if (OPEN.includes(ch)) {
461
+ let j = valueStart;
462
+ for (; j < obj.code.length; j++) {
463
+ const c = obj.code[j];
464
+ if (OPEN.includes(c)) {
359
465
  depth++;
360
466
  continue;
361
467
  }
362
- if (CLOSE.includes(ch)) {
468
+ if (depth === 0 && (c === "," || c === "}"))
469
+ break;
470
+ if (CLOSE.includes(c))
363
471
  depth--;
364
- continue;
365
- }
366
- if (depth !== 1)
367
- continue;
368
- at.lastIndex = i;
369
- const m = at.exec(objSource);
370
- if (m === null)
371
- continue;
372
- let j = i + m[0].length;
373
- let valueDepth = 0;
374
- for (; j < objSource.length; j++) {
375
- const c = objSource[j];
376
- if (c === '"' || c === "'") {
377
- j = skipQuoted(objSource, j, c) - 1;
378
- continue;
379
- }
380
- if (OPEN.includes(c)) {
381
- valueDepth++;
382
- continue;
383
- }
384
- if (valueDepth === 0 && (c === "," || c === "}"))
385
- break;
386
- if (CLOSE.includes(c))
387
- valueDepth--;
388
- }
389
- return objSource.slice(i + m[0].length, j).trim();
390
472
  }
391
- return undefined;
473
+ return obj.text.slice(valueStart, j).trim();
392
474
  }
393
475
  function splitArrayEntries(body) {
394
476
  const parts = [];
395
477
  let start = 0;
396
- for (let i = 0; i < body.length; i++) {
397
- const ch = body[i];
398
- if (ch === '"' || ch === "'") {
399
- i = skipQuoted(body, i, ch) - 1;
400
- continue;
401
- }
402
- if (ch === ",") {
403
- parts.push(body.slice(start, i));
478
+ for (let i = 0; i < body.code.length; i++) {
479
+ if (body.code[i] === ",") {
480
+ parts.push(body.text.slice(start, i));
404
481
  start = i + 1;
405
482
  }
406
483
  }
407
- parts.push(body.slice(start));
484
+ parts.push(body.text.slice(start));
408
485
  return parts.map((p) => p.trim()).filter((p) => p !== "");
409
486
  }
410
487
  function stringLiteralValue(raw) {
@@ -415,10 +492,18 @@ function stringLiteralValue(raw) {
415
492
  return /^'((?:[^'\\]|\\.)*)'$/.exec(s)?.[1];
416
493
  return undefined;
417
494
  }
418
- function extractFetchUnfilteredReads(masked) {
419
- const configObj = extractExportedConfigObject(masked);
495
+ const duplicateKeyError = (where, key, count) => `config.fetch.unfilteredReads is ambiguous: ${where} declares a duplicate \`${key}\` key (${count} times) — JavaScript keeps only the LAST one, so the declaration in the source is not the declaration that runs. Remove the extra \`${key}\` key(s) so exactly one remains, then push again`;
496
+ const ambiguousKeyError = (where) => `config.fetch.unfilteredReads is ambiguous: ${where} has a property key this push-time parser cannot read statically — a computed \`[key]\`, a spread, an accessor/generator, or a quoted name with escapes. Any of those could shadow \`fetch\`/\`unfilteredReads\`/\`dbs\`/\`why\`, so write those keys literally (bare or plainly quoted) alongside no unreadable siblings, then push again`;
497
+ function readableTopLevelKeys(obj, where) {
498
+ const keys = topLevelKeys(obj);
499
+ if (keys.some((k) => k.ambiguous))
500
+ return { errors: [ambiguousKeyError(where)] };
501
+ return { keys, errors: [] };
502
+ }
503
+ function extractFetchUnfilteredReads(src) {
504
+ const configObj = extractExportedConfigObject(src);
420
505
  if (configObj === undefined) {
421
- if (!hasLiveKey(masked, "unfilteredReads"))
506
+ if (!mentionsKey(src, "unfilteredReads"))
422
507
  return { errors: [] };
423
508
  return {
424
509
  errors: [
@@ -426,21 +511,53 @@ function extractFetchUnfilteredReads(masked) {
426
511
  ],
427
512
  };
428
513
  }
429
- if (!hasLiveKey(configObj, "unfilteredReads"))
514
+ if (!mentionsKey(configObj, "unfilteredReads"))
430
515
  return { errors: [] };
431
516
  const shape = 'config.fetch.unfilteredReads must be { dbs: ["db-name"], why: "…" } with static string literals';
432
- const fetchObj = extractTopLevelObject(configObj, "fetch");
433
- const decl = fetchObj === undefined ? undefined : extractTopLevelObject(fetchObj, "unfilteredReads");
517
+ const configKeys = readableTopLevelKeys(configObj, "the exported config object");
518
+ if (configKeys.keys === undefined)
519
+ return { errors: configKeys.errors };
520
+ const fetchKeys = named(configKeys.keys, "fetch");
521
+ if (fetchKeys.length > 1)
522
+ return { errors: [duplicateKeyError("the exported config object", "fetch", fetchKeys.length)] };
523
+ const fetchStart = fetchKeys[0]?.valueStart;
524
+ const fetchObj = fetchStart === undefined ? undefined : objectAt(configObj, fetchStart);
525
+ if (fetchObj === undefined)
526
+ return { errors: [shape] };
527
+ const fetchLaneKeys = readableTopLevelKeys(fetchObj, "config.fetch");
528
+ if (fetchLaneKeys.keys === undefined)
529
+ return { errors: fetchLaneKeys.errors };
530
+ const declKeys = named(fetchLaneKeys.keys, "unfilteredReads");
531
+ if (declKeys.length > 1)
532
+ return { errors: [duplicateKeyError("config.fetch", "unfilteredReads", declKeys.length)] };
533
+ const declStart = declKeys[0]?.valueStart;
534
+ const decl = declStart === undefined ? undefined : objectAt(fetchObj, declStart);
434
535
  if (decl === undefined)
435
536
  return { errors: [shape] };
436
537
  const errors = [];
437
- const dbsValue = extractTopLevelValue(decl, "dbs");
538
+ const declInner = readableTopLevelKeys(decl, "config.fetch.unfilteredReads");
539
+ if (declInner.keys === undefined)
540
+ return { errors: declInner.errors };
541
+ const dbsKeys = named(declInner.keys, "dbs");
542
+ const whyKeys = named(declInner.keys, "why");
543
+ if (dbsKeys.length > 1 || whyKeys.length > 1) {
544
+ const dupes = [];
545
+ if (dbsKeys.length > 1)
546
+ dupes.push(duplicateKeyError("config.fetch.unfilteredReads", "dbs", dbsKeys.length));
547
+ if (whyKeys.length > 1)
548
+ dupes.push(duplicateKeyError("config.fetch.unfilteredReads", "why", whyKeys.length));
549
+ return { errors: dupes };
550
+ }
551
+ const dbsStart = dbsKeys[0]?.valueStart;
552
+ const whyStart = whyKeys[0]?.valueStart;
553
+ const dbsValue = dbsStart === undefined ? undefined : valueTextAt(decl, dbsStart);
438
554
  let dbs;
439
555
  if (dbsValue === undefined || !dbsValue.startsWith("[") || !dbsValue.endsWith("]")) {
440
556
  errors.push(`${shape} — dbs must be an array literal of string literals`);
441
557
  }
442
558
  else {
443
- const parts = splitArrayEntries(dbsValue.slice(1, -1));
559
+ const bodyStart = dbsStart + 1;
560
+ const parts = splitArrayEntries(sliceAligned(decl, bodyStart, bodyStart + dbsValue.length - 2));
444
561
  const parsed = parts.map(stringLiteralValue);
445
562
  if (parts.length === 0) {
446
563
  errors.push(`${shape} — dbs must name at least one database`);
@@ -455,7 +572,7 @@ function extractFetchUnfilteredReads(masked) {
455
572
  dbs = parsed;
456
573
  }
457
574
  }
458
- const whyValue = extractTopLevelValue(decl, "why");
575
+ const whyValue = whyStart === undefined ? undefined : valueTextAt(decl, whyStart);
459
576
  const why = whyValue === undefined ? undefined : stringLiteralValue(whyValue);
460
577
  if (why === undefined || why.trim() === "") {
461
578
  errors.push(`${shape} — why must be a non-empty string literal stating why this handler authorizes these reads itself`);
@@ -469,12 +586,12 @@ export function parseBackendConfig(source) {
469
586
  if (source === undefined || source.trim() === "") {
470
587
  return { handlers: [], hasConfig: false, errors };
471
588
  }
472
- const masked = maskNonCode(source);
473
- const handlers = HANDLER_NAMES.filter((name) => exportsName(masked, name));
474
- const hasConfig = exportsName(masked, "config");
589
+ const src = viewsOf(source);
590
+ const handlers = HANDLER_NAMES.filter((name) => exportsName(src.code, name));
591
+ const hasConfig = exportsName(src.code, "config");
475
592
  const hasScheduled = handlers.includes("scheduled");
476
593
  let schedule;
477
- const rawInterval = extractIntervalLiteral(masked);
594
+ const rawInterval = extractIntervalLiteral(src);
478
595
  if (hasScheduled) {
479
596
  if (rawInterval === undefined) {
480
597
  errors.push('scheduled handler requires config.scheduled.interval as a static string-literal duration, e.g. { scheduled: { interval: "5m" } }; computed/indirect values are unsupported');
@@ -497,7 +614,7 @@ export function parseBackendConfig(source) {
497
614
  }
498
615
  let fetchUnfilteredReads;
499
616
  if (handlers.includes("fetch")) {
500
- const parsedReads = extractFetchUnfilteredReads(masked);
617
+ const parsedReads = extractFetchUnfilteredReads(src);
501
618
  errors.push(...parsedReads.errors);
502
619
  fetchUnfilteredReads = parsedReads.value;
503
620
  }