zod-compiler 1.0.1 → 1.0.3

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.
Files changed (48) hide show
  1. package/README.md +59 -22
  2. package/dist/core/codegen/context.d.ts +27 -8
  3. package/dist/core/codegen/context.d.ts.map +1 -1
  4. package/dist/core/codegen/context.js +29 -14
  5. package/dist/core/codegen/context.js.map +1 -1
  6. package/dist/core/codegen/index.d.ts.map +1 -1
  7. package/dist/core/codegen/index.js +19 -1
  8. package/dist/core/codegen/index.js.map +1 -1
  9. package/dist/core/codegen/schemas/array.js +2 -2
  10. package/dist/core/codegen/schemas/array.js.map +1 -1
  11. package/dist/core/codegen/schemas/discriminated-union.js +2 -2
  12. package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
  13. package/dist/core/codegen/schemas/map.js +3 -3
  14. package/dist/core/codegen/schemas/map.js.map +1 -1
  15. package/dist/core/codegen/schemas/number.d.ts.map +1 -1
  16. package/dist/core/codegen/schemas/number.js +15 -8
  17. package/dist/core/codegen/schemas/number.js.map +1 -1
  18. package/dist/core/codegen/schemas/record.d.ts.map +1 -1
  19. package/dist/core/codegen/schemas/record.js +13 -5
  20. package/dist/core/codegen/schemas/record.js.map +1 -1
  21. package/dist/core/codegen/schemas/recursive-ref.d.ts +1 -1
  22. package/dist/core/codegen/schemas/recursive-ref.d.ts.map +1 -1
  23. package/dist/core/codegen/schemas/recursive-ref.js +9 -3
  24. package/dist/core/codegen/schemas/recursive-ref.js.map +1 -1
  25. package/dist/core/codegen/schemas/set.js +2 -2
  26. package/dist/core/codegen/schemas/set.js.map +1 -1
  27. package/dist/core/codegen/schemas/tuple.js +2 -2
  28. package/dist/core/codegen/schemas/tuple.js.map +1 -1
  29. package/dist/core/diagnostic.js +1 -1
  30. package/dist/core/diagnostic.js.map +1 -1
  31. package/dist/core/iife.d.ts +9 -1
  32. package/dist/core/iife.d.ts.map +1 -1
  33. package/dist/core/iife.js +9 -1
  34. package/dist/core/iife.js.map +1 -1
  35. package/dist/unplugin/hoist.d.ts +28 -0
  36. package/dist/unplugin/hoist.d.ts.map +1 -0
  37. package/dist/unplugin/hoist.js +815 -0
  38. package/dist/unplugin/hoist.js.map +1 -0
  39. package/dist/unplugin/index.d.ts.map +1 -1
  40. package/dist/unplugin/index.js +12 -5
  41. package/dist/unplugin/index.js.map +1 -1
  42. package/dist/unplugin/transform.d.ts.map +1 -1
  43. package/dist/unplugin/transform.js +29 -12
  44. package/dist/unplugin/transform.js.map +1 -1
  45. package/dist/unplugin/types.d.ts +42 -7
  46. package/dist/unplugin/types.d.ts.map +1 -1
  47. package/dist/unplugin/types.js.map +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,815 @@
