@amritk/generate-validators 0.12.2 → 0.13.1

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/AI.md CHANGED
@@ -40,7 +40,8 @@ const files = await buildValidatorSchema(schema, 'Document')
40
40
  validator accepts. `unevaluatedProperties`/`unevaluatedItems` *are* generated;
41
41
  four shapes still refuse (coverage through a `$dynamicRef`, an unresolvable or
42
42
  cyclic `$ref` at the same instance location, a walk deeper than eight
43
- applicators, a node under `additionalItems`), and generation **throws** for
44
- those rather than widening the verdict.
43
+ applicators, a node under an *inert* `additionalItems` one with no array
44
+ `items`, or with `prefixItems` alongside), and generation **throws** for those
45
+ rather than widening the verdict.
45
46
 
46
47
  Only the `.` entry. Install: `bun add @amritk/generate-validators`.
package/README.md CHANGED
@@ -160,7 +160,10 @@ conjunct of a validator that also asserts them — while conditional applicators
160
160
  condition, hoisted out of the per-key loop. Four shapes still refuse, each named as
161
161
  a shape rather than as a keyword: coverage running through a `$dynamicRef`, an
162
162
  unresolvable or cyclic `$ref` at the same instance location, a walk deeper than
163
- eight applicators, and a node under `additionalItems`.
163
+ eight applicators, and a node under an *inert* `additionalItems` — one with no
164
+ array `items` to be the tail of, or with a `prefixItems` that took the positions
165
+ out from under it. The draft-07 tail itself is validated, so the draft-07
166
+ spelling of a schema whose 2020-12 spelling generates is accepted too.
164
167
 
165
168
  One edge worth calling out: **`NaN` fails a constrained number but satisfies an
166
169
  unconstrained one.** Every bound is emitted as the negated *pass* condition
@@ -180,7 +183,7 @@ official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema
180
183
  (the required Draft 2020-12 tests — 1281 cases), compiles and links the emitted
181
184
  files in memory, and runs the suite's instances through the real generated code:
182
185
 
183
- **1271 / 1281 cases pass (99.2%).**
186
+ **1274 / 1281 cases pass (99.5%).**
184
187
 
185
188
  The suite's `remotes/` documents and the 2020-12 dialect metaschema are supplied
186
189
  through the `schemas` option, which is how the suite intends a validator that does
@@ -188,12 +191,11 @@ no I/O to answer the retrieval step. Everything else — applying the base URIs,
188
191
  walking anchors across documents, naming and emitting a file per definition — the
189
192
  generator still has to do.
190
193
 
191
- Of the 10 that do not pass: five `$dynamicRef`s whose binding depends on the
194
+ Of the 7 that do not pass: four `$dynamicRef`s whose binding depends on the
192
195
  evaluation path (a generator emits one function per definition, shared by every
193
196
  path that reaches it, so it cannot bind per path), two definitions in different
194
- embedded resources that reduce to one filename, two `$id`-scoped in-document
195
- pointers, and `$vocabulary`. Nothing on the list is a keyword that silently
196
- returns the wrong answer.
197
+ embedded resources that reduce to one filename, and `$vocabulary`. Nothing on the
198
+ list is a keyword that silently returns the wrong answer.
197
199
 
198
200
  Every case is named in
199
201
  `src/generators/conformance-expected-failures.test-utils.ts` with its reason, and
@@ -13,7 +13,7 @@ const SINGLE_SUBSCHEMA_KEYS = [
13
13
  ];
14
14
  const SUBSCHEMA_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
15
15
  const SUBSCHEMA_MAP_KEYS = ["properties", "patternProperties", "dependentSchemas", "dependencies"];
