openapi-contract-kit 0.0.1 → 0.0.4
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/CHANGELOG.md +14 -0
- package/README.md +13 -5
- package/dist/bin/openapi-contract-kit.d.ts +2 -0
- package/dist/bin/openapi-contract-kit.js +3 -0
- package/dist/src/cli/formatOutput.d.ts +6 -0
- package/dist/src/cli/formatOutput.js +32 -0
- package/dist/src/generator/config.d.ts +3 -0
- package/dist/src/generator/config.js +72 -0
- package/dist/src/generator/documents.d.ts +11 -0
- package/dist/src/generator/documents.js +92 -0
- package/dist/src/generator/emitEndpoints.d.ts +2 -0
- package/{src/generator/emitEndpoints.mjs → dist/src/generator/emitEndpoints.js} +48 -80
- package/dist/src/generator/emitMakers.d.ts +2 -0
- package/dist/src/generator/emitMakers.js +328 -0
- package/dist/src/generator/emitTypes.d.ts +3 -0
- package/dist/src/generator/emitTypes.js +87 -0
- package/dist/src/generator/generateOpenApiRuntime.d.ts +3 -0
- package/dist/src/generator/generateOpenApiRuntime.js +41 -0
- package/dist/src/generator/model.d.ts +2 -0
- package/dist/src/generator/model.js +278 -0
- package/dist/src/generator/schemaModel.d.ts +9 -0
- package/dist/src/generator/schemaModel.js +408 -0
- package/dist/src/generator/types.d.ts +82 -0
- package/dist/src/generator/writeOutput.d.ts +5 -0
- package/dist/src/generator/writeOutput.js +141 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/runtime.d.ts +14 -0
- package/dist/src/runtime.js +1 -0
- package/package.json +30 -18
- package/bin/openapi-contract-kit.mjs +0 -5
- package/src/generator/config.mjs +0 -92
- package/src/generator/documents.mjs +0 -119
- package/src/generator/emitMakers.mjs +0 -459
- package/src/generator/emitTypes.mjs +0 -104
- package/src/generator/generateOpenApiRuntime.mjs +0 -48
- package/src/generator/model.mjs +0 -415
- package/src/generator/schemaModel.mjs +0 -497
- package/src/generator/writeOutput.mjs +0 -196
- package/src/index.d.ts +0 -16
- package/src/index.mjs +0 -4
- package/src/runtime.d.ts +0 -14
- /package/{src/runtime.mjs → dist/src/generator/types.js} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type UnknownRecord = Record<string, unknown>;
|
|
2
|
+
export type JsonPrimitive = boolean | null | number | string;
|
|
3
|
+
export type SchemaType = 'array' | 'boolean' | 'integer' | 'null' | 'number' | 'object' | 'string';
|
|
4
|
+
export type DocumentContext = {
|
|
5
|
+
readonly documentPath: string;
|
|
6
|
+
readonly pointer: string;
|
|
7
|
+
};
|
|
8
|
+
export type ResolvedReference = DocumentContext & {
|
|
9
|
+
readonly canonicalKey: string;
|
|
10
|
+
readonly value: unknown;
|
|
11
|
+
};
|
|
12
|
+
export type BooleanSchema = {
|
|
13
|
+
readonly booleanSchema: boolean;
|
|
14
|
+
readonly location: string;
|
|
15
|
+
};
|
|
16
|
+
export type SchemaProperty = {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly required: boolean;
|
|
19
|
+
readonly schema: NormalizedSchema;
|
|
20
|
+
};
|
|
21
|
+
export type StandardSchema = {
|
|
22
|
+
readonly additionalProperties: boolean | NormalizedSchema;
|
|
23
|
+
readonly allOf: readonly NormalizedSchema[];
|
|
24
|
+
readonly anyOf: readonly NormalizedSchema[];
|
|
25
|
+
readonly booleanSchema: null;
|
|
26
|
+
readonly constValue: JsonPrimitive | undefined;
|
|
27
|
+
readonly enumValues: readonly JsonPrimitive[] | null;
|
|
28
|
+
readonly exclusiveMaximum: number | null;
|
|
29
|
+
readonly exclusiveMinimum: number | null;
|
|
30
|
+
readonly format: 'email' | null;
|
|
31
|
+
readonly items: NormalizedSchema | null;
|
|
32
|
+
readonly location: string;
|
|
33
|
+
readonly maximum: number | null;
|
|
34
|
+
readonly maxLength: number | null;
|
|
35
|
+
readonly minimum: number | null;
|
|
36
|
+
readonly minLength: number | null;
|
|
37
|
+
readonly oneOf: readonly NormalizedSchema[];
|
|
38
|
+
readonly pattern: string | null;
|
|
39
|
+
readonly properties: readonly SchemaProperty[];
|
|
40
|
+
readonly reference: string | null;
|
|
41
|
+
readonly types: readonly SchemaType[] | null;
|
|
42
|
+
};
|
|
43
|
+
export type NormalizedSchema = BooleanSchema | StandardSchema;
|
|
44
|
+
export type SchemaEntry = {
|
|
45
|
+
readonly canonicalKey: string;
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly schema: NormalizedSchema;
|
|
48
|
+
};
|
|
49
|
+
export type ResponseModel = {
|
|
50
|
+
readonly isSuccess: boolean;
|
|
51
|
+
readonly schemaName: string | null;
|
|
52
|
+
readonly status: number;
|
|
53
|
+
};
|
|
54
|
+
export type OperationModel = {
|
|
55
|
+
readonly method: string;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly operationId: string;
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly request: {
|
|
60
|
+
readonly schemaName: string | null;
|
|
61
|
+
};
|
|
62
|
+
readonly responses: readonly ResponseModel[];
|
|
63
|
+
};
|
|
64
|
+
export type OpenApiModel = {
|
|
65
|
+
readonly operations: readonly OperationModel[];
|
|
66
|
+
readonly schemas: readonly SchemaEntry[];
|
|
67
|
+
};
|
|
68
|
+
export type GeneratorConfig = {
|
|
69
|
+
readonly configPath: string;
|
|
70
|
+
readonly outDir: string;
|
|
71
|
+
readonly runtimeImport: string;
|
|
72
|
+
readonly specPath: string;
|
|
73
|
+
};
|
|
74
|
+
export type GenerateOpenApiRuntimeOptions = {
|
|
75
|
+
readonly argv?: readonly string[];
|
|
76
|
+
readonly cwd?: string;
|
|
77
|
+
};
|
|
78
|
+
export type GenerateOpenApiRuntimeResult = {
|
|
79
|
+
readonly operationCount: number;
|
|
80
|
+
readonly operationNames: readonly string[];
|
|
81
|
+
readonly schemaCount: number;
|
|
82
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { access, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile, } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, parse, relative, } from 'node:path';
|
|
3
|
+
const MANIFEST_FILE = '.openapi-runtime-manifest.json';
|
|
4
|
+
const LEGACY_GENERATED_FILE = /^(?:quickpay-api\.ts|(?:endpoints|schemas)\/[^/]+\.ts)$/u;
|
|
5
|
+
function isManifest(value) {
|
|
6
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const files = Reflect.get(value, 'files');
|
|
10
|
+
return (Array.isArray(files) && files.every((file) => typeof file === 'string'));
|
|
11
|
+
}
|
|
12
|
+
async function exists(path) {
|
|
13
|
+
try {
|
|
14
|
+
await access(path);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function listFiles(directory, prefix = '') {
|
|
22
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
23
|
+
const files = [];
|
|
24
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
25
|
+
const relativePath = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
26
|
+
const absolutePath = join(directory, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
files.push(...(await listFiles(absolutePath, relativePath)));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
files.push(relativePath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
function validateGeneratedPath(path) {
|
|
37
|
+
if (path.length === 0 ||
|
|
38
|
+
isAbsolute(path) ||
|
|
39
|
+
path.split('/').some((segment) => segment === '..' || segment.length === 0)) {
|
|
40
|
+
throw new Error(`Unsafe generated output path "${path}"`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isAncestor(parent, child) {
|
|
44
|
+
const pathFromParent = relative(parent, child);
|
|
45
|
+
return (pathFromParent.length === 0 ||
|
|
46
|
+
(!pathFromParent.startsWith('..') && !isAbsolute(pathFromParent)));
|
|
47
|
+
}
|
|
48
|
+
function validateOutputRoot(outDir, protectedPaths) {
|
|
49
|
+
if (parse(outDir).root === outDir) {
|
|
50
|
+
throw new Error(`Unsafe OpenAPI output directory "${outDir}"`);
|
|
51
|
+
}
|
|
52
|
+
for (const protectedPath of protectedPaths) {
|
|
53
|
+
if (isAncestor(outDir, protectedPath)) {
|
|
54
|
+
throw new Error(`OpenAPI output directory "${outDir}" contains protected input "${protectedPath}"`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function validateExistingOutput(outDir) {
|
|
59
|
+
if (!(await exists(outDir))) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const existingFiles = await listFiles(outDir);
|
|
63
|
+
if (existingFiles.length === 0) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (!existingFiles.includes(MANIFEST_FILE)) {
|
|
67
|
+
const unexpected = existingFiles.find((file) => !LEGACY_GENERATED_FILE.test(file));
|
|
68
|
+
if (unexpected !== undefined) {
|
|
69
|
+
throw new Error(`OpenAPI output directory contains unexpected file "${unexpected}"`);
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
let manifest;
|
|
74
|
+
try {
|
|
75
|
+
manifest = JSON.parse(await readFile(join(outDir, MANIFEST_FILE), 'utf8'));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error(`OpenAPI output manifest is invalid at ${outDir}`);
|
|
79
|
+
}
|
|
80
|
+
if (!isManifest(manifest)) {
|
|
81
|
+
throw new Error(`OpenAPI output manifest is invalid at ${outDir}`);
|
|
82
|
+
}
|
|
83
|
+
const ownedFiles = new Set([...manifest.files, MANIFEST_FILE]);
|
|
84
|
+
const unexpected = existingFiles.find((file) => !ownedFiles.has(file));
|
|
85
|
+
if (unexpected !== undefined) {
|
|
86
|
+
throw new Error(`OpenAPI output directory contains unexpected file "${unexpected}"`);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
async function writeStage(stagePath, files) {
|
|
91
|
+
await mkdir(stagePath, { recursive: true });
|
|
92
|
+
for (const [relativePath, content] of files) {
|
|
93
|
+
validateGeneratedPath(relativePath);
|
|
94
|
+
const targetPath = join(stagePath, relativePath);
|
|
95
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
96
|
+
await writeFile(targetPath, content);
|
|
97
|
+
}
|
|
98
|
+
const manifest = {
|
|
99
|
+
version: 1,
|
|
100
|
+
files: [...files.keys()].sort(),
|
|
101
|
+
};
|
|
102
|
+
await writeFile(join(stagePath, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
export async function writeGeneratedOutput(outDir, files, options = {}) {
|
|
105
|
+
const protectedPaths = options.protectedPaths ?? [];
|
|
106
|
+
validateOutputRoot(outDir, protectedPaths);
|
|
107
|
+
const hadExistingOutput = await validateExistingOutput(outDir);
|
|
108
|
+
const parent = dirname(outDir);
|
|
109
|
+
await mkdir(parent, { recursive: true });
|
|
110
|
+
const swapRoot = await mkdtemp(join(parent, `.${basename(outDir)}.openapi-runtime-`));
|
|
111
|
+
const stagedOutput = join(swapRoot, 'next');
|
|
112
|
+
const previousOutput = join(swapRoot, 'previous');
|
|
113
|
+
let movedPreviousOutput = false;
|
|
114
|
+
try {
|
|
115
|
+
await writeStage(stagedOutput, files);
|
|
116
|
+
if (hadExistingOutput) {
|
|
117
|
+
await rename(outDir, previousOutput);
|
|
118
|
+
movedPreviousOutput = true;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await rename(stagedOutput, outDir);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (movedPreviousOutput && !(await exists(outDir))) {
|
|
125
|
+
await rename(previousOutput, outDir);
|
|
126
|
+
movedPreviousOutput = false;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (movedPreviousOutput) {
|
|
131
|
+
await rm(previousOutput, { force: true, recursive: true });
|
|
132
|
+
movedPreviousOutput = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if (movedPreviousOutput && !(await exists(outDir))) {
|
|
137
|
+
await rename(previousOutput, outDir);
|
|
138
|
+
}
|
|
139
|
+
await rm(swapRoot, { force: true, recursive: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { generateOpenApiRuntime, main, } from './generator/generateOpenApiRuntime.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ValidationPath = readonly (string | number)[];
|
|
2
|
+
export type ValidationIssue = {
|
|
3
|
+
readonly path: ValidationPath;
|
|
4
|
+
readonly keyword: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
};
|
|
7
|
+
export type Result<T> = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly value: T;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly errors: readonly ValidationIssue[];
|
|
13
|
+
};
|
|
14
|
+
export type MakeResult<T> = Result<T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,41 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openapi-contract-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Generate type-safe TypeScript contracts and runtime validators from JSON OpenAPI 3.1 documents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=24.0.0"
|
|
9
9
|
},
|
|
10
|
-
"main": "./src/index.
|
|
11
|
-
"types": "./src/index.d.ts",
|
|
10
|
+
"main": "./dist/src/index.js",
|
|
11
|
+
"types": "./dist/src/index.d.ts",
|
|
12
12
|
"bin": {
|
|
13
|
-
"openapi-contract-kit": "./bin/openapi-contract-kit.
|
|
13
|
+
"openapi-contract-kit": "./dist/bin/openapi-contract-kit.js"
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
17
|
-
"types": "./src/index.d.ts",
|
|
18
|
-
"import": "./src/index.
|
|
17
|
+
"types": "./dist/src/index.d.ts",
|
|
18
|
+
"import": "./dist/src/index.js"
|
|
19
19
|
},
|
|
20
20
|
"./runtime": {
|
|
21
|
-
"types": "./src/runtime.d.ts",
|
|
22
|
-
"import": "./src/runtime.
|
|
21
|
+
"types": "./dist/src/runtime.d.ts",
|
|
22
|
+
"import": "./dist/src/runtime.js"
|
|
23
23
|
},
|
|
24
24
|
"./package.json": "./package.json"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"bin",
|
|
28
|
-
"src",
|
|
27
|
+
"dist/bin",
|
|
28
|
+
"dist/src",
|
|
29
29
|
"CHANGELOG.md",
|
|
30
30
|
"LICENSE",
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
|
-
"scripts": {
|
|
34
|
-
"test": "node --test test/*.test.mjs",
|
|
35
|
-
"pack:check": "npm pack --dry-run"
|
|
36
|
-
},
|
|
37
33
|
"devDependencies": {
|
|
38
|
-
"
|
|
34
|
+
"@eslint/js": "^10.0.1",
|
|
35
|
+
"@types/node": "^24.0.0",
|
|
36
|
+
"eslint": "^10.10.0",
|
|
37
|
+
"prettier": "^3.9.6",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"typescript-eslint": "^8.0.0",
|
|
40
|
+
"vitest": "^5.0.0"
|
|
39
41
|
},
|
|
40
42
|
"repository": {
|
|
41
43
|
"type": "git",
|
|
@@ -54,5 +56,15 @@
|
|
|
54
56
|
"api-contracts",
|
|
55
57
|
"json-schema",
|
|
56
58
|
"cli"
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsc --project tsconfig.json",
|
|
62
|
+
"format": "prettier --write .",
|
|
63
|
+
"format:check": "prettier --check .",
|
|
64
|
+
"lint": "eslint .",
|
|
65
|
+
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
66
|
+
"test:unit": "vitest run test/generateOpenApiRuntime.test.ts test/model.test.ts --pool=forks",
|
|
67
|
+
"test": "pnpm run build && pnpm run test:unit",
|
|
68
|
+
"pack:check": "pnpm run build && pnpm pack --dry-run"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/generator/config.mjs
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
3
|
-
|
|
4
|
-
const CONFIG_KEYS = new Set(['specPath', 'outDir', 'runtimeImport']);
|
|
5
|
-
const FLAG_NAMES = new Map([
|
|
6
|
-
['--config', 'configPath'],
|
|
7
|
-
['--spec', 'specPath'],
|
|
8
|
-
['--out', 'outDir'],
|
|
9
|
-
['--runtime-import', 'runtimeImport'],
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
function isRecord(value) {
|
|
13
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function parseArguments(argv, cwd) {
|
|
17
|
-
const values = {};
|
|
18
|
-
|
|
19
|
-
for (let index = 0; index < argv.length; index += 2) {
|
|
20
|
-
const flag = argv[index];
|
|
21
|
-
const key = FLAG_NAMES.get(flag);
|
|
22
|
-
const value = argv[index + 1];
|
|
23
|
-
|
|
24
|
-
if (key === undefined) {
|
|
25
|
-
throw new Error(`Unknown argument "${flag}"`);
|
|
26
|
-
}
|
|
27
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
28
|
-
throw new Error(`Missing value for "${flag}"`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
values[key] = isAbsolute(value) ? value : resolve(cwd, value);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return values;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function requirePath(config, key, configPath) {
|
|
38
|
-
const value = config[key];
|
|
39
|
-
|
|
40
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
41
|
-
throw new Error(`Config "${configPath}" requires a non-empty "${key}"`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function resolveGeneratorConfig({
|
|
48
|
-
argv = [],
|
|
49
|
-
cwd = process.cwd(),
|
|
50
|
-
} = {}) {
|
|
51
|
-
const cli = parseArguments(argv, cwd);
|
|
52
|
-
const configPath = cli.configPath ?? resolve(cwd, 'openapi.config.json');
|
|
53
|
-
let config;
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
57
|
-
} catch (error) {
|
|
58
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
-
throw new Error(
|
|
60
|
-
`Unable to read OpenAPI config "${configPath}": ${message}`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!isRecord(config)) {
|
|
65
|
-
throw new Error(`OpenAPI config "${configPath}" must contain an object`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const key of Object.keys(config)) {
|
|
69
|
-
if (!CONFIG_KEYS.has(key)) {
|
|
70
|
-
throw new Error(`Unknown OpenAPI config key "${key}"`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const configDirectory = dirname(configPath);
|
|
75
|
-
const resolveConfigPath = (key) => {
|
|
76
|
-
const value = requirePath(config, key, configPath);
|
|
77
|
-
return isAbsolute(value) ? value : resolve(configDirectory, value);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const runtimeImport =
|
|
81
|
-
cli.runtimeImport ?? requirePath(config, 'runtimeImport', configPath);
|
|
82
|
-
if (runtimeImport.includes('\\') || runtimeImport.startsWith('.')) {
|
|
83
|
-
throw new Error(`Config "${configPath}" requires a package runtimeImport`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
configPath,
|
|
88
|
-
specPath: cli.specPath ?? resolveConfigPath('specPath'),
|
|
89
|
-
outDir: cli.outDir ?? resolveConfigPath('outDir'),
|
|
90
|
-
runtimeImport,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, extname, resolve } from 'node:path';
|
|
3
|
-
|
|
4
|
-
export function isRecord(value) {
|
|
5
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function escapePointerSegment(segment) {
|
|
9
|
-
return segment.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function pointerChild(pointer, segment) {
|
|
13
|
-
return `${pointer}/${escapePointerSegment(String(segment))}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function locationOf(documentPath, pointer) {
|
|
17
|
-
return `${documentPath}#${pointer}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function requireRecord(value, location, label) {
|
|
21
|
-
if (!isRecord(value)) {
|
|
22
|
-
throw new Error(`${label} at ${location} must be an object`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class DocumentStore {
|
|
29
|
-
#documents = new Map();
|
|
30
|
-
|
|
31
|
-
async load(documentPath) {
|
|
32
|
-
const absolutePath = resolve(documentPath);
|
|
33
|
-
const existing = this.#documents.get(absolutePath);
|
|
34
|
-
|
|
35
|
-
if (existing !== undefined) {
|
|
36
|
-
return existing;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const pending = this.#read(absolutePath);
|
|
40
|
-
this.#documents.set(absolutePath, pending);
|
|
41
|
-
return pending;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async #read(documentPath) {
|
|
45
|
-
let source;
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
source = await readFile(documentPath, 'utf8');
|
|
49
|
-
} catch (error) {
|
|
50
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
-
throw new Error(
|
|
52
|
-
`Unable to read OpenAPI document "${documentPath}": ${message}`
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const extension = extname(documentPath).toLowerCase();
|
|
58
|
-
if (extension !== '.json') {
|
|
59
|
-
throw new Error('Only JSON OpenAPI documents are supported');
|
|
60
|
-
}
|
|
61
|
-
const value = JSON.parse(source);
|
|
62
|
-
return requireRecord(value, documentPath, 'OpenAPI document');
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
-
throw new Error(
|
|
66
|
-
`Unable to parse OpenAPI document "${documentPath}": ${message}`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async resolveReference(reference, fromDocumentPath) {
|
|
72
|
-
if (typeof reference !== 'string' || reference.length === 0) {
|
|
73
|
-
throw new Error(`Invalid $ref at ${fromDocumentPath}`);
|
|
74
|
-
}
|
|
75
|
-
if (/^[A-Za-z][A-Za-z\d+.-]*:/u.test(reference)) {
|
|
76
|
-
throw new Error(`Remote $ref "${reference}" is unsupported`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const hashIndex = reference.indexOf('#');
|
|
80
|
-
const filePart =
|
|
81
|
-
hashIndex === -1 ? reference : reference.slice(0, hashIndex);
|
|
82
|
-
const fragment = hashIndex === -1 ? '' : reference.slice(hashIndex + 1);
|
|
83
|
-
const documentPath =
|
|
84
|
-
filePart.length === 0
|
|
85
|
-
? fromDocumentPath
|
|
86
|
-
: resolve(dirname(fromDocumentPath), decodeURIComponent(filePart));
|
|
87
|
-
const document = await this.load(documentPath);
|
|
88
|
-
const decodedFragment = decodeURIComponent(fragment);
|
|
89
|
-
|
|
90
|
-
if (decodedFragment.length > 0 && !decodedFragment.startsWith('/')) {
|
|
91
|
-
throw new Error(`Anchor $ref "${reference}" is unsupported`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let value = document;
|
|
95
|
-
for (const encodedSegment of decodedFragment.split('/').slice(1)) {
|
|
96
|
-
const segment = encodedSegment
|
|
97
|
-
.replaceAll('~1', '/')
|
|
98
|
-
.replaceAll('~0', '~');
|
|
99
|
-
if (!isRecord(value) && !Array.isArray(value)) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
`Unresolved $ref "${reference}" from ${fromDocumentPath}`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
if (!Object.prototype.hasOwnProperty.call(value, segment)) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
`Unresolved $ref "${reference}" from ${fromDocumentPath}`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
value = Reflect.get(value, segment);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
canonicalKey: locationOf(documentPath, decodedFragment),
|
|
114
|
-
documentPath,
|
|
115
|
-
pointer: decodedFragment,
|
|
116
|
-
value,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|