@amritk/helpers
Advanced tools
@@ -38,6 +38,6 @@ /** | ||
| const lineTerminatorEscapes = { | ||
| 0x0a: '\\n', // LF | ||
| 0x0d: '\\r', // CR | ||
| 0x2028: '\\u2028', // LINE SEPARATOR | ||
| 0x2029: '\\u2029', // PARAGRAPH SEPARATOR | ||
| 10: '\\n', // LF | ||
| 13: '\\r', // CR | ||
| 8232: '\\u2028', // LINE SEPARATOR | ||
| 8233: '\\u2029', // PARAGRAPH SEPARATOR | ||
| }; | ||
@@ -44,0 +44,0 @@ // Match either an escape sequence (`\` + any char, kept verbatim) or a single |
@@ -9,3 +9,8 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export type RefNode = { | ||
| /** The `$ref` string this node was reached through, or `undefined` for the root schema. */ | ||
| /** | ||
| * The `$ref` string this node was reached through, or `undefined` for the | ||
| * root schema. Exception: an alias root (a bare `$ref` document) whose | ||
| * derived filename collides with its target's carries the target's ref, so | ||
| * generators can exclude self-imports from the merged file. | ||
| */ | ||
| ref: string | undefined; | ||
@@ -12,0 +17,0 @@ /** PascalCase type name — the root type name verbatim, or `refToName(ref, typeSuffix)`. */ |
@@ -10,2 +10,34 @@ import { buildDynamicRefMap } from './build-dynamic-ref-map.js'; | ||
| const rootCaches = new WeakMap(); | ||
| /** | ||
| * Keywords that give a schema a shape of its own. A root carrying any of these | ||
| * next to its `$ref` is a real composition, not a pure alias, and keeps the | ||
| * default root handling. | ||
| */ | ||
| const SHAPE_KEYWORDS = [ | ||
| 'properties', | ||
| 'type', | ||
| 'oneOf', | ||
| 'anyOf', | ||
| 'allOf', | ||
| 'patternProperties', | ||
| 'additionalProperties', | ||
| 'enum', | ||
| 'const', | ||
| 'items', | ||
| 'prefixItems', | ||
| 'required', | ||
| 'if', | ||
| 'then', | ||
| 'else', | ||
| 'not', | ||
| ]; | ||
| /** The `$ref` of a pure-alias root document (only `$ref` + metadata keys), or null. */ | ||
| const getAliasRootRef = (schema) => { | ||
| if (typeof schema !== 'object' || schema === null) | ||
| return null; | ||
| const record = schema; | ||
| if (typeof record['$ref'] !== 'string') | ||
| return null; | ||
| return SHAPE_KEYWORDS.some((key) => key in record) ? null : record['$ref']; | ||
| }; | ||
| const getRootCache = (rootSchema) => { | ||
@@ -82,7 +114,23 @@ // Only object roots can key a WeakMap. A boolean root has no refs to walk and | ||
| processedFilenames.add(rootFilename); | ||
| // An alias root (a document that is just `$ref: '#/$defs/x'`) whose derived | ||
| // filename equals its target's would reserve the filename for a wrapper that | ||
| // re-exports the target — and the target, mapping to the same file, would | ||
| // then never be generated, leaving a file that imports itself. Generate the | ||
| // *target's* schema under the root's name instead, carrying the ref so | ||
| // generators can exclude the self-import. | ||
| let rootNodeSchema = upgraded; | ||
| let rootNodeRef; | ||
| const aliasRef = getAliasRootRef(upgraded); | ||
| if (aliasRef && refToFilename(aliasRef) === rootFilename) { | ||
| const resolved = cachedResolveRef(cache, aliasRef); | ||
| if (resolved) { | ||
| rootNodeSchema = resolved; | ||
| rootNodeRef = aliasRef; | ||
| } | ||
| } | ||
| visit({ | ||
| ref: undefined, | ||
| ref: rootNodeRef, | ||
| typeName: rootTypeName, | ||
| filename: rootFilename, | ||
| schema: resolveDynamicRefs(upgraded, dynamicRefMap), | ||
| schema: resolveDynamicRefs(rootNodeSchema, dynamicRefMap), | ||
| rootSchema: upgraded, | ||
@@ -89,0 +137,0 @@ isRoot: true, |
+1
-1
| { | ||
| "name": "@amritk/helpers", | ||
| "version": "0.10.2", | ||
| "version": "0.10.3", | ||
| "description": "Shared utilities for the mjst code generation ecosystem.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -40,6 +40,6 @@ /** | ||
| const lineTerminatorEscapes: Record<number, string> = { | ||
| 0x0a: '\\n', // LF | ||
| 0x0d: '\\r', // CR | ||
| 0x2028: '\\u2028', // LINE SEPARATOR | ||
| 0x2029: '\\u2029', // PARAGRAPH SEPARATOR | ||
| 10: '\\n', // LF | ||
| 13: '\\r', // CR | ||
| 8232: '\\u2028', // LINE SEPARATOR | ||
| 8233: '\\u2029', // PARAGRAPH SEPARATOR | ||
| } | ||
@@ -46,0 +46,0 @@ |
@@ -19,3 +19,8 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| export type RefNode = { | ||
| /** The `$ref` string this node was reached through, or `undefined` for the root schema. */ | ||
| /** | ||
| * The `$ref` string this node was reached through, or `undefined` for the | ||
| * root schema. Exception: an alias root (a bare `$ref` document) whose | ||
| * derived filename collides with its target's carries the target's ref, so | ||
| * generators can exclude self-imports from the merged file. | ||
| */ | ||
| ref: string | undefined | ||
@@ -61,2 +66,34 @@ /** PascalCase type name — the root type name verbatim, or `refToName(ref, typeSuffix)`. */ | ||
| /** | ||
| * Keywords that give a schema a shape of its own. A root carrying any of these | ||
| * next to its `$ref` is a real composition, not a pure alias, and keeps the | ||
| * default root handling. | ||
| */ | ||
| const SHAPE_KEYWORDS = [ | ||
| 'properties', | ||
| 'type', | ||
| 'oneOf', | ||
| 'anyOf', | ||
| 'allOf', | ||
| 'patternProperties', | ||
| 'additionalProperties', | ||
| 'enum', | ||
| 'const', | ||
| 'items', | ||
| 'prefixItems', | ||
| 'required', | ||
| 'if', | ||
| 'then', | ||
| 'else', | ||
| 'not', | ||
| ] as const | ||
| /** The `$ref` of a pure-alias root document (only `$ref` + metadata keys), or null. */ | ||
| const getAliasRootRef = (schema: JSONSchema): string | null => { | ||
| if (typeof schema !== 'object' || schema === null) return null | ||
| const record = schema as Record<string, unknown> | ||
| if (typeof record['$ref'] !== 'string') return null | ||
| return SHAPE_KEYWORDS.some((key) => key in record) ? null : record['$ref'] | ||
| } | ||
| const getRootCache = (rootSchema: JSONSchema): RootCache => { | ||
@@ -141,7 +178,25 @@ // Only object roots can key a WeakMap. A boolean root has no refs to walk and | ||
| processedFilenames.add(rootFilename) | ||
| // An alias root (a document that is just `$ref: '#/$defs/x'`) whose derived | ||
| // filename equals its target's would reserve the filename for a wrapper that | ||
| // re-exports the target — and the target, mapping to the same file, would | ||
| // then never be generated, leaving a file that imports itself. Generate the | ||
| // *target's* schema under the root's name instead, carrying the ref so | ||
| // generators can exclude the self-import. | ||
| let rootNodeSchema = upgraded as JSONSchema | ||
| let rootNodeRef: string | undefined | ||
| const aliasRef = getAliasRootRef(upgraded as JSONSchema) | ||
| if (aliasRef && refToFilename(aliasRef) === rootFilename) { | ||
| const resolved = cachedResolveRef(cache, aliasRef) | ||
| if (resolved) { | ||
| rootNodeSchema = resolved as JSONSchema | ||
| rootNodeRef = aliasRef | ||
| } | ||
| } | ||
| visit({ | ||
| ref: undefined, | ||
| ref: rootNodeRef, | ||
| typeName: rootTypeName, | ||
| filename: rootFilename, | ||
| schema: resolveDynamicRefs(upgraded as JSONSchema, dynamicRefMap), | ||
| schema: resolveDynamicRefs(rootNodeSchema, dynamicRefMap), | ||
| rootSchema: upgraded, | ||
@@ -148,0 +203,0 @@ isRoot: true, |
210058
1.79%4606
2.31%