zod-compiler 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/loader.d.ts +8 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +21 -0
- package/dist/loader.js.map +1 -1
- package/dist/unplugin/hoist-compile.d.ts +49 -0
- package/dist/unplugin/hoist-compile.d.ts.map +1 -0
- package/dist/unplugin/hoist-compile.js +125 -0
- package/dist/unplugin/hoist-compile.js.map +1 -0
- package/dist/unplugin/hoist.d.ts +36 -0
- package/dist/unplugin/hoist.d.ts.map +1 -1
- package/dist/unplugin/hoist.js +333 -86
- package/dist/unplugin/hoist.js.map +1 -1
- package/dist/unplugin/transform.d.ts.map +1 -1
- package/dist/unplugin/transform.js +69 -17
- package/dist/unplugin/transform.js.map +1 -1
- package/package.json +1 -1
package/dist/unplugin/hoist.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Parser } from "acorn";
|
|
2
2
|
/**
|
|
3
3
|
* Hoist Zod schema construction out of functions to module scope —
|
|
4
4
|
* equivalent of babel-plugin-zod-hoist.
|
|
@@ -16,14 +16,27 @@ import { parseExpressionAt } from "acorn";
|
|
|
16
16
|
* return _zh_94b7f5c1; // built once
|
|
17
17
|
* }
|
|
18
18
|
*
|
|
19
|
-
* Safety rules (
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Safety rules (babel-plugin-zod-hoist's `canSafelyHoist`, hardened for
|
|
20
|
+
* lexical analysis):
|
|
21
|
+
* - A free identifier must be an import or a KNOWN_GLOBALS member, and must
|
|
22
|
+
* never be bound anywhere in the file (function params, locals, catch
|
|
23
|
+
* clauses, class names, module-level const/let/var — hoisting above those
|
|
24
|
+
* would change meaning or hit the TDZ). The babel plugin additionally
|
|
25
|
+
* allows arbitrary unbound identifiers because it has real scope
|
|
26
|
+
* information; this port's binding collector is lexical, so an unknown
|
|
27
|
+
* bare name is treated as a possibly-missed binding rather than a global
|
|
28
|
+
* (a wrong guess crashes at module load with a ReferenceError).
|
|
29
|
+
* `this`/`super` disqualify anywhere. Eager `await`/`yield` also
|
|
30
|
+
* disqualify (stricter than the babel plugin, which never encounters
|
|
31
|
+
* them: hoisting one would emit top-level await / orphaned yield).
|
|
24
32
|
* - Eligible roots: any binding imported from zod, an imported identifier
|
|
25
33
|
* matching /ZodSchema$/, or an imported identifier whose chain contains
|
|
26
34
|
* an inline z.* reference (e.g. `Base.extend({ a: z.string() })`).
|
|
35
|
+
* - Nesting (babel's `isNestedInZodCall`): the interior of a zod-rooted
|
|
36
|
+
* chain never hoists separately — it goes with the outer schema or not at
|
|
37
|
+
* all. Chains rooted elsewhere (`sql.type(...)`, `api.get(...)`) do NOT
|
|
38
|
+
* suppress their arguments: when the outer chain is rejected, an inner
|
|
39
|
+
* `z.object({...})` still hoists on its own.
|
|
27
40
|
* - Declarations are inserted at the top of the module (after shebang and
|
|
28
41
|
* directive prologue). Imports are initialized before module code runs,
|
|
29
42
|
* so referencing them from above their textual position is safe.
|
|
@@ -47,21 +60,23 @@ const ZOD_MODULES = new Set(["zod", "zod/v4", "zod/mini", "zod/v4/mini"]);
|
|
|
47
60
|
export function collectImportBindings(code) {
|
|
48
61
|
const all = new Set();
|
|
49
62
|
const zod = new Set();
|
|
63
|
+
const details = new Map();
|
|
50
64
|
const importPattern = /import\s+(type\s+)?([^'";]+?)\s+from\s*["']([^"']+)["']/g;
|
|
51
65
|
for (const match of code.matchAll(importPattern)) {
|
|
52
66
|
const [, typeOnly, clause, specifier] = match;
|
|
53
67
|
if (typeOnly || clause === undefined || specifier === undefined)
|
|
54
68
|
continue;
|
|
55
69
|
const isZod = ZOD_MODULES.has(specifier);
|
|
56
|
-
for (const
|
|
57
|
-
all.add(
|
|
70
|
+
for (const { local, imported } of parseImportClause(clause)) {
|
|
71
|
+
all.add(local);
|
|
58
72
|
if (isZod)
|
|
59
|
-
zod.add(
|
|
73
|
+
zod.add(local);
|
|
74
|
+
details.set(local, { specifier, imported });
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
|
-
return { all, zod };
|
|
77
|
+
return { all, zod, details };
|
|
63
78
|
}
|
|
64
|
-
/** Extract local binding names from an import clause. */
|
|
79
|
+
/** Extract local binding names (with their source export) from an import clause. */
|
|
65
80
|
function parseImportClause(clause) {
|
|
66
81
|
const names = [];
|
|
67
82
|
const namedStart = clause.indexOf("{");
|
|
@@ -73,10 +88,10 @@ function parseImportClause(clause) {
|
|
|
73
88
|
continue;
|
|
74
89
|
const ns = trimmed.match(/^\*\s*as\s+([A-Za-z_$][\w$]*)$/);
|
|
75
90
|
if (ns?.[1]) {
|
|
76
|
-
names.push(ns[1]);
|
|
91
|
+
names.push({ local: ns[1], imported: "*" });
|
|
77
92
|
}
|
|
78
93
|
else if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
|
|
79
|
-
names.push(trimmed);
|
|
94
|
+
names.push({ local: trimmed, imported: "default" });
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
97
|
if (namedStart !== -1) {
|
|
@@ -87,10 +102,13 @@ function parseImportClause(clause) {
|
|
|
87
102
|
if (!spec || spec.startsWith("type "))
|
|
88
103
|
continue;
|
|
89
104
|
// `a as b` binds b; plain `a` binds a
|
|
90
|
-
const asMatch = spec.match(/^[A-Za-z_$][\w$]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
const asMatch = spec.match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
106
|
+
if (asMatch?.[1] && asMatch[2]) {
|
|
107
|
+
names.push({ local: asMatch[2], imported: asMatch[1] });
|
|
108
|
+
}
|
|
109
|
+
else if (/^[A-Za-z_$][\w$]*$/.test(spec)) {
|
|
110
|
+
names.push({ local: spec, imported: spec });
|
|
111
|
+
}
|
|
94
112
|
}
|
|
95
113
|
}
|
|
96
114
|
return names;
|
|
@@ -301,9 +319,23 @@ function skipRegexLiteral(code, start) {
|
|
|
301
319
|
}
|
|
302
320
|
return code.length;
|
|
303
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Parse a single assignment-level expression at `pos` — the same entry point
|
|
324
|
+
* acorn's own parseExpressionAt uses, one precedence level down. At
|
|
325
|
+
* expression level a trailing comma in an argument position
|
|
326
|
+
* (`sql.type(z.object({...}),\n)`) starts a sequence-expression parse that
|
|
327
|
+
* throws on the surrounding `)`; assignment level treats the comma as
|
|
328
|
+
* trailing garbage and stops cleanly. parseMaybeAssign is the entry point
|
|
329
|
+
* the acorn plugin ecosystem overrides — stable across versions.
|
|
330
|
+
*/
|
|
331
|
+
function parseAssignmentAt(code, pos) {
|
|
332
|
+
const parser = new Parser({ ecmaVersion: "latest", sourceType: "module" }, code, pos);
|
|
333
|
+
parser.nextToken();
|
|
334
|
+
return parser.parseMaybeAssign();
|
|
335
|
+
}
|
|
304
336
|
/**
|
|
305
337
|
* Narrow a parsed expression to the largest call chain starting at `start`.
|
|
306
|
-
*
|
|
338
|
+
* The parse can overshoot into surrounding operators
|
|
307
339
|
* (`z.string().min(1) || fallback` parses as a LogicalExpression) — descend
|
|
308
340
|
* through same-start children until a CallExpression is found.
|
|
309
341
|
*/
|
|
@@ -361,9 +393,34 @@ function describeChain(node) {
|
|
|
361
393
|
}
|
|
362
394
|
/**
|
|
363
395
|
* Combinators eligible on non-z chain bases (babel-plugin-zod-hoist's
|
|
364
|
-
*
|
|
396
|
+
* SCHEMA_COMBINATOR_METHODS, verbatim) — `Base.extend({...})`,
|
|
397
|
+
* `Base.pick({...})`, `UserZodSchema.optional()`, ...
|
|
365
398
|
*/
|
|
366
|
-
const COMBINATOR_METHODS = new Set([
|
|
399
|
+
const COMBINATOR_METHODS = new Set([
|
|
400
|
+
"and",
|
|
401
|
+
"array",
|
|
402
|
+
"brand",
|
|
403
|
+
"catchall",
|
|
404
|
+
"deepPartial",
|
|
405
|
+
"describe",
|
|
406
|
+
"extend",
|
|
407
|
+
"merge",
|
|
408
|
+
"nullable",
|
|
409
|
+
"nullish",
|
|
410
|
+
"omit",
|
|
411
|
+
"optional",
|
|
412
|
+
"or",
|
|
413
|
+
"partial",
|
|
414
|
+
"passthrough",
|
|
415
|
+
"pick",
|
|
416
|
+
"readonly",
|
|
417
|
+
"refine",
|
|
418
|
+
"required",
|
|
419
|
+
"strict",
|
|
420
|
+
"strip",
|
|
421
|
+
"superRefine",
|
|
422
|
+
"transform",
|
|
423
|
+
]);
|
|
367
424
|
/**
|
|
368
425
|
* Methods that evaluate data rather than construct schemas. Hoisting one
|
|
369
426
|
* would move the evaluation (and any throw) to module load.
|
|
@@ -379,12 +436,19 @@ const PARSE_METHODS = new Set([
|
|
|
379
436
|
"encodeAsync",
|
|
380
437
|
]);
|
|
381
438
|
/**
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
439
|
+
* Standard globals a hoisted expression may reference (eager or deferred —
|
|
440
|
+
* babel-parity behaviors like hoisting `z.date().default(new Date())` rely
|
|
441
|
+
* on Date/Math being recognized).
|
|
442
|
+
*
|
|
443
|
+
* The babel plugin allows ANY unbound identifier because it has real scope
|
|
444
|
+
* information; this port's binding collector is a lexical approximation, so
|
|
445
|
+
* an unknown bare name must be assumed to be a binding the collector missed
|
|
446
|
+
* — hoisting it would crash at module load with
|
|
447
|
+
* `ReferenceError: <name> is not defined`. A fixed allowlist converts that
|
|
448
|
+
* failure mode into a missed optimization.
|
|
386
449
|
*/
|
|
387
|
-
const
|
|
450
|
+
const KNOWN_GLOBALS = new Set([
|
|
451
|
+
"globalThis",
|
|
388
452
|
"NaN",
|
|
389
453
|
"Infinity",
|
|
390
454
|
"Math",
|
|
@@ -400,11 +464,21 @@ const SAFE_DEFERRED_GLOBALS = new Set([
|
|
|
400
464
|
"Symbol",
|
|
401
465
|
"Map",
|
|
402
466
|
"Set",
|
|
467
|
+
"WeakMap",
|
|
468
|
+
"WeakSet",
|
|
403
469
|
"Promise",
|
|
470
|
+
"Proxy",
|
|
471
|
+
"Reflect",
|
|
472
|
+
"Intl",
|
|
404
473
|
"Error",
|
|
405
474
|
"TypeError",
|
|
406
475
|
"RangeError",
|
|
407
476
|
"SyntaxError",
|
|
477
|
+
"EvalError",
|
|
478
|
+
"URIError",
|
|
479
|
+
"AggregateError",
|
|
480
|
+
"ArrayBuffer",
|
|
481
|
+
"Uint8Array",
|
|
408
482
|
"parseInt",
|
|
409
483
|
"parseFloat",
|
|
410
484
|
"isNaN",
|
|
@@ -414,6 +488,16 @@ const SAFE_DEFERRED_GLOBALS = new Set([
|
|
|
414
488
|
"encodeURIComponent",
|
|
415
489
|
"decodeURIComponent",
|
|
416
490
|
"structuredClone",
|
|
491
|
+
"URL",
|
|
492
|
+
"URLSearchParams",
|
|
493
|
+
"TextEncoder",
|
|
494
|
+
"TextDecoder",
|
|
495
|
+
"atob",
|
|
496
|
+
"btoa",
|
|
497
|
+
"crypto",
|
|
498
|
+
"console",
|
|
499
|
+
"process",
|
|
500
|
+
"Buffer",
|
|
417
501
|
]);
|
|
418
502
|
/**
|
|
419
503
|
* Scope-aware free-variable analysis of an expression. Nested function
|
|
@@ -507,11 +591,10 @@ function analyzeCaptures(expr) {
|
|
|
507
591
|
return;
|
|
508
592
|
case "AwaitExpression":
|
|
509
593
|
case "YieldExpression":
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// Deferred occurrences run per call
|
|
514
|
-
// — hoisting does not change when they evaluate.
|
|
594
|
+
// Eager occurrences cannot be moved to module scope: hoisting would
|
|
595
|
+
// emit top-level await (broken in CJS output) or an orphaned yield.
|
|
596
|
+
// (Stricter than the babel plugin, whose suite never exercises
|
|
597
|
+
// these.) Deferred occurrences run per call — fine to move.
|
|
515
598
|
if (!deferred) {
|
|
516
599
|
impure = true;
|
|
517
600
|
return;
|
|
@@ -594,64 +677,208 @@ const NON_BINDING_KEYWORDS = "if|for|while|switch|return|typeof|await|yield|new|
|
|
|
594
677
|
/**
|
|
595
678
|
* Conservative shadow detection: collect every identifier appearing in a
|
|
596
679
|
* binding-like position — function/method/arrow parameter lists, catch
|
|
597
|
-
* clauses, const/let/var declarator
|
|
598
|
-
* information, so an import referenced from a hoisted expression
|
|
599
|
-
* resolve to that import everywhere; a name that is ever re-bound
|
|
600
|
-
* trusted and disqualifies hoists referencing it.
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
680
|
+
* clauses, class names, const/let/var declarator patterns. The scanner has
|
|
681
|
+
* no scope information, so an import referenced from a hoisted expression
|
|
682
|
+
* must resolve to that import everywhere; a name that is ever re-bound
|
|
683
|
+
* cannot be trusted and disqualifies hoists referencing it.
|
|
684
|
+
*
|
|
685
|
+
* Collection is depth-aware and multiline (balanced delimiter scanning, not
|
|
686
|
+
* line-bounded regexes): multiline destructuring declarations and parameter
|
|
687
|
+
* defaults containing calls are real production patterns whose bindings a
|
|
688
|
+
* line-based collector misses — and a missed binding here used to mean a
|
|
689
|
+
* hoist referencing it crashed at module load (`ReferenceError`).
|
|
690
|
+
* Over-collection (type names, destructuring default values, for-of
|
|
691
|
+
* iterables) is deliberate and safe: it can only suppress a hoist.
|
|
604
692
|
*/
|
|
605
693
|
function collectBoundNames(code) {
|
|
606
694
|
const bound = new Set();
|
|
607
|
-
// Per comma
|
|
608
|
-
//
|
|
695
|
+
// Per top-level-comma piece, keep only the binding side: names before any
|
|
696
|
+
// top-level `:` (type annotation) or `=` (default/initializer). Inside
|
|
697
|
+
// destructuring patterns the `:`/`=` sit at depth > 0, so the whole
|
|
698
|
+
// pattern is collected (renames and default values over-collect — safe).
|
|
609
699
|
const addBindingSegment = (segment) => {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
if (eq !== -1)
|
|
617
|
-
cut = cut.slice(0, eq);
|
|
618
|
-
for (const id of cut.matchAll(/[A-Za-z_$][\w$]*/g)) {
|
|
700
|
+
let depth = 0;
|
|
701
|
+
let pieceStart = 0;
|
|
702
|
+
let cut = -1;
|
|
703
|
+
const flush = (end) => {
|
|
704
|
+
const prefix = segment.slice(pieceStart, cut === -1 ? end : cut);
|
|
705
|
+
for (const id of prefix.matchAll(/[A-Za-z_$][\w$]*/g)) {
|
|
619
706
|
bound.add(id[0]);
|
|
620
707
|
}
|
|
708
|
+
cut = -1;
|
|
709
|
+
};
|
|
710
|
+
for (let i = 0; i < segment.length; i++) {
|
|
711
|
+
const ch = segment[i];
|
|
712
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
713
|
+
depth++;
|
|
714
|
+
}
|
|
715
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
716
|
+
depth--;
|
|
717
|
+
}
|
|
718
|
+
else if (depth === 0 && ch === ",") {
|
|
719
|
+
flush(i);
|
|
720
|
+
pieceStart = i + 1;
|
|
721
|
+
}
|
|
722
|
+
else if (depth === 0 && cut === -1 && (ch === ":" || ch === "=")) {
|
|
723
|
+
cut = i;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
flush(segment.length);
|
|
727
|
+
};
|
|
728
|
+
/** Balanced `( ... )` span starting at `open` (must point at `(`). */
|
|
729
|
+
const parenSpan = (open) => {
|
|
730
|
+
let depth = 0;
|
|
731
|
+
for (let i = open; i < code.length; i++) {
|
|
732
|
+
const ch = code[i];
|
|
733
|
+
if (ch === "(")
|
|
734
|
+
depth++;
|
|
735
|
+
else if (ch === ")") {
|
|
736
|
+
depth--;
|
|
737
|
+
if (depth === 0)
|
|
738
|
+
return { inner: code.slice(open + 1, i), end: i };
|
|
739
|
+
}
|
|
621
740
|
}
|
|
741
|
+
return null;
|
|
622
742
|
};
|
|
623
|
-
// function f(a, b) / function (a
|
|
624
|
-
for (const m of code.matchAll(/\bfunction\b
|
|
625
|
-
|
|
743
|
+
// function f(a, b) / function (a = call()) — balanced + multiline
|
|
744
|
+
for (const m of code.matchAll(/\bfunction\b/g)) {
|
|
745
|
+
let i = m.index + m[0].length;
|
|
746
|
+
while (i < code.length && code[i] !== "(" && code[i] !== "{" && code[i] !== ";")
|
|
747
|
+
i++;
|
|
748
|
+
if (code[i] !== "(")
|
|
749
|
+
continue;
|
|
750
|
+
const span = parenSpan(i);
|
|
751
|
+
if (span)
|
|
752
|
+
addBindingSegment(span.inner);
|
|
626
753
|
}
|
|
627
|
-
// (a, b) => / (a
|
|
628
|
-
for (const m of code.matchAll(
|
|
629
|
-
|
|
754
|
+
// (a, b) => / (a = call()): Ret => — balanced params located from the arrow
|
|
755
|
+
for (const m of code.matchAll(/=>/g)) {
|
|
756
|
+
let i = m.index - 1;
|
|
757
|
+
while (i >= 0 && /\s/.test(code[i]))
|
|
758
|
+
i--;
|
|
759
|
+
if (i < 0)
|
|
760
|
+
continue;
|
|
761
|
+
let closeAt = -1;
|
|
762
|
+
if (code[i] === ")") {
|
|
763
|
+
closeAt = i;
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
// Possible return-type annotation between `)` and `=>`:
|
|
767
|
+
// `(a): Promise<T> =>`. Find the nearest `)` whose gap to the arrow
|
|
768
|
+
// looks like a type annotation; give up otherwise (over-approximation
|
|
769
|
+
// elsewhere keeps this safe).
|
|
770
|
+
const before = code.slice(0, m.index);
|
|
771
|
+
const lastClose = before.lastIndexOf(")");
|
|
772
|
+
if (lastClose !== -1 && /^\s*:[^(){};]*$/.test(before.slice(lastClose + 1))) {
|
|
773
|
+
closeAt = lastClose;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (closeAt === -1)
|
|
777
|
+
continue;
|
|
778
|
+
// walk back to the matching `(`
|
|
779
|
+
let depth = 0;
|
|
780
|
+
for (let j = closeAt; j >= 0; j--) {
|
|
781
|
+
const ch = code[j];
|
|
782
|
+
if (ch === ")")
|
|
783
|
+
depth++;
|
|
784
|
+
else if (ch === "(") {
|
|
785
|
+
depth--;
|
|
786
|
+
if (depth === 0) {
|
|
787
|
+
addBindingSegment(code.slice(j + 1, closeAt));
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
630
792
|
}
|
|
631
793
|
// bare arrow param: a =>
|
|
632
794
|
for (const m of code.matchAll(/([A-Za-z_$][\w$]*)\s*=>/g)) {
|
|
633
795
|
bound.add(m[1] ?? "");
|
|
634
796
|
}
|
|
635
|
-
// method(a, b) { / method(a): Ret { — excluding control flow
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
797
|
+
// method(a, b) { / method(a = call()): Ret { — excluding control flow.
|
|
798
|
+
// The balanced span must be directly followed by `{` (after an optional
|
|
799
|
+
// return type), which call expressions essentially never are.
|
|
800
|
+
const methodAnchor = new RegExp(String.raw `(?<![.\w$])(?!(?:${NON_BINDING_KEYWORDS})\b)[A-Za-z_$][\w$]*\s*\(`, "g");
|
|
801
|
+
for (const m of code.matchAll(methodAnchor)) {
|
|
802
|
+
const span = parenSpan(m.index + m[0].length - 1);
|
|
803
|
+
if (!span)
|
|
804
|
+
continue;
|
|
805
|
+
const after = code.slice(span.end + 1);
|
|
806
|
+
if (/^\s*(?::[^{};()]*)?\{/.test(after))
|
|
807
|
+
addBindingSegment(span.inner);
|
|
639
808
|
}
|
|
640
809
|
// catch (e)
|
|
641
|
-
for (const m of code.matchAll(/\bcatch\s*\(
|
|
642
|
-
|
|
810
|
+
for (const m of code.matchAll(/\bcatch\s*\(/g)) {
|
|
811
|
+
const span = parenSpan(m.index + m[0].length - 1);
|
|
812
|
+
if (span)
|
|
813
|
+
addBindingSegment(span.inner);
|
|
814
|
+
}
|
|
815
|
+
// class names are TDZ bindings (function declarations hoist, classes don't)
|
|
816
|
+
for (const m of code.matchAll(/\bclass\s+([A-Za-z_$][\w$]*)/g)) {
|
|
817
|
+
bound.add(m[1] ?? "");
|
|
643
818
|
}
|
|
644
|
-
// const/let/var declarator
|
|
645
|
-
|
|
646
|
-
|
|
819
|
+
// const/let/var declarator patterns — balanced + multiline. The span runs
|
|
820
|
+
// to the first top-level `;` (or an unbalanced closer: for-headers), so
|
|
821
|
+
// `const {\n inputSchema,\n} = getSchemas();` collects inputSchema.
|
|
822
|
+
for (const m of code.matchAll(/\b(?:const|let|var)\b/g)) {
|
|
823
|
+
const start = m.index + m[0].length;
|
|
824
|
+
let depth = 0;
|
|
825
|
+
let end = code.length;
|
|
826
|
+
for (let i = start; i < code.length; i++) {
|
|
827
|
+
const ch = code[i];
|
|
828
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
829
|
+
depth++;
|
|
830
|
+
}
|
|
831
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
832
|
+
depth--;
|
|
833
|
+
if (depth < 0) {
|
|
834
|
+
end = i;
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else if (depth === 0 && ch === ";") {
|
|
839
|
+
end = i;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
addBindingSegment(code.slice(start, end));
|
|
647
844
|
}
|
|
648
845
|
return bound;
|
|
649
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Free-variable analysis of a hoisted expression's source text, for the
|
|
849
|
+
* build-time compile step. Returns null when the text does not parse (it
|
|
850
|
+
* always should — it was extracted by this module).
|
|
851
|
+
*/
|
|
852
|
+
export function analyzeHoistedExpression(text) {
|
|
853
|
+
try {
|
|
854
|
+
const parsed = parseAssignmentAt(text, 0);
|
|
855
|
+
const { eagerFree, deferredFree, impure } = analyzeCaptures(parsed);
|
|
856
|
+
if (impure)
|
|
857
|
+
return null;
|
|
858
|
+
return { eagerFree, deferredFree };
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
650
864
|
/**
|
|
651
865
|
* Hoist eligible Zod schema expressions to module scope.
|
|
652
866
|
* Returns the rewritten source, or null when nothing was hoisted.
|
|
653
867
|
*/
|
|
654
868
|
export function hoistZodSchemas(code, options) {
|
|
869
|
+
return hoistZodSchemasMeta(code, options)?.code ?? null;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* hoistZodSchemas + metadata about each hoisted declaration, so the
|
|
873
|
+
* transform can compile the hoisted schemas into optimized validators.
|
|
874
|
+
*/
|
|
875
|
+
export function hoistZodSchemasMeta(code, options) {
|
|
876
|
+
// Idempotency: a file carrying hoisted declarations IS this pass's output.
|
|
877
|
+
// Plain hoists are naturally inert on re-runs (depth-0 masking), but a
|
|
878
|
+
// compiled hoisted decl embeds its original z.* expression inside the IIFE
|
|
879
|
+
// (depth > 0) — re-hoisting it would emit a duplicate _zh_ declaration.
|
|
880
|
+
if (/\bconst _zh_[0-9a-f]{8} = /.test(code))
|
|
881
|
+
return null;
|
|
655
882
|
const imports = collectImportBindings(code);
|
|
656
883
|
if (imports.all.size === 0)
|
|
657
884
|
return null;
|
|
@@ -694,10 +921,7 @@ export function hoistZodSchemas(code, options) {
|
|
|
694
921
|
continue;
|
|
695
922
|
let parsed;
|
|
696
923
|
try {
|
|
697
|
-
parsed =
|
|
698
|
-
ecmaVersion: "latest",
|
|
699
|
-
sourceType: "module",
|
|
700
|
-
});
|
|
924
|
+
parsed = parseAssignmentAt(code, candidate.index);
|
|
701
925
|
}
|
|
702
926
|
catch {
|
|
703
927
|
continue;
|
|
@@ -705,14 +929,25 @@ export function hoistZodSchemas(code, options) {
|
|
|
705
929
|
let chain = narrowToCallChain(parsed, candidate.index);
|
|
706
930
|
if (!chain)
|
|
707
931
|
continue;
|
|
708
|
-
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
|
|
932
|
+
const rootIsZod = imports.zod.has(candidate.name);
|
|
933
|
+
// Masking (babel-plugin-zod-hoist nesting semantics):
|
|
934
|
+
// - Depth-0 chains are top-level statements/initializers — already
|
|
935
|
+
// evaluated once per module load, nothing to gain — and their interior
|
|
936
|
+
// must not hoist separately either. Exception: concise arrow bodies
|
|
937
|
+
// (`const make = () => z.object(...)`) re-run per call.
|
|
938
|
+
// - Zod-rooted chains mask their interior REGARDLESS of eligibility (an
|
|
939
|
+
// inner z.string() of `z.object({ a: z.string(), b: local })` belongs
|
|
940
|
+
// to the outer schema — babel's isNestedInZodCall).
|
|
941
|
+
// - Chains rooted elsewhere (`sql.type(...)`, `api.get(...)`,
|
|
942
|
+
// `Base.extend(...)`) mask their interior only when actually hoisted:
|
|
943
|
+
// a rejected outer chain leaves its arguments free, so the inner
|
|
944
|
+
// `z.object({...})` of `sql.type(z.object({...}))` hoists on its own.
|
|
945
|
+
if (candidate.depth === 0 && !candidate.afterArrow) {
|
|
946
|
+
consumedUntil = chain.end;
|
|
715
947
|
continue;
|
|
948
|
+
}
|
|
949
|
+
if (rootIsZod)
|
|
950
|
+
consumedUntil = chain.end;
|
|
716
951
|
// Peel trailing parse calls: for `z.object({...}).safeParse(input)`,
|
|
717
952
|
// hoist the construction and leave `.safeParse(input)` — with its
|
|
718
953
|
// local-variable arguments — at the call site.
|
|
@@ -728,11 +963,16 @@ export function hoistZodSchemas(code, options) {
|
|
|
728
963
|
}
|
|
729
964
|
if (chain === null || described === null)
|
|
730
965
|
continue;
|
|
966
|
+
// Tighten the zod-rooted mask to the peeled construction extent: the
|
|
967
|
+
// arguments of a peeled `.parse(...)` are not part of the schema, so
|
|
968
|
+
// candidates inside them stay free (babel traverses execution-method
|
|
969
|
+
// arguments normally).
|
|
970
|
+
if (rootIsZod)
|
|
971
|
+
consumedUntil = chain.end;
|
|
731
972
|
if (described.root !== candidate.name)
|
|
732
973
|
continue;
|
|
733
974
|
if (described.methods.some((m) => PARSE_METHODS.has(m)))
|
|
734
975
|
continue;
|
|
735
|
-
const rootIsZod = imports.zod.has(candidate.name);
|
|
736
976
|
const rootMatchesPattern = namePattern?.test(candidate.name) === true;
|
|
737
977
|
// Non-z bases must look like schema derivation: the chain must START
|
|
738
978
|
// with a combinator (`Base.extend({...}).optional()` qualifies via
|
|
@@ -746,24 +986,25 @@ export function hoistZodSchemas(code, options) {
|
|
|
746
986
|
const { eagerFree, deferredFree, impure } = analyzeCaptures(chain);
|
|
747
987
|
if (impure)
|
|
748
988
|
continue;
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
989
|
+
// Capture rule (babel-plugin-zod-hoist's canSafelyHoist, hardened): a
|
|
990
|
+
// free name is safe only when it is an import or a recognized standard
|
|
991
|
+
// global, and is never re-bound anywhere in the file (no scope info, so
|
|
992
|
+
// a name bound in ANY function cannot be trusted to mean the import —
|
|
993
|
+
// over-rejection only costs a missed hoist). The babel plugin also
|
|
994
|
+
// allows arbitrary unbound identifiers, but it has real scope analysis;
|
|
995
|
+
// here an unknown bare name is more likely a binding the lexical
|
|
996
|
+
// collector missed than a genuine global, and hoisting it would crash
|
|
997
|
+
// at module load (`ReferenceError: <name> is not defined`).
|
|
756
998
|
let eligible = true;
|
|
757
999
|
for (const name of eagerFree) {
|
|
758
|
-
if (!imports.all.has(name) || isShadowed(name)) {
|
|
1000
|
+
if ((!imports.all.has(name) && !KNOWN_GLOBALS.has(name)) || isShadowed(name)) {
|
|
759
1001
|
eligible = false;
|
|
760
1002
|
break;
|
|
761
1003
|
}
|
|
762
1004
|
}
|
|
763
1005
|
if (eligible) {
|
|
764
1006
|
for (const name of deferredFree) {
|
|
765
|
-
|
|
766
|
-
if (!allowed || isShadowed(name)) {
|
|
1007
|
+
if ((!imports.all.has(name) && !KNOWN_GLOBALS.has(name)) || isShadowed(name)) {
|
|
767
1008
|
eligible = false;
|
|
768
1009
|
break;
|
|
769
1010
|
}
|
|
@@ -797,6 +1038,9 @@ export function hoistZodSchemas(code, options) {
|
|
|
797
1038
|
declByText.set(text, declName);
|
|
798
1039
|
}
|
|
799
1040
|
hoists.push({ start: candidate.index, end: chain.end, name: declName });
|
|
1041
|
+
// Non-zod-rooted chains mask their interior only on success — the inner
|
|
1042
|
+
// parts are consumed by this hoist's replacement.
|
|
1043
|
+
consumedUntil = Math.max(consumedUntil, chain.end);
|
|
800
1044
|
}
|
|
801
1045
|
if (hoists.length === 0)
|
|
802
1046
|
return null;
|
|
@@ -810,6 +1054,9 @@ export function hoistZodSchemas(code, options) {
|
|
|
810
1054
|
.map(([text, name]) => `const ${name} = ${text};`)
|
|
811
1055
|
.join("\n");
|
|
812
1056
|
const offset = insertionOffset(result);
|
|
813
|
-
return
|
|
1057
|
+
return {
|
|
1058
|
+
code: `${result.slice(0, offset)}${decls}\n${result.slice(offset)}`,
|
|
1059
|
+
schemas: [...declByText.entries()].map(([text, name]) => ({ name, text })),
|
|
1060
|
+
};
|
|
814
1061
|
}
|
|
815
1062
|
//# sourceMappingURL=hoist.js.map
|