diff --git a/.changeset/forty-ducks-allow.md b/.changeset/forty-ducks-allow.md new file mode 100644 index 0000000..f563a90 --- /dev/null +++ b/.changeset/forty-ducks-allow.md @@ -0,0 +1,5 @@ +--- +"openapi-ts-json-schema": minor +--- + +`PluginInput['makeRelativePath']` type renamed to `PluginInput['makeRelativeModulePath']` diff --git a/.changeset/real-keys-happen.md b/.changeset/real-keys-happen.md new file mode 100644 index 0000000..7065d92 --- /dev/null +++ b/.changeset/real-keys-happen.md @@ -0,0 +1,5 @@ +--- +"openapi-ts-json-schema": minor +--- + +Support Windows OS diff --git a/.changeset/wicked-carrots-mate.md b/.changeset/wicked-carrots-mate.md new file mode 100644 index 0000000..9d44e93 --- /dev/null +++ b/.changeset/wicked-carrots-mate.md @@ -0,0 +1,5 @@ +--- +"openapi-ts-json-schema": minor +--- + +Remove `schemaFileName` meta data prop diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d66b805..90830ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,20 +14,20 @@ jobs: strategy: matrix: - node-version: [18, 20] - os: [ubuntu-latest] + node-version: [20] + os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: ${{ matrix.version }} + node-version: ${{ matrix.node-version }} - run: npm ci - uses: actions/upload-artifact@v4 - if: matrix.node-version == '18' + if: matrix.node-version == '20' && matrix.os == 'ubuntu-latest' with: name: code-coverage path: coverage diff --git a/.gitignore b/.gitignore index 03fcfa7..d512c49 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ coverage !.github !.nvmrc !.changeset +!.gitattributes diff --git a/README.md b/README.md index 8a31b3f..f343acb 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,6 @@ Beside generating the expected schema files under `outputPath`, `openapiToTsJson { schemaId: string; // JSON schema Compound Schema Document `$id`. Eg: `"/components/schemas/MySchema"` - schemaFileName: string; - // Valid filename for given schema (without extension). Eg: `"MySchema"` schemaAbsoluteDirName: string; // Absolute path pointing to schema folder. Eg: `"/output/path/components/schemas"` schemaAbsolutePath: string; diff --git a/package.json b/package.json index 34edcc4..164e1e4 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "vitest": "^1.1.0" }, "engines": { - "node": "^18.0.0" + "node": ">=18.0.0" }, "simple-git-hooks": { "pre-commit": "npm run source:check && npm test -- --run" diff --git a/src/openapiToTsJsonSchema.ts b/src/openapiToTsJsonSchema.ts index f6840ab..eec39e2 100644 --- a/src/openapiToTsJsonSchema.ts +++ b/src/openapiToTsJsonSchema.ts @@ -12,7 +12,7 @@ import { pathToRef, formatTypeScript, saveFile, - makeRelativePath, + makeRelativeModulePath, } from './utils'; import type { SchemaPatcher, @@ -166,7 +166,7 @@ export async function openapiToTsJsonSchema({ for (const plugin of plugins) { await plugin({ ...returnPayload, - utils: { makeRelativePath, formatTypeScript, saveFile }, + utils: { makeRelativeModulePath, formatTypeScript, saveFile }, }); } diff --git a/src/plugins/fastifyIntegrationPlugin.ts b/src/plugins/fastifyIntegrationPlugin.ts index 04a5e77..de2bae5 100644 --- a/src/plugins/fastifyIntegrationPlugin.ts +++ b/src/plugins/fastifyIntegrationPlugin.ts @@ -15,7 +15,7 @@ const fastifyIntegrationPlugin: Plugin< .map( ({ schemaAbsoluteImportPath, schemaUniqueName, schemaId, isRef }) => { return { - importPath: utils.makeRelativePath({ + importPath: utils.makeRelativeModulePath({ fromDirectory: outputPath, to: schemaAbsoluteImportPath, }), diff --git a/src/types.ts b/src/types.ts index 70e76b4..2e46508 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,10 +3,13 @@ export type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; export type JSONSchemaWithPlaceholders = JSONSchema | string; export type OpenApiSchema = Record; export type SchemaPatcher = (params: { schema: JSONSchema }) => void; -import type { makeRelativePath, formatTypeScript, saveFile } from './utils'; +import type { + makeRelativeModulePath, + formatTypeScript, + saveFile, +} from './utils'; /** - * @prop `schemaFileName` - Valid filename for given schema (without extension). Eg: `"MySchema"` * @prop `schemaAbsoluteDirName` - Absolute path pointing to schema folder. Eg: `"/output/path/components/schemas"` * @prop `schemaAbsolutePath` - Absolute path pointing to schema file. Eg: `"/output/path/components/schemas/MySchema.ts"` * @prop `schemaAbsoluteImportPath` - Absolute import path (without extension). Eg: `"/output/path/components/schemas/MySchema"` @@ -16,7 +19,6 @@ import type { makeRelativePath, formatTypeScript, saveFile } from './utils'; * @prop `isRef` - True if schemas is used as `$ref` */ export type SchemaMetaData = { - schemaFileName: string; schemaAbsoluteDirName: string; schemaAbsolutePath: string; schemaAbsoluteImportPath: string; @@ -38,7 +40,7 @@ export type ReturnPayload = { type PluginInput = ReturnPayload & { utils: { - makeRelativePath: typeof makeRelativePath; + makeRelativeModulePath: typeof makeRelativeModulePath; formatTypeScript: typeof formatTypeScript; saveFile: typeof saveFile; }; diff --git a/src/utils/addSchemaToMetaData.ts b/src/utils/addSchemaToMetaData.ts index 73b74ad..e08a648 100644 --- a/src/utils/addSchemaToMetaData.ts +++ b/src/utils/addSchemaToMetaData.ts @@ -1,7 +1,7 @@ import path from 'node:path'; // @ts-expect-error no type defs for namify import namify from 'namify'; -import { refToPath, filenamify } from '.'; +import { parseRef, refToPath, filenamify } from '.'; import type { SchemaMetaDataMap, SchemaMetaData, JSONSchema } from '../types'; /* @@ -23,8 +23,8 @@ export function addSchemaToMetaData({ }): void { // Do not override existing meta info of inlined schemas if (!schemaMetaDataMap.has(ref)) { - const { schemaRelativeDirName, schemaName, schemaRelativePath } = - refToPath(ref); + const refPath = parseRef(ref); + const { schemaRelativeDirName, schemaName } = refToPath(ref); const schemaAbsoluteDirName = path.join(outputPath, schemaRelativeDirName); const schemaFileName = filenamify(schemaName); const schemaAbsoluteImportPath = path.join( @@ -34,12 +34,11 @@ export function addSchemaToMetaData({ const metaInfo: SchemaMetaData = { originalSchema: schema, - schemaId: `/${schemaRelativePath}`, - schemaFileName, + schemaId: `/${refPath}`, schemaAbsoluteDirName, schemaAbsoluteImportPath, schemaAbsolutePath: schemaAbsoluteImportPath + '.ts', - schemaUniqueName: namify(schemaRelativePath), + schemaUniqueName: namify(refPath), isRef, }; schemaMetaDataMap.set(ref, metaInfo); diff --git a/src/utils/index.ts b/src/utils/index.ts index 8f7a2d8..420df2b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,6 +3,7 @@ export { makeTsJsonSchema } from './makeTsJsonSchema'; export { convertOpenApiPathsParameters } from './convertOpenApiPathsParameters'; export { convertOpenApiToJsonSchema } from './convertOpenApiToJsonSchema'; export { makeTsJsonSchemaFiles } from './makeTsJsonSchemaFiles'; +export { parseRef } from './parseRef'; export { refToPath } from './refToPath'; export { pathToRef } from './pathToRef'; export { @@ -17,6 +18,6 @@ export { isObject } from './isObject'; export { filenamify } from './filenamify'; export { clearFolder } from './clearFolder'; -export { makeRelativePath } from './makeRelativePath'; +export { makeRelativeModulePath } from './makeRelativeModulePath'; export { formatTypeScript } from './formatTypeScript'; export { saveFile } from './saveFile'; diff --git a/src/utils/makeRelativeModulePath.ts b/src/utils/makeRelativeModulePath.ts new file mode 100644 index 0000000..3a498a1 --- /dev/null +++ b/src/utils/makeRelativeModulePath.ts @@ -0,0 +1,17 @@ +import path from 'node:path'; + +/** + * Evaluate the relative module path from/to 2 absolute paths. + * Accepts posix and win32 absolute urls and return a relative modules path (""./foo/bar") + */ +export function makeRelativeModulePath({ + fromDirectory, + to, +}: { + fromDirectory: string; + to: string; +}): string { + return ( + './' + path.relative(fromDirectory, to).split(path.sep).join(path.posix.sep) + ); +} diff --git a/src/utils/makeRelativePath.ts b/src/utils/makeRelativePath.ts deleted file mode 100644 index 99d1d38..0000000 --- a/src/utils/makeRelativePath.ts +++ /dev/null @@ -1,14 +0,0 @@ -import path from 'node:path'; - -/** - * Evaluate the relative path from/to the given absolute paths - */ -export function makeRelativePath({ - fromDirectory, - to, -}: { - fromDirectory: string; - to: string; -}): string { - return './' + path.relative(fromDirectory, to); -} diff --git a/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts b/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts index f2238b7..aac2147 100644 --- a/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts +++ b/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts @@ -1,4 +1,4 @@ -import { makeRelativePath, PLACEHOLDER_REGEX } from '..'; +import { makeRelativeModulePath, PLACEHOLDER_REGEX } from '..'; import type { SchemaMetaDataMap } from '../../types'; /** @@ -28,7 +28,7 @@ export function replacePlaceholdersWithImportedSchemas({ /* c8 ignore stop */ // Evaluate imported schema relative path from current schema file - const importedSchemaRelativePath = makeRelativePath({ + const importedSchemaRelativePath = makeRelativeModulePath({ fromDirectory: schemaAbsoluteDirName, to: importedSchema.schemaAbsoluteImportPath, }); diff --git a/src/utils/parseRef.ts b/src/utils/parseRef.ts new file mode 100644 index 0000000..2fdf9cb --- /dev/null +++ b/src/utils/parseRef.ts @@ -0,0 +1,12 @@ +/** + * Parses OpenApi ref: + * "#/components/schema/Foo" --> "components/schema/Foo" + */ +export function parseRef(ref: string): string { + if (!ref.startsWith('#/')) { + throw new Error(`[openapi-ts-json-schema] Unsupported ref value: "${ref}"`); + } + + const refPath = ref.replace('#/', ''); + return refPath; +} diff --git a/src/utils/pathToRef.ts b/src/utils/pathToRef.ts index 1cbc661..1d4c00a 100644 --- a/src/utils/pathToRef.ts +++ b/src/utils/pathToRef.ts @@ -4,6 +4,7 @@ import { filenamify } from './'; /** * Generate a local OpenAPI ref from a relative path and a schema name */ +const TRALING_SLASH_REGEX = /\/$/; export function pathToRef({ schemaRelativeDirName, schemaName, @@ -13,9 +14,12 @@ export function pathToRef({ }): string { return ( '#/' + - path.join( - schemaRelativeDirName.replaceAll('.', '/'), - filenamify(schemaName), - ) + path + .normalize(schemaRelativeDirName) + .replaceAll('.', '/') + .replaceAll('\\', '/') + .replace(TRALING_SLASH_REGEX, '') + + '/' + + filenamify(schemaName) ); } diff --git a/src/utils/refToPath.ts b/src/utils/refToPath.ts index 5575bee..bbe1b4e 100644 --- a/src/utils/refToPath.ts +++ b/src/utils/refToPath.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import { parseRef } from '.'; /** * Parses OpenAPI local refs (#/components/schema/Foo) to the derive the expected schema output path @@ -7,23 +8,12 @@ import path from 'node:path'; * @NOTE Remote and url refs should have been already resolved and inlined */ export function refToPath(ref: string): { - schemaRelativePath: string; schemaRelativeDirName: string; schemaName: string; } { - /* c8 ignore start */ - if (!ref.startsWith('#/')) { - throw new Error(`[openapi-ts-json-schema] Unsupported ref value: "${ref}"`); - } - /* c8 ignore stop */ - - const refPath = ref.replace('#/', ''); - const schemaName = path.basename(refPath); - const schemaRelativeDirName = path.dirname(refPath); - + const refPath = parseRef(ref); return { - schemaRelativePath: path.join(schemaRelativeDirName, schemaName), - schemaRelativeDirName, - schemaName, + schemaRelativeDirName: path.dirname(refPath), + schemaName: path.basename(refPath), }; } diff --git a/test/metaData.test.ts b/test/metaData.test.ts index a1c2b62..1b82d35 100644 --- a/test/metaData.test.ts +++ b/test/metaData.test.ts @@ -28,10 +28,14 @@ describe('Returned "metaData"', async () => { } const expectedAnswerMetaData: SchemaMetaData = { - schemaFileName: 'Answer', - schemaAbsoluteDirName: `${outputPath}/components/schemas`, - schemaAbsolutePath: `${outputPath}/components/schemas/Answer.ts`, - schemaAbsoluteImportPath: `${outputPath}/components/schemas/Answer`, + schemaAbsoluteDirName: `${outputPath}/components/schemas`.replaceAll( + '/', + path.sep, + ), + schemaAbsolutePath: + `${outputPath}/components/schemas/Answer.ts`.replaceAll('/', path.sep), + schemaAbsoluteImportPath: + `${outputPath}/components/schemas/Answer`.replaceAll('/', path.sep), schemaUniqueName: 'componentsSchemasAnswer', schemaId: '/components/schemas/Answer', originalSchema: expect.any(Object), @@ -39,10 +43,14 @@ describe('Returned "metaData"', async () => { }; const expectedJanuaryMetaData: SchemaMetaData = { - schemaFileName: 'January', - schemaAbsoluteDirName: `${outputPath}/components/months`, - schemaAbsolutePath: `${outputPath}/components/months/January.ts`, - schemaAbsoluteImportPath: `${outputPath}/components/months/January`, + schemaAbsoluteDirName: `${outputPath}/components/months`.replaceAll( + '/', + path.sep, + ), + schemaAbsolutePath: + `${outputPath}/components/months/January.ts`.replaceAll('/', path.sep), + schemaAbsoluteImportPath: + `${outputPath}/components/months/January`.replaceAll('/', path.sep), schemaUniqueName: 'componentsMonthsJanuary', schemaId: '/components/months/January', originalSchema: expect.any(Object), diff --git a/test/test-utils/makeTestOutputPath.ts b/test/test-utils/makeTestOutputPath.ts index 0140f18..944ee95 100644 --- a/test/test-utils/makeTestOutputPath.ts +++ b/test/test-utils/makeTestOutputPath.ts @@ -5,6 +5,6 @@ import { fixtures } from './fixtures'; * Generate output paths in the fixtures folder * as a schemas-autogenerated-* folder */ -export function makeTestOutputPath(id: string = 'no-id'): string { +export function makeTestOutputPath(id: string): string { return path.resolve(fixtures, `schemas-autogenerated-${id}-${Date.now()}`); } diff --git a/test/unit/addSchemaToMetaData.test.ts b/test/unit/addSchemaToMetaData.test.ts new file mode 100644 index 0000000..73fc400 --- /dev/null +++ b/test/unit/addSchemaToMetaData.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import path from 'node:path'; +import { addSchemaToMetaData } from '../../src/utils'; +import type { SchemaMetaData } from '../../src/types'; + +describe('addSchemaToMetaData', () => { + it('generates expected metadata', () => { + const ref = '#/components/schemas/Foo'; + const schemaMetaDataMap = new Map(); + const outputPath = path.normalize('/absolute/output/path'); + const schema = { + description: 'Schema description', + type: 'object' as const, + required: ['bar'], + properties: { bar: { type: 'string' } } as const, + }; + + addSchemaToMetaData({ + ref, + schemaMetaDataMap, + schema, + outputPath, + isRef: true, + }); + + const actual = schemaMetaDataMap.get(ref); + const expected: SchemaMetaData = { + isRef: true, + originalSchema: schema, + schemaAbsoluteDirName: + '/absolute/output/path/components/schemas'.replaceAll('/', path.sep), + schemaAbsoluteImportPath: + '/absolute/output/path/components/schemas/Foo'.replaceAll( + '/', + path.sep, + ), + schemaAbsolutePath: + '/absolute/output/path/components/schemas/Foo.ts'.replaceAll( + '/', + path.sep, + ), + schemaId: '/components/schemas/Foo', + schemaUniqueName: 'componentsSchemasFoo', + }; + + expect(actual).toEqual(expected); + }); +}); diff --git a/test/unit/makeRelativeModulePath.test.ts b/test/unit/makeRelativeModulePath.test.ts new file mode 100644 index 0000000..a316776 --- /dev/null +++ b/test/unit/makeRelativeModulePath.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import path from 'node:path'; +import { makeRelativeModulePath } from '../../src/utils'; + +describe('makeRelativeModulePath', () => { + it.each([ + { + fromDirectory: path.join(__dirname, 'test'), + to: path.join(__dirname, 'foo', 'bar'), + expected: './../foo/bar', + }, + { + fromDirectory: path.join(__dirname, 'test', 'aaa'), + to: path.join(__dirname, 'foo', 'bar'), + expected: './../../foo/bar', + }, + { + fromDirectory: path.join(__dirname, 'test'), + to: path.join(__dirname, 'foo', 'bar'), + expected: './../foo/bar', + }, + { + fromDirectory: path.join(__dirname, 'test'), + to: path.join(__dirname, 'foo'), + expected: './../foo', + }, + { + fromDirectory: path.join(__dirname, 'test'), + to: path.join(__dirname), + expected: './..', + }, + ])('generates expected relative path', ({ fromDirectory, to, expected }) => { + const actual = makeRelativeModulePath({ fromDirectory, to }); + expect(actual).toBe(expected); + }); +}); diff --git a/test/unit/makeRelativePath.test.ts b/test/unit/makeRelativePath.test.ts deleted file mode 100644 index 5afbf8d..0000000 --- a/test/unit/makeRelativePath.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { makeRelativePath } from '../../src/utils'; - -describe('makeRelativePath', () => { - it.each([ - { - fromDirectory: '/data/orandea/test/aaa', - to: '/data/orandea/impl/bbb', - expected: './../../impl/bbb', - }, - { - fromDirectory: '/data/orandea/test', - to: '/data/orandea/impl/bbb', - expected: './../impl/bbb', - }, - { - fromDirectory: '/data/orandea/test', - to: '/data/orandea/test/bbb', - expected: './bbb', - }, - ])('generates expected relative path', ({ fromDirectory, to, expected }) => { - const actual = makeRelativePath({ fromDirectory, to }); - expect(actual).toBe(expected); - }); -}); diff --git a/test/unit/parseRef.test.ts b/test/unit/parseRef.test.ts new file mode 100644 index 0000000..c041892 --- /dev/null +++ b/test/unit/parseRef.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { parseRef } from '../../src/utils'; + +describe('parseRef', () => { + describe('Valid ref', () => { + it('returns, ref path', () => { + const actual = parseRef('#/components/schemas/Foo'); + const expected = 'components/schemas/Foo'; + expect(actual).toBe(expected); + }); + }); + + describe('Invalid ref', () => { + it('throws error', () => { + expect(() => parseRef('/components/schemas/Foo')).toThrow( + new Error( + `[openapi-ts-json-schema] Unsupported ref value: "/components/schemas/Foo"`, + ), + ); + }); + }); +}); diff --git a/test/unit/pathToRef.test.ts b/test/unit/pathToRef.test.ts index e46fae7..93859fe 100644 --- a/test/unit/pathToRef.test.ts +++ b/test/unit/pathToRef.test.ts @@ -18,6 +18,17 @@ describe('pathToRef', () => { schemaName: 'Foo', expected: '#/components/schemas/Foo', }, + // Windows path separators + { + schemaRelativeDirName: 'components\\schemas', + schemaName: 'Foo', + expected: '#/components/schemas/Foo', + }, + { + schemaRelativeDirName: 'components\\schemas\\', + schemaName: 'Foo', + expected: '#/components/schemas/Foo', + }, ])( 'generates expected ref', ({ schemaRelativeDirName, schemaName, expected }) => { diff --git a/test/unit/refToPath.test.ts b/test/unit/refToPath.test.ts index a512276..cc1f6f7 100644 --- a/test/unit/refToPath.test.ts +++ b/test/unit/refToPath.test.ts @@ -5,9 +5,8 @@ describe('refToPath', () => { it('generates expected ref paths', () => { const actual = refToPath('#/components/schema/Foo'); const expected = { - schemaName: 'Foo', schemaRelativeDirName: 'components/schema', - schemaRelativePath: 'components/schema/Foo', + schemaName: 'Foo', }; expect(actual).toEqual(expected);