Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: export and import interfaces as types when using named imports and ESM Module Syntax #1873

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ These are all specific examples only relevant to the TypeScript generator:
- [typescript-generate-example](./typescript-generate-example) - A basic example of how to use Modelina and output a TypeScript class with an example function.
- [typescript-generate-marshalling](./typescript-generate-marshalling) - A basic example of how to use the un/marshalling functionality of the typescript class.
- [typescript-generate-comments](./typescript-generate-comments) - A basic example of how to generate TypeScript interfaces with comments from description and example fields.
- [typescript-interfaces-as-types](./typescript-interfaces-as-types) - A basic example that generate the models as interfaces and uses ESM module system syntax to import and export them as types.
- [typescript-use-esm](./typescript-use-esm) - A basic example that generate the models to use ESM module system.
- [typescript-use-cjs](./typescript-use-cjs) - A basic example that generate the models to use CJS module system.
- [typescript-generate-jsonbinpack](./typescript-generate-jsonbinpack) - A basic example showing how to generate models that include [jsonbinpack](https://github.com/sourcemeta/jsonbinpack) functionality.
Expand Down
17 changes: 17 additions & 0 deletions examples/typescript-interfaces-as-types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# TypeScript interface as types

A basic example of how to use Modelina and output TypeScript interfaces that are exported as types using ESM Module Syntax.

## How to run this example

Run this example using:

```sh
npm i && npm run start
```

If you are on Windows, use the `start:windows` script instead:

```sh
npm i && npm run start:windows
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Should be able to render typescript interfaces as types and should log expected output to console 1`] = `
Array [
"import type {OtherModel} from './OtherModel';
interface Root {
email?: string;
otherModel?: OtherModel;
}
export type { Root };",
]
`;

exports[`Should be able to render typescript interfaces as types and should log expected output to console 2`] = `
Array [
"
interface OtherModel {
streetName?: string;
additionalProperties?: Map<string, any>;
}
export type { OtherModel };",
]
`;
16 changes: 16 additions & 0 deletions examples/typescript-interfaces-as-types/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const spy = jest.spyOn(global.console, 'log').mockImplementation(() => {
return;
});
import { generate } from './index';

describe('Should be able to render typescript interfaces as types', () => {
afterAll(() => {
jest.restoreAllMocks();
});
test('and should log expected output to console', async () => {
await generate();
expect(spy.mock.calls.length).toEqual(2);
expect(spy.mock.calls[0]).toMatchSnapshot();
expect(spy.mock.calls[1]).toMatchSnapshot();
});
});
34 changes: 34 additions & 0 deletions examples/typescript-interfaces-as-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TypeScriptGenerator } from '../../src';

const generator = new TypeScriptGenerator({
modelType: 'interface',
moduleSystem: 'ESM'
});
const jsonSchemaDraft7 = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
additionalProperties: false,
properties: {
email: {
type: 'string',
format: 'email'
},
other_model: {
type: 'object',
$id: 'OtherModel',
properties: { street_name: { type: 'string' } }
}
}
};

export async function generate(): Promise<void> {
const models = await generator.generateCompleteModels(jsonSchemaDraft7, {
exportType: 'named'
});
for (const model of models) {
console.log(model.result);
}
}
if (require.main === module) {
generate();
}
10 changes: 10 additions & 0 deletions examples/typescript-interfaces-as-types/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions examples/typescript-interfaces-as-types/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"config" : { "example_name" : "typescript-interfaces-as-types" },
"scripts": {
"install": "cd ../.. && npm i",
"start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts",
"start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts",
"test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts",
"test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts"
}
}
59 changes: 49 additions & 10 deletions src/generators/typescript/TypeScriptDependencyManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { AbstractDependencyManager } from '../AbstractDependencyManager';
import { renderJavaScriptDependency } from '../../helpers';
import { ConstrainedMetaModel } from '../../models';
import { TypeScriptExportType, TypeScriptOptions } from './TypeScriptGenerator';
import {
ConstrainedMetaModel,
ConstrainedObjectModel,
ConstrainedReferenceModel
} from '../../models';
import {
TypeScriptExportType,
TypeScriptModelType,
TypeScriptOptions
} from './TypeScriptGenerator';