1
+ import { parseExpressionAt } from "acorn";
2
+ /**
3
+ * Hoist Zod schema construction out of functions to module scope —
4
+ * equivalent of babel-plugin-zod-hoist.
5
+ *
6
+ * Schemas built inside function bodies are re-constructed on every call:
7
+ *
8
+ * function getSchema() {
9
+ * return z.object({ name: z.string() }); // rebuilt per call
10
+ * }
11
+ *
12
+ * becomes
13
+ *
14
+ * const _zh_94b7f5c1 = z.object({ name: z.string() });
15
+ * function getSchema() {
16
+ * return _zh_94b7f5c1; // built once
17
+ * }
18
+ *
19
+ * Safety rules (mirroring babel-plugin-zod-hoist):
20
+ * - Only expressions built solely from **imported bindings and literals**
21
+ * are hoisted. Local variables, module-level const/let/var, `this`,
22
+ * `await`/`yield`, and globals (`new Date()`, `Math.random()`) all
23
+ * disqualify — hoisting would freeze their evaluation at module load.
24
+ * - Eligible roots: any binding imported from zod, an imported identifier
25
+ * matching /ZodSchema$/, or an imported identifier whose chain contains
26
+ * an inline z.* reference (e.g. `Base.extend({ a: z.string() })`).
27
+ * - Declarations are inserted at the top of the module (after shebang and
28
+ * directive prologue). Imports are initialized before module code runs,
29
+ * so referencing them from above their textual position is safe.
30
+ * - Names are content-hashed, so identical schemas dedupe to one binding.
31
+ *
32
+ * The source is TypeScript, which acorn cannot fully parse — candidates are
33
+ * located with a string/comment/depth-aware scanner and extracted with
34
+ * parseExpressionAt (the same technique as the autoDiscover rewrite).
35
+ * Anything unparseable (TS generics, `as` casts) is skipped: a miss leaves
36
+ * the schema unhoisted, never breaks the code.
37
+ */
38
+ /** Imported identifiers matching this pattern are treated as schema roots. */
39
+ const SCHEMA_NAME_PATTERN = /ZodSchema$/;
40
+ /** Module specifiers whose bindings count as the zod namespace. */
41
+ const ZOD_MODULES = new Set(["zod", "zod/v4", "zod/mini", "zod/v4/mini"]);
42
+ /**
43
+ * Collect runtime import bindings with a regex over import statements.
44
+ * Type-only imports and `type` specifiers are excluded — they cannot be
45
+ * referenced at runtime, so excluding them keeps the capture rule sound.
46
+ */
47
+ export function collectImportBindings(code) {
48
+ const all = new Set();
49
+ const zod = new Set();
50
+ const importPattern = /import\s+(type\s+)?([^'";]+?)\s+from\s*["']([^"']+)["']/g;
51
+ for (const match of code.matchAll(importPattern)) {
52
+ const [, typeOnly, clause, specifier] = match;
53
+ if (typeOnly || clause === undefined || specifier === undefined)
54
+ continue;
55
+ const isZod = ZOD_MODULES.has(specifier);
56
+ for (const name of parseImportClause(clause)) {
57
+ all.add(name);
58
+ if (isZod)
59
+ zod.add(name);
60
+ }
61
+ }
62
+ return { all, zod };
63
+ }
64
+ /** Extract local binding names from an import clause. */
65
+ function parseImportClause(clause) {
66
+ const names = [];
67
+ const namedStart = clause.indexOf("{");
68
+ // Default import and/or namespace import before the named group
69
+ const head = namedStart === -1 ? clause : clause.slice(0, namedStart);
70
+ for (const part of head.split(",")) {
71
+ const trimmed = part.trim();
72
+ if (!trimmed)
73
+ continue;
74
+ const ns = trimmed.match(/^\*\s*as\s+([A-Za-z_$][\w$]*)$/);
75
+ if (ns?.[1]) {
76
+ names.push(ns[1]);
77
+ }
78
+ else if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
79
+ names.push(trimmed);
80
+ }
81
+ }
82
+ if (namedStart !== -1) {
83
+ const namedEnd = clause.indexOf("}", namedStart);
84
+ const inner = clause.slice(namedStart + 1, namedEnd === -1 ? undefined : namedEnd);
85
+ for (const part of inner.split(",")) {
86
+ const spec = part.trim();
87
+ if (!spec || spec.startsWith("type "))
88
+ continue;
89
+ // `a as b` binds b; plain `a` binds a
90
+ const asMatch = spec.match(/^[A-Za-z_$][\w$]*\s+as\s+([A-Za-z_$][\w$]*)$/);
91
+ const name = asMatch?.[1] ?? (/^[A-Za-z_$][\w$]*$/.test(spec) ? spec : null);
92
+ if (name)
93
+ names.push(name);
94
+ }
95
+ }
96
+ return names;
97
+ }
98
+ /** Deterministic FNV-1a 32-bit hash, hex-encoded. */
99
+ function fnv1a(text) {
100
+ let hash = 0x811c9dc5;
101
+ for (let i = 0; i < text.length; i++) {
102
+ hash ^= text.charCodeAt(i);
103
+ hash = Math.imul(hash, 0x01000193);
104
+ }
105
+ return (hash >>> 0).toString(16).padStart(8, "0");
106
+ }
107
+ const IDENT_START = /[A-Za-z_$]/;
108
+ const IDENT_PART = /[\w$]/;
109
+ /** After these tokens a `/` starts a regex literal, not division. */
110
+ const REGEX_PRECEDING_KEYWORDS = new Set([
111
+ "return",
112
+ "typeof",
113
+ "instanceof",
114
+ "in",
115
+ "of",
116
+ "new",
117
+ "delete",
118
+ "void",
119
+ "throw",
120
+ "do",
121
+ "else",
122
+ "case",
123
+ "yield",
124
+ "await",
125
+ ]);
126
+ /** Is `/` after this token division (true) or a regex literal (false)? */
127
+ function isDivisionContext(lastToken) {
128
+ if (lastToken === ")" || lastToken === "]")
129
+ return true;
130
+ if (/^["'`]$/.test(lastToken))
131
+ return true;
132
+ if (/^[\w$]+$/.test(lastToken))
133
+ return !REGEX_PRECEDING_KEYWORDS.has(lastToken);
134
+ return false;
135
+ }
136
+ /**
137
+ * Scan the source for candidate root identifiers (`z`, `FooZodSchema`, ...)
138
+ * followed by a `.`, tracking string/template/comment state and nesting
139
+ * depth. Depth 0 candidates are top-level initializers — already evaluated
140
+ * once — and are recorded only so their extents mask nested candidates.
141
+ */
142
+ function scanSource(code, roots) {
143
+ const candidates = [];
144
+ const out = code.split("");
145
+ const mask = (from, to) => {
146
+ for (let p = from; p < to; p++) {
147
+ if (out[p] !== "\n")
148
+ out[p] = " ";
149
+ }
150
+ };
151
+ let depth = 0;
152
+ // Template literals interleave string and expression states; the stack
153
+ // records the brace depth at which each `${` opened so the matching `}`
154
+ // resumes string state.
155
+ const templateStack = [];
156
+ let lastToken = "";
157
+ let i = 0;
158
+ while (i < code.length) {
159
+ const ch = code[i];
160
+ const next = code[i + 1];
161
+ // Comments
162
+ if (ch === "/" && next === "/") {
163
+ const nl = code.indexOf("\n", i);
164
+ const end = nl === -1 ? code.length : nl + 1;
165
+ mask(i, end);
166
+ i = end;
167
+ continue;
168
+ }
169
+ if (ch === "/" && next === "*") {
170
+ const close = code.indexOf("*/", i + 2);
171
+ const end = close === -1 ? code.length : close + 2;
172
+ mask(i, end);
173
+ i = end;
174
+ continue;
175
+ }
176
+ if (ch === "/") {
177
+ if (!isDivisionContext(lastToken)) {
178
+ const end = skipRegexLiteral(code, i);
179
+ mask(i, end);
180
+ i = end;
181
+ lastToken = ")"; // a regex literal is an operand
182
+ continue;
183
+ }
184
+ lastToken = ch;
185
+ i++;
186
+ continue;
187
+ }
188
+ // Strings
189
+ if (ch === '"' || ch === "'") {
190
+ const end = skipString(code, i, ch);
191
+ mask(i, end);
192
+ i = end;
193
+ lastToken = ch;
194
+ continue;
195
+ }
196
+ // Template literals
197
+ if (ch === "`") {
198
+ const end = skipTemplateChunk(code, i + 1, templateStack, depth);
199
+ mask(i, end);
200
+ i = end;
201
+ lastToken = "`";
202
+ continue;
203
+ }
204
+ if (templateStack.length > 0 &&
205
+ ch === "}" &&
206
+ depth === templateStack[templateStack.length - 1]) {
207
+ // End of a ${ } expression — back into template string state
208
+ templateStack.pop();
209
+ const end = skipTemplateChunk(code, i + 1, templateStack, depth);
210
+ mask(i, end);
211
+ i = end;
212
+ lastToken = "`";
213
+ continue;
214
+ }
215
+ if (ch === "(" || ch === "[" || ch === "{") {
216
+ depth++;
217
+ lastToken = ch;
218
+ i++;
219
+ continue;
220
+ }
221
+ if (ch === ")" || ch === "]" || ch === "}") {
222
+ depth--;
223
+ lastToken = ch;
224
+ i++;
225
+ continue;
226
+ }
227
+ if (IDENT_START.test(ch) && !IDENT_PART.test(code[i - 1] ?? "")) {
228
+ let j = i + 1;
229
+ while (j < code.length && IDENT_PART.test(code[j]))
230
+ j++;
231
+ const word = code.slice(i, j);
232
+ // Skip whitespace to check for the member access
233
+ let k = j;
234
+ while (k < code.length && /\s/.test(code[k]))
235
+ k++;
236
+ if (roots.has(word) && code[k] === "." && lastToken !== ".") {
237
+ candidates.push({ index: i, depth, name: word, afterArrow: lastToken === "=>" });
238
+ }
239
+ lastToken = word;
240
+ i = j;
241
+ continue;
242
+ }
243
+ if (!/\s/.test(ch)) {
244
+ lastToken = ch === ">" && lastToken === "=" ? "=>" : ch;
245
+ }
246
+ i++;
247
+ }
248
+ return { candidates, stripped: out.join("") };
249
+ }
250
+ function skipString(code, start, quote) {
251
+ for (let i = start + 1; i < code.length; i++) {
252
+ if (code[i] === "\\") {
253
+ i++;
254
+ }
255
+ else if (code[i] === quote || code[i] === "\n") {
256
+ return i + 1;
257
+ }
258
+ }
259
+ return code.length;
260
+ }
261
+ /**
262
+ * Skip a template string chunk; returns the offset after the closing
263
+ * `` ` `` or after a `${`, recording the current depth so the scanner can
264
+ * recognize the matching `}` later.
265
+ */
266
+ function skipTemplateChunk(code, start, templateStack, depth) {
267
+ for (let i = start; i < code.length; i++) {
268
+ if (code[i] === "\\") {
269
+ i++;
270
+ }
271
+ else if (code[i] === "`") {
272
+ return i + 1;
273
+ }
274
+ else if (code[i] === "$" && code[i + 1] === "{") {
275
+ templateStack.push(depth);
276
+ return i + 2;
277
+ }
278
+ }
279
+ return code.length;
280
+ }
281
+ function skipRegexLiteral(code, start) {
282
+ let inClass = false;
283
+ for (let i = start + 1; i < code.length; i++) {
284
+ const ch = code[i];
285
+ if (ch === "\\") {
286
+ i++;
287
+ }
288
+ else if (ch === "[") {
289
+ inClass = true;
290
+ }
291
+ else if (ch === "]") {
292
+ inClass = false;
293
+ }
294
+ else if (ch === "/" && !inClass) {
295
+ return i + 1;
296
+ }
297
+ else if (ch === "\n") {
298
+ // Not a regex after all (unterminated) — treat as division
299
+ return start + 1;
300
+ }
301
+ }
302
+ return code.length;
303
+ }
304
+ /**
305
+ * Narrow a parsed expression to the largest call chain starting at `start`.
306
+ * parseExpressionAt can overshoot into surrounding operators
307
+ * (`z.string().min(1) || fallback` parses as a LogicalExpression) — descend
308
+ * through same-start children until a CallExpression is found.
309
+ */
310
+ function narrowToCallChain(node, start) {
311
+ let current = node;
312
+ while (current.start === start) {
313
+ if (current.type === "CallExpression")
314
+ return current;
315
+ const child = sameStartChild(current, start);
316
+ if (!child)
317
+ return null;
318
+ current = child;
319
+ }
320
+ return null;
321
+ }
322
+ function sameStartChild(node, start) {
323
+ const record = node;
324
+ for (const key of ["left", "object", "callee", "test", "tag", "expression", "expressions"]) {
325
+ const value = record[key];
326
+ const child = Array.isArray(value) ? value[0] : value;
327
+ if (typeof child === "object" &&
328
+ child !== null &&
329
+ child.start === start &&
330
+ typeof child.type === "string") {
331
+ return child;
332
+ }
333
+ }
334
+ return null;
335
+ }
336
+ /**
337
+ * Walk a member/call chain down to its base identifier, collecting the
338
+ * non-computed method names along the spine. Computed members
339
+ * (`base[name]()`) are unanalyzable — the chain is rejected.
340
+ */
341
+ function describeChain(node) {
342
+ const methods = [];
343
+ let current = node;
344
+ while (true) {
345
+ if (current.type === "CallExpression") {
346
+ current = current.callee;
347
+ }
348
+ else if (current.type === "MemberExpression") {
349
+ if (current.computed || current.property.type !== "Identifier")
350
+ return null;
351
+ methods.push(current.property.name);
352
+ current = current.object;
353
+ }
354
+ else if (current.type === "Identifier") {
355
+ return { root: current.name, methods };
356
+ }
357
+ else {
358
+ return null;
359
+ }
360
+ }
361
+ }
362
+ /**
363
+ * Combinators eligible on non-z chain bases (babel-plugin-zod-hoist's
364
+ * documented set) — `Base.extend({...})`, `Base.pick({...})`, ...
365
+ */
366
+ const COMBINATOR_METHODS = new Set(["extend", "pick", "omit", "merge", "partial"]);
367
+ /**
368
+ * Methods that evaluate data rather than construct schemas. Hoisting one
369
+ * would move the evaluation (and any throw) to module load.
370
+ */
371
+ const PARSE_METHODS = new Set([
372
+ "parse",
373
+ "safeParse",
374
+ "parseAsync",
375
+ "safeParseAsync",
376
+ "decode",
377
+ "encode",
378
+ "decodeAsync",
379
+ "encodeAsync",
380
+ ]);
381
+ /**
382
+ * Globals that are safe to reference from a DEFERRED position (inside a
383
+ * callback): the callback still runs per parse, so hoisting cannot change
384
+ * when these evaluate. Eager positions stay imports-only — an eager
385
+ * `Math.random()` / `Date.now()` would freeze at module load.
386
+ */
387
+ const SAFE_DEFERRED_GLOBALS = new Set([
388
+ "NaN",
389
+ "Infinity",
390
+ "Math",
391
+ "Number",
392
+ "String",
393
+ "Boolean",
394
+ "Array",
395
+ "Object",
396
+ "JSON",
397
+ "Date",
398
+ "RegExp",
399
+ "BigInt",
400
+ "Symbol",
401
+ "Map",
402
+ "Set",
403
+ "Promise",
404
+ "Error",
405
+ "TypeError",
406
+ "RangeError",
407
+ "SyntaxError",
408
+ "parseInt",
409
+ "parseFloat",
410
+ "isNaN",
411
+ "isFinite",
412
+ "encodeURI",
413
+ "decodeURI",
414
+ "encodeURIComponent",
415
+ "decodeURIComponent",
416
+ "structuredClone",
417
+ ]);
418
+ /**
419
+ * Scope-aware free-variable analysis of an expression. Nested function
420
+ * params and local declarations are bound names, not captures — so
421
+ * `z.string().refine((v) => v.length > 0)` has no free variables beyond `z`.
422
+ * References inside nested function bodies are tracked separately: they
423
+ * evaluate per call even after hoisting, so safe globals are allowed there.
424
+ */
425
+ function analyzeCaptures(expr) {
426
+ const eagerFree = new Set();
427
+ const deferredFree = new Set();
428
+ let impure = false;
429
+ function patternNames(node, into) {
430
+ switch (node.type) {
431
+ case "Identifier":
432
+ into.add(node.name);
433
+ return;
434
+ case "ObjectPattern":
435
+ for (const prop of node.properties) {
436
+ if (prop.type === "Property")
437
+ patternNames(prop.value, into);
438
+ else
439
+ patternNames(prop.argument, into);
440
+ }
441
+ return;
442
+ case "ArrayPattern":
443
+ for (const el of node.elements) {
444
+ if (el)
445
+ patternNames(el, into);
446
+ }
447
+ return;
448
+ case "AssignmentPattern":
449
+ patternNames(node.left, into);
450
+ return;
451
+ case "RestElement":
452
+ patternNames(node.argument, into);
453
+ return;
454
+ default:
455
+ return;
456
+ }
457
+ }
458
+ function collectFunctionScope(body, into) {
459
+ // Flatten block scoping into the function scope — over-approximating
460
+ // bound names can only suppress a hoist's free set, and the failure
461
+ // mode is a loud ReferenceError, never silent misvalidation.
462
+ const stack = [body];
463
+ while (stack.length > 0) {
464
+ const node = stack.pop();
465
+ if (!node || typeof node !== "object")
466
+ continue;
467
+ if (node.type === "VariableDeclaration") {
468
+ for (const decl of node.declarations)
469
+ patternNames(decl.id, into);
470
+ }
471
+ else if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") {
472
+ if (node.id)
473
+ into.add(node.id.name);
474
+ continue; // do not descend into nested declarations' bodies here
475
+ }
476
+ else if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
477
+ continue; // nested functions get their own scope in visit()
478
+ }
479
+ for (const value of Object.values(node)) {
480
+ if (Array.isArray(value)) {
481
+ for (const item of value) {
482
+ if (typeof item === "object" && item !== null && "type" in item) {
483
+ stack.push(item);
484
+ }
485
+ }
486
+ }
487
+ else if (typeof value === "object" && value !== null && "type" in value) {
488
+ stack.push(value);
489
+ }
490
+ }
491
+ }
492
+ }
493
+ function visit(node, scopes, deferred) {
494
+ switch (node.type) {
495
+ case "Identifier": {
496
+ const name = node.name;
497
+ if (name !== "undefined" && !scopes.some((s) => s.has(name))) {
498
+ (deferred ? deferredFree : eagerFree).add(name);
499
+ }
500
+ return;
501
+ }
502
+ case "ThisExpression":
503
+ case "Super":
504
+ // Arrows bind `this` lexically — hoisting changes it even when the
505
+ // reference sits inside a callback. Always disqualifying.
506
+ impure = true;
507
+ return;
508
+ case "AwaitExpression":
509
+ case "YieldExpression":
510
+ case "NewExpression":
511
+ case "TaggedTemplateExpression":
512
+ case "MetaProperty":
513
+ // Deferred occurrences run per call (`refine(d => d < new Date())`)
514
+ // — hoisting does not change when they evaluate.
515
+ if (!deferred) {
516
+ impure = true;
517
+ return;
518
+ }
519
+ break;
520
+ case "MemberExpression":
521
+ visit(node.object, scopes, deferred);
522
+ if (node.computed)
523
+ visit(node.property, scopes, deferred);
524
+ return;
525
+ case "Property":
526
+ if (node.computed)
527
+ visit(node.key, scopes, deferred);
528
+ visit(node.value, scopes, deferred);
529
+ return;
530
+ case "FunctionExpression":
531
+ case "ArrowFunctionExpression": {
532
+ const scope = new Set();
533
+ for (const param of node.params)
534
+ patternNames(param, scope);
535
+ if (node.type === "FunctionExpression" && node.id)
536
+ scope.add(node.id.name);
537
+ collectFunctionScope(node.body, scope);
538
+ visit(node.body, [...scopes, scope], true);
539
+ return;
540
+ }
541
+ default:
542
+ break;
543
+ }
544
+ for (const value of Object.values(node)) {
545
+ if (Array.isArray(value)) {
546
+ for (const item of value) {
547
+ if (typeof item === "object" && item !== null && "type" in item) {
548
+ visit(item, scopes, deferred);
549
+ }
550
+ }
551
+ }
552
+ else if (typeof value === "object" && value !== null && "type" in value) {
553
+ visit(value, scopes, deferred);
554
+ }
555
+ }
556
+ }
557
+ visit(expr, [], false);
558
+ return { eagerFree, deferredFree, impure };
559
+ }
560
+ /** Offset after the shebang and directive prologue ("use client", ...). */
561
+ function insertionOffset(code) {
562
+ let i = 0;
563
+ if (code.startsWith("#!")) {
564
+ const nl = code.indexOf("\n");
565
+ i = nl === -1 ? code.length : nl + 1;
566
+ }
567
+ while (true) {
568
+ // Skip whitespace and comments between directives
569
+ while (i < code.length) {
570
+ const ch = code[i];
571
+ if (/\s/.test(ch)) {
572
+ i++;
573
+ }
574
+ else if (ch === "/" && code[i + 1] === "/") {
575
+ const nl = code.indexOf("\n", i);
576
+ i = nl === -1 ? code.length : nl + 1;
577
+ }
578
+ else if (ch === "/" && code[i + 1] === "*") {
579
+ const end = code.indexOf("*/", i + 2);
580
+ i = end === -1 ? code.length : end + 2;
581
+ }
582
+ else {
583
+ break;
584
+ }
585
+ }
586
+ const directive = code.slice(i).match(/^(["'])use [^"'\n]*\1\s*;?[^\S\n]*\n?/);
587
+ if (!directive)
588
+ return i;
589
+ i += directive[0].length;
590
+ }
591
+ }
592
+ /** Keywords that precede `( ... ) {` without binding anything. */
593
+ const NON_BINDING_KEYWORDS = "if|for|while|switch|return|typeof|await|yield|new|do|else|case|in|of|delete|void";
594
+ /**
595
+ * Conservative shadow detection: collect every identifier appearing in a
596
+ * binding-like position — function/method/arrow parameter lists, catch
597
+ * clauses, const/let/var declarator names. The scanner has no scope
598
+ * information, so an import referenced from a hoisted expression must
599
+ * resolve to that import everywhere; a name that is ever re-bound cannot be
600
+ * trusted and disqualifies hoists referencing it. Over-collection only
601
+ * costs missed hoists; the identifiers extracted from each binding segment
602
+ * are cut at `:` and `=` so TypeScript annotations and initializers
603
+ * (`schema: z.ZodType`, `x = z.object()`) never poison the set.
604
+ */
605
+ function collectBoundNames(code) {
606
+ const bound = new Set();
607
+ // Per comma-separated piece, keep only the binding side: names before any
608
+ // type annotation (`a: T`) or default/initializer (`a = v`).
609
+ const addBindingSegment = (segment) => {
610
+ for (const piece of segment.split(",")) {
611
+ let cut = piece;
612
+ const colon = cut.indexOf(":");
613
+ if (colon !== -1)
614
+ cut = cut.slice(0, colon);
615
+ const eq = cut.indexOf("=");
616
+ if (eq !== -1)
617
+ cut = cut.slice(0, eq);
618
+ for (const id of cut.matchAll(/[A-Za-z_$][\w$]*/g)) {
619
+ bound.add(id[0]);
620
+ }
621
+ }
622
+ };
623
+ // function f(a, b) / function (a, b)
624
+ for (const m of code.matchAll(/\bfunction\b[^(]*\(([^()]*)\)/g)) {
625
+ addBindingSegment(m[1] ?? "");
626
+ }
627
+ // (a, b) => / (a, b): Ret =>
628
+ for (const m of code.matchAll(/\(([^()]*)\)\s*(?::[^=>{};()]*)?=>/g)) {
629
+ addBindingSegment(m[1] ?? "");
630
+ }
631
+ // bare arrow param: a =>
632
+ for (const m of code.matchAll(/([A-Za-z_$][\w$]*)\s*=>/g)) {
633
+ bound.add(m[1] ?? "");
634
+ }
635
+ // method(a, b) { / method(a): Ret { — excluding control flow (`if (x) {`)
636
+ const methodPattern = new RegExp(String.raw `(?<![.\w$])(?!(?:${NON_BINDING_KEYWORDS})\b)[A-Za-z_$][\w$]*\s*\(([^()]*)\)\s*(?::[^{};()]*)?\{`, "g");
637
+ for (const m of code.matchAll(methodPattern)) {
638
+ addBindingSegment(m[1] ?? "");
639
+ }
640
+ // catch (e)
641
+ for (const m of code.matchAll(/\bcatch\s*\(([^()]*)\)/g)) {
642
+ addBindingSegment(m[1] ?? "");
643
+ }
644
+ // const/let/var declarator names (cut per comma at `:` / `=`)
645
+ for (const m of code.matchAll(/\b(?:const|let|var)\s+([^;\n]*)/g)) {
646
+ addBindingSegment(m[1] ?? "");
647
+ }
648
+ return bound;
649
+ }
650
+ /**
651
+ * Hoist eligible Zod schema expressions to module scope.
652
+ * Returns the rewritten source, or null when nothing was hoisted.
653
+ */
654
+ export function hoistZodSchemas(code, options) {
655
+ const imports = collectImportBindings(code);
656
+ if (imports.all.size === 0)
657
+ return null;
658
+ const rawPattern = options?.schemaNamePattern;
659
+ const namePattern = rawPattern === null
660
+ ? null
661
+ : rawPattern === undefined
662
+ ? SCHEMA_NAME_PATTERN
663
+ : typeof rawPattern === "string"
664
+ ? new RegExp(rawPattern)
665
+ : rawPattern;
666
+ const roots = new Set(imports.zod);
667
+ if (namePattern) {
668
+ for (const name of imports.all) {
669
+ if (namePattern.test(name))
670
+ roots.add(name);
671
+ }
672
+ }
673
+ // Imported roots that only qualify via an inline z.* reference in the
674
+ // chain are validated per-expression below; scan them as candidates too.
675
+ if (imports.zod.size > 0) {
676
+ for (const name of imports.all)
677
+ roots.add(name);
678
+ }
679
+ if (roots.size === 0)
680
+ return null;
681
+ const { candidates, stripped } = scanSource(code, roots);
682
+ if (candidates.length === 0)
683
+ return null;
684
+ let boundNames;
685
+ function isShadowed(name) {
686
+ boundNames ??= collectBoundNames(stripped);
687
+ return boundNames.has(name);
688
+ }
689
+ const hoists = [];
690
+ const declByText = new Map();
691
+ let consumedUntil = 0;
692
+ for (const candidate of candidates) {
693
+ if (candidate.index < consumedUntil)
694
+ continue;
695
+ let parsed;
696
+ try {
697
+ parsed = parseExpressionAt(code, candidate.index, {
698
+ ecmaVersion: "latest",
699
+ sourceType: "module",
700
+ });
701
+ }
702
+ catch {
703
+ continue;
704
+ }
705
+ let chain = narrowToCallChain(parsed, candidate.index);
706
+ if (!chain)
707
+ continue;
708
+ // Mask nested candidates inside this extent regardless of eligibility —
709
+ // an inner z.string() of a top-level schema must not hoist on its own.
710
+ consumedUntil = chain.end;
711
+ // Depth 0 expressions are top-level statements/initializers: already
712
+ // evaluated once per module load, nothing to gain. Exception: concise
713
+ // arrow bodies (`const make = () => z.object(...)`) re-run per call.
714
+ if (candidate.depth === 0 && !candidate.afterArrow)
715
+ continue;
716
+ // Peel trailing parse calls: for `z.object({...}).safeParse(input)`,
717
+ // hoist the construction and leave `.safeParse(input)` — with its
718
+ // local-variable arguments — at the call site.
719
+ let described = describeChain(chain);
720
+ while (chain !== null &&
721
+ described !== null &&
722
+ described.methods.length > 0 &&
723
+ PARSE_METHODS.has(described.methods[0])) {
724
+ const callee = chain.type === "CallExpression" ? chain.callee : null;
725
+ const inner = callee !== null && callee.type === "MemberExpression" ? callee.object : null;
726
+ chain = inner !== null && inner.type === "CallExpression" ? inner : null;
727
+ described = chain === null ? null : describeChain(chain);
728
+ }
729
+ if (chain === null || described === null)
730
+ continue;
731
+ if (described.root !== candidate.name)
732
+ continue;
733
+ if (described.methods.some((m) => PARSE_METHODS.has(m)))
734
+ continue;
735
+ const rootIsZod = imports.zod.has(candidate.name);
736
+ const rootMatchesPattern = namePattern?.test(candidate.name) === true;
737
+ // Non-z bases must look like schema derivation: the chain must START
738
+ // with a combinator (`Base.extend({...}).optional()` qualifies via
739
+ // extend), so an arbitrary imported object with a z-mentioning argument
740
+ // (`api.get(z.string())`) is never hoisted. describeChain records
741
+ // methods outermost-first — the deepest method is last.
742
+ const deepestMethod = described.methods[described.methods.length - 1];
743
+ if (!rootIsZod && (deepestMethod === undefined || !COMBINATOR_METHODS.has(deepestMethod))) {
744
+ continue;
745
+ }
746
+ const { eagerFree, deferredFree, impure } = analyzeCaptures(chain);
747
+ if (impure)
748
+ continue;
749
+ // Imports-and-literals rule (babel-plugin-zod-hoist): eager positions
750
+ // may reference only imports — locals, module-level bindings, and
751
+ // globals all disqualify. Deferred positions (callback bodies) run per
752
+ // call regardless, so safe globals are additionally allowed there.
753
+ // Every referenced import must also be shadow-free: without scope
754
+ // information, a name that is ever re-bound (function parameter, local)
755
+ // cannot be trusted to mean the import.
756
+ let eligible = true;
757
+ for (const name of eagerFree) {
758
+ if (!imports.all.has(name) || isShadowed(name)) {
759
+ eligible = false;
760
+ break;
761
+ }
762
+ }
763
+ if (eligible) {
764
+ for (const name of deferredFree) {
765
+ const allowed = imports.all.has(name) || SAFE_DEFERRED_GLOBALS.has(name);
766
+ if (!allowed || isShadowed(name)) {
767
+ eligible = false;
768
+ break;
769
+ }
770
+ }
771
+ }
772
+ if (!eligible)
773
+ continue;
774
+ // Non-zod, non-pattern roots qualify only when the chain itself
775
+ // references a zod binding (`Base.extend({ a: z.string() })`).
776
+ if (!rootIsZod && !rootMatchesPattern) {
777
+ let referencesZod = false;
778
+ for (const name of eagerFree) {
779
+ if (imports.zod.has(name)) {
780
+ referencesZod = true;
781
+ break;
782
+ }
783
+ }
784
+ for (const name of deferredFree) {
785
+ if (imports.zod.has(name)) {
786
+ referencesZod = true;
787
+ break;
788
+ }
789
+ }
790
+ if (!referencesZod)
791
+ continue;
792
+ }
793
+ const text = code.slice(candidate.index, chain.end);
794
+ let declName = declByText.get(text);
795
+ if (!declName) {
796
+ declName = `_zh_${fnv1a(text)}`;
797
+ declByText.set(text, declName);
798
+ }
799
+ hoists.push({ start: candidate.index, end: chain.end, name: declName });
800
+ }
801
+ if (hoists.length === 0)
802
+ return null;
803
+ // Apply replacements from the end so earlier offsets stay valid.
804
+ let result = code;
805
+ for (let i = hoists.length - 1; i >= 0; i--) {
806
+ const hoist = hoists[i];
807
+ result = result.slice(0, hoist.start) + hoist.name + result.slice(hoist.end);
808
+ }
809
+ const decls = [...declByText.entries()]
810
+ .map(([text, name]) => `const ${name} = ${text};`)
811
+ .join("\n");
812
+ const offset = insertionOffset(result);
813
+ return `${result.slice(0, offset)}${decls}\n${result.slice(offset)}`;
814
+ }
815
+ //# sourceMappingURL=hoist.js.map