@amritk/generate-parsers 0.23.0 → 0.24.0

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.
@@ -22,7 +22,8 @@ const generateFile = (schema, typeName, options) => {
22
22
  selfFilename: options?.selfFilename,
23
23
  rootSchema,
24
24
  typeSuffix,
25
- importExt
25
+ importExt,
26
+ usedIn: typeDefinition
26
27
  });
27
28
  let result2 = "";
28
29
  if (imports2.length > 0) {
@@ -61,7 +62,16 @@ const generateFile = (schema, typeName, options) => {
61
62
  ${parserFunction}`;
62
63
  const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix, importExt);
63
64
  const imports = [
64
- ...collectImports(schema, { selfRef, selfFilename: options?.selfFilename, rootSchema, typeSuffix, importExt }),
65
+ ...collectImports(schema, {
66
+ selfRef,
67
+ selfFilename: options?.selfFilename,
68
+ rootSchema,
69
+ typeSuffix,
70
+ importExt,
71
+ // Both halves of the file: the type names the type definition spells and
72
+ // the functions the emitted code calls are one question about one text.
73
+ usedIn: typeDefinition + "\n\n" + combinedFunctions
74
+ }),
65
75
  ...helpers.imports
66
76
  ];
67
77
  let result = "";
@@ -1,6 +1,7 @@
1
1
  import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition";
3
3
  import { quoteJsString } from "@amritk/helpers/quote-js-string";
4
+ import { declaresKey, readKey } from "@amritk/helpers/read-key";
4
5
  import { refToName } from "@amritk/helpers/ref-to-name";
5
6
  import { resolveRef } from "@amritk/helpers/resolve-ref";
6
7
  import { hasOwnCheck, missingCheck, safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
@@ -208,7 +209,10 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
208
209
  }
209
210
  }
210
211
  result += "\n }";
211
- return declaresPrototypeMemberProperty(schema) ? `${result} as unknown as ${typeName}` : result;
212
+ if (declaresPrototypeMemberProperty(schema))
213
+ return `${result} as unknown as ${typeName}`;
214
+ const composesUnion = isSchemaObject(schema) && (hasOneOf(schema) || hasAnyOf(schema));
215
+ return composesUnion ? `${result} as ${typeName}` : result;
212
216
  };
213
217
  const SCALAR_ROOT_CONSTRAINTS = [
214
218
  "pattern",
@@ -1699,14 +1703,28 @@ const getConditionalObjectSchema = (schema) => {
1699
1703
  ...required.size > 0 ? { required: Array.from(required) } : {}
1700
1704
  };
1701
1705
  };
1702
- const generateSchemaObjectParser = (typeName) => {
1706
+ const OBJECT_SHAPE_KEYWORDS = ["properties", "patternProperties", "additionalProperties", "required"];
1707
+ const DELEGATING_KEYWORDS = ["$ref", "$dynamicRef", "allOf", "anyOf", "oneOf", "if", "not"];
1708
+ const admitsBooleanSchema = (schema) => {
1709
+ if (typeof schema === "boolean")
1710
+ return true;
1711
+ if (!isSchemaObject(schema))
1712
+ return false;
1713
+ const record = schema;
1714
+ const type = readKey(record, "type");
1715
+ if (type !== void 0)
1716
+ return Array.isArray(type) ? type.includes("boolean") : type === "boolean";
1717
+ return ![...OBJECT_SHAPE_KEYWORDS, ...DELEGATING_KEYWORDS].some((keyword) => declaresKey(record, keyword));
1718
+ };
1719
+ const generateSchemaObjectParser = (typeName, schema) => {
1703
1720
  const functionName = generateParserName(typeName);
1704
- return `export const ${functionName} = (input: unknown): ${typeName} => {
1705
- if (typeof input === 'boolean') {
1721
+ const booleanBranch = admitsBooleanSchema(schema) ? ` if (typeof input === 'boolean') {
1706
1722
  return input as ${typeName};
1707
1723
  }
1708
1724
 
1709
- if (!isObject(input)) {
1725
+ ` : "";
1726
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
1727
+ ${booleanBranch} if (!isObject(input)) {
1710
1728
  return {} as ${typeName};
1711
1729
  }
1712
1730
 
@@ -1730,7 +1748,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1730
1748
  };
1731
1749
  const copyLoopContext = { ...context, subParsers: false };
1732
1750
  if (!options?.isRoot && typeName === `Schema${suffix}`) {
1733
- return generateSchemaObjectParser(typeName);
1751
+ return generateSchemaObjectParser(typeName, schema);
1734
1752
  }
1735
1753
  const isObjectLikeSchema = isObjectSchema(schema) || isSchemaObject(schema) && ("patternProperties" in schema || "additionalProperties" in schema);
1736
1754
  if (!isObjectLikeSchema && !isSchemaObject(schema)) {
@@ -42,6 +42,17 @@ type CollectImportsOptions = {
42
42
  * type stripping.
43
43
  */
44
44
  readonly importExt?: ImportExtension;
45
+ /**
46
+ * The file body this import list is being written above. When given, only the
47
+ * `$ref` imports whose names the body actually spells are emitted, and one
48
+ * whose parser is never called is narrowed to a type-only import.
49
+ *
50
+ * The walk below can only approximate what the emitters wrote: it stops at a
51
+ * `$ref` the type emitter inlines *through*, and it cannot see a branch that
52
+ * folded away. Asking the emitted text settles both directions, the way
53
+ * `collectHelpers` already settles which runtime helpers a file needs.
54
+ */
55
+ readonly usedIn?: string | undefined;
45
56
  };
46
57
  /**
47
58
  * Collects all import statements needed for a schema by finding $ref references
@@ -1,16 +1,28 @@
1
+ import { identifierMentions } from "@amritk/helpers/identifier-mentions";
2
+ import { readKey } from "@amritk/helpers/read-key";
1
3
  import { refToFilename } from "@amritk/helpers/ref-to-filename";
2
4
  import { refToName } from "@amritk/helpers/ref-to-name";
5
+ import { referencedConditional } from "@amritk/helpers/referenced-conditional";
3
6
  import { resolveRef } from "@amritk/helpers/resolve-ref";
4
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from "@amritk/helpers/schema-guards";
7
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef, isSchemaObject } from "@amritk/helpers/schema-guards";
5
8
  const getImportPathForFilename = (filename, ext) => `./${filename}.${ext}`;
6
9
  const collectImports = (schema, options) => {
7
10
  const typesOnly = options?.typesOnly === true;
8
11
  const importExt = options?.importExt ?? "js";
9
12
  const importMap = /* @__PURE__ */ new Map();
13
+ const mentions = options?.usedIn === void 0 ? () => true : identifierMentions(options.usedIn);
10
14
  for (const [filename, { typeName, typeOnly }] of collectImportTargets(schema, options)) {
11
15
  const importPath = getImportPathForFilename(filename, importExt);
12
- const importStatement = typesOnly || typeOnly ? `import type { ${typeName} } from '${importPath}';` : `import { type ${typeName}, parse${typeName}, validate${typeName}Shape } from '${importPath}';`;
13
- importMap.set(filename, importStatement);
16
+ const values = typesOnly || typeOnly ? [] : [`parse${typeName}`, `validate${typeName}Shape`].filter((name) => mentions(name));
17
+ const named = mentions(typeName);
18
+ if (values.length === 0) {
19
+ if (!named)
20
+ continue;
21
+ importMap.set(filename, `import type { ${typeName} } from '${importPath}';`);
22
+ continue;
23
+ }
24
+ const specifiers = named ? [`type ${typeName}`, ...values] : values;
25
+ importMap.set(filename, `import { ${specifiers.join(", ")} } from '${importPath}';`);
14
26
  }
15
27
  return Array.from(importMap.values()).sort();
16
28
  };
@@ -27,6 +39,24 @@ const collectImportTargets = (schema, options) => {
27
39
  const refs = /* @__PURE__ */ new Set();
28
40
  const valueRefs = /* @__PURE__ */ new Set();
29
41
  let typeOnlyDepth = 0;
42
+ const inlinedRefs = /* @__PURE__ */ new Set();
43
+ const collectRefsFromAllOfMember = (entry) => {
44
+ collectRefsFromValue(entry);
45
+ const ref = isSchemaObject(entry) ? readKey(entry, "$ref") : void 0;
46
+ if (typeof ref === "string" && inlinedRefs.has(ref))
47
+ return;
48
+ const inlined = referencedConditional(entry, rootSchema);
49
+ if (inlined === void 0)
50
+ return;
51
+ if (typeof ref === "string")
52
+ inlinedRefs.add(ref);
53
+ typeOnlyDepth++;
54
+ for (const arm of ["if", "then", "else"]) {
55
+ if (Object.hasOwn(inlined, arm))
56
+ collectRefsFromValue(readKey(inlined, arm));
57
+ }
58
+ typeOnlyDepth--;
59
+ };
30
60
  const collectRefsFromValue = (value) => {
31
61
  if (typeof value !== "object" || value === null) {
32
62
  return;
@@ -87,7 +117,7 @@ const collectImportTargets = (schema, options) => {
87
117
  }
88
118
  if (hasAllOf(record)) {
89
119
  for (const item of record.allOf) {
90
- collectRefsFromValue(item);
120
+ collectRefsFromAllOfMember(item);
91
121
  }
92
122
  }
93
123
  if (hasItems(record) && !Array.isArray(record.items)) {
@@ -163,7 +193,7 @@ const collectImportTargets = (schema, options) => {
163
193
  }
164
194
  if (typeof schema === "object" && schema !== null && hasAllOf(schema)) {
165
195
  for (const item of schema.allOf) {
166
- collectRefsFromValue(item);
196
+ collectRefsFromAllOfMember(item);
167
197
  }
168
198
  }
169
199
  if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "if")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -60,10 +60,10 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "json-schema-typed": "^8.0.1",
63
- "@amritk/helpers": "^0.21.0"
63
+ "@amritk/helpers": "^0.22.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@amritk/generate-markdown": "^0.7.0",
66
+ "@amritk/generate-markdown": "^0.8.0",
67
67
  "@sinclair/typebox": "^0.34.49",
68
68
  "ajv": "^8.17.1",
69
69
  "benny": "^3.7.1",