@amritk/generate-parsers 0.13.0 → 0.15.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.
- package/README.md +1 -1
- package/dist/generators/build-schema.d.ts +9 -1
- package/dist/generators/build-schema.js +24 -5
- package/dist/generators/generate-files.d.ts +15 -0
- package/dist/generators/generate-files.js +13 -5
- package/dist/generators/generate-parser-function.d.ts +17 -1
- package/dist/generators/generate-parser-function.js +303 -82
- package/dist/generators/generate-strict-assertion.js +14 -8
- package/dist/generators/generate-type-checks.d.ts +17 -0
- package/dist/generators/generate-type-checks.js +77 -9
- package/dist/generators/generate-validation-expression.d.ts +7 -0
- package/dist/generators/generate-validation-expression.js +17 -5
- package/dist/helpers/collect-helpers.d.ts +1 -9
- package/dist/helpers/collect-helpers.js +36 -9
- package/dist/helpers/collect-imports.d.ts +15 -0
- package/dist/helpers/collect-imports.js +36 -14
- package/dist/index.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|

|
|
10
10
|

|
|
11
11
|

|
|
12
|
-

|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -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,13 @@ 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'` (this function's default) is the standard TS NodeNext form; `'ts'` emits the
|
|
52
|
+
* literal on-disk paths so the generated sources run directly under Node's type
|
|
53
|
+
* stripping. Note the `mjst` CLI defaults this to `'ts'` (falling back to `'js'` only
|
|
54
|
+
* under `--build`, where tsc must compile the sources) so generated output runs under
|
|
55
|
+
* Node without a build step; direct callers of `buildSchema` opt in explicitly.
|
|
48
56
|
* @returns An array of generated TypeScript files
|
|
49
57
|
*
|
|
50
58
|
* @example
|
|
@@ -77,4 +85,4 @@ export type GeneratedFile = {
|
|
|
77
85
|
* });
|
|
78
86
|
* ```
|
|
79
87
|
*/
|
|
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[]>;
|
|
88
|
+
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,7 +16,7 @@ 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 {
|
|
@@ -31,10 +31,17 @@ const readHelperSource = async (helper) => {
|
|
|
31
31
|
// never matches) and is also semantically correct: an import keyword always
|
|
32
32
|
// sits at a word boundary. The dist smoke test (scripts/dist-smoke.test.ts)
|
|
33
33
|
// fails the build if this ever regresses.
|
|
34
|
-
return { ext: 'ts', content: source.replace(/\bfrom '(\.\/[^'".]+)'/g,
|
|
34
|
+
return { ext: 'ts', content: source.replace(/\bfrom '(\.\/[^'".]+)'/g, `from '$1.${importExt}'`) };
|
|
35
35
|
}
|
|
36
36
|
catch {
|
|
37
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
|
+
}
|
|
38
45
|
return { ext: 'js', content: compiled };
|
|
39
46
|
}
|
|
40
47
|
};
|
|
@@ -75,6 +82,13 @@ const readHelperSource = async (helper) => {
|
|
|
75
82
|
* @param typeSuffix - Suffix appended to every type/parser name derived from a `$ref`
|
|
76
83
|
* (e.g. `'Object'` → `ContactObject`). Defaults to `''` (no suffix). The root type name
|
|
77
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'` (this function's default) is the standard TS NodeNext form; `'ts'` emits the
|
|
88
|
+
* literal on-disk paths so the generated sources run directly under Node's type
|
|
89
|
+
* stripping. Note the `mjst` CLI defaults this to `'ts'` (falling back to `'js'` only
|
|
90
|
+
* under `--build`, where tsc must compile the sources) so generated output runs under
|
|
91
|
+
* Node without a build step; direct callers of `buildSchema` opt in explicitly.
|
|
78
92
|
* @returns An array of generated TypeScript files
|
|
79
93
|
*
|
|
80
94
|
* @example
|
|
@@ -107,7 +121,7 @@ const readHelperSource = async (helper) => {
|
|
|
107
121
|
* });
|
|
108
122
|
* ```
|
|
109
123
|
*/
|
|
110
|
-
export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = 'package', helpersImportPrefix = './', readonly = false, stripUnknown = false, typeSuffix = '') => {
|
|
124
|
+
export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = 'package', helpersImportPrefix = './', readonly = false, stripUnknown = false, typeSuffix = '', importExt = 'js') => {
|
|
111
125
|
const files = [];
|
|
112
126
|
const usedHelpers = new Set();
|
|
113
127
|
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
@@ -125,6 +139,8 @@ export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnl
|
|
|
125
139
|
helpersImportPrefix,
|
|
126
140
|
readonly,
|
|
127
141
|
typeSuffix,
|
|
142
|
+
importExt,
|
|
143
|
+
isRoot: node.isRoot,
|
|
128
144
|
...(node.ref !== undefined ? { selfRef: node.ref } : {}),
|
|
129
145
|
...(logWarnings !== undefined ? { logWarnings } : {}),
|
|
130
146
|
...(strict !== undefined ? { strict } : {}),
|
|
@@ -139,10 +155,13 @@ export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnl
|
|
|
139
155
|
// typesOnly skips parser generation entirely, so no runtime helpers are needed.
|
|
140
156
|
if (helpersMode === 'embedded' && !typesOnly) {
|
|
141
157
|
for (const helper of usedHelpers) {
|
|
142
|
-
const { ext, content } = await readHelperSource(helper);
|
|
158
|
+
const { ext, content } = await readHelperSource(helper, importExt);
|
|
143
159
|
files.push({ filename: `_helpers/${helper}.${ext}`, content });
|
|
144
160
|
}
|
|
145
161
|
}
|
|
146
|
-
files.push({
|
|
162
|
+
files.push({
|
|
163
|
+
filename: 'index.ts',
|
|
164
|
+
content: generateIndexBarrel(files, { typesOnly: typesOnly ?? false, importExt }),
|
|
165
|
+
});
|
|
147
166
|
return files;
|
|
148
167
|
};
|
|
@@ -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,20 @@ 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;
|
|
74
|
+
/**
|
|
75
|
+
* True when this file is the root document rather than a `$ref`-reached
|
|
76
|
+
* definition. Forwarded to the parser generator so the JSON Schema
|
|
77
|
+
* meta-schema special case (type name `Schema`) never applies to a
|
|
78
|
+
* user-named root.
|
|
79
|
+
*/
|
|
80
|
+
readonly isRoot?: boolean;
|
|
66
81
|
};
|
|
67
82
|
/** Result of generating a single parser file. */
|
|
68
83
|
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,25 @@ 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,
|
|
65
|
+
...(options?.isRoot !== undefined ? { isRoot: options.isRoot } : {}),
|
|
58
66
|
...(rootSchema !== undefined ? { rootSchema } : {}),
|
|
59
67
|
...(options?.logWarnings !== undefined ? { logWarnings: options.logWarnings } : {}),
|
|
60
68
|
...(options?.strict !== undefined ? { strict: options.strict } : {}),
|
|
61
69
|
...(options?.stripUnknown !== undefined ? { stripUnknown: options.stripUnknown } : {}),
|
|
62
70
|
});
|
|
63
|
-
const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown);
|
|
71
|
+
const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown, reservedNames);
|
|
64
72
|
const combinedFunctions = `${shapeValidator}\n\n${parserFunction}`;
|
|
65
|
-
const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix);
|
|
66
|
-
const imports = [...collectImports(schema, { selfRef, rootSchema, typeSuffix }), ...helpers.imports];
|
|
73
|
+
const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix, importExt);
|
|
74
|
+
const imports = [...collectImports(schema, { selfRef, rootSchema, typeSuffix, importExt }), ...helpers.imports];
|
|
67
75
|
// Build file output using string concatenation instead of array join for performance
|
|
68
76
|
let result = '';
|
|
69
77
|
if (imports.length > 0) {
|
|
@@ -42,6 +42,22 @@ 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>;
|
|
53
|
+
/**
|
|
54
|
+
* True when this schema is the root document (not a `$ref`-reached definition).
|
|
55
|
+
* The root type name is user-derived (schema `title`, filename, or `--root-type`),
|
|
56
|
+
* so the JSON Schema meta-schema special case — keyed on the literal name
|
|
57
|
+
* `Schema` — must not apply to it, or a common `schema.json` root would silently
|
|
58
|
+
* generate a validation-free pass-through parser.
|
|
59
|
+
*/
|
|
60
|
+
readonly isRoot?: boolean;
|
|
45
61
|
};
|
|
46
62
|
/**
|
|
47
63
|
* Generates a safe parser as an arrow function that never throws.
|
|
@@ -71,5 +87,5 @@ export declare const shapeValidatorFunctionName: (typeName: string) => string;
|
|
|
71
87
|
* in additionalProperties). The stub is safe: parents calling it will fall
|
|
72
88
|
* through to the slow path, matching the pre-deep-fast-path behavior.
|
|
73
89
|
*/
|
|
74
|
-
export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean) => string;
|
|
90
|
+
export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean, reservedNames?: ReadonlySet<string>) => string;
|
|
75
91
|
export {};
|