Skip to content

Commit 55f7188

Browse files
committed
[heft-json-schema-typings-plugin] Add snapshot unit tests for JsonSchemaTypingsGenerator
- Replace compileFromFile with compile (reads file via TypingsGenerator base class) - Parse and strip x-tsdoc-tag from schema before passing to compile() - Pass format: false to skip prettier (avoids dynamic import issue in Jest) - Add jest.config.json with moduleNameMapper to stub prettier at load time - Add 3 snapshot tests: basic schema, x-tsdoc-tag injection, cross-file $ref - Add test fixture schemas in src/test/schemas/
1 parent 67d6f70 commit 55f7188

9 files changed

Lines changed: 263 additions & 14 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "local-node-rig/profiles/default/config/jest.config.json",
3+
"moduleNameMapper": {
4+
"^prettier$": "<configDir>/jestMocks/prettier.js"
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Stub for prettier. json-schema-to-typescript eagerly require('prettier') at
2+
// module load time. Prettier v3's CJS entry does a top-level dynamic import()
3+
// which crashes inside Jest's VM sandbox on Node 22+. Since compile() is called
4+
// with format: false, prettier is never invoked — this stub just prevents the
5+
// module-load crash.
6+
module.exports = {};

heft-plugins/heft-json-schema-typings-plugin/src/JsonSchemaTypingsGenerator.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,58 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import { readFile } from 'node:fs/promises';
5-
import path from 'node:path';
4+
import * as path from 'node:path';
65

7-
import { compileFromFile } from 'json-schema-to-typescript';
6+
import { compile } from 'json-schema-to-typescript';
87

98
import { type ITypingsGeneratorBaseOptions, TypingsGenerator } from '@rushstack/typings-generator';
109

1110
import { _addTsDocTagToExports } from './TsDocTagHelpers';
1211

1312
interface IJsonSchemaTypingsGeneratorBaseOptions extends ITypingsGeneratorBaseOptions {}
1413

14+
const SCHEMA_FILE_EXTENSION: '.schema.json' = '.schema.json';
15+
const X_TSDOC_TAG_KEY: 'x-tsdoc-tag' = 'x-tsdoc-tag';
16+
17+
type Json4Schema = Parameters<typeof compile>[0];
18+
interface IExtendedJson4Schema extends Json4Schema {
19+
[X_TSDOC_TAG_KEY]?: string;
20+
}
21+
1522
export class JsonSchemaTypingsGenerator extends TypingsGenerator {
1623
public constructor(options: IJsonSchemaTypingsGeneratorBaseOptions) {
1724
super({
1825
...options,
19-
fileExtensions: ['.schema.json'],
20-
// Don't bother reading the file contents, compileFromFile will read the file
21-
readFile: () => '',
26+
fileExtensions: [SCHEMA_FILE_EXTENSION],
2227
// eslint-disable-next-line @typescript-eslint/naming-convention
23-
parseAndGenerateTypings: async (fileContents: string, filePath: string): Promise<string> => {
24-
const typings: string = await compileFromFile(filePath, {
28+
parseAndGenerateTypings: async (
29+
fileContents: string,
30+
filePath: string,
31+
relativePath: string
32+
): Promise<string> => {
33+
const parsedFileContents: IExtendedJson4Schema = JSON.parse(fileContents);
34+
const { [X_TSDOC_TAG_KEY]: tsdocTag, ...jsonSchemaWithoutTsDocTag } = parsedFileContents;
35+
36+
// Use the absolute directory of the schema file so that cross-file $ref
37+
// (e.g. { "$ref": "./other.schema.json" }) resolves correctly.
38+
const dirname: string = path.dirname(filePath);
39+
const filenameWithoutExtension: string = filePath.slice(
40+
dirname.length + 1,
41+
-SCHEMA_FILE_EXTENSION.length
42+
);
43+
let typings: string = await compile(jsonSchemaWithoutTsDocTag, filenameWithoutExtension, {
2544
// The typings generator adds its own banner comment
2645
bannerComment: '',
27-
cwd: path.dirname(filePath)
46+
cwd: dirname,
47+
// The generated typings are machine-produced .d.ts files that do not need
48+
// prettier formatting.
49+
format: false
2850
});
2951

3052
// Check for an "x-tsdoc-tag" property in the schema (e.g. "@public" or "@beta").
3153
// If present, inject the tag into JSDoc comments for all exported declarations.
32-
const schemaContent: string = await readFile(filePath, 'utf-8');
33-
const schemaJson: { 'x-tsdoc-tag'?: string } = JSON.parse(schemaContent);
34-
const tsdocTag: string | undefined = schemaJson['x-tsdoc-tag'];
35-
3654
if (tsdocTag) {
37-
return _addTsDocTagToExports(typings, tsdocTag);
55+
typings = _addTsDocTagToExports(typings, tsdocTag);
3856
}
3957

4058
return typings;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import { FileSystem, PackageJsonLookup } from '@rushstack/node-core-library';
5+
6+
import { JsonSchemaTypingsGenerator } from '../JsonSchemaTypingsGenerator';
7+
8+
const projectFolder: string = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname)!;
9+
const schemasFolder: string = `${__dirname}/schemas`;
10+
const outputFolder: string = `${projectFolder}/temp/test-typings-output`;
11+
12+
async function readGeneratedTypings(schemaRelativePath: string): Promise<string> {
13+
const outputPath: string = `${outputFolder}/${schemaRelativePath}.d.ts`;
14+
return await FileSystem.readFileAsync(outputPath);
15+
}
16+
17+
describe('JsonSchemaTypingsGenerator', () => {
18+
beforeEach(async () => {
19+
await FileSystem.ensureEmptyFolderAsync(outputFolder);
20+
});
21+
22+
it('generates typings for a basic object schema', async () => {
23+
const generator = new JsonSchemaTypingsGenerator({
24+
srcFolder: schemasFolder,
25+
generatedTsFolder: outputFolder
26+
});
27+
28+
await generator.generateTypingsAsync(['basic.schema.json']);
29+
const typings: string = await readGeneratedTypings('basic.schema.json');
30+
expect(typings).toMatchSnapshot();
31+
});
32+
33+
it('injects x-tsdoc-tag into exported declarations', async () => {
34+
const generator = new JsonSchemaTypingsGenerator({
35+
srcFolder: schemasFolder,
36+
generatedTsFolder: outputFolder
37+
});
38+
39+
await generator.generateTypingsAsync(['with-tsdoc-tag.schema.json']);
40+
const typings: string = await readGeneratedTypings('with-tsdoc-tag.schema.json');
41+
expect(typings).toMatchSnapshot();
42+
expect(typings).toContain('@public');
43+
});
44+
45+
it('resolves cross-file $ref between schema files', async () => {
46+
const generator = new JsonSchemaTypingsGenerator({
47+
srcFolder: schemasFolder,
48+
generatedTsFolder: outputFolder
49+
});
50+
51+
await generator.generateTypingsAsync(['child.schema.json', 'parent.schema.json']);
52+
const [parentTypings, childTypings]: string[] = await Promise.all([
53+
readGeneratedTypings('parent.schema.json'),
54+
readGeneratedTypings('child.schema.json')
55+
]);
56+
57+
expect(childTypings).toMatchSnapshot('child output');
58+
expect(parentTypings).toMatchSnapshot('parent output');
59+
60+
// The parent typings should reference the child type
61+
expect(parentTypings).toContain('ChildType');
62+
});
63+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`JsonSchemaTypingsGenerator generates typings for a basic object schema 1`] = `
4+
"// This file was generated by a tool. Modifying it will produce unexpected behavior
5+
6+
export interface BasicConfig {
7+
/**
8+
* The name of the item.
9+
*/
10+
name: string
11+
/**
12+
* The number of items.
13+
*/
14+
count?: number
15+
/**
16+
* Whether the feature is enabled.
17+
*/
18+
enabled?: boolean
19+
}
20+
"
21+
`;
22+
23+
exports[`JsonSchemaTypingsGenerator injects x-tsdoc-tag into exported declarations 1`] = `
24+
"// This file was generated by a tool. Modifying it will produce unexpected behavior
25+
26+
/**
27+
* @public
28+
*/
29+
export interface PublicConfig {
30+
/**
31+
* A value.
32+
*/
33+
value?: string
34+
}
35+
"
36+
`;
37+
38+
exports[`JsonSchemaTypingsGenerator resolves cross-file $ref between schema files: child output 1`] = `
39+
"// This file was generated by a tool. Modifying it will produce unexpected behavior
40+
41+
/**
42+
* A reusable child type.
43+
*/
44+
export interface ChildType {
45+
/**
46+
* The name of the child.
47+
*/
48+
childName: string
49+
/**
50+
* The value of the child.
51+
*/
52+
childValue?: number
53+
}
54+
"
55+
`;
56+
57+
exports[`JsonSchemaTypingsGenerator resolves cross-file $ref between schema files: parent output 1`] = `
58+
"// This file was generated by a tool. Modifying it will produce unexpected behavior
59+
60+
export interface ParentConfig {
61+
/**
62+
* A label for the parent.
63+
*/
64+
label: string
65+
child: ChildType
66+
/**
67+
* A list of children.
68+
*/
69+
children?: ChildType[]
70+
}
71+
/**
72+
* A reusable child type.
73+
*/
74+
export interface ChildType {
75+
/**
76+
* The name of the child.
77+
*/
78+
childName: string
79+
/**
80+
* The value of the child.
81+
*/
82+
childValue?: number
83+
}
84+
"
85+
`;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"title": "Basic Config",
3+
"type": "object",
4+
"properties": {
5+
"name": {
6+
"description": "The name of the item.",
7+
"type": "string"
8+
},
9+
"count": {
10+
"description": "The number of items.",
11+
"type": "integer"
12+
},
13+
"enabled": {
14+
"description": "Whether the feature is enabled.",
15+
"type": "boolean"
16+
}
17+
},
18+
"additionalProperties": false,
19+
"required": ["name"]
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"title": "Child Type",
3+
"description": "A reusable child type.",
4+
"type": "object",
5+
"properties": {
6+
"childName": {
7+
"description": "The name of the child.",
8+
"type": "string"
9+
},
10+
"childValue": {
11+
"description": "The value of the child.",
12+
"type": "number"
13+
}
14+
},
15+
"additionalProperties": false,
16+
"required": ["childName"]
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"title": "Parent Config",
3+
"type": "object",
4+
"properties": {
5+
"label": {
6+
"description": "A label for the parent.",
7+
"type": "string"
8+
},
9+
"child": {
10+
"$ref": "./child.schema.json"
11+
},
12+
"children": {
13+
"description": "A list of children.",
14+
"type": "array",
15+
"items": {
16+
"$ref": "./child.schema.json"
17+
}
18+
}
19+
},
20+
"additionalProperties": false,
21+
"required": ["label", "child"]
22+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"x-tsdoc-tag": "@public",
3+
"title": "Public Config",
4+
"type": "object",
5+
"properties": {
6+
"value": {
7+
"description": "A value.",
8+
"type": "string"
9+
}
10+
},
11+
"additionalProperties": false
12+
}

0 commit comments

Comments
 (0)