16
- const UNENFORCED_SUBSCHEMA_KEYS = ["additionalItems"];
16
+ const additionalItemsIsEnforced = (node) => Array.isArray(node["items"]) && !Array.isArray(node["prefixItems"]);
17
17
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18
18
  const check = (node, typeName, rootSchema, match, enforced) => {
19
19
  const record = node;
@@ -59,10 +59,8 @@ const assertUnevaluatedGeneratable = (schema, typeName, rootSchema, match) => {
59
59
  if (!Array.isArray(entry))
60
60
  visit(entry, enforced);
61
61
  }
62
- for (const key of UNENFORCED_SUBSCHEMA_KEYS) {
63
- if (key in node)
64
- visit(node[key], false);
65
- }
62
+ if ("additionalItems" in node)
63
+ visit(node["additionalItems"], enforced && additionalItemsIsEnforced(node));
66
64
  };
67
65
  visit(schema, true);
68
66
  };
@@ -6,6 +6,12 @@ export type GeneratedFile = {
6
6
  filename: string;
7
7
  content: string;
8
8
  };
9
+ /**
10
+ * The runtime contract every generated validator imports: the `ValidationResult`
11
+ * types plus the helpers emitted code calls as free identifiers. Exported so tests
12
+ * can evaluate the very source that ships instead of reimplementing it.
13
+ */
14
+ export declare const VALIDATION_RESULT_CONTENT = "/**\n * A single validation error with a human-readable message and a JSON Pointer\n * path indicating where in the document the error occurred.\n */\nexport type ValidationError = {\n message: string\n path: string\n}\n\n/**\n * The result of a generated validator function.\n * Returns `true` when the input is valid, or an object with `valid: false`\n * and a list of errors when it is not.\n */\nexport type ValidationResult = true | { valid: false; errors: ValidationError[] }\n\n/**\n * Structural deep equality used by generated `const` checks. Objects compare by\n * their key sets rather than serialization, so `{ a: 1, b: 2 }` and\n * `{ b: 2, a: 1 }` are equal \u2014 unlike `JSON.stringify`, which is key-order\n * sensitive and would reject a reordered-but-equal value.\n */\nexport const valuesEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false\n const aArray = Array.isArray(a)\n const bArray = Array.isArray(b)\n if (aArray !== bArray) return false\n if (aArray) {\n const aa = a as unknown[]\n const bb = b as unknown[]\n if (aa.length !== bb.length) return false\n for (let i = 0; i < aa.length; i++) if (!valuesEqual(aa[i], bb[i])) return false\n return true\n }\n const ao = a as Record<string, unknown>\n const bo = b as Record<string, unknown>\n const keys = Object.keys(ao)\n if (keys.length !== Object.keys(bo).length) return false\n for (const key of keys) {\n if (!Object.hasOwn(bo, key) || !valuesEqual(ao[key], bo[key])) return false\n }\n return true\n}\n\n/**\n * True when every element of `arr` is distinct under structural equality\n * ({@link valuesEqual}). Backs generated `uniqueItems` checks whose items may be\n * objects or arrays, where a `JSON.stringify` dedupe key would be key-order\n * sensitive and let a reordered-but-equal duplicate (`{ a: 1, b: 2 }` vs\n * `{ b: 2, a: 1 }`) slip through. A native `Set` dedupes the all-primitive case\n * in one linear pass; object/array elements fall back to an exact pairwise\n * structural comparison.\n */\nexport const allUnique = (arr: readonly unknown[]): boolean => {\n const len = arr.length\n if (len < 2) return true\n let allPrimitive = true\n for (let i = 0; i < len; i++) {\n const v = arr[i]\n if (v !== null && typeof v === 'object') {\n allPrimitive = false\n break\n }\n }\n if (allPrimitive) return new Set(arr).size === len\n for (let i = 0; i < len; i++) {\n for (let j = i + 1; j < len; j++) {\n if (valuesEqual(arr[i], arr[j])) return false\n }\n }\n return true\n}\n\n/**\n * Escapes one JSON Pointer segment (RFC 6901): `~` \u2192 `~0`, `/` \u2192 `~1`, in that\n * order. Generated error paths are built from *runtime* keys wherever the schema\n * did not name them \u2014 a `patternProperties` match, an `additionalProperties`\n * sweep, a `propertyNames` loop \u2014 and a key containing a `/` would otherwise read\n * back as two segments, so an error on `{\"a/b\": \u2026}` pointed at `/a/b`, which is\n * the child `b` of a property `a`. Keys the schema *does* name are escaped at\n * generation time instead, and `@amritk/runtime-validators` escapes the same way,\n * so all three agree.\n *\n * The `indexOf` pre-test keeps the common key \u2014 no `/`, no `~` \u2014 off the replace\n * path entirely, which is what the interpreter does for the same reason.\n */\nexport const escapePointer = (key: string): string =>\n key.indexOf('/') !== -1 || key.indexOf('~') !== -1 ? key.replace(/~/g, '~0').replace(/\\//g, '~1') : key\n";
9
15
  /**
10
16
  * Builds all TypeScript validator files from a JSON Schema by traversing all
11
17
  * `$ref` / `$dynamicRef` references recursively (via the shared
@@ -74,12 +74,31 @@ export const allUnique = (arr: readonly unknown[]): boolean => {
74
74
  }
75
75
  return true
76
76
  }
77
+
78
+ /**
79
+ * Escapes one JSON Pointer segment (RFC 6901): \`~\` \u2192 \`~0\`, \`/\` \u2192 \`~1\`, in that
80
+ * order. Generated error paths are built from *runtime* keys wherever the schema
81
+ * did not name them \u2014 a \`patternProperties\` match, an \`additionalProperties\`
82
+ * sweep, a \`propertyNames\` loop \u2014 and a key containing a \`/\` would otherwise read
83
+ * back as two segments, so an error on \`{"a/b": \u2026}\` pointed at \`/a/b\`, which is
84
+ * the child \`b\` of a property \`a\`. Keys the schema *does* name are escaped at
85
+ * generation time instead, and \`@amritk/runtime-validators\` escapes the same way,
86
+ * so all three agree.
87
+ *
88
+ * The \`indexOf\` pre-test keeps the common key \u2014 no \`/\`, no \`~\` \u2014 off the replace
89
+ * path entirely, which is what the interpreter does for the same reason.
90
+ */
91
+ export const escapePointer = (key: string): string =>
92
+ key.indexOf('/') !== -1 || key.indexOf('~') !== -1 ? key.replace(/~/g, '~0').replace(/\\//g, '~1') : key
77
93
  `;
78
94
  const buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "", schemas) => {
79
95
  const files = [];
80
96
  walkRefGraph(rootSchema, rootTypeName, { typeSuffix, ...schemas !== void 0 ? { schemas } : {} }, (node) => {
81
- if (node.filename === "validation-result" || node.filename === "index")
82
- return;
97
+ if (node.filename === "validation-result" || node.filename === "index") {
98
+ const owner = node.isRoot ? `the root type "${node.typeName}"` : `"${node.ref}"`;
99
+ const purpose = node.filename === "index" ? "the generated barrel" : "the generated validators' runtime contract";
100
+ throw new Error(`${owner} generates the file "${node.filename}.ts", which is reserved for ${purpose}. Rename the definition (or pass a different root type name) so it gets a file of its own.`);
101
+ }
83
102
  const content = generateValidatorFile(node.schema, node.typeName, {
84
103
  rootSchema: node.rootSchema,
85
104
  typeSuffix,
@@ -92,5 +111,6 @@ const buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "", s
92
111
  return files;
93
112
  };
94
113
  export {
114
+ VALIDATION_RESULT_CONTENT,
95
115
  buildValidatorSchema
96
116
  };
@@ -23,4 +23,4 @@
23
23
  * root document there is nothing to resolve the target against, so the extra
24
24
  * refs are simply not collected.
25
25
  */
26
- export declare const collectEmittedRefs: (value: unknown, refs?: string[], rootSchema?: Record<string, unknown>) => string[];
26
+ export declare const collectEmittedRefs: (value: unknown, refs?: string[], rootSchema?: Record<string, unknown>, includeTypeOnly?: boolean) => string[];
@@ -1,10 +1,26 @@
1
+ import { isSchemaObject } from "@amritk/helpers/schema-guards";
2
+ import { foldsToConstant } from "./folds-to-constant.js";
3
+ import { tupleShapeOf } from "./tuple-shape.js";
1
4
  import { unevaluatedItemsExpr, unevaluatedPropertiesExpr } from "./unevaluated-match.js";
2
- const collectEmittedRefs = (value, refs = [], rootSchema) => {
5
+ const armsOf = (schema) => {
6
+ if (!("if" in schema))
7
+ return [];
8
+ const condition = schema["if"];
9
+ if (!isSchemaObject(condition) && typeof condition !== "boolean")
10
+ return [];
11
+ const decided = foldsToConstant(condition);
12
+ if (decided === true)
13
+ return ["then"];
14
+ if (decided === false)
15
+ return ["else"];
16
+ return ["then", "else"];
17
+ };
18
+ const collectEmittedRefs = (value, refs = [], rootSchema, includeTypeOnly = false) => {
3
19
  if (typeof value !== "object" || value === null)
4
20
  return refs;
5
21
  if (Array.isArray(value)) {
6
22
  for (const item of value)
7
- collectEmittedRefs(item, refs, rootSchema);
23
+ collectEmittedRefs(item, refs, rootSchema, includeTypeOnly);
8
24
  return refs;
9
25
  }
10
26
  const schema = value;
@@ -13,23 +29,36 @@ const collectEmittedRefs = (value, refs = [], rootSchema) => {
13
29
  }
14
30
  if (typeof schema["$ref"] === "string") {
15
31
  refs.push(schema["$ref"]);
16
- return refs;
17
32
  }
18
33
  for (const mapKey of ["properties", "patternProperties", "dependentSchemas", "dependencies"]) {
19
34
  const map = schema[mapKey];
20
35
  if (typeof map === "object" && map !== null && !Array.isArray(map)) {
21
36
  for (const sub of Object.values(map))
22
- collectEmittedRefs(sub, refs, rootSchema);
37
+ collectEmittedRefs(sub, refs, rootSchema, includeTypeOnly);
38
+ }
39
+ }
40
+ const { tuple, tail } = tupleShapeOf(schema);
41
+ if (tail !== void 0)
42
+ collectEmittedRefs(tail, refs, rootSchema, includeTypeOnly);
43
+ if (includeTypeOnly && Array.isArray(schema["items"])) {
44
+ const additional = schema["additionalItems"];
45
+ if (additional !== void 0 && additional !== tail) {
46
+ collectEmittedRefs(additional, refs, rootSchema, includeTypeOnly);
23
47
  }
24
48
  }
25
49
  for (const key of [
26
- "items",
27
50
  "additionalProperties",
28
51
  "propertyNames",
29
52
  "contains",
30
53
  "if",
31
- "then",
32
- "else",
54
+ // `then` / `else` only apply when there is an `if` to branch on, and neither
55
+ // emitter reads them without one — so a `$ref` there is never referenced,
56
+ // and collecting it refused schemas whose ref happened to be unresolvable.
57
+ //
58
+ // An `if` the emitter can decide picks its arm here too, and unlike `anyOf`
59
+ // the type generator reads neither arm — it types the whole node `unknown` —
60
+ // so the dropped arm is referenced by nobody. See {@link armsOf}.
61
+ ...armsOf(schema),
33
62
  "not",
34
63
  // The `unevaluated*` subschemas are validated against the leftover keys /
35
64
  // indices, so a `$ref` inside one becomes a `validateX(...)` call like any
@@ -38,15 +67,18 @@ const collectEmittedRefs = (value, refs = [], rootSchema) => {
38
67
  "unevaluatedItems"
39
68
  ]) {
40
69
  if (key in schema)
41
- collectEmittedRefs(schema[key], refs, rootSchema);
70
+ collectEmittedRefs(schema[key], refs, rootSchema, includeTypeOnly);
42
71
  }
43
- for (const key of ["oneOf", "anyOf", "allOf", "prefixItems"]) {
72
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
44
73
  const list = schema[key];
45
- if (Array.isArray(list)) {
46
- for (const sub of list)
47
- collectEmittedRefs(sub, refs, rootSchema);
48
- }
74
+ if (!Array.isArray(list))
75
+ continue;
76
+ for (const sub of list)
77
+ collectEmittedRefs(sub, refs, rootSchema, includeTypeOnly);
49
78
  }
79
+ if (tuple !== void 0)
80
+ for (const sub of tuple)
81
+ collectEmittedRefs(sub, refs, rootSchema, includeTypeOnly);
50
82
  return refs;
51
83
  };
52
84
  const collectCoverageRefs = (schema, refs, rootSchema) => {
@@ -8,19 +8,15 @@ const buildImport = (ref, suffix) => {
8
8
  const validatorName = `validate${typeName}`;
9
9
  return `import { type ${typeName}, ${validatorName} } from './${filename}.js'`;
10
10
  };
11
- const canonicalFilename = (ref) => {
12
- const base = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
13
- return refToFilename(base);
14
- };
15
11
  const collectValidatorImports = (schema, options) => {
16
12
  const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
17
13
  const rootSchema = options?.rootSchema;
18
14
  const typeSuffix = options?.typeSuffix ?? "";
19
- const refs = collectEmittedRefs(schema, [], rootSchema);
15
+ const refs = collectEmittedRefs(schema, [], rootSchema, true);
20
16
  const seen = /* @__PURE__ */ new Set();
21
17
  const imports = [];
22
18
  for (const ref of refs) {
23
- const filename = canonicalFilename(ref);
19
+ const filename = refToFilename(ref);
24
20
  if (seen.has(filename))
25
21
  continue;
26
22
  if (selfFilename && filename === selfFilename)
@@ -31,8 +27,7 @@ const collectValidatorImports = (schema, options) => {
31
27
  continue;
32
28
  }
33
29
  seen.add(filename);
34
- const importRef = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
35
- imports.push(buildImport(importRef, typeSuffix));
30
+ imports.push(buildImport(ref, typeSuffix));
36
31
  }
37
32
  return imports;
38
33
  };
@@ -0,0 +1,21 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Every keyword this generator turns into a runtime check. Annotations
4
+ * (`title`, `description`, `default`, `$defs`, `format`, …) are deliberately
5
+ * absent: they change no verdict, so a node carrying only those is still "just"
6
+ * whatever its one validation keyword says.
7
+ */
8
+ export declare const ENFORCED_KEYWORDS: Set<string>;
9
+ /**
10
+ * True when a node declares an enforced keyword outside the set an emitter
11
+ * `owns`.
12
+ *
13
+ * The specialised emitters — the top-level `$ref` delegation, the `const` and
14
+ * `enum` roots, the flat boolean guards — each recognise one keyword and write
15
+ * out a shape built around it. That shape has nowhere to put a sibling, so every
16
+ * one of them used to drop the siblings silently: `{ $ref, minLength: 3 }`
17
+ * accepted `"q"`, `{ type: 'string', const: 1 }` accepted `1`. Asking this first
18
+ * lets each of them keep its tight output for the node it really does describe,
19
+ * and hand anything richer to the general path.
20
+ */
21
+ export declare const declaresKeywordOutside: (schema: JSONSchema, owned: readonly string[]) => boolean;
@@ -0,0 +1,60 @@
1
+ import { MJST_EXTENSION_KEY } from "@amritk/helpers/mjst-extension";
2
+ import { isSchemaObject } from "@amritk/helpers/schema-guards";
3
+ const ENFORCED_KEYWORDS = /* @__PURE__ */ new Set([
4
+ "$ref",
5
+ "type",
6
+ "enum",
7
+ "const",
8
+ MJST_EXTENSION_KEY,
9
+ "pattern",
10
+ "minLength",
11
+ "maxLength",
12
+ "minimum",
13
+ "maximum",
14
+ "exclusiveMinimum",
15
+ "exclusiveMaximum",
16
+ "multipleOf",
17
+ "items",
18
+ "prefixItems",
19
+ "additionalItems",
20
+ "contains",
21
+ "minContains",
22
+ "maxContains",
23
+ "minItems",
24
+ "maxItems",
25
+ "uniqueItems",
26
+ "properties",
27
+ "patternProperties",
28
+ "additionalProperties",
29
+ "required",
30
+ "propertyNames",
31
+ "dependentRequired",
32
+ "dependentSchemas",
33
+ "dependencies",
34
+ "minProperties",
35
+ "maxProperties",
36
+ "allOf",
37
+ "anyOf",
38
+ "oneOf",
39
+ "not",
40
+ "if",
41
+ "then",
42
+ "else",
43
+ "unevaluatedProperties",
44
+ "unevaluatedItems"
45
+ ]);
46
+ const declaresKeywordOutside = (schema, owned) => {
47
+ if (!isSchemaObject(schema))
48
+ return false;
49
+ for (const keyword of Object.keys(schema)) {
50
+ if (owned.includes(keyword))
51
+ continue;
52
+ if (ENFORCED_KEYWORDS.has(keyword))
53
+ return true;
54
+ }
55
+ return false;
56
+ };
57
+ export {
58
+ ENFORCED_KEYWORDS,
59
+ declaresKeywordOutside
60
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Whether a subschema's verdict is decidable without looking at the instance —
3
+ * `true` when it accepts everything, `false` when it accepts nothing, and
4
+ * `undefined` when it depends on the value.
5
+ *
6
+ * The emitter asks the same question by building the subschema's checks and
7
+ * seeing whether any came out (`generateMatchesExpr` returns the literal
8
+ * `'true'`), and folds the branch away when they did not. Anything reading the
9
+ * emitter's *output* — which arms it emitted, which `validateX` calls it made —
10
+ * has to agree with it, and a predicate that under-answers costs only a branch
11
+ * kept alive, while one that over-answers drops a call that really is emitted.
12
+ * Hence the conservative `undefined`: this recognises the spellings that are
13
+ * decidable from the node alone and declines everything else.
14
+ */
15
+ export declare const foldsToConstant: (schema: unknown) => boolean | undefined;
@@ -0,0 +1,39 @@
1
+ import { isSchemaObject } from "@amritk/helpers/schema-guards";
2
+ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
3
+ "$anchor",
4
+ "$comment",
5
+ "$defs",
6
+ "$dynamicAnchor",
7
+ "$id",
8
+ "$schema",
9
+ "$vocabulary",
10
+ "contentEncoding",
11
+ "contentMediaType",
12
+ "contentSchema",
13
+ "default",
14
+ "definitions",
15
+ "deprecated",
16
+ "description",
17
+ "discriminator",
18
+ "example",
19
+ "examples",
20
+ "externalDocs",
21
+ "format",
22
+ "nullable",
23
+ "readOnly",
24
+ "title",
25
+ "writeOnly",
26
+ "xml"
27
+ ]);
28
+ const foldsToConstant = (schema) => {
29
+ if (schema === true)
30
+ return true;
31
+ if (schema === false)
32
+ return false;
33
+ if (!isSchemaObject(schema))
34
+ return void 0;
35
+ return Object.keys(schema).every((key) => ANNOTATION_KEYWORDS.has(key)) ? true : void 0;
36
+ };
37
+ export {
38
+ foldsToConstant
39
+ };
@@ -14,10 +14,11 @@ const generateValidatorFile = (schema, typeName, options) => {
14
14
  });
15
15
  const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix, options?.rootSchema);
16
16
  const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix);
17
- let result = `import type { ValidationResult, ValidationError } from './validation-result.js'
18
- `;
19
17
  const body = validatorFunction + booleanGuard;
20
- const runtimeHelpers = ["valuesEqual", "allUnique"].filter((name) => body.includes(`${name}(`));
18
+ const resultTypes = ["ValidationResult", .../\bValidationError\b/.test(body) ? ["ValidationError"] : []];
19
+ let result = `import type { ${resultTypes.join(", ")} } from './validation-result.js'
20
+ `;
21
+ const runtimeHelpers = ["valuesEqual", "allUnique", "escapePointer"].filter((name) => body.includes(`${name}(`));
21
22
  if (runtimeHelpers.length > 0) {
22
23
  result += `import { ${runtimeHelpers.join(", ")} } from './validation-result.js'
23
24
  `;