@amritk/generate-parsers 0.12.3 → 0.14.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.
@@ -1,5 +1,6 @@
1
1
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
2
  import type { HelpersMode } from '../helpers/collect-helpers.js';
3
+ import type { ImportExtension } from '../helpers/collect-imports.js';
3
4
  import type { SchemaExtensions } from '../types/schema-extensions.js';
4
5
  /**
5
6
  * Represents a generated file with its filename and content.
@@ -45,6 +46,10 @@ export type GeneratedFile = {
45
46
  * @param typeSuffix - Suffix appended to every type/parser name derived from a `$ref`
46
47
  * (e.g. `'Object'` → `ContactObject`). Defaults to `''` (no suffix). The root type name
47
48
  * is used verbatim and is not affected by this suffix.
49
+ * @param importExt - Extension used on every relative import specifier in the generated
50
+ * output (cross-file `$ref` imports, the index barrel, and embedded-helper imports).
51
+ * `'js'` (default) is the standard TS NodeNext form; `'ts'` emits the literal on-disk
52
+ * paths so the generated sources run directly under Node's type stripping.
48
53
  * @returns An array of generated TypeScript files
49
54
  *
50
55
  * @example
@@ -77,4 +82,4 @@ export type GeneratedFile = {
77
82
  * });
78
83
  * ```
79
84
  */
80
- export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, stripUnknown?: boolean, typeSuffix?: string) => Promise<GeneratedFile[]>;
85
+ export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, stripUnknown?: boolean, typeSuffix?: string, importExt?: ImportExtension) => Promise<GeneratedFile[]>;
@@ -16,15 +16,32 @@ import { generateFile } from './generate-files.js';
16
16
  * on this lookup), it falls back to the always-published compiled `dist/<helper>.js`
17
17
  * — which tsc already emits with `.js`-extension imports — and writes it verbatim.
18
18
  */
