Skip to content

Commit

Permalink
feat: export and import interfaces as types when using named imports …
Browse files Browse the repository at this point in the history
…and ESM Module Syntax (#1800)
  • Loading branch information
marakalwa authored Feb 17, 2024
1 parent 4451dc2 commit 20c5736
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 16 deletions.
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"
}
}
47 changes: 38 additions & 9 deletions src/generators/typescript/TypeScriptDependencyManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { AbstractDependencyManager } from '../AbstractDependencyManager';
import { renderJavaScriptDependency } from '../../helpers';
import { ConstrainedMetaModel } from '../../models';
import { TypeScriptExportType, TypeScriptOptions } from './TypeScriptGenerator';
import {
TypeScriptExportType,
TypeScriptModelType,
TypeScriptOptions
} from './TypeScriptGenerator';

export class TypeScriptDependencyManager extends AbstractDependencyManager {
constructor(
Expand All @@ -14,19 +18,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 +52,40 @@ export class TypeScriptDependencyManager extends AbstractDependencyManager {
*/
renderCompleteModelDependencies(
model: ConstrainedMetaModel,
exportType: TypeScriptExportType
exportType: TypeScriptExportType,
modelType: TypeScriptModelType
): string {
const dependencyObject =
exportType === 'named' ? `{${model.name}}` : model.name;
return this.renderDependency(dependencyObject, `./${model.name}`);
return this.renderDependency(
dependencyObject,
`./${model.name}`,
modelType
);
}

/**
* 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' ? 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
13 changes: 13 additions & 0 deletions test/generators/typescript/TypeScriptGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,19 @@ ${content}`;
expect(models[1].result).toMatchSnapshot();
});

test('should render models and their dependencies for ESM module system with named exports as interfaces', async () => {
generator = new TypeScriptGenerator({
moduleSystem: 'ESM',
modelType: 'interface'
});
const models = await generator.generateCompleteModels(doc, {
exportType: 'named'
});
expect(models).toHaveLength(2);
expect(models[0].result).toMatchSnapshot();
expect(models[1].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,31 @@ class OtherModel {
export { OtherModel };"
`;

exports[`TypeScriptGenerator should render models and their dependencies for ESM module system with named exports as interfaces 1`] = `
"import type {OtherModel} from './OtherModel';
interface Address {
streetName: string;
city: string;
state: string;
houseNumber: number;
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`] = `
"
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

0 comments on commit 20c5736

Please sign in to comment.