export class TypeScriptDependencyManager extends AbstractDependencyManager {
constructor(
Expand All @@ -14,19 +22,32 @@ export class TypeScriptDependencyManager extends AbstractDependencyManager {
/**
* Simple helper function to add a dependency correct based on the module system that the user defines.
*/
addTypeScriptDependency(toImport: string, fromModule: string): void {
const dependencyImport = this.renderDependency(toImport, fromModule);
addTypeScriptDependency(
toImport: string,
fromModule: string,
modelType: TypeScriptModelType
): void {
const dependencyImport = this.renderDependency(
toImport,
fromModule,
modelType
);
this.addDependency(dependencyImport);
}

/**
* Simple helper function to render a dependency based on the module system that the user defines.
*/
renderDependency(toImport: string, fromModule: string): string {
renderDependency(
toImport: string,
fromModule: string,
modelType: TypeScriptModelType
): string {
return renderJavaScriptDependency(
toImport,
fromModule,
this.options.moduleSystem
this.options.moduleSystem,
modelType === 'interface' ? 'type' : 'value'
);
}

Expand All @@ -35,28 +56,46 @@ export class TypeScriptDependencyManager extends AbstractDependencyManager {
*/
renderCompleteModelDependencies(
model: ConstrainedMetaModel,
exportType: TypeScriptExportType
exportType: TypeScriptExportType,
modelType: TypeScriptModelType
): string {
const isInterface =
model instanceof ConstrainedObjectModel ||
(model instanceof ConstrainedReferenceModel &&
model.ref instanceof ConstrainedObjectModel);
const dependencyObject =
exportType === 'named' ? `{${model.name}}` : model.name;
return this.renderDependency(dependencyObject, `./${model.name}`);
return this.renderDependency(
dependencyObject,
`./${model.name}`,
isInterface ? modelType : 'class'
);
}

/**
* Render the exported statement for the model based on the options
*/
renderExport(
model: ConstrainedMetaModel,
exportType: TypeScriptExportType
exportType: TypeScriptExportType,
modelType: TypeScriptModelType
): string {
const cjsExport =
exportType === 'default'
? `module.exports = ${model.name};`
: `exports.${model.name} = ${model.name};`;
const esmExport =
const esmExportValue =
exportType === 'default'
? `export default ${model.name};\n`
: `export { ${model.name} };`;
const esmExportType =
exportType === 'default'
? `export default ${model.name};\n`
: `export type { ${model.name} };`;
const esmExport =
modelType === 'interface' && model instanceof ConstrainedObjectModel
? esmExportType
: esmExportValue;
return this.options.moduleSystem === 'CJS' ? cjsExport : esmExport;
}
}
9 changes: 6 additions & 3 deletions src/generators/typescript/TypeScriptGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ import { TypeScriptDependencyManager } from './TypeScriptDependencyManager';

export type TypeScriptModuleSystemType = 'ESM' | 'CJS';
export type TypeScriptExportType = 'named' | 'default';
export type TypeScriptModelType = 'class' | 'interface';
export interface TypeScriptOptions
extends CommonGeneratorOptions<
TypeScriptPreset,
TypeScriptDependencyManager
> {
renderTypes: boolean;
modelType: 'class' | 'interface';
modelType: TypeScriptModelType;
enumType: 'enum' | 'union';
mapType: 'indexedObject' | 'map' | 'record';
typeMapping: TypeMapping<TypeScriptOptions, TypeScriptDependencyManager>;
Expand Down Expand Up @@ -210,12 +211,14 @@ export class TypeScriptGenerator extends AbstractGenerator<
const modelDependencyImports = modelDependencies.map((model) => {
return dependencyManagerToUse.renderCompleteModelDependencies(
model,
completeModelOptionsToUse.exportType
completeModelOptionsToUse.exportType,
optionsToUse.modelType
);
});
const modelExport = dependencyManagerToUse.renderExport(
args.constrainedModel,
completeModelOptionsToUse.exportType
completeModelOptionsToUse.exportType,
optionsToUse.modelType
);

const modelCode = `${outputModel.result}\n${modelExport}`;
Expand Down
15 changes: 11 additions & 4 deletions src/helpers/DependencyHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ import { ConstrainedMetaModel } from '../models';
export function renderJavaScriptDependency(
toImport: string,
fromModule: string,
moduleSystem: 'CJS' | 'ESM'
moduleSystem: 'CJS' | 'ESM',
importType: 'type' | 'value' = 'value'
): string {
return moduleSystem === 'CJS'
? `const ${toImport} = require('${fromModule}');`
: `import ${toImport} from '${fromModule}';`;
const importValueStatement =
moduleSystem === 'CJS'
? `const ${toImport} = require('${fromModule}');`
: `import ${toImport} from '${fromModule}';`;
const importTypeStatement =
moduleSystem === 'CJS'
? `const ${toImport} = require('${fromModule}');`
: `import type ${toImport} from '${fromModule}';`;
return importType === 'type' ? importTypeStatement : importValueStatement;
}

/**
Expand Down
52 changes: 52 additions & 0 deletions test/generators/typescript/TypeScriptGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,58 @@ ${content}`;
expect(models[1].result).toMatchSnapshot();
});

test('should render models and their dependencies for ESM module system with named exports as interfaces', async () => {
const doc = {
$id: 'Address',
type: 'object',
properties: {
street_name: { type: 'string' },
city: { type: 'string', description: 'City description' },
state: { type: 'string' },
house_number: { type: 'number' },
house_type: {
type: 'string',
title: 'HouseType',
enum: ['apartment', 'house', 'condo']
},
marriage: {
type: 'boolean',
description: 'Status if marriage live in given house'
},
members: {
oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }]
},
array_type: {
type: 'array',
items: [{ type: 'string' }, { type: 'number' }]
},
other_model: {
type: 'object',
$id: 'OtherModel',
properties: { street_name: { type: 'string' } }
}
},
patternProperties: {
'^S(.?*)test&': {
type: 'string'
}
},
required: ['street_name', 'city', 'state', 'house_number', 'array_type']
};

generator = new TypeScriptGenerator({
moduleSystem: 'ESM',
modelType: 'interface'
});
const models = await generator.generateCompleteModels(doc, {
exportType: 'named'
});
expect(models).toHaveLength(3);
expect(models[0].result).toMatchSnapshot();
expect(models[1].result).toMatchSnapshot();
expect(models[2].result).toMatchSnapshot();
});

describe('AsyncAPI with polymorphism', () => {
const asyncapiDoc = {
asyncapi: '2.4.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,43 @@ class OtherModel {
export { OtherModel };"
`;

exports[`TypeScriptGenerator should render models and their dependencies for ESM module system with named exports as interfaces 1`] = `
"import {HouseType} from './HouseType';
import type {OtherModel} from './OtherModel';
interface Address {
streetName: string;
city: string;
state: string;
houseNumber: number;
houseType?: HouseType;
marriage?: boolean;
members?: string | number | boolean;
arrayType: (string | number | any)[];
otherModel?: OtherModel;
additionalProperties?: Map<string, any | string>;
}
export type { Address };"
`;

exports[`TypeScriptGenerator should render models and their dependencies for ESM module system with named exports as interfaces 2`] = `
"
enum HouseType {
APARTMENT = \\"apartment\\",
HOUSE = \\"house\\",
CONDO = \\"condo\\",
}
export { HouseType };"
`;

exports[`TypeScriptGenerator should render models and their dependencies for ESM module system with named exports as interfaces 3`] = `
"
interface OtherModel {
streetName?: string;
additionalProperties?: Map<string, any>;
}
export type { OtherModel };"
`;

exports[`TypeScriptGenerator should render union \`enum\` values 1`] = `
"enum States {
NUMBER_2 = 2,
Expand Down
Loading