19
- const readHelperSource = async (helper) => {
19
+ const readHelperSource = async (helper, importExt) => {
20
20
  const require = createRequire(import.meta.url);
21
21
  const helpersRoot = dirname(require.resolve('@amritk/helpers/package.json'));
22
22
  try {
23
23
  const source = await readFile(resolvePath(helpersRoot, 'src', `${helper}.ts`), 'utf-8');
24
- return { ext: 'ts', content: source.replace(/from '(/./[^'".]+)'/g, "from '$1.js'") };
24
+ // The leading `\b` in this pattern is load-bearing beyond its regex meaning.
25
+ // Our build runs tsc-alias --resolveFullPaths over the compiled output, and its
26
+ // scanner rewrites anything that looks like `from '<path>'` — even inside a
27
+ // regex literal. Without the `\b`, normalize-path turned this pattern's
28
+ // backslashes into slashes and v0.12.3 shipped with an unparseable regex that
29
+ // crashed the CLI on load. The `\b` blocks that scan (in the emitted text,
30
+ // `from` is preceded by the word character `b`, so tsc-alias's own `\bfrom`
31
+ // never matches) and is also semantically correct: an import keyword always
32
+ // sits at a word boundary. The dist smoke test (scripts/dist-smoke.test.ts)
33
+ // fails the build if this ever regresses.
34
+ return { ext: 'ts', content: source.replace(/\bfrom '(\.\/[^'".]+)'/g, `from '$1.${importExt}'`) };
25
35
  }
26
36
  catch {
27
37
  const compiled = await readFile(resolvePath(helpersRoot, 'dist', `${helper}.js`), 'utf-8');
38
+ // With `.ts` import specifiers the on-disk file must literally be `.ts`, so
39
+ // ship the compiled JS (valid TS) under that name and retarget its own
40
+ // sibling imports to match. The pattern keeps the same `\bfrom` shape as
41
+ // above so tsc-alias's scanner can't corrupt it (see the comment there).
42
+ if (importExt === 'ts') {
43
+ return { ext: 'ts', content: compiled.replace(/\bfrom '(\.\/[^'"]+)\.js'/g, "from '$1.ts'") };
44
+ }
28
45
  return { ext: 'js', content: compiled };
29
46
  }
30
47
  };
@@ -65,6 +82,10 @@ const readHelperSource = async (helper) => {
65
82
  * @param typeSuffix - Suffix appended to every type/parser name derived from a `$ref`
66
83
  * (e.g. `'Object'` → `ContactObject`). Defaults to `''` (no suffix). The root type name
67
84
  * is used verbatim and is not affected by this suffix.
85
+ * @param importExt - Extension used on every relative import specifier in the generated
86
+ * output (cross-file `$ref` imports, the index barrel, and embedded-helper imports).
87
+ * `'js'` (default) is the standard TS NodeNext form; `'ts'` emits the literal on-disk
88
+ * paths so the generated sources run directly under Node's type stripping.
68
89
  * @returns An array of generated TypeScript files
69
90
  *
70
91
  * @example
@@ -97,7 +118,7 @@ const readHelperSource = async (helper) => {
97
118
  * });
98
119
  * ```
99
120
  */
100
- export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = 'package', helpersImportPrefix = './', readonly = false, stripUnknown = false, typeSuffix = '') => {
121
+ export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = 'package', helpersImportPrefix = './', readonly = false, stripUnknown = false, typeSuffix = '', importExt = 'js') => {
101
122
  const files = [];
102
123
  const usedHelpers = new Set();
103
124
  walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
@@ -115,6 +136,7 @@ export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnl
115
136
  helpersImportPrefix,
116
137
  readonly,
117
138
  typeSuffix,
139
+ importExt,
118
140
  ...(node.ref !== undefined ? { selfRef: node.ref } : {}),
119
141
  ...(logWarnings !== undefined ? { logWarnings } : {}),
120
142
  ...(strict !== undefined ? { strict } : {}),
@@ -129,10 +151,13 @@ export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnl
129
151
  // typesOnly skips parser generation entirely, so no runtime helpers are needed.
130
152
  if (helpersMode === 'embedded' && !typesOnly) {
131
153
  for (const helper of usedHelpers) {
132
- const { ext, content } = await readHelperSource(helper);
154
+ const { ext, content } = await readHelperSource(helper, importExt);
133
155
  files.push({ filename: `_helpers/${helper}.${ext}`, content });
134
156
  }
135
157
  }
136
- files.push({ filename: 'index.ts', content: generateIndexBarrel(files, { typesOnly: typesOnly ?? false }) });
158
+ files.push({
159
+ filename: 'index.ts',
160
+ content: generateIndexBarrel(files, { typesOnly: typesOnly ?? false, importExt }),
161
+ });
137
162
  return files;
138
163
  };
@@ -1,5 +1,6 @@
1
1
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
2
  import { type HelpersMode, type RuntimeHelperName } from '../helpers/collect-helpers.js';
3
+ import { type ImportExtension } from '../helpers/collect-imports.js';
3
4
  /**
4
5
  * Options for controlling what gets generated in a file.
5
6
  */
@@ -63,6 +64,13 @@ type GenerateFileOptions = {
63
64
  * Defaults to `''` (no suffix). Set to e.g. `'Object'` to emit `ContactObject`.
64
65
  */
65
66
  readonly typeSuffix?: string;
67
+ /**
68
+ * Extension used on every relative import specifier (cross-file `$ref`
69
+ * imports and embedded-helper imports). Defaults to `'js'` (the TS NodeNext
70
+ * form); `'ts'` makes the generated sources runnable under Node's type
71
+ * stripping.
72
+ */
73
+ readonly importExt?: ImportExtension;
66
74
  };
67
75
  /** Result of generating a single parser file. */
68
76
  export type GeneratedFileContent = {
@@ -1,6 +1,6 @@
1
1
  import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition';
2
2
  import { collectHelpers, } from '../helpers/collect-helpers.js';
3
- import { collectImports } from '../helpers/collect-imports.js';
3
+ import { collectImports, collectImportTypeNames } from '../helpers/collect-imports.js';
4
4
  import { generateParserFunction, generateShapeValidator } from './generate-parser-function.js';
5
5
  /**
6
6
  * Generates a complete TypeScript file from a JSON Schema.
@@ -37,10 +37,11 @@ export const generateFile = (schema, typeName, options) => {
37
37
  const rootSchema = options?.rootSchema;
38
38
  const helpersMode = options?.helpersMode ?? 'package';
39
39
  const typeSuffix = options?.typeSuffix ?? '';
40
+ const importExt = options?.importExt ?? 'js';
40
41
  const typeDefinition = generateTypeDefinition(schema, typeName, { readonly: options?.readonly ?? false, typeSuffix });
41
42
  if (typesOnly) {
42
43
  // In types-only mode, skip the parser function and use type-only imports
43
- const imports = collectImports(schema, { typesOnly: true, selfRef, rootSchema, typeSuffix });
44
+ const imports = collectImports(schema, { typesOnly: true, selfRef, rootSchema, typeSuffix, importExt });
44
45
  let result = '';
45
46
  if (imports.length > 0) {
46
47
  result += imports[0];
@@ -52,18 +53,24 @@ export const generateFile = (schema, typeName, options) => {
52
53
  return { content: result + typeDefinition, usedHelpers: new Set() };
53
54
  }
54
55
  const stripUnknown = options?.stripUnknown ?? false;
56
+ // Names this file imports for its $refs: synthesized private sub-type names
57
+ // dedup against them so they can never shadow an imported identifier. The
58
+ // parser and the shape validator must receive the SAME set — their derived
59
+ // sub-names have to agree.
60
+ const reservedNames = collectImportTypeNames(schema, { selfRef, rootSchema, typeSuffix });
55
61
  const parserFunction = generateParserFunction(schema, typeName, {
56
62
  useRefImports: true,
57
63
  typeSuffix,
64
+ reservedNames,
58
65
  ...(rootSchema !== undefined ? { rootSchema } : {}),
59
66
  ...(options?.logWarnings !== undefined ? { logWarnings: options.logWarnings } : {}),
60
67
  ...(options?.strict !== undefined ? { strict: options.strict } : {}),
61
68
  ...(options?.stripUnknown !== undefined ? { stripUnknown: options.stripUnknown } : {}),
62
69
  });
63
- const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown);
70
+ const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown, reservedNames);
64
71
  const combinedFunctions = `${shapeValidator}\n\n${parserFunction}`;
65
- const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix);
66
- const imports = [...collectImports(schema, { selfRef, rootSchema, typeSuffix }), ...helpers.imports];
72
+ const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix, importExt);
73
+ const imports = [...collectImports(schema, { selfRef, rootSchema, typeSuffix, importExt }), ...helpers.imports];
67
74
  // Build file output using string concatenation instead of array join for performance
68
75
  let result = '';
69
76
  if (imports.length > 0) {
@@ -42,6 +42,14 @@ type GenerateParserOptions = {
42
42
  * dispatch to the branch parsers, instead of falling back to a passthrough cast.
43
43
  */
44
44
  readonly rootSchema?: Record<string, unknown>;
45
+ /**
46
+ * Type names the generated file imports for its `$ref`s. Synthesized private
47
+ * sub-type names (nested objects, array items, the root array's `{Type}Item`)
48
+ * dedup against this set so they can never shadow an imported identifier —
49
+ * which would both fail to compile (duplicate declaration) and silently
50
+ * validate against the wrong schema.
51
+ */
52
+ readonly reservedNames?: ReadonlySet<string>;
45
53
  };
46
54
  /**
47
55
  * Generates a safe parser as an arrow function that never throws.
@@ -71,5 +79,5 @@ export declare const shapeValidatorFunctionName: (typeName: string) => string;
71
79
  * in additionalProperties). The stub is safe: parents calling it will fall
72
80
  * through to the slow path, matching the pre-deep-fast-path behavior.
73
81
  */
74
- export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean) => string;
82
+ export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean, reservedNames?: ReadonlySet<string>) => string;
75
83
  export {};