@b9g/shovel 0.2.0-beta.2 → 0.2.0-beta.20

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.
@@ -0,0 +1,1158 @@
1
+ // src/utils/project.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ function findProjectRoot(startDir = process.cwd()) {
5
+ let dir = startDir;
6
+ while (dir !== dirname(dir)) {
7
+ if (existsSync(join(dir, "package.json"))) {
8
+ return dir;
9
+ }
10
+ dir = dirname(dir);
11
+ }
12
+ return startDir;
13
+ }
14
+ function findWorkspaceRoot(startDir = process.cwd()) {
15
+ let dir = startDir;
16
+ while (dir !== dirname(dir)) {
17
+ const packageJsonPath = join(dir, "package.json");
18
+ if (existsSync(packageJsonPath)) {
19
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
20
+ if (packageJson.workspaces) {
21
+ return dir;
22
+ }
23
+ }
24
+ dir = dirname(dir);
25
+ }
26
+ return null;
27
+ }
28
+ function getNodeModulesPath(startDir) {
29
+ return join(findProjectRoot(startDir), "node_modules");
30
+ }
31
+
32
+ // src/utils/config.ts
33
+ import { readFileSync as readFileSync2 } from "fs";
34
+ import { join as join2 } from "path";
35
+ import { z } from "zod";
36
+ var DEFAULTS = {
37
+ SERVER: {
38
+ PORT: 3e3,
39
+ HOST: "localhost"
40
+ },
41
+ WORKERS: 1
42
+ // Single worker for development - user can override with --workers flag
43
+ };
44
+ var EXPRESSION_PATTERN = /(\|\||\?\?|&&|===|!==|==|!=|\$[A-Za-z_]|\[outdir\]|\[tmpdir\]|\[git\])/;
45
+ function getEnv() {
46
+ if (typeof import.meta !== "undefined" && import.meta.env) {
47
+ return import.meta.env;
48
+ }
49
+ if (typeof process !== "undefined" && process.env) {
50
+ return process.env;
51
+ }
52
+ return {};
53
+ }
54
+ var Tokenizer = class {
55
+ #input;
56
+ #pos;
57
+ constructor(input) {
58
+ this.#input = input;
59
+ this.#pos = 0;
60
+ }
61
+ #peek() {
62
+ return this.#input[this.#pos] || "";
63
+ }
64
+ #advance() {
65
+ return this.#input[this.#pos++] || "";
66
+ }
67
+ #skipWhitespace() {
68
+ while (/\s/.test(this.#peek())) {
69
+ this.#advance();
70
+ }
71
+ }
72
+ /**
73
+ * Check if an operator at the current position has required whitespace.
74
+ * Infix operators must have whitespace on both sides.
75
+ * Returns true if the operator should be tokenized, false if it should be
76
+ * treated as part of an identifier.
77
+ */
78
+ #hasInfixWhitespace(operatorLength) {
79
+ const charBefore = this.#pos > 0 ? this.#input[this.#pos - 1] : " ";
80
+ const charAfter = this.#input[this.#pos + operatorLength] || " ";
81
+ return /\s/.test(charBefore) && /\s/.test(charAfter);
82
+ }
83
+ next() {
84
+ this.#skipWhitespace();
85
+ const start = this.#pos;
86
+ const ch = this.#peek();
87
+ if (!ch) {
88
+ return { type: "EOF" /* EOF */, value: null, start, end: start };
89
+ }
90
+ if (ch === '"' || ch === "'") {
91
+ const quote = ch;
92
+ this.#advance();
93
+ let value = "";
94
+ while (this.#peek() && this.#peek() !== quote) {
95
+ if (this.#peek() === "\\") {
96
+ this.#advance();
97
+ const next = this.#advance();
98
+ if (next === "n")
99
+ value += "\n";
100
+ else if (next === "t")
101
+ value += " ";
102
+ else
103
+ value += next;
104
+ } else {
105
+ value += this.#advance();
106
+ }
107
+ }
108
+ if (this.#peek() !== quote) {
109
+ throw new Error(`Unterminated string at position ${start}`);
110
+ }
111
+ this.#advance();
112
+ return { type: "STRING" /* STRING */, value, start, end: this.#pos };
113
+ }
114
+ if (/\d/.test(ch)) {
115
+ let value = "";
116
+ while (/\d/.test(this.#peek())) {
117
+ value += this.#advance();
118
+ }
119
+ return {
120
+ type: "NUMBER" /* NUMBER */,
121
+ value: parseInt(value, 10),
122
+ start,
123
+ end: this.#pos
124
+ };
125
+ }
126
+ if (ch === "=" && this.#input[this.#pos + 1] === "=" && this.#input[this.#pos + 2] === "=" && this.#hasInfixWhitespace(3)) {
127
+ this.#pos += 3;
128
+ return { type: "===" /* EQ_STRICT */, value: "===", start, end: this.#pos };
129
+ }
130
+ if (ch === "!" && this.#input[this.#pos + 1] === "=" && this.#input[this.#pos + 2] === "=" && this.#hasInfixWhitespace(3)) {
131
+ this.#pos += 3;
132
+ return { type: "!==" /* NE_STRICT */, value: "!==", start, end: this.#pos };
133
+ }
134
+ if (ch === "=" && this.#input[this.#pos + 1] === "=" && this.#hasInfixWhitespace(2)) {
135
+ this.#pos += 2;
136
+ return { type: "==" /* EQ */, value: "==", start, end: this.#pos };
137
+ }
138
+ if (ch === "!" && this.#input[this.#pos + 1] === "=" && this.#hasInfixWhitespace(2)) {
139
+ this.#pos += 2;
140
+ return { type: "!=" /* NE */, value: "!=", start, end: this.#pos };
141
+ }
142
+ if (ch === "|" && this.#input[this.#pos + 1] === "|" && this.#hasInfixWhitespace(2)) {
143
+ this.#pos += 2;
144
+ return { type: "||" /* OR */, value: "||", start, end: this.#pos };
145
+ }
146
+ if (ch === "&" && this.#input[this.#pos + 1] === "&" && this.#hasInfixWhitespace(2)) {
147
+ this.#pos += 2;
148
+ return { type: "&&" /* AND */, value: "&&", start, end: this.#pos };
149
+ }
150
+ if (ch === "?" && this.#input[this.#pos + 1] === "?" && this.#hasInfixWhitespace(2)) {
151
+ this.#pos += 2;
152
+ return { type: "??" /* NULLISH */, value: "??", start, end: this.#pos };
153
+ }
154
+ if (ch === "?" && this.#hasInfixWhitespace(1)) {
155
+ this.#advance();
156
+ return { type: "?" /* QUESTION */, value: "?", start, end: this.#pos };
157
+ }
158
+ if (ch === ":" && this.#hasInfixWhitespace(1)) {
159
+ this.#advance();
160
+ return { type: ":" /* COLON */, value: ":", start, end: this.#pos };
161
+ }
162
+ if (ch === "!") {
163
+ this.#advance();
164
+ return { type: "!" /* NOT */, value: "!", start, end: this.#pos };
165
+ }
166
+ if (ch === "(") {
167
+ this.#advance();
168
+ return { type: "(" /* LPAREN */, value: "(", start, end: this.#pos };
169
+ }
170
+ if (ch === ")") {
171
+ this.#advance();
172
+ return { type: ")" /* RPAREN */, value: ")", start, end: this.#pos };
173
+ }
174
+ if (ch === "$") {
175
+ this.#advance();
176
+ let name = "";
177
+ while (/[A-Za-z0-9_]/.test(this.#peek())) {
178
+ name += this.#advance();
179
+ }
180
+ if (!name) {
181
+ throw new Error(`Expected env var name after $ at position ${start}`);
182
+ }
183
+ return { type: "ENV_VAR" /* ENV_VAR */, value: name, start, end: this.#pos };
184
+ }
185
+ if (ch === "/") {
186
+ this.#advance();
187
+ return { type: "SLASH" /* SLASH */, value: "/", start, end: this.#pos };
188
+ }
189
+ if (ch === "[") {
190
+ const remaining = this.#input.slice(this.#pos);
191
+ if (remaining.startsWith("[outdir]")) {
192
+ this.#pos += 8;
193
+ return {
194
+ type: "OUTDIR" /* OUTDIR */,
195
+ value: "[outdir]",
196
+ start,
197
+ end: this.#pos
198
+ };
199
+ }
200
+ if (remaining.startsWith("[tmpdir]")) {
201
+ this.#pos += 8;
202
+ return {
203
+ type: "TMPDIR" /* TMPDIR */,
204
+ value: "[tmpdir]",
205
+ start,
206
+ end: this.#pos
207
+ };
208
+ }
209
+ if (remaining.startsWith("[git]")) {
210
+ this.#pos += 5;
211
+ return {
212
+ type: "GIT" /* GIT */,
213
+ value: "[git]",
214
+ start,
215
+ end: this.#pos
216
+ };
217
+ }
218
+ throw new Error(
219
+ `Unknown placeholder at position ${start}: ${remaining.slice(0, 10)}`
220
+ );
221
+ }
222
+ if (/\S/.test(ch) && !/[$()/]/.test(ch)) {
223
+ let value = "";
224
+ while (this.#peek() && /\S/.test(this.#peek()) && !/[$()]/.test(this.#peek())) {
225
+ if (this.#peek() === "/") {
226
+ if (this.#input[this.#pos + 1] === "/") {
227
+ value += this.#advance();
228
+ value += this.#advance();
229
+ continue;
230
+ }
231
+ break;
232
+ }
233
+ value += this.#advance();
234
+ }
235
+ if (value === "true")
236
+ return { type: "TRUE" /* TRUE */, value: true, start, end: this.#pos };
237
+ if (value === "false")
238
+ return { type: "FALSE" /* FALSE */, value: false, start, end: this.#pos };
239
+ if (value === "null")
240
+ return { type: "NULL" /* NULL */, value: null, start, end: this.#pos };
241
+ if (value === "undefined")
242
+ return {
243
+ type: "UNDEFINED" /* UNDEFINED */,
244
+ value: void 0,
245
+ start,
246
+ end: this.#pos
247
+ };
248
+ return { type: "IDENTIFIER" /* IDENTIFIER */, value, start, end: this.#pos };
249
+ }
250
+ throw new Error(`Unexpected character '${ch}' at position ${start}`);
251
+ }
252
+ };
253
+ var Parser = class _Parser {
254
+ #tokens;
255
+ #pos;
256
+ #env;
257
+ /**
258
+ * Parse and evaluate a config expression.
259
+ */
260
+ static parse(expr, env = {}) {
261
+ const parser = new _Parser(expr, env);
262
+ return parser.#parse();
263
+ }
264
+ constructor(input, env) {
265
+ const tokenizer = new Tokenizer(input);
266
+ this.#tokens = [];
267
+ let token;
268
+ do {
269
+ token = tokenizer.next();
270
+ this.#tokens.push(token);
271
+ } while (token.type !== "EOF" /* EOF */);
272
+ this.#pos = 0;
273
+ this.#env = env;
274
+ }
275
+ #peek() {
276
+ return this.#tokens[this.#pos];
277
+ }
278
+ #advance() {
279
+ return this.#tokens[this.#pos++];
280
+ }
281
+ #expect(type) {
282
+ const token = this.#peek();
283
+ if (token.type !== type) {
284
+ throw new Error(
285
+ `Expected ${type} but got ${token.type} at position ${token.start}`
286
+ );
287
+ }
288
+ return this.#advance();
289
+ }
290
+ #parse() {
291
+ const result = this.#parseExpr();
292
+ this.#expect("EOF" /* EOF */);
293
+ return result;
294
+ }
295
+ // Expr := Ternary
296
+ #parseExpr() {
297
+ return this.#parseTernary();
298
+ }
299
+ // Ternary := LogicalOr ('?' Expr ':' Expr)?
300
+ #parseTernary() {
301
+ let left = this.#parseLogicalOr();
302
+ if (this.#peek().type === "?" /* QUESTION */) {
303
+ this.#advance();
304
+ const trueBranch = this.#parseExpr();
305
+ this.#expect(":" /* COLON */);
306
+ const falseBranch = this.#parseExpr();
307
+ return left ? trueBranch : falseBranch;
308
+ }
309
+ return left;
310
+ }
311
+ // LogicalOr := LogicalAnd (('||' | '??') LogicalAnd)*
312
+ // ?? and || have same precedence, evaluated left-to-right
313
+ #parseLogicalOr() {
314
+ let left = this.#parseLogicalAnd();
315
+ while (this.#peek().type === "||" /* OR */ || this.#peek().type === "??" /* NULLISH */) {
316
+ const isNullish = this.#peek().type === "??" /* NULLISH */;
317
+ this.#advance();
318
+ const right = this.#parseLogicalAnd();
319
+ left = isNullish ? left ?? right : left || right;
320
+ }
321
+ return left;
322
+ }
323
+ // LogicalAnd := Equality ('&&' Equality)*
324
+ #parseLogicalAnd() {
325
+ let left = this.#parseEquality();
326
+ while (this.#peek().type === "&&" /* AND */) {
327
+ this.#advance();
328
+ const right = this.#parseEquality();
329
+ left = left && right;
330
+ }
331
+ return left;
332
+ }
333
+ // Equality := Unary (('===' | '!==' | '==' | '!=') Unary)*
334
+ #parseEquality() {
335
+ let left = this.#parseUnary();
336
+ while (true) {
337
+ const token = this.#peek();
338
+ if (token.type === "===" /* EQ_STRICT */) {
339
+ this.#advance();
340
+ const right = this.#parseUnary();
341
+ left = left === right;
342
+ } else if (token.type === "!==" /* NE_STRICT */) {
343
+ this.#advance();
344
+ const right = this.#parseUnary();
345
+ left = left !== right;
346
+ } else if (token.type === "==" /* EQ */) {
347
+ this.#advance();
348
+ const right = this.#parseUnary();
349
+ left = left == right;
350
+ } else if (token.type === "!=" /* NE */) {
351
+ this.#advance();
352
+ const right = this.#parseUnary();
353
+ left = left != right;
354
+ } else {
355
+ break;
356
+ }
357
+ }
358
+ return left;
359
+ }
360
+ // Unary := '!' Unary | Primary
361
+ #parseUnary() {
362
+ if (this.#peek().type === "!" /* NOT */) {
363
+ this.#advance();
364
+ return !this.#parseUnary();
365
+ }
366
+ return this.#parsePrimary();
367
+ }
368
+ // Primary := PathExpr | Literal | '(' Expr ')'
369
+ // PathExpr := (EnvVar | Dunder | Identifier) PathSuffix?
370
+ // PathSuffix := ('/' Segment)+
371
+ #parsePrimary() {
372
+ const token = this.#peek();
373
+ if (token.type === "(" /* LPAREN */) {
374
+ this.#advance();
375
+ const value = this.#parseExpr();
376
+ this.#expect(")" /* RPAREN */);
377
+ return this.#parsePathSuffix(value);
378
+ }
379
+ if (token.type === "STRING" /* STRING */) {
380
+ this.#advance();
381
+ return token.value;
382
+ }
383
+ if (token.type === "NUMBER" /* NUMBER */) {
384
+ this.#advance();
385
+ return token.value;
386
+ }
387
+ if (token.type === "TRUE" /* TRUE */) {
388
+ this.#advance();
389
+ return true;
390
+ }
391
+ if (token.type === "FALSE" /* FALSE */) {
392
+ this.#advance();
393
+ return false;
394
+ }
395
+ if (token.type === "NULL" /* NULL */) {
396
+ this.#advance();
397
+ return null;
398
+ }
399
+ if (token.type === "UNDEFINED" /* UNDEFINED */) {
400
+ this.#advance();
401
+ return void 0;
402
+ }
403
+ if (token.type === "ENV_VAR" /* ENV_VAR */) {
404
+ this.#advance();
405
+ const name = token.value;
406
+ const value = this.#env[name];
407
+ let result = value;
408
+ if (typeof value === "string" && /^\d+$/.test(value)) {
409
+ result = parseInt(value, 10);
410
+ }
411
+ return this.#parsePathSuffix(result);
412
+ }
413
+ if (token.type === "OUTDIR" /* OUTDIR */ || token.type === "TMPDIR" /* TMPDIR */ || token.type === "GIT" /* GIT */) {
414
+ throw new Error(
415
+ `${token.value} placeholder is only valid in build config, not runtime`
416
+ );
417
+ }
418
+ if (token.type === "IDENTIFIER" /* IDENTIFIER */) {
419
+ this.#advance();
420
+ return this.#parsePathSuffix(token.value);
421
+ }
422
+ throw new Error(
423
+ `Unexpected token ${token.type} at position ${token.start}`
424
+ );
425
+ }
426
+ // Parse optional path suffix: ('/' Segment)+
427
+ // Returns the value joined with any path segments
428
+ #parsePathSuffix(base) {
429
+ const segments = [];
430
+ while (this.#peek().type === "SLASH" /* SLASH */) {
431
+ this.#advance();
432
+ const segToken = this.#peek();
433
+ if (segToken.type === "IDENTIFIER" /* IDENTIFIER */ || segToken.type === "STRING" /* STRING */) {
434
+ this.#advance();
435
+ segments.push(segToken.value);
436
+ } else if (segToken.type === "NUMBER" /* NUMBER */) {
437
+ this.#advance();
438
+ segments.push(String(segToken.value));
439
+ } else {
440
+ throw new Error(
441
+ `Expected path segment after / at position ${segToken.start}`
442
+ );
443
+ }
444
+ }
445
+ if (segments.length === 0) {
446
+ return base;
447
+ }
448
+ if (base === void 0) {
449
+ return void 0;
450
+ }
451
+ return join2(String(base), ...segments);
452
+ }
453
+ };
454
+ function processConfigValue(value, env) {
455
+ if (typeof value === "string") {
456
+ if (EXPRESSION_PATTERN.test(value)) {
457
+ const result = Parser.parse(value, env);
458
+ if (result === void 0) {
459
+ throw new Error(
460
+ `Config expression "${value}" evaluated to undefined.
461
+ Add a fallback: ${value} || defaultValue`
462
+ );
463
+ }
464
+ return result;
465
+ }
466
+ return value;
467
+ }
468
+ if (Array.isArray(value)) {
469
+ return value.map((item) => processConfigValue(item, env));
470
+ }
471
+ if (value !== null && typeof value === "object") {
472
+ const processed = {};
473
+ for (const [key, val] of Object.entries(value)) {
474
+ processed[key] = processConfigValue(val, env);
475
+ }
476
+ return processed;
477
+ }
478
+ return value;
479
+ }
480
+ var CodeGenerator = class {
481
+ #tokens;
482
+ #pos;
483
+ constructor(input) {
484
+ const tokenizer = new Tokenizer(input);
485
+ this.#tokens = [];
486
+ let token;
487
+ do {
488
+ token = tokenizer.next();
489
+ this.#tokens.push(token);
490
+ } while (token.type !== "EOF" /* EOF */);
491
+ this.#pos = 0;
492
+ }
493
+ #peek() {
494
+ return this.#tokens[this.#pos];
495
+ }
496
+ #advance() {
497
+ return this.#tokens[this.#pos++];
498
+ }
499
+ #expect(type) {
500
+ const token = this.#peek();
501
+ if (token.type !== type) {
502
+ throw new Error(
503
+ `Expected ${type} but got ${token.type} at position ${token.start}`
504
+ );
505
+ }
506
+ return this.#advance();
507
+ }
508
+ generate() {
509
+ const result = this.#generateExpr();
510
+ this.#expect("EOF" /* EOF */);
511
+ return result;
512
+ }
513
+ #generateExpr() {
514
+ return this.#generateTernary();
515
+ }
516
+ #generateTernary() {
517
+ let left = this.#generateLogicalOr();
518
+ if (this.#peek().type === "?" /* QUESTION */) {
519
+ this.#advance();
520
+ const trueBranch = this.#generateExpr();
521
+ this.#expect(":" /* COLON */);
522
+ const falseBranch = this.#generateExpr();
523
+ return `(${left} ? ${trueBranch} : ${falseBranch})`;
524
+ }
525
+ return left;
526
+ }
527
+ #generateLogicalOr() {
528
+ let left = this.#generateLogicalAnd();
529
+ while (this.#peek().type === "||" /* OR */ || this.#peek().type === "??" /* NULLISH */) {
530
+ const op = this.#peek().type === "??" /* NULLISH */ ? "??" : "||";
531
+ this.#advance();
532
+ const right = this.#generateLogicalAnd();
533
+ left = `(${left} ${op} ${right})`;
534
+ }
535
+ return left;
536
+ }
537
+ #generateLogicalAnd() {
538
+ let left = this.#generateEquality();
539
+ while (this.#peek().type === "&&" /* AND */) {
540
+ this.#advance();
541
+ const right = this.#generateEquality();
542
+ left = `(${left} && ${right})`;
543
+ }
544
+ return left;
545
+ }
546
+ #generateEquality() {
547
+ let left = this.#generateUnary();
548
+ while (true) {
549
+ const token = this.#peek();
550
+ if (token.type === "===" /* EQ_STRICT */) {
551
+ this.#advance();
552
+ const right = this.#generateUnary();
553
+ left = `(${left} === ${right})`;
554
+ } else if (token.type === "!==" /* NE_STRICT */) {
555
+ this.#advance();
556
+ const right = this.#generateUnary();
557
+ left = `(${left} !== ${right})`;
558
+ } else if (token.type === "==" /* EQ */) {
559
+ this.#advance();
560
+ const right = this.#generateUnary();
561
+ left = `(${left} == ${right})`;
562
+ } else if (token.type === "!=" /* NE */) {
563
+ this.#advance();
564
+ const right = this.#generateUnary();
565
+ left = `(${left} != ${right})`;
566
+ } else {
567
+ break;
568
+ }
569
+ }
570
+ return left;
571
+ }
572
+ #generateUnary() {
573
+ if (this.#peek().type === "!" /* NOT */) {
574
+ this.#advance();
575
+ return `!${this.#generateUnary()}`;
576
+ }
577
+ return this.#generatePrimary();
578
+ }
579
+ #generatePrimary() {
580
+ const token = this.#peek();
581
+ if (token.type === "(" /* LPAREN */) {
582
+ this.#advance();
583
+ const value = this.#generateExpr();
584
+ this.#expect(")" /* RPAREN */);
585
+ return this.#generatePathSuffix(`(${value})`);
586
+ }
587
+ if (token.type === "STRING" /* STRING */) {
588
+ this.#advance();
589
+ return JSON.stringify(token.value);
590
+ }
591
+ if (token.type === "NUMBER" /* NUMBER */) {
592
+ this.#advance();
593
+ return String(token.value);
594
+ }
595
+ if (token.type === "TRUE" /* TRUE */) {
596
+ this.#advance();
597
+ return "true";
598
+ }
599
+ if (token.type === "FALSE" /* FALSE */) {
600
+ this.#advance();
601
+ return "false";
602
+ }
603
+ if (token.type === "NULL" /* NULL */) {
604
+ this.#advance();
605
+ return "null";
606
+ }
607
+ if (token.type === "UNDEFINED" /* UNDEFINED */) {
608
+ this.#advance();
609
+ return "undefined";
610
+ }
611
+ if (token.type === "ENV_VAR" /* ENV_VAR */) {
612
+ this.#advance();
613
+ const name = token.value;
614
+ const code = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) ? `process.env.${name}` : `process.env[${JSON.stringify(name)}]`;
615
+ return this.#generatePathSuffix(code, { requiredEnvVar: name });
616
+ }
617
+ if (token.type === "OUTDIR" /* OUTDIR */) {
618
+ this.#advance();
619
+ return this.#generatePathSuffix("__SHOVEL_OUTDIR__");
620
+ }
621
+ if (token.type === "TMPDIR" /* TMPDIR */) {
622
+ this.#advance();
623
+ return this.#generatePathSuffix("tmpdir()");
624
+ }
625
+ if (token.type === "GIT" /* GIT */) {
626
+ this.#advance();
627
+ return this.#generatePathSuffix("__SHOVEL_GIT__");
628
+ }
629
+ if (token.type === "IDENTIFIER" /* IDENTIFIER */) {
630
+ this.#advance();
631
+ return this.#generatePathSuffix(JSON.stringify(token.value));
632
+ }
633
+ throw new Error(
634
+ `Unexpected token ${token.type} at position ${token.start}`
635
+ );
636
+ }
637
+ // Generate path suffix code: base/segment/... → [base, "segment", ...].join("/")
638
+ // If requiredEnvVar is set, generate code that throws if the env var is missing
639
+ #generatePathSuffix(baseCode, options = {}) {
640
+ const segments = [];
641
+ while (this.#peek().type === "SLASH" /* SLASH */) {
642
+ this.#advance();
643
+ const segToken = this.#peek();
644
+ if (segToken.type === "IDENTIFIER" /* IDENTIFIER */ || segToken.type === "STRING" /* STRING */) {
645
+ this.#advance();
646
+ segments.push(JSON.stringify(segToken.value));
647
+ } else if (segToken.type === "NUMBER" /* NUMBER */) {
648
+ this.#advance();
649
+ segments.push(JSON.stringify(String(segToken.value)));
650
+ } else {
651
+ throw new Error(
652
+ `Expected path segment after / at position ${segToken.start}`
653
+ );
654
+ }
655
+ }
656
+ if (segments.length === 0) {
657
+ return baseCode;
658
+ }
659
+ if (options.requiredEnvVar) {
660
+ const envName = options.requiredEnvVar;
661
+ return `(() => { const v = ${baseCode}; if (v == null) throw new Error("Required env var ${envName} is not set"); return [v, ${segments.join(", ")}].join("/"); })()`;
662
+ }
663
+ return `[${baseCode}, ${segments.join(", ")}].filter(Boolean).join("/")`;
664
+ }
665
+ };
666
+ function exprToCode(expr) {
667
+ if (EXPRESSION_PATTERN.test(expr)) {
668
+ try {
669
+ const generator = new CodeGenerator(expr);
670
+ const code = generator.generate();
671
+ return { code };
672
+ } catch (error) {
673
+ throw new Error(
674
+ `Invalid config expression: ${expr}
675
+ Error: ${error instanceof Error ? error.message : String(error)}`
676
+ );
677
+ }
678
+ }
679
+ return { code: JSON.stringify(expr) };
680
+ }
681
+ function sanitizeVarName(pattern) {
682
+ return pattern.replace(/\*/g, "default").replace(/[^a-zA-Z0-9_]/g, "_").replace(/^(\d)/, "_$1");
683
+ }
684
+ var PLACEHOLDER_PREFIX = "__SHOVEL_";
685
+ function needsQuoting(key) {
686
+ return !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key);
687
+ }
688
+ function containsRuntimeExpressions(code) {
689
+ return code.includes("process.env") || code.includes("tmpdir()") || code.includes(".filter(Boolean).join");
690
+ }
691
+ function toJSLiteral(value, placeholders, indent = "") {
692
+ if (value === null)
693
+ return { code: "null", isDynamic: false };
694
+ if (value === void 0)
695
+ return { code: "undefined", isDynamic: false };
696
+ if (typeof value === "string") {
697
+ if (value.startsWith(PLACEHOLDER_PREFIX) && placeholders.has(value)) {
698
+ return { code: placeholders.get(value), isDynamic: false };
699
+ }
700
+ const result = exprToCode(value);
701
+ return {
702
+ code: result.code,
703
+ isDynamic: containsRuntimeExpressions(result.code)
704
+ };
705
+ }
706
+ if (typeof value === "number" || typeof value === "boolean") {
707
+ return { code: String(value), isDynamic: false };
708
+ }
709
+ if (Array.isArray(value)) {
710
+ if (value.length === 0)
711
+ return { code: "[]", isDynamic: false };
712
+ let anyDynamic = false;
713
+ const items = value.map((v) => {
714
+ const result = toJSLiteral(v, placeholders, indent + " ");
715
+ if (result.isDynamic)
716
+ anyDynamic = true;
717
+ return result.code;
718
+ });
719
+ return {
720
+ code: `[
721
+ ${indent} ${items.join(`,
722
+ ${indent} `)}
723
+ ${indent}]`,
724
+ isDynamic: anyDynamic
725
+ };
726
+ }
727
+ if (typeof value === "object") {
728
+ const entries = Object.entries(value);
729
+ if (entries.length === 0)
730
+ return { code: "{}", isDynamic: false };
731
+ let anyDynamic = false;
732
+ const props = entries.map(([k, v]) => {
733
+ const keyStr = needsQuoting(k) ? JSON.stringify(k) : k;
734
+ const result = toJSLiteral(v, placeholders, indent + " ");
735
+ if (result.isDynamic)
736
+ anyDynamic = true;
737
+ if (result.isDynamic) {
738
+ return `get ${keyStr}() { return ${result.code}; }`;
739
+ }
740
+ return `${keyStr}: ${result.code}`;
741
+ });
742
+ return {
743
+ code: `{
744
+ ${indent} ${props.join(`,
745
+ ${indent} `)}
746
+ ${indent}}`,
747
+ isDynamic: anyDynamic
748
+ };
749
+ }
750
+ return { code: JSON.stringify(value), isDynamic: false };
751
+ }
752
+ function generateConfigModule(rawConfig, options) {
753
+ const { platformDefaults = {}, lifecycle } = options;
754
+ const imports = [];
755
+ const placeholders = /* @__PURE__ */ new Map();
756
+ let placeholderCounter = 0;
757
+ function createPlaceholder(jsCode) {
758
+ const placeholder = `${PLACEHOLDER_PREFIX}${placeholderCounter++}__`;
759
+ placeholders.set(placeholder, jsCode);
760
+ return placeholder;
761
+ }
762
+ const addedImports = /* @__PURE__ */ new Map();
763
+ function processModule(modulePath, exportName, type, name) {
764
+ if (!modulePath)
765
+ return null;
766
+ const varName = `${type}_${sanitizeVarName(name)}`;
767
+ const actualExport = exportName || "default";
768
+ const importKey = `${modulePath}:${actualExport}`;
769
+ const existingVarName = addedImports.get(importKey);
770
+ if (existingVarName) {
771
+ return createPlaceholder(existingVarName);
772
+ }
773
+ addedImports.set(importKey, varName);
774
+ if (actualExport === "default") {
775
+ imports.push(`import ${varName} from ${JSON.stringify(modulePath)};`);
776
+ } else {
777
+ imports.push(
778
+ `import { ${actualExport} as ${varName} } from ${JSON.stringify(modulePath)};`
779
+ );
780
+ }
781
+ return createPlaceholder(varName);
782
+ }
783
+ function reifyModule(config2, type, name) {
784
+ const { module: modulePath, export: exportName, ...rest } = config2;
785
+ const implPlaceholder = processModule(modulePath, exportName, type, name);
786
+ if (implPlaceholder) {
787
+ return { ...rest, impl: implPlaceholder };
788
+ }
789
+ return rest;
790
+ }
791
+ function processSink(sink, sinkName) {
792
+ return reifyModule(sink, "sink", sinkName);
793
+ }
794
+ function buildConfig() {
795
+ const config2 = {};
796
+ if (rawConfig.platform !== void 0) {
797
+ config2.platform = rawConfig.platform;
798
+ }
799
+ if (rawConfig.port !== void 0) {
800
+ config2.port = rawConfig.port;
801
+ } else {
802
+ config2.port = createPlaceholder(
803
+ "process.env.PORT ? parseInt(process.env.PORT, 10) : 3000"
804
+ );
805
+ }
806
+ if (rawConfig.host !== void 0) {
807
+ config2.host = rawConfig.host;
808
+ } else {
809
+ config2.host = createPlaceholder('process.env.HOST || "localhost"');
810
+ }
811
+ if (rawConfig.workers !== void 0) {
812
+ config2.workers = rawConfig.workers;
813
+ } else {
814
+ config2.workers = createPlaceholder(
815
+ "process.env.WORKERS ? parseInt(process.env.WORKERS, 10) : 1"
816
+ );
817
+ }
818
+ const logging = {};
819
+ const sinks = {};
820
+ if (rawConfig.logging?.sinks) {
821
+ for (const [name, sinkConfig] of Object.entries(
822
+ rawConfig.logging.sinks
823
+ )) {
824
+ sinks[name] = processSink(sinkConfig, name);
825
+ }
826
+ }
827
+ logging.sinks = sinks;
828
+ const loggers = [];
829
+ if (rawConfig.logging?.loggers) {
830
+ for (const loggerConfig of rawConfig.logging.loggers) {
831
+ const logger = {
832
+ category: loggerConfig.category
833
+ };
834
+ if (loggerConfig.level) {
835
+ logger.level = loggerConfig.level;
836
+ }
837
+ if (loggerConfig.sinks) {
838
+ logger.sinks = loggerConfig.sinks;
839
+ }
840
+ if (loggerConfig.parentSinks) {
841
+ logger.parentSinks = loggerConfig.parentSinks;
842
+ }
843
+ loggers.push(logger);
844
+ }
845
+ }
846
+ logging.loggers = loggers;
847
+ config2.logging = logging;
848
+ const platformCaches = platformDefaults.caches || {};
849
+ const userCaches = rawConfig.caches || {};
850
+ const allCacheNames = /* @__PURE__ */ new Set([
851
+ ...Object.keys(platformCaches),
852
+ ...Object.keys(userCaches)
853
+ ]);
854
+ if (allCacheNames.size > 0) {
855
+ const caches = {};
856
+ for (const name of allCacheNames) {
857
+ const platformConfig = platformCaches[name] || {};
858
+ const userConfig = userCaches[name] || {};
859
+ const mergedConfig = { ...platformConfig, ...userConfig };
860
+ caches[name] = reifyModule(mergedConfig, "cache", name);
861
+ }
862
+ config2.caches = caches;
863
+ }
864
+ const platformDirectories = platformDefaults.directories || {};
865
+ const userDirectories = rawConfig.directories || {};
866
+ const allDirectoryNames = /* @__PURE__ */ new Set([
867
+ ...Object.keys(platformDirectories),
868
+ ...Object.keys(userDirectories)
869
+ ]);
870
+ if (allDirectoryNames.size > 0) {
871
+ const directories = {};
872
+ for (const name of allDirectoryNames) {
873
+ const platformConfig = platformDirectories[name] || {};
874
+ const userConfig = userDirectories[name] || {};
875
+ const mergedConfig = { ...platformConfig, ...userConfig };
876
+ directories[name] = reifyModule(mergedConfig, "directory", name);
877
+ }
878
+ config2.directories = directories;
879
+ }
880
+ if (rawConfig.databases && Object.keys(rawConfig.databases).length > 0) {
881
+ const databases = {};
882
+ for (const [name, dbConfig] of Object.entries(rawConfig.databases)) {
883
+ databases[name] = reifyModule(dbConfig, "database", name);
884
+ }
885
+ config2.databases = databases;
886
+ }
887
+ if (lifecycle) {
888
+ config2.lifecycle = {
889
+ stage: lifecycle.stage
890
+ };
891
+ }
892
+ return config2;
893
+ }
894
+ const config = buildConfig();
895
+ const { code: configCode } = toJSLiteral(config, placeholders, "");
896
+ const needsTmpdirImport = configCode.includes("tmpdir()");
897
+ if (needsTmpdirImport) {
898
+ imports.unshift('import {tmpdir} from "os";');
899
+ }
900
+ const providerImports = imports.length > 0 ? `${imports.join("\n")}
901
+ ` : "";
902
+ return `${providerImports}
903
+ export const config = ${configCode};
904
+ `;
905
+ }
906
+ function loadRawConfig(cwd) {
907
+ let rawConfig = {};
908
+ let configSource = "defaults";
909
+ try {
910
+ const shovelPath = `${cwd}/shovel.json`;
911
+ const content = readFileSync2(shovelPath, "utf-8");
912
+ rawConfig = JSON.parse(content);
913
+ configSource = "shovel.json";
914
+ } catch (error) {
915
+ if (error?.code !== "ENOENT") {
916
+ throw error;
917
+ }
918
+ try {
919
+ const pkgPath = `${cwd}/package.json`;
920
+ const content = readFileSync2(pkgPath, "utf-8");
921
+ const pkgJSON = JSON.parse(content);
922
+ if (pkgJSON.shovel) {
923
+ rawConfig = pkgJSON.shovel;
924
+ configSource = "package.json";
925
+ }
926
+ } catch (error2) {
927
+ if (error2?.code !== "ENOENT") {
928
+ throw error2;
929
+ }
930
+ }
931
+ }
932
+ try {
933
+ return ShovelConfigSchema.parse(rawConfig);
934
+ } catch (error) {
935
+ if (error instanceof z.ZodError) {
936
+ const issues = error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
937
+ throw new Error(`Invalid config in ${configSource}:
938
+ ${issues}`);
939
+ }
940
+ throw error;
941
+ }
942
+ }
943
+ var configExpr = z.union([z.string(), z.number()]);
944
+ var CacheConfigSchema = z.object({
945
+ module: z.string().optional(),
946
+ export: z.string().optional(),
947
+ url: configExpr.optional(),
948
+ maxEntries: configExpr.optional(),
949
+ TTL: configExpr.optional()
950
+ }).strict();
951
+ var DirectoryConfigSchema = z.object({
952
+ module: z.string().optional(),
953
+ export: z.string().optional(),
954
+ path: configExpr.optional(),
955
+ binding: configExpr.optional(),
956
+ bucket: configExpr.optional(),
957
+ region: configExpr.optional(),
958
+ endpoint: configExpr.optional()
959
+ }).strict();
960
+ var DatabaseConfigSchema = z.object({
961
+ /** Module path to import (e.g., "@b9g/zen/bun") */
962
+ module: z.string(),
963
+ /** Named export to use (defaults to "default") */
964
+ export: z.string().optional(),
965
+ /** Database connection URL (can be null for intentional null fallbacks) */
966
+ url: z.string().nullable()
967
+ }).passthrough();
968
+ var LogLevelSchema = z.enum(["debug", "info", "warning", "error"]);
969
+ var SinkConfigSchema = z.object({
970
+ module: z.string(),
971
+ export: z.string().optional()
972
+ }).passthrough();
973
+ var LoggerConfigSchema = z.object({
974
+ category: z.union([z.string(), z.array(z.string())]),
975
+ level: LogLevelSchema.optional(),
976
+ sinks: z.array(z.string()).optional(),
977
+ parentSinks: z.literal("override").optional()
978
+ }).strict();
979
+ var LoggingConfigSchema = z.object({
980
+ sinks: z.record(z.string(), SinkConfigSchema).optional(),
981
+ loggers: z.array(LoggerConfigSchema).optional()
982
+ }).strict();
983
+ var BuildPluginConfigSchema = z.object({
984
+ /** Module path to import (e.g., "esbuild-plugin-tailwindcss") */
985
+ module: z.string(),
986
+ /** Named export to use (defaults to "default") */
987
+ export: z.string().optional()
988
+ }).passthrough();
989
+ var BuildConfigSchema = z.object({
990
+ /** ES target (e.g., "es2020", "es2022", ["es2020", "chrome100"]) */
991
+ target: z.union([z.string(), z.array(z.string())]).optional(),
992
+ /** Enable minification for production builds */
993
+ minify: z.boolean().optional(),
994
+ /** Sourcemap generation: false, true, "inline", "external", "linked" */
995
+ sourcemap: z.union([z.boolean(), z.enum(["inline", "external", "linked"])]).optional(),
996
+ /** Enable tree-shaking (default: true) */
997
+ treeShaking: z.boolean().optional(),
998
+ /** Global constant definitions (added to Shovel's defaults) */
999
+ define: z.record(z.string(), z.string()).optional(),
1000
+ /** Path aliases (e.g., {"@": "./src"}) */
1001
+ alias: z.record(z.string(), z.string()).optional(),
1002
+ /** Additional external packages (added to platform defaults) */
1003
+ external: z.array(z.string()).optional(),
1004
+ /** ESBuild plugins using module/export pattern */
1005
+ plugins: z.array(BuildPluginConfigSchema).optional()
1006
+ }).strict();
1007
+ var ShovelConfigSchema = z.object({
1008
+ platform: z.string().optional(),
1009
+ port: z.union([z.number(), z.string()]).optional(),
1010
+ host: z.string().optional(),
1011
+ workers: z.union([z.number(), z.string()]).optional(),
1012
+ logging: LoggingConfigSchema.optional(),
1013
+ build: BuildConfigSchema.optional(),
1014
+ caches: z.record(z.string(), CacheConfigSchema).optional(),
1015
+ directories: z.record(z.string(), DirectoryConfigSchema).optional(),
1016
+ databases: z.record(z.string(), DatabaseConfigSchema).optional()
1017
+ }).strict();
1018
+ function loadConfig(cwd) {
1019
+ const env = getEnv();
1020
+ let rawConfig = {};
1021
+ try {
1022
+ const shovelPath = `${cwd}/shovel.json`;
1023
+ const content = readFileSync2(shovelPath, "utf-8");
1024
+ rawConfig = JSON.parse(content);
1025
+ } catch (error) {
1026
+ if (error?.code !== "ENOENT") {
1027
+ throw error;
1028
+ }
1029
+ try {
1030
+ const pkgPath = `${cwd}/package.json`;
1031
+ const content = readFileSync2(pkgPath, "utf-8");
1032
+ const pkgJSON = JSON.parse(content);
1033
+ rawConfig = pkgJSON.shovel || {};
1034
+ } catch (error2) {
1035
+ if (error2?.code !== "ENOENT") {
1036
+ throw error2;
1037
+ }
1038
+ }
1039
+ }
1040
+ const processed = processConfigValue(rawConfig, env);
1041
+ const config = {
1042
+ platform: processed.platform ?? env.PLATFORM ?? void 0,
1043
+ port: processed.port !== void 0 ? typeof processed.port === "number" ? processed.port : parseInt(String(processed.port), 10) : env.PORT ? parseInt(env.PORT, 10) : 3e3,
1044
+ host: processed.host ?? env.HOST ?? "localhost",
1045
+ workers: processed.workers !== void 0 ? typeof processed.workers === "number" ? processed.workers : parseInt(String(processed.workers), 10) : env.WORKERS ? parseInt(env.WORKERS, 10) : 1,
1046
+ logging: {
1047
+ sinks: processed.logging?.sinks || {},
1048
+ loggers: processed.logging?.loggers || []
1049
+ },
1050
+ build: {
1051
+ target: processed.build?.target ?? "es2022",
1052
+ minify: processed.build?.minify ?? false,
1053
+ sourcemap: processed.build?.sourcemap ?? false,
1054
+ treeShaking: processed.build?.treeShaking ?? true,
1055
+ define: processed.build?.define ?? {},
1056
+ alias: processed.build?.alias ?? {},
1057
+ external: processed.build?.external ?? [],
1058
+ plugins: processed.build?.plugins ?? []
1059
+ },
1060
+ caches: processed.caches || {},
1061
+ directories: processed.directories || {},
1062
+ databases: processed.databases || {}
1063
+ };
1064
+ return config;
1065
+ }
1066
+ function generateStorageTypes(config, options = {}) {
1067
+ const { platformDefaults = {} } = options;
1068
+ const mergedDirectories = {
1069
+ ...platformDefaults.directories || {},
1070
+ ...config.directories || {}
1071
+ };
1072
+ const mergedCaches = {
1073
+ ...platformDefaults.caches || {},
1074
+ ...config.caches || {}
1075
+ };
1076
+ const databaseNames = config.databases ? Object.keys(config.databases) : [];
1077
+ const directoryNames = Object.keys(mergedDirectories);
1078
+ const cacheNames = Object.keys(mergedCaches);
1079
+ if (databaseNames.length === 0 && directoryNames.length === 0 && cacheNames.length === 0) {
1080
+ return "";
1081
+ }
1082
+ const imports = [];
1083
+ const sections = [];
1084
+ if (databaseNames.length > 0) {
1085
+ imports.push(`import type {Database} from "@b9g/zen";`);
1086
+ imports.push(`import type {DatabaseUpgradeEvent} from "@b9g/platform";`);
1087
+ const dbUnion = databaseNames.map((n) => `"${n}"`).join(" | ");
1088
+ sections.push(` /**
1089
+ * Valid database names from shovel.json.
1090
+ * Using an invalid name will cause a TypeScript error.
1091
+ */
1092
+ type ValidDatabaseName = ${dbUnion};
1093
+
1094
+ interface DatabaseStorage {
1095
+ /** Open a database at a specific version, running migrations if needed */
1096
+ open(
1097
+ name: ValidDatabaseName,
1098
+ version: number,
1099
+ onUpgrade?: (event: DatabaseUpgradeEvent) => void,
1100
+ ): Promise<Database>;
1101
+ /** Get an already-opened database (throws if not opened) */
1102
+ get(name: ValidDatabaseName): Database;
1103
+ /** Close a specific database */
1104
+ close(name: ValidDatabaseName): Promise<void>;
1105
+ /** Close all databases */
1106
+ closeAll(): Promise<void>;
1107
+ }`);
1108
+ }
1109
+ if (directoryNames.length > 0) {
1110
+ const dirUnion = directoryNames.map((n) => `"${n}"`).join(" | ");
1111
+ sections.push(` /**
1112
+ * Valid directory names from shovel.json and platform defaults.
1113
+ * Using an invalid name will cause a TypeScript error.
1114
+ */
1115
+ type ValidDirectoryName = ${dirUnion};
1116
+
1117
+ interface DirectoryStorage {
1118
+ open(name: ValidDirectoryName): Promise<FileSystemDirectoryHandle>;
1119
+ has(name: ValidDirectoryName): Promise<boolean>;
1120
+ }`);
1121
+ }
1122
+ if (cacheNames.length > 0) {
1123
+ const cacheUnion = cacheNames.map((n) => `"${n}"`).join(" | ");
1124
+ sections.push(` /**
1125
+ * Valid cache names from shovel.json and platform defaults.
1126
+ * Using an invalid name will cause a TypeScript error.
1127
+ */
1128
+ type ValidCacheName = ${cacheUnion};
1129
+
1130
+ interface CacheStorage {
1131
+ open(name: ValidCacheName): Promise<Cache>;
1132
+ has(name: ValidCacheName): Promise<boolean>;
1133
+ delete(name: ValidCacheName): Promise<boolean>;
1134
+ keys(): Promise<string[]>;
1135
+ }`);
1136
+ }
1137
+ return `// Generated by Shovel - DO NOT EDIT
1138
+ // This file provides typed overloads for self.databases, self.directories, and self.caches
1139
+ ${imports.join("\n")}
1140
+
1141
+ declare global {
1142
+ ${sections.join("\n\n")}
1143
+ }
1144
+
1145
+ export {};
1146
+ `;
1147
+ }
1148
+
1149
+ export {
1150
+ findProjectRoot,
1151
+ findWorkspaceRoot,
1152
+ getNodeModulesPath,
1153
+ DEFAULTS,
1154
+ generateConfigModule,
1155
+ loadRawConfig,
1156
+ loadConfig,
1157
+ generateStorageTypes
1158
+ };