diff --git a/.knipignore b/.knipignore new file mode 100644 index 000000000..501cbb48a --- /dev/null +++ b/.knipignore @@ -0,0 +1,5 @@ +# Test helpers are intentionally exported for use across test files +test/helpers/** + +# Keep all exported types - they might be used by TypeScript consumers +**/*.d.ts diff --git a/Makefile b/Makefile index e9609fe30..d6faf6d6d 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ AIDBOX_LICENSE_ID ?= TYPECHECK = bunx tsc --noEmit FORMAT = bunx biome format --write -LINT = bunx biome lint --diagnostic-level=error --write --unsafe +LINT = bunx biome check --write TEST = bun test .PHONY: all typecheck test-typeschema test-register test-codegen test-typescript-r4-example diff --git a/biome.json b/biome.json index 309e4e9a0..ad32b0069 100644 --- a/biome.json +++ b/biome.json @@ -42,7 +42,7 @@ "noExcessiveCognitiveComplexity": { "level": "warn", "options": { - "maxAllowedComplexity": 50 + "maxAllowedComplexity": 39 } } }, diff --git a/bun.lock b/bun.lock index 64bd5969a..7bf8525ab 100644 --- a/bun.lock +++ b/bun.lock @@ -6,11 +6,6 @@ "dependencies": { "@atomic-ehr/fhir-canonical-manager": "^0.0.15", "@atomic-ehr/fhirschema": "^0.0.5", - "@inquirer/prompts": "^7.8.1", - "ajv": "^8.17.1", - "glob": "^12.0.0", - "handlebars": "^4.7.8", - "ora": "^8.2.0", "picocolors": "^1.1.1", "yaml": "^2.8.1", "yargs": "^18.0.0", @@ -18,10 +13,9 @@ "devDependencies": { "@biomejs/biome": "^2.1.4", "@types/bun": "^1.2.23", - "@types/handlebars": "^4.1.0", "@types/node": "^22.17.1", "@types/yargs": "^17.0.33", - "ts-prune": "^0.10.3", + "knip": "^5.27.2", "tsup": "^8.5.1", "typescript": "^5.9.2", }, @@ -32,10 +26,6 @@ "@atomic-ehr/fhirschema": ["@atomic-ehr/fhirschema@0.0.5", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-B/8ScNnnQUIR6d3FsIuGGvanOyE2j7W3mAubVmpPE2I/tho+meEBRrGgs5E+AY4jDz9mviTdOta08RpIhH2kew=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - "@biomejs/biome": ["@biomejs/biome@2.1.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.4", "@biomejs/cli-darwin-x64": "2.1.4", "@biomejs/cli-linux-arm64": "2.1.4", "@biomejs/cli-linux-arm64-musl": "2.1.4", "@biomejs/cli-linux-x64": "2.1.4", "@biomejs/cli-linux-x64-musl": "2.1.4", "@biomejs/cli-win32-arm64": "2.1.4", "@biomejs/cli-win32-x64": "2.1.4" }, "bin": { "biome": "bin/biome" } }, "sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg=="], @@ -54,6 +44,12 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw=="], + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], @@ -106,55 +102,61 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], - "@inquirer/checkbox": ["@inquirer/checkbox@4.2.0", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@inquirer/confirm": ["@inquirer/confirm@5.1.14", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@inquirer/core": ["@inquirer/core@10.1.15", "", { "dependencies": { "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], - "@inquirer/editor": ["@inquirer/editor@4.2.16", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/external-editor": "^1.0.0", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-iSzLjT4C6YKp2DU0fr8T7a97FnRRxMO6CushJnW5ktxLNM2iNeuyUuUA5255eOLPORoGYCrVnuDOEBdGkHGkpw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - "@inquirer/expand": ["@inquirer/expand@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw=="], + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - "@inquirer/external-editor": ["@inquirer/external-editor@1.0.0", "", { "dependencies": { "chardet": "^2.1.0", "iconv-lite": "^0.6.3" }, "peerDependencies": { "@types/node": ">=18" } }, "sha512-5v3YXc5ZMfL6OJqXPrX9csb4l7NlQA2doO1yynUjpUChT9hg4JcuBVP0RbsEJ/3SL/sxWEyFjT2W69ZhtoBWqg=="], + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="], + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.13.2", "", { "os": "android", "cpu": "arm" }, "sha512-vWd1NEaclg/t2DtEmYzRRBNQOueMI8tixw/fSNZ9XETXLRJiAjQMYpYeflQdRASloGze6ZelHE/wIBNt4S+pkw=="], - "@inquirer/input": ["@inquirer/input@4.2.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow=="], + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.13.2", "", { "os": "android", "cpu": "arm64" }, "sha512-jxZrYcxgpI6IuQpguQVAQNrZfUyiYfMVqR4pKVU3PRLCM7AsfXNKp0TIgcvp+l6dYVdoZ1MMMMa5Ayjd09rNOw=="], - "@inquirer/number": ["@inquirer/number@3.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg=="], + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.13.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RDS3HUe1FvgjNS1xfBUqiEJ8938Zb5r7iKABwxEblp3K4ufZZNAtoaHjdUH2TJ0THDmuf0OxxVUO/Y+4Ep4QfQ=="], - "@inquirer/password": ["@inquirer/password@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA=="], + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.13.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tDcyWtkUzkt6auJLP2dOjL84BxqHkKW4mz2lNRIGPTq7b+HBraB+m8RdRH6BgqTvbnNECOxR3XAMaKBKC8J51g=="], - "@inquirer/prompts": ["@inquirer/prompts@7.8.1", "", { "dependencies": { "@inquirer/checkbox": "^4.2.0", "@inquirer/confirm": "^5.1.14", "@inquirer/editor": "^4.2.16", "@inquirer/expand": "^4.0.17", "@inquirer/input": "^4.2.1", "@inquirer/number": "^3.0.17", "@inquirer/password": "^4.0.17", "@inquirer/rawlist": "^4.1.5", "@inquirer/search": "^3.1.0", "@inquirer/select": "^4.3.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LpBPeIpyCF1H3C7SK/QxJQG4iV1/SRmJdymfcul8PuwtVhD0JI1CSwqmd83VgRgt1QEsDojQYFSXJSgo81PVMw=="], + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.13.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fpaeN8Q0kWvKns9uSMg6CcKo7cdgmWt6J91stPf8sdM+EKXzZ0YcRnWWyWF8SM16QcLUPCy5Iwt5Z8aYBGaZYA=="], - "@inquirer/rawlist": ["@inquirer/rawlist@4.1.5", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA=="], + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.13.2", "", { "os": "linux", "cpu": "arm" }, "sha512-idBgJU5AvSsGOeaIWiFBKbNBjpuduHsJmrG4CBbEUNW/Ykx+ISzcuj1PHayiYX6R9stVsRhj3d2PyymfC5KWRg=="], - "@inquirer/search": ["@inquirer/search@3.1.0", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q=="], + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.13.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BlBvQUhvvIM/7s96KlKhMk0duR2sj8T7Hyii46/5QnwfN/pHwobvOL5czZ6/SKrHNB/F/qDY4hGsBuB1y7xgTg=="], - "@inquirer/select": ["@inquirer/select@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA=="], + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.13.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lUmDTmYOGpbIK+FBfZ0ySaQTo7g1Ia/WnDnQR2wi/0AtehZIg/ZZIgiT/fD0iRvKEKma612/0PVo8dXdAKaAGA=="], - "@inquirer/type": ["@inquirer/type@3.0.8", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw=="], + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.13.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dkGzOxo+I9lA4Er6qzFgkFevl3JvwyI9i0T/PkOJHva04rb1p9dz8GPogTO9uMK4lrwLWzm/piAu+tHYC7v7+w=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.13.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-53kWsjLkVFnoSA7COdps38pBssN48zI8LfsOvupsmQ0/4VeMYb+0Ao9O6r52PtmFZsGB3S1Qjqbjl/Pswj1a3g=="], - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.13.2", "", { "os": "linux", "cpu": "none" }, "sha512-MfxN6DMpvmdCbGlheJ+ihy11oTcipqDfcEIQV9ah3FGXBRCZtBOHJpQDk8qI2Y+nCXVr3Nln7OSsOzoC4+rSYQ=="], - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.13.2", "", { "os": "linux", "cpu": "none" }, "sha512-WXrm4YiRU0ijqb72WHSjmfYaQZ7t6/kkQrFc4JtU+pUE4DZA/DEdxOuQEd4Q43VqmLvICTJWSaZMlCGQ4PSRUg=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.13.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-4pISWIlOFRUhWyvGCB3XUhtcwyvwGGhlXhHz7IXCXuGufaQtvR05trvw8U1ZnaPhsdPBkRhOMIedX11ayi5uXw=="], - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.13.2", "", { "os": "linux", "cpu": "x64" }, "sha512-DVo6jS8n73yNAmCsUOOk2vBeC60j2RauDXQM8p7RDl0afsEaA2le22vD8tky7iNoM5tsxfBmE4sOJXEKgpwWRw=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.13.2", "", { "os": "linux", "cpu": "x64" }, "sha512-6WqrE+hQBFP35KdwQjWcZpldbTq6yJmuTVThISu+rY3+j6MaDp2ciLHTr1X68r2H/7ocOIl4k3NnOVIzeRJE3w=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.13.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-YpxvQmP2D+mNUkLQZbBjGz20g/pY8XoOBdPPoWMl9X68liFFjXxkPQTrZxWw4zzG/UkTM5z6dPRTyTePRsMcjw=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.13.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-1SKBw6KcCmvPBdEw1/Qdpv6eSDf23lCXTWz9VxTe6QUQ/1wR+HZR2uS4q6C8W6jnIswMTQbxpTvVwdRXl+ufeA=="], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.13.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-KEVV7wggDucxRn3vvyHnmTCPXoCT7vWpH18UVLTygibHJvNRP2zl5lBaQcCIdIaYYZjKt1aGI/yZqxZvHoiCdg=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.13.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6AAdN9v/wO5c3td1yidgNLKYlzuNgfOtEqBq60WE469bJWR7gHgG/S5aLR2pH6/gyPLs9UXtItxi934D+0Estg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -202,18 +204,14 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], - "@ts-morph/common": ["@ts-morph/common@0.12.3", "", { "dependencies": { "fast-glob": "^3.2.7", "minimatch": "^3.0.4", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/handlebars": ["@types/handlebars@4.1.0", "", { "dependencies": { "handlebars": "*" } }, "sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA=="], - "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], - "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], - "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], @@ -222,19 +220,17 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -244,38 +240,20 @@ "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="], - - "chardet": ["chardet@2.1.0", "", {}, "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], - "code-block-writer": ["code-block-writer@11.0.3", "", {}, "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], @@ -286,20 +264,16 @@ "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - "esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -308,49 +282,37 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], - "glob": ["glob@12.0.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="], + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], - - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], - - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "knip": ["knip@5.70.0", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.1", "minimist": "^1.2.8", "oxc-resolver": "^11.13.2", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-ZRO7GzegusadOqR0ICxEQfbM1RS+1Uu/LtATpzO71pHXZQnoj4K47/QtuCtfvJVjWb2R4a7YwHv+Ey9xoxjQCw=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -358,11 +320,7 @@ "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], - - "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], @@ -370,45 +328,27 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - - "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "oxc-resolver": ["oxc-resolver@11.13.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.13.2", "@oxc-resolver/binding-android-arm64": "11.13.2", "@oxc-resolver/binding-darwin-arm64": "11.13.2", "@oxc-resolver/binding-darwin-x64": "11.13.2", "@oxc-resolver/binding-freebsd-x64": "11.13.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.13.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.13.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.13.2", "@oxc-resolver/binding-linux-arm64-musl": "11.13.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.13.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.13.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.13.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.13.2", "@oxc-resolver/binding-linux-x64-gnu": "11.13.2", "@oxc-resolver/binding-linux-x64-musl": "11.13.2", "@oxc-resolver/binding-wasm32-wasi": "11.13.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.13.2", "@oxc-resolver/binding-win32-ia32-msvc": "11.13.2", "@oxc-resolver/binding-win32-x64-msvc": "11.13.2" } }, "sha512-1SXVyYQ9bqMX3uZo8Px81EG7jhZkO9PvvR5X9roY5TLYVm4ZA7pbPDNlYaDBBeF9U+YO3OeMNoHde52hrcCu8w=="], "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], - - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -426,29 +366,23 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], - "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -458,6 +392,8 @@ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -472,29 +408,21 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "true-myth": ["true-myth@4.1.1", "", {}, "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg=="], - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - "ts-morph": ["ts-morph@13.0.3", "", { "dependencies": { "@ts-morph/common": "~0.12.3", "code-block-writer": "^11.0.0" } }, "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw=="], - - "ts-prune": ["ts-prune@0.10.3", "", { "dependencies": { "commander": "^6.2.1", "cosmiconfig": "^7.0.1", "json5": "^2.1.3", "lodash": "^4.17.21", "true-myth": "^4.1.0", "ts-morph": "^13.0.1" }, "bin": { "ts-prune": "lib/index.js" } }, "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], - "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], - "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], @@ -508,26 +436,14 @@ "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], - - "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "@ts-morph/common/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "bun-types/@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="], - "cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], - - "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -536,44 +452,20 @@ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "sucrase/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], } } diff --git a/examples/typescript-r4/demo.ts b/examples/typescript-r4/demo.ts index a8877b72e..162db8cfc 100644 --- a/examples/typescript-r4/demo.ts +++ b/examples/typescript-r4/demo.ts @@ -276,14 +276,15 @@ function runDemo() { const patient = createPatient(); console.log("✓ Created patient:", patient.id); - const observation = createObservation(patient.id!); + if (!patient.id) throw new Error("Failed to create patient"); + const observation = createObservation(patient.id); console.log("✓ Created glucose observation:", observation.id); console.log(`\n${"=".repeat(60)}`); console.log("Bodyweight profile attach/extract demo:"); console.log("=".repeat(60)); - const bodyweightObs = createBodyWeightObservation(patient.id!); + const bodyweightObs = createBodyWeightObservation(patient.id); console.log("✓ Created body weight observation with profile:", bodyweightObs.id); const bundle = createBundle(patient, observation, bodyweightObs); diff --git a/knip.json b/knip.json new file mode 100644 index 000000000..7b1d71981 --- /dev/null +++ b/knip.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": [ + "src/api/index.ts" + ], + "project": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "ignore": [ + "dist/**", + "generated/**", + ".typeschema-cache/**", + "**/*.d.ts" + ], + "ignoreDependencies": [ + ], + "ignoreBinaries": [ + ], + "ignoreExportsUsedInFile": true, + "rules": { + "files": "warn", + "dependencies": "warn", + "devDependencies": "warn", + "unlisted": "error", + "binaries": "warn", + "unresolved": "error", + "exports": "warn", + "types": "warn", + "nsExports": "warn", + "nsTypes": "warn", + "enumMembers": "warn", + "classMembers": "warn", + "duplicates": "error" + }, + "compilers": { + "css": "css", + "js": "tsx", + "json": "json", + "svg": "tsx", + "ts": "tsx" + } +} diff --git a/package.json b/package.json index 544a90ae3..40ea0ff2b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,9 @@ "cli": "bun run src/cli/index.ts", "codegen": "bun run src/cli/index.ts", "codegen:all": "bun run src/cli/index.ts generate", - "prune": "ts-prune --project tsconfig.json" + "prune": "knip", + "prune:strict": "knip --strict", + "prune:fix": "knip --fix" }, "repository": { "type": "git", @@ -62,11 +64,6 @@ "dependencies": { "@atomic-ehr/fhir-canonical-manager": "^0.0.15", "@atomic-ehr/fhirschema": "^0.0.5", - "@inquirer/prompts": "^7.8.1", - "ajv": "^8.17.1", - "glob": "^12.0.0", - "handlebars": "^4.7.8", - "ora": "^8.2.0", "picocolors": "^1.1.1", "yaml": "^2.8.1", "yargs": "^18.0.0" @@ -74,10 +71,9 @@ "devDependencies": { "@biomejs/biome": "^2.1.4", "@types/bun": "^1.2.23", - "@types/handlebars": "^4.1.0", "@types/node": "^22.17.1", "@types/yargs": "^17.0.33", - "ts-prune": "^0.10.3", + "knip": "^5.27.2", "tsup": "^8.5.1", "typescript": "^5.9.2" } diff --git a/src/api/builder.ts b/src/api/builder.ts index acd90ee9e..23bbc6f97 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -9,16 +9,13 @@ import * as fs from "node:fs"; import * as afs from "node:fs/promises"; import * as Path from "node:path"; import { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager"; -import type { GeneratedFile } from "@root/api/generators/base/types"; import { CSharp } from "@root/api/writer-generator/csharp/csharp"; import { registerFromManager } from "@root/typeschema/register"; -import { mkTypeSchemaIndex, type TreeShake, treeShake } from "@root/typeschema/utils"; -import { generateTypeSchemas, TypeSchemaCache, TypeSchemaGenerator, TypeSchemaParser } from "@typeschema/index"; +import { mkTypeSchemaIndex, type TreeShake, type TypeSchemaIndex, treeShake } from "@root/typeschema/utils"; +import { generateTypeSchemas, TypeSchemaGenerator, TypeSchemaParser } from "@typeschema/index"; import { extractNameFromCanonical, packageMetaToFhir, packageMetaToNpm, type TypeSchema } from "@typeschema/types"; -import type { Config, TypeSchemaConfig } from "../config"; +import type { TypeSchemaConfig } from "../config"; import { CodegenLogger, createLogger } from "../utils/codegen-logger"; -import type { GeneratorInput } from "./generators/base/BaseGenerator"; -import { TypeScriptGenerator as TypeScriptGeneratorDepricated } from "./generators/typescript"; import { TypeScript, type TypeScriptOptions } from "./writer-generator/typescript"; import type { Writer, WriterOptions } from "./writer-generator/writer"; @@ -57,6 +54,14 @@ export interface GenerationResult { duration: number; } +export interface GeneratedFile { + path: string; + filename: string; + timestamp: Date; +} + +export type GeneratorInput = { schemas: TypeSchema[]; index: TypeSchemaIndex }; + interface Generator { generate: (input: GeneratorInput) => Promise; setOutputDir: (outputDir: string) => void; @@ -65,13 +70,10 @@ interface Generator { const writerToGenerator = (writerGen: Writer): Generator => { const getGeneratedFiles = () => { - return writerGen.writtenFiles().map((fn: string) => { + return writerGen.writtenFiles().map((fn: string): GeneratedFile => { return { path: Path.normalize(Path.join(writerGen.opts.outputDir, fn)), filename: fn.replace(/^.*[\\/]/, ""), - content: "", - exports: [], - size: 0, timestamp: new Date(), }; }); @@ -206,7 +208,6 @@ export class APIBuilder { private schemas: TypeSchema[] = []; private options: APIBuilderConfig; private generators: Map = new Map(); - private cache?: TypeSchemaCache; private pendingOperations: Promise[] = []; private typeSchemaGenerator?: TypeSchemaGenerator; private logger: CodegenLogger; @@ -238,10 +239,6 @@ export class APIBuilder { verbose: this.options.verbose, prefix: "API", }); - - if (this.options.cache) { - this.cache = new TypeSchemaCache(this.typeSchemaConfig); - } } fromPackage(packageName: string, version?: string): APIBuilder { @@ -268,47 +265,6 @@ export class APIBuilder { return this; } - typescriptDepricated( - options: { - moduleFormat?: "esm" | "cjs"; - generateIndex?: boolean; - includeDocuments?: boolean; - namingConvention?: "PascalCase" | "camelCase"; - includeExtensions?: boolean; - includeProfiles?: boolean; - generateValueSets?: boolean; - includeValueSetHelpers?: boolean; - valueSetStrengths?: ("required" | "preferred" | "extensible" | "example")[]; - valueSetMode?: "all" | "required-only" | "custom"; - valueSetDirectory?: string; - } = {}, - ): APIBuilder { - const typesOutputDir = `${this.options.outputDir}/types`; - - const generator = new TypeScriptGeneratorDepricated({ - outputDir: typesOutputDir, - moduleFormat: options.moduleFormat || "esm", - generateIndex: options.generateIndex ?? true, - includeDocuments: options.includeDocuments ?? true, - namingConvention: options.namingConvention || "PascalCase", - includeExtensions: options.includeExtensions ?? false, - includeProfiles: options.includeProfiles ?? false, - generateValueSets: options.generateValueSets ?? false, - includeValueSetHelpers: options.includeValueSetHelpers ?? false, - valueSetStrengths: options.valueSetStrengths ?? ["required"], - logger: this.logger.child("TS"), - valueSetMode: options.valueSetMode ?? "required-only", - valueSetDirectory: options.valueSetDirectory ?? "valuesets", - verbose: this.options.verbose, - validate: true, // Enable validation for debugging - overwrite: this.options.overwrite, - }); - - this.generators.set("typescript", generator); - this.logger.debug(`Configured TypeScript generator (${options.moduleFormat || "esm"})`); - return this; - } - typescript(userOpts: Partial) { const defaultWriterOpts: WriterOptions = { logger: this.logger, @@ -510,14 +466,11 @@ export class APIBuilder { private async loadFromFiles(filePaths: string[]): Promise { if (!this.typeSchemaGenerator) { - this.typeSchemaGenerator = new TypeSchemaGenerator( - { - verbose: this.options.verbose, - logger: this.logger.child("Schema"), - treeshake: this.typeSchemaConfig?.treeshake, - }, - this.typeSchemaConfig, - ); + this.typeSchemaGenerator = new TypeSchemaGenerator({ + verbose: this.options.verbose, + logger: this.logger.child("Schema"), + treeshake: this.typeSchemaConfig?.treeshake, + }); } const parser = new TypeSchemaParser({ @@ -526,10 +479,6 @@ export class APIBuilder { const schemas = await parser.parseFromFiles(filePaths); this.schemas = [...this.schemas, ...schemas]; - - if (this.cache) { - this.cache.setMany(schemas); - } } private async executeGenerators(result: GenerationResult, input: GeneratorInput): Promise { @@ -549,36 +498,3 @@ export class APIBuilder { } } } - -/** - * Create an API builder instance from a configuration object - */ -export function createAPIFromConfig(config: Config): APIBuilder { - const builder = new APIBuilder({ - outputDir: config.outputDir, - verbose: config.verbose, - overwrite: config.overwrite, - cache: config.cache, - cleanOutput: config.cleanOutput, - typeSchemaConfig: config.typeSchema, - }); - - // Add packages if specified - if (config.packages && config.packages.length > 0) { - for (const pkg of config.packages) { - builder.fromPackage(pkg); - } - } - - // Add files if specified - if (config.files && config.files.length > 0) { - builder.fromFiles(...config.files); - } - - // Configure TypeScript generator if specified - if (config.typescript) { - builder.typescriptDepricated(config.typescript); - } - - return builder; -} diff --git a/src/api/generators/base/BaseGenerator.ts b/src/api/generators/base/BaseGenerator.ts deleted file mode 100644 index 99718bacd..000000000 --- a/src/api/generators/base/BaseGenerator.ts +++ /dev/null @@ -1,772 +0,0 @@ -/** - * Abstract base generator class - * - * This is the foundation of the generator system. All language-specific generators - * extend this class to inherit common functionality while implementing their own - * specific logic for type mapping, content generation, and validation. - */ - -import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { TypeSchema } from "@typeschema/types"; -import type { CodegenLogger } from "../../../utils/codegen-logger"; -import { createLogger } from "../../../utils/codegen-logger"; -import { ErrorHandler, GeneratorErrorBoundary } from "./error-handler"; -import { ConfigurationError, SchemaValidationError } from "./errors"; -import { FileManager } from "./FileManager"; -import type { - BaseGeneratorOptions, - ConfigValidationResult, - GeneratedFile, - GeneratorCapabilities, - ProgressCallback, - TemplateContext, - TemplateEngine, - TypeMapper, -} from "./types"; - -export type GeneratorInput = { schemas: TypeSchema[]; index: TypeSchemaIndex }; - -/** - * Abstract base generator class with comprehensive functionality - * - * Provides common functionality for all generators including: - * - Schema validation and processing - * - File management with fluent API - * - Template processing - * - Error handling and recovery - * - Progress monitoring - * - Performance optimization - */ -export abstract class BaseGenerator< - TOptions extends BaseGeneratorOptions = BaseGeneratorOptions, - TResult extends GeneratedFile[] = GeneratedFile[], -> { - /** Validated and merged options */ - protected options: Required; - - /** Logger instance for this generator */ - protected readonly logger: CodegenLogger; - - /** File manager for all file operations */ - protected readonly fileManager: FileManager; - - /** Template engine for content generation (optional) */ - protected readonly templateEngine?: TemplateEngine; - - /** Language-specific type mapper */ - protected readonly typeMapper: TypeMapper; - - /** Enhanced error handler for comprehensive error reporting */ - protected readonly errorHandler: ErrorHandler; - - /** Error boundary for catching and handling all generator errors */ - protected readonly errorBoundary: GeneratorErrorBoundary; - - /** Progress callback if provided */ - private progressCallback?: ProgressCallback; - - /** Generated files tracking */ - private generatedFiles: GeneratedFile[] = []; - - /** Generation start time for performance metrics */ - private generationStartTime = 0; - - /** Cache for expensive operations */ - private readonly cache = new Map(); - - constructor(options: TOptions) { - // Validate configuration first - const validation = this.validateConfiguration(options); - if (!validation.isValid) { - throw new ConfigurationError( - `Invalid generator configuration: ${validation.errors.join(", ")}`, - "configuration", - options, - ); - } - - // Merge with defaults and store - this.options = this.mergeWithDefaults(options); - - // Initialize logger - this.logger = - options.logger || - createLogger({ - prefix: this.getLanguageName(), - verbose: this.options.verbose || false, - }); - - // Initialize core components - this.fileManager = this.createFileManager(); - this.templateEngine = this.createTemplateEngine(); - this.typeMapper = this.createTypeMapper(); - - // Initialize enhanced error handling - this.errorHandler = new ErrorHandler({ - logger: this.logger, - verbose: this.options.verbose || false, - beginnerMode: this.options.beginnerMode || false, - outputFormat: "this.options.logger", - }); - - this.errorBoundary = new GeneratorErrorBoundary(this.errorHandler); - - this.logger.debug(`${this.getLanguageName()} generator initialized`); - - // Log any configuration warnings - if (validation.warnings.length > 0) { - validation.warnings.forEach((warning) => { - this.logger.warn(`Configuration warning: ${warning}`); - }); - } - } - - // ========================================== - // Abstract Methods - Must be implemented by subclasses - // ========================================== - - /** - * Get the name of the target language (e.g., "TypeScript", "Python", "Rust") - */ - protected abstract getLanguageName(): string; - - /** - * Get the file extension for the target language (e.g., ".ts", ".py", ".rs") - */ - protected abstract getFileExtension(): string; - - /** - * Create a language-specific type mapper - */ - protected abstract createTypeMapper(): TypeMapper; - - /** - * Generate content for a single schema - * @param schema - The TypeSchema to generate code for - * @param context - Additional context for generation - */ - protected abstract generateSchemaContent(schema: TypeSchema, context: TemplateContext): Promise; - - /** - * Validate generated content before writing - * @param content - The generated content - * @param context - The generation context - */ - protected abstract validateContent(content: string, context: TemplateContext): Promise; - - /** - * Filter and sort schemas with language-specific logic - * @param schemas - Input schemas - */ - protected abstract filterAndSortSchemas(schemas: TypeSchema[]): TypeSchema[]; - - // ========================================== - // Optional Abstract Methods - Can be overridden - // ========================================== - - /** - * Get generator capabilities - can be overridden for introspection - */ - getCapabilities(): GeneratorCapabilities { - return { - language: this.getLanguageName(), - fileExtensions: [this.getFileExtension()], - supportsTemplates: true, - supportsCustomTypeMapping: true, - supportsIncrementalGeneration: false, - supportsValidation: true, - supportedSchemaKinds: ["resource", "complex-type", "profile", "logical"], - version: "1.0.0", - }; - } - - /** - * Create file manager instance - can be overridden for custom file handling - */ - protected createFileManager(): FileManager { - return new FileManager({ - outputDir: this.options.outputDir, - logger: this.logger.child("FileManager"), - overwrite: this.options.overwrite, - }); - } - - /** - * Create template engine instance - can be overridden for custom templates - * Returns undefined if template engine is not needed - */ - protected createTemplateEngine(): TemplateEngine | undefined { - // Default implementation returns undefined (no template engine) - // Subclasses can override to provide template engine if needed - return undefined; - } - - // ========================================== - // Public API - Main entry points - // ========================================== - - /** - * Generate code from TypeSchema documents - * This is the main method that orchestrates the entire generation process - * @param schemas - Array of TypeSchema documents - */ - public async generate({ schemas }: GeneratorInput): Promise { - return this.errorBoundary.withErrorBoundary( - async () => { - this.generationStartTime = performance.now(); - this.generatedFiles = []; - - this.logger.info(`Starting ${this.getLanguageName()} generation for ${schemas.length} schemas`); - - // Phase 1: Schema validation - this.reportProgress("validation", 0, schemas.length, "Validating schemas..."); - await this.validateSchemas(schemas); - - // Phase 2: Schema processing and filtering - this.reportProgress("generation", 0, schemas.length, "Processing schemas..."); - const processedSchemas = this.filterAndSortSchemas(schemas); - this.logger.debug(`Filtered to ${processedSchemas.length} schemas for generation`); - - // Phase 3: Content generation - await this.generateFiles(processedSchemas); - - // Phase 4: Post-generation hooks - await this.runPostGenerationHooks(); - - this.reportProgress("complete", schemas.length, schemas.length, "Generation complete"); - - const duration = performance.now() - this.generationStartTime; - this.logger.info( - `Generated ${this.generatedFiles.length} files in ${duration.toFixed(2)}ms ` + - `(avg ${(duration / this.generatedFiles.length).toFixed(2)}ms per file)`, - ); - - return this.generatedFiles as TResult; - }, - { operationName: "generate" }, - ); - } - - /** - * Generate and return content without writing files (useful for testing) - * @param schemas - Array of TypeSchema documents - */ - public async build(schemas: TypeSchema[]): Promise { - // Temporarily disable file writing by mocking the writeFile method - const originalWriteFile = this.fileManager.writeFile; - const mockWriteResults = new Map(); - - this.fileManager.writeFile = async (path: string, content: string) => { - const result = { - path: `${this.options.outputDir}/${path}`, - size: Buffer.byteLength(content, "utf-8"), - writeTime: 0, - }; - mockWriteResults.set(path, result); - return result; - }; - - try { - const result = await this.generate({ schemas, index: null as any }); - - // Update file paths to reflect what would have been written - result.forEach((file) => { - const mockResult = mockWriteResults.get(file.filename); - if (mockResult) { - file.path = mockResult.path; - file.size = mockResult.size; - } - }); - - return result; - } finally { - // Restore original file writing function - this.fileManager.writeFile = originalWriteFile; - } - } - - // ========================================== - // Fluent API - Builder pattern methods - // ========================================== - - /** - * Create a file builder for fluent file generation - * @param filename - Name of the file to create - */ - public file(filename: string): import("./builders/FileBuilder").FileBuilder { - if (!this.templateEngine) { - throw new Error( - "Template engine is required for fluent file generation. Override createTemplateEngine() in your generator.", - ); - } - const { FileBuilder } = require("./builders/FileBuilder"); - return new FileBuilder({ - filename: this.ensureFileExtension(filename), - fileManager: this.fileManager, - templateEngine: this.templateEngine, - typeMapper: this.typeMapper, - logger: this.logger.child("FileBuilder"), - }); - } - - /** - * Create a directory builder for batch operations - * @param path - Directory path relative to output directory - */ - public directory(path: string): import("./builders/DirectoryBuilder").DirectoryBuilder { - const { DirectoryBuilder } = require("./builders/DirectoryBuilder"); - return new DirectoryBuilder({ - path, - fileManager: this.fileManager, - logger: this.logger.child("DirectoryBuilder"), - }); - } - - /** - * Create an index file builder - * @param directory - Directory to create index for - */ - public index(directory: string = "."): import("./builders/IndexBuilder").IndexBuilder { - if (!this.templateEngine) { - throw new Error( - "Template engine is required for index file generation. Override createTemplateEngine() in your generator.", - ); - } - const { IndexBuilder } = require("./builders/IndexBuilder"); - return new IndexBuilder({ - directory, - fileManager: this.fileManager, - templateEngine: this.templateEngine, - logger: this.logger.child("IndexBuilder"), - }); - } - - /** - * Set progress callback for monitoring generation - * @param callback - Progress callback function - */ - public onProgress(callback: ProgressCallback): this { - this.progressCallback = callback; - return this; - } - - // ========================================== - // Configuration and Validation - // ========================================== - - /** - * Validate generator configuration - */ - private validateConfiguration(options: TOptions): ConfigValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - const suggestions: string[] = []; - - // Required options validation - if (!options.outputDir) { - errors.push("outputDir is required"); - suggestions.push("Provide a valid output directory path"); - } - - // Type validation - if (options.outputDir && typeof options.outputDir !== "string") { - errors.push("outputDir must be a string"); - } - - if (options.overwrite !== undefined && typeof options.overwrite !== "boolean") { - errors.push("overwrite must be a boolean"); - } - - if (options.validate !== undefined && typeof options.validate !== "boolean") { - errors.push("validate must be a boolean"); - } - - // Warnings for suboptimal configuration - if (options.outputDir && !require("node:path").isAbsolute(options.outputDir)) { - warnings.push("Using relative path for outputDir - consider using absolute path"); - suggestions.push("Use path.resolve() to convert to absolute path"); - } - - if (options.validate === false) { - warnings.push("Validation is disabled - this may lead to invalid generated code"); - suggestions.push("Consider enabling validation for better code quality"); - } - - return { - isValid: errors.length === 0, - errors, - warnings, - suggestions, - }; - } - - /** - * Merge options with defaults - */ - private mergeWithDefaults(options: TOptions): Required { - return { - overwrite: true, - validate: true, - verbose: false, - beginnerMode: false, - errorFormat: "console" as const, - ...options, - } as Required; - } - - // ========================================== - // Schema Processing - // ========================================== - - /** - * Validate schemas before processing - */ - private async validateSchemas(schemas: TypeSchema[]): Promise { - if (!this.options.validate) { - this.logger.debug("Schema validation disabled"); - return; - } - - this.logger.info(`🔍 Starting schema validation for ${schemas.length} schemas`); - this.logger.debug("Schema validation enabled - performing comprehensive validation"); - - const operations = schemas.map( - (schema) => () => - this.errorBoundary.withErrorBoundary( - async () => { - await this.validateSchema(schema); - this.reportProgress( - "validation", - schemas.indexOf(schema) + 1, - schemas.length, - `Validated ${schema.identifier?.name || "schema"}`, - ); - }, - { schema, operationName: "validateSchema" }, - ), - ); - - await this.errorBoundary.withBatchErrorBoundary(operations, { - operationName: "validateSchemas", - }); - - this.logger.debug(`Successfully validated ${schemas.length} schemas`); - } - - /** - * Validate individual schema - */ - protected async validateSchema(schema: TypeSchema): Promise { - const errors: string[] = []; - const schemaName = schema.identifier?.name || "unknown"; - - this.logger.debug(`🔍 Validating schema: ${schemaName} (kind: ${schema.identifier?.kind})`); - - // Basic structure validation - if (!schema.identifier) { - errors.push("Schema missing identifier"); - this.logger.warn(`❌ Schema missing identifier: ${JSON.stringify(schema, null, 2).substring(0, 200)}...`); - } else { - if (!schema.identifier.name) { - errors.push("Schema identifier missing name"); - } - - if (!schema.identifier.kind) { - errors.push("Schema identifier missing kind"); - } else { - const validKinds = [ - "resource", - "complex-type", - "profile", - "primitive-type", - "logical", - "value-set", - "binding", - "extension", - ]; - if (!validKinds.includes(schema.identifier.kind)) { - errors.push(`Schema identifier.kind must be one of: ${validKinds.join(", ")}`); - } - } - } - - // Field validation - if ("fields" in schema && schema.fields) { - for (const [fieldName, field] of Object.entries(schema.fields)) { - if (!fieldName.trim()) { - errors.push("Field name cannot be empty"); - } - - if (!field) { - errors.push(`Field '${fieldName}' is null or undefined`); - } - - // Add more field-specific validation as needed - } - } - - // Circular reference detection (make it a warning, not an error for FHIR schemas) - if (await this.detectCircularReferences(schema)) { - this.logger.warn( - `⚠️ Circular reference detected in schema '${schemaName}' - this may be expected for FHIR primitive types`, - ); - // Don't add to errors - FHIR schemas often have legitimate circular references - } - - if (errors.length > 0) { - this.logger.error(`❌ Schema validation failed for '${schemaName}': ${errors.join(", ")}`); - this.logger.debug(`Schema details: ${JSON.stringify(schema, null, 2)}`); - throw new SchemaValidationError( - `Schema validation failed for '${schema.identifier?.name || "unknown"}'`, - schema, - errors, - ); - } - - this.logger.debug(`✅ Schema validation passed for '${schemaName}'`); - } - - /** - * Detect circular references in schema dependencies - */ - private async detectCircularReferences(schema: TypeSchema): Promise { - // Simple implementation - can be enhanced for complex cases - const visited = new Set(); - const visiting = new Set(); - - const checkCircular = (currentSchema: TypeSchema): boolean => { - const name = currentSchema.identifier?.name; - if (!name) return false; - - if (visiting.has(name)) { - return true; // Circular reference found - } - - if (visited.has(name)) { - return false; // Already processed - } - - visiting.add(name); - - // Check field references - if ("fields" in currentSchema && currentSchema.fields) { - for (const field of Object.values(currentSchema.fields)) { - if ((field as any)?.type?.name === name) { - return true; // Self-reference - } - // Add more complex reference checking as needed - } - } - - visiting.delete(name); - visited.add(name); - return false; - }; - - return checkCircular(schema); - } - - // ========================================== - // File Generation - // ========================================== - - /** - * Generate files from processed schemas - */ - private async generateFiles(schemas: TypeSchema[]): Promise { - const operations = schemas.map( - (schema, index) => () => - this.errorBoundary.withErrorBoundary( - async () => { - const file = await this.generateFileForSchema(schema, index, schemas.length); - this.generatedFiles.push(file); - return file; - }, - { schema, operationName: "generateFile" }, - ), - ); - - await this.errorBoundary.withBatchErrorBoundary(operations, { - operationName: "generateFiles", - }); - - this.logger.debug(`Generated ${this.generatedFiles.length} files`); - } - - /** - * Generate a single file from a schema - */ - private async generateFileForSchema(schema: TypeSchema, index: number, total: number): Promise { - const fileStartTime = performance.now(); - - // Create template context - const context: TemplateContext = { - schema, - typeMapper: this.typeMapper, - filename: this.typeMapper.formatFileName(schema.identifier?.name || "unknown"), - language: this.getLanguageName(), - timestamp: new Date().toISOString(), - imports: new Map(), - exports: new Set(), - }; - - // Generate content - const content = await this.generateSchemaContent(schema, context); - - // Validate content if enabled - if (this.options.validate) { - await this.validateContent(content, context); - } - - // Write file - const filename = context.filename + this.getFileExtension(); - const writeResult = await this.fileManager.writeFile(filename, content); - - const generationTime = performance.now() - fileStartTime; - - // Report progress - this.reportProgress("writing", index + 1, total, `Generated ${filename} (${writeResult.size} bytes)`); - - // Create GeneratedFile result - const generatedFile: GeneratedFile = { - path: writeResult.path, - filename, - content, - exports: this.extractExports(content), - size: writeResult.size, - timestamp: new Date(), - metadata: { - generationTime, - schemaCount: 1, - templateName: context.templateName?.toString(), - warnings: [], - }, - }; - - return generatedFile; - } - - // ========================================== - // Helper Methods - // ========================================== - - /** - * Ensure filename has correct extension - */ - private ensureFileExtension(filename: string): string { - const extension = this.getFileExtension(); - return filename.endsWith(extension) ? filename : `${filename}${extension}`; - } - - /** - * Extract exported symbols from generated content - * Can be overridden by language-specific implementations - */ - protected extractExports(content: string): string[] { - const exports: string[] = []; - - // Handle export { name1, name2 } syntax - const exportListRegex = /export\s*\{\s*([^}]+)\s*\}/g; - let match; - while ((match = exportListRegex.exec(content)) !== null) { - if (match[1]) { - const names = match[1] - .split(",") - .map((name) => name.trim()) - .filter(Boolean); - exports.push(...names); - } - } - - // Handle direct export declarations - const directExportRegex = /export\s+(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/g; - while ((match = directExportRegex.exec(content)) !== null) { - if (match[1]) { - exports.push(match[1]); - } - } - - // Remove duplicates - return [...new Set(exports)]; - } - - /** - * Report progress to callback if provided - */ - protected reportProgress( - phase: "validation" | "generation" | "writing" | "complete", - current: number, - total: number, - message?: string, - schema?: TypeSchema, - ): void { - if (this.progressCallback) { - this.progressCallback(phase, current, total, message, schema); - } - - if (message && this.options.verbose) { - this.logger.debug(`[${phase}] ${message} (${current}/${total})`); - } - } - - /** - * Run post-generation hooks - * Can be overridden to add custom post-processing - */ - protected async runPostGenerationHooks(): Promise { - // Default implementation does nothing - // Subclasses can override to add custom logic - } - - /** - * Get cached value or compute and cache it - */ - protected getCachedOrCompute(key: string, computeFn: () => T | Promise): T | Promise { - if (this.cache.has(key)) { - return this.cache.get(key) as T; - } - - const result = computeFn(); - - if (result instanceof Promise) { - return result.then((value) => { - this.cache.set(key, value); - return value; - }); - } else { - this.cache.set(key, result); - return result; - } - } - - /** - * Clear internal cache - */ - protected clearCache(): void { - this.cache.clear(); - } - - /** - * Get generation statistics - */ - public getGenerationStats(): { - filesGenerated: number; - totalSize: number; - averageFileSize: number; - generationTime: number; - averageTimePerFile: number; - cacheHitRate: number; - } { - const totalSize = this.generatedFiles.reduce((sum, file) => sum + file.size, 0); - const generationTime = performance.now() - this.generationStartTime; - - return { - filesGenerated: this.generatedFiles.length, - totalSize, - averageFileSize: this.generatedFiles.length > 0 ? totalSize / this.generatedFiles.length : 0, - generationTime, - averageTimePerFile: this.generatedFiles.length > 0 ? generationTime / this.generatedFiles.length : 0, - cacheHitRate: 0, // TODO: Implement cache hit tracking - }; - } -} diff --git a/src/api/generators/base/FileManager.ts b/src/api/generators/base/FileManager.ts deleted file mode 100644 index 5823b895a..000000000 --- a/src/api/generators/base/FileManager.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * Core file management system with batching and performance optimizations - * - * This replaces scattered writeFile calls with a comprehensive file management - * system that provides better error handling, performance, and maintainability. - */ - -import { access, mkdir, rm, stat, writeFile } from "node:fs/promises"; -import { dirname, join, relative } from "node:path"; -import type { CodegenLogger } from "../../../utils/codegen-logger"; -import { FileOperationError } from "./errors"; -import type { FileStats } from "./types"; - -export interface FileManagerOptions { - outputDir: string; - logger: CodegenLogger; - overwrite?: boolean; - batchSize?: number; -} - -export interface WriteFileResult { - path: string; - size: number; - writeTime: number; -} - -/** - * High-performance file manager with batching and error recovery - * - * Features: - * - Automatic directory creation - * - Batch operations for better performance - * - Comprehensive error handling with recovery suggestions - * - Import path resolution - * - File existence checks - */ -export class FileManager { - private readonly options: Required; - private readonly logger: CodegenLogger; - - constructor(options: FileManagerOptions) { - this.options = { - overwrite: true, - batchSize: 10, - ...options, - }; - this.logger = options.logger; - } - - /** - * Write a file with automatic directory creation - * @param relativePath Path relative to output directory - * @param content File content - * @param options Write options - */ - async writeFile( - relativePath: string, - content: string, - options: { encoding?: BufferEncoding; overwrite?: boolean } = {}, - ): Promise { - const startTime = performance.now(); - const fullPath = join(this.options.outputDir, relativePath); - const encoding = options.encoding || "utf-8"; - const overwrite = options.overwrite ?? this.options.overwrite; - - try { - // Check if file exists and overwrite is disabled - if (!overwrite) { - try { - await access(fullPath); - this.logger.debug(`Skipping existing file: ${relativePath}`); - const stats = await stat(fullPath); - return { - path: fullPath, - size: stats.size, - writeTime: 0, - }; - } catch { - // File doesn't exist, continue with write - } - } - - // Ensure directory exists - await this.ensureDirectory(dirname(fullPath)); - - // Write file - await writeFile(fullPath, content, encoding); - - const writeTime = performance.now() - startTime; - const size = Buffer.byteLength(content, encoding); - - this.logger.debug(`Written ${relativePath} (${size} bytes, ${writeTime.toFixed(2)}ms)`); - - return { - path: fullPath, - size, - writeTime, - }; - } catch (error) { - throw new FileOperationError( - `Failed to write file '${relativePath}': ${error}`, - "write", - fullPath, - error instanceof Error ? error : undefined, - { - canRetry: true, - alternativePaths: [join(process.cwd(), "backup-output", relativePath)], - }, - ); - } - } - - /** - * Write multiple files in batch for better performance - * @param files Map of relative path to content - */ - async writeBatch(files: Map): Promise { - this.logger.debug(`Writing batch of ${files.size} files`); - - const entries = Array.from(files.entries()); - const results: WriteFileResult[] = []; - - // Process in batches to avoid overwhelming the filesystem - for (let i = 0; i < entries.length; i += this.options.batchSize) { - const batch = entries.slice(i, i + this.options.batchSize); - const batchPromises = batch.map(([path, content]) => this.writeFile(path, content)); - - const batchResults = await Promise.all(batchPromises); - results.push(...batchResults); - - // Small delay between batches to be filesystem-friendly - if (i + this.options.batchSize < entries.length) { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } - - return results; - } - - /** - * Ensure directory exists, creating parent directories as needed - * @param dirPath Full directory path - */ - async ensureDirectory(dirPath: string): Promise { - try { - await mkdir(dirPath, { recursive: true }); - } catch (error) { - throw new FileOperationError( - `Failed to create directory '${dirPath}': ${error}`, - "create", - dirPath, - error instanceof Error ? error : undefined, - { - canRetry: true, - permissionFix: `chmod 755 "${dirname(dirPath)}"`, - }, - ); - } - } - - /** - * Clean directory by removing all contents - * @param relativePath Path relative to output directory - */ - async cleanDirectory(relativePath: string = "."): Promise { - const fullPath = join(this.options.outputDir, relativePath); - - try { - await access(fullPath); - this.logger.debug(`Cleaning directory: ${relativePath}`); - await rm(fullPath, { recursive: true, force: true }); - } catch (error) { - // Directory doesn't exist - that's fine - if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") { - throw new FileOperationError( - `Failed to clean directory '${relativePath}': ${error}`, - "delete", - fullPath, - error instanceof Error ? error : undefined, - { - canRetry: true, - }, - ); - } - } - } - - /** - * Get relative import path between two files - * @param fromFile Source file path - * @param toFile Target file path - */ - getRelativeImportPath(fromFile: string, toFile: string): string { - const from = dirname(join(this.options.outputDir, fromFile)); - const to = join(this.options.outputDir, toFile); - - let relativePath = relative(from, to); - - // Ensure relative imports start with './' or '../' - if (!relativePath.startsWith(".")) { - relativePath = `./${relativePath}`; - } - - // Remove file extension for imports (handle .d.ts files properly) - return relativePath.replace(/\.(d\.ts|ts|tsx|js|jsx)$/, ""); - } - - /** - * Check if a file would be overwritten - * @param relativePath Path relative to output directory - */ - async wouldOverwrite(relativePath: string): Promise { - const fullPath = join(this.options.outputDir, relativePath); - try { - await access(fullPath); - return true; - } catch { - return false; - } - } - - /** - * Get file statistics - * @param relativePath Path relative to output directory - */ - async getFileStats(relativePath: string): Promise { - const fullPath = join(this.options.outputDir, relativePath); - try { - const stats = await stat(fullPath); - return { - size: stats.size, - generationTime: 0, // Will be set by caller - writeTime: 0, // Will be set by caller - }; - } catch { - return null; - } - } - - /** - * Get output directory - */ - getOutputDirectory(): string { - return this.options.outputDir; - } - - /** - * Set batch size for operations - * @param size Batch size - */ - setBatchSize(size: number): void { - this.options.batchSize = Math.max(1, Math.min(50, size)); - } - - /** - * Get current batch size - */ - getBatchSize(): number { - return this.options.batchSize; - } -} diff --git a/src/api/generators/base/PythonTypeMapper.ts b/src/api/generators/base/PythonTypeMapper.ts deleted file mode 100644 index 0b4e55b24..000000000 --- a/src/api/generators/base/PythonTypeMapper.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Python type mapper implementation (basic version) - */ - -import type { Identifier } from "@typeschema/index"; -import { type LanguageType, TypeMapper } from "./TypeMapper"; - -export class PythonTypeMapper extends TypeMapper { - getLanguageName(): string { - return "Python"; - } - - mapPrimitive(fhirType: string): LanguageType { - const primitiveMap: Record = { - string: "str", - integer: "int", - decimal: "float", - boolean: "bool", - dateTime: "datetime", - date: "date", - time: "time", - }; - - return { - name: primitiveMap[fhirType] || "Any", - isPrimitive: true, - nullable: false, - }; - } - - mapReference(_targets: Identifier[]): LanguageType { - return { - name: "Reference", - isPrimitive: false, - importPath: ".reference", - nullable: false, - }; - } - - mapArray(elementType: LanguageType): LanguageType { - return { - name: `List[${elementType.name}]`, - isPrimitive: false, - importPath: "typing", - isArray: true, - nullable: false, - }; - } - - mapOptional(type: LanguageType, required: boolean): LanguageType { - if (required) return type; - - return { - ...type, - name: `Optional[${type.name}]`, - nullable: true, - }; - } - - mapEnum(_values: string[], name?: string): LanguageType { - return { - name: name ? this.formatTypeName(name) : "Literal", - isPrimitive: false, - nullable: false, - }; - } - - formatTypeName(name: string): string { - return this.applyNamingConvention(name); - } - - formatFieldName(name: string): string { - // Convert camelCase to snake_case - return name - .replace(/([A-Z])/g, "_$1") - .toLowerCase() - .replace(/^_/, ""); - } - - formatFileName(name: string): string { - return this.formatFieldName(name); - } -} diff --git a/src/api/generators/base/TemplateEngine.ts b/src/api/generators/base/TemplateEngine.ts deleted file mode 100644 index c0183c527..000000000 --- a/src/api/generators/base/TemplateEngine.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Abstract template engine for code generation - * - * Supports multiple template formats: - * - Handlebars templates (.hbs) - * - String templates (.template) - * - Function templates (TypeScript functions) - * - Inline templates (template literals) - */ - -import type { CodegenLogger } from "../../../utils/codegen-logger"; -import { TemplateError } from "./errors"; - -/** - * Template context passed to templates - */ -export interface TemplateContext { - /** The schema being processed */ - schema?: any; - - /** Type mapper for language-specific types */ - typeMapper?: any; - - /** File name being generated */ - filename?: string; - - /** Target language */ - language?: string; - - /** Timestamp for generation */ - timestamp?: string; - - /** Import statements */ - imports?: Map; - - /** Export names */ - exports?: Set; - - /** Any additional context */ - [key: string]: unknown; -} - -/** - * Template registration options - */ -export interface TemplateOptions { - /** Template format */ - format?: "handlebars" | "string" | "function"; - - /** Whether to cache compiled templates */ - cache?: boolean; - - /** Custom helpers for the template */ - helpers?: Record; - - /** Template-specific options */ - options?: Record; -} - -/** - * Template metadata - */ -export interface TemplateInfo { - name: string; - format: string; - path?: string; - description?: string; - examples?: Array<{ - context: TemplateContext; - expected: string; - }>; -} - -/** - * Abstract template engine - */ -export abstract class TemplateEngine { - protected readonly logger: CodegenLogger; - protected readonly templates = new Map(); - protected readonly templateCache = new Map(); - protected readonly helpers = new Map(); - - constructor(options: { logger: CodegenLogger }) { - this.logger = options.logger; - this.registerDefaultHelpers(); - } - - // ========================================== - // Abstract Methods - // ========================================== - - /** - * Render a template with context - * @param templateName Name of template to render - * @param context Template context - */ - abstract render(templateName: string, context: TemplateContext): Promise; - - /** - * Register a template - * @param name Template name - * @param template Template content or function - * @param options Template options - */ - abstract registerTemplate(name: string, template: string | Function, options?: TemplateOptions): void; - - /** - * Load templates from directory - * @param directory Directory containing templates - */ - abstract loadTemplatesFromDirectory(directory: string): Promise; - - // ========================================== - // Concrete Methods - // ========================================== - - /** - * Register a template helper function - * @param name Helper name - * @param helper Helper function - */ - registerHelper(name: string, helper: Function): void { - this.helpers.set(name, helper); - this.logger.debug(`Registered template helper: ${name}`); - } - - /** - * Get available template names - */ - getAvailableTemplates(): string[] { - return Array.from(this.templates.keys()).sort(); - } - - /** - * Get template information - * @param templateName Template name - */ - getTemplateInfo(templateName: string): TemplateInfo | undefined { - const template = this.templates.get(templateName); - if (!template) return undefined; - - return { - name: templateName, - format: template.format || "unknown", - path: template.path, - description: template.description, - examples: template.examples || [], - }; - } - - /** - * Check if template exists - * @param templateName Template name - */ - hasTemplate(templateName: string): boolean { - return this.templates.has(templateName); - } - - /** - * Remove a template - * @param templateName Template name - */ - unregisterTemplate(templateName: string): boolean { - const removed = this.templates.delete(templateName); - this.templateCache.delete(templateName); - if (removed) { - this.logger.debug(`Unregistered template: ${templateName}`); - } - return removed; - } - - /** - * Clear all templates and cache - */ - clearTemplates(): void { - this.templates.clear(); - this.templateCache.clear(); - this.logger.debug("Cleared all templates"); - } - - /** - * Validate template context - * @param context Template context - * @param requiredFields Required context fields - */ - protected validateContext(context: TemplateContext, requiredFields: string[] = []): void { - for (const field of requiredFields) { - if (!(field in context)) { - throw new TemplateError(`Missing required context field: ${field}`, "unknown", context, { - missingVariables: [field], - availableTemplates: this.getAvailableTemplates(), - }); - } - } - } - - /** - * Register default template helpers - */ - protected registerDefaultHelpers(): void { - // String manipulation helpers - this.registerHelper("capitalize", (str: string) => str.charAt(0).toUpperCase() + str.slice(1)); - - this.registerHelper("lowercase", (str: string) => str.toLowerCase()); - this.registerHelper("uppercase", (str: string) => str.toUpperCase()); - - this.registerHelper("camelCase", (str: string) => str.replace(/[-_\s]+(.)/g, (_, char) => char.toUpperCase())); - - this.registerHelper("pascalCase", (str: string) => { - const camelCase = str.replace(/[-_\s]+(.)/g, (_, char) => char.toUpperCase()); - return camelCase.charAt(0).toUpperCase() + camelCase.slice(1); - }); - - this.registerHelper("snakeCase", (str: string) => - str - .replace(/([A-Z])/g, "_$1") - .replace(/[-\s]+/g, "_") - .toLowerCase() - .replace(/^_/, ""), - ); - - // Array helpers - this.registerHelper("join", (arr: any[], separator = ", ") => (Array.isArray(arr) ? arr.join(separator) : "")); - - this.registerHelper("length", (arr: any[]) => (Array.isArray(arr) ? arr.length : 0)); - - // Logic helpers - this.registerHelper("eq", (a: any, b: any) => a === b); - this.registerHelper("ne", (a: any, b: any) => a !== b); - this.registerHelper("gt", (a: number, b: number) => a > b); - this.registerHelper("lt", (a: number, b: number) => a < b); - - // Utility helpers - this.registerHelper("json", (obj: any) => JSON.stringify(obj, null, 2)); - - this.registerHelper("indent", (str: string, spaces = 2) => - str - .split("\n") - .map((line) => " ".repeat(spaces) + line) - .join("\n"), - ); - - this.registerHelper("timestamp", () => new Date().toISOString()); - } -} diff --git a/src/api/generators/base/TypeMapper.ts b/src/api/generators/base/TypeMapper.ts deleted file mode 100644 index 749178975..000000000 --- a/src/api/generators/base/TypeMapper.ts +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Abstract base class for language-specific type mapping - * - * This provides the interface that all language generators must implement - * to convert FHIR TypeSchema types into their target language types. - */ - -import type { Identifier } from "@typeschema/types"; - -/** - * Represents a type in the target language - */ -export interface LanguageType { - /** The type name in the target language */ - name: string; - - /** Whether this is a primitive type (doesn't need imports) */ - isPrimitive: boolean; - - /** Import path if this type needs to be imported */ - importPath?: string; - - /** Generic type parameters if applicable */ - generics?: string[]; - - /** Whether this type can be null/undefined */ - nullable?: boolean; - - /** Whether this is an array type */ - isArray?: boolean; - - /** Additional metadata for language-specific features */ - metadata?: Record; -} - -/** - * Configuration for type mapping behavior - */ -export interface TypeMapperOptions { - /** Whether to generate nullable types (e.g., T | null) */ - generateNullable?: boolean; - - /** Whether to use strict type checking */ - strictTypes?: boolean; - - /** Custom type mappings */ - customMappings?: Record; - - /** Whether to generate array types or use generic collections */ - preferArraySyntax?: boolean; - - /** Naming convention strategy */ - namingConvention?: "camelCase" | "PascalCase" | "snake_case" | "kebab-case"; -} - -/** - * Abstract type mapper for language-specific type conversion - */ -export abstract class TypeMapper { - protected readonly options: Required; - - constructor(options: TypeMapperOptions = {}) { - this.options = { - generateNullable: true, - strictTypes: true, - customMappings: {}, - preferArraySyntax: true, - namingConvention: "PascalCase", - ...options, - }; - } - - // ========================================== - // Abstract Methods - Must be implemented by subclasses - // ========================================== - - /** - * Get the target language name (e.g., "TypeScript", "Python") - */ - abstract getLanguageName(): string; - - /** - * Map a FHIR primitive type to target language - * @param fhirType FHIR primitive type (string, integer, boolean, etc.) - */ - abstract mapPrimitive(fhirType: string): LanguageType; - - /** - * Map a reference type to target language - * @param targets Array of possible reference targets - */ - abstract mapReference(targets: Identifier[]): LanguageType; - - /** - * Map an array type to target language - * @param elementType The type of array elements - */ - abstract mapArray(elementType: LanguageType): LanguageType; - - /** - * Map an optional/nullable type - * @param type The base type - * @param required Whether the field is required - */ - abstract mapOptional(type: LanguageType, required: boolean): LanguageType; - - /** - * Map an enum/coded type - * @param values Possible enum values - * @param name Optional enum name - */ - abstract mapEnum(values: string[], name?: string): LanguageType; - - /** - * Format a type name according to language conventions - * @param name Raw type name - */ - abstract formatTypeName(name: string): string; - - /** - * Format a field name according to language conventions - * @param name Raw field name - */ - abstract formatFieldName(name: string): string; - - /** - * Format a file name according to language conventions - * @param name Raw file name (without extension) - */ - abstract formatFileName(name: string): string; - - // ========================================== - // Concrete Methods - Shared functionality - // ========================================== - - /** - * Main entry point for type mapping - * @param schemaType Type from TypeSchema - */ - mapType(schemaType: any): LanguageType { - // Handle primitive types - if (typeof schemaType === "string") { - return this.mapPrimitive(schemaType); - } - - // Handle complex types - if (schemaType && typeof schemaType === "object") { - const kind = schemaType.kind || schemaType.type; - - switch (kind) { - case "primitive-type": - return this.mapPrimitive(schemaType.name); - - case "reference": - return this.mapReference(schemaType.targets || []); - - case "array": { - const elementType = this.mapType(schemaType.element); - return this.mapArray(elementType); - } - - case "enum": - case "coded": - return this.mapEnum(schemaType.values || [], schemaType.name); - - case "complex-type": - case "resource": - return this.mapComplexType(schemaType); - - default: - return this.mapUnknownType(schemaType); - } - } - - return this.mapUnknownType(schemaType); - } - - /** - * Map a complex type (resource, complex-type) - * @param schemaType Complex type from schema - */ - protected mapComplexType(schemaType: any): LanguageType { - const typeName = this.formatTypeName(schemaType.name || "Unknown"); - - return { - name: typeName, - isPrimitive: false, - importPath: this.calculateImportPath(schemaType), - nullable: !schemaType.required && this.options.generateNullable, - metadata: { - kind: schemaType.kind, - package: schemaType.package, - }, - }; - } - - /** - * Handle unknown/unmapped types - * @param schemaType Unknown type - */ - protected mapUnknownType(schemaType: any): LanguageType { - // console.warn(`Unknown type encountered:`, schemaType); - - return { - name: "unknown", - isPrimitive: true, - nullable: true, - metadata: { - originalType: schemaType, - warning: "unmapped_type", - }, - }; - } - - /** - * Calculate import path for a type - * @param schemaType Type to calculate import for - */ - protected calculateImportPath(schemaType: any): string | undefined { - if (!schemaType.name) return undefined; - - const fileName = this.formatFileName(schemaType.name); - return `./${fileName}`; - } - - /** - * Apply naming convention to a string - * @param name Input name - */ - protected applyNamingConvention(name: string): string { - switch (this.options.namingConvention) { - case "camelCase": - return toCamelCase(name); - case "PascalCase": - return toPascalCase(name); - case "snake_case": - return toSnakeCase(name); - case "kebab-case": - return toKebabCase(name); - default: - return name; - } - } - - /** - * Get custom mapping if available - * @param type Original type name - */ - protected getCustomMapping(type: string): string | undefined { - return this.options.customMappings[type]; - } - - /** - * Check if type should be nullable - * @param required Whether field is required - */ - protected shouldBeNullable(required: boolean): boolean { - return !required && this.options.generateNullable; - } -} - -// ========================================== -// Utility Functions -// ========================================== - -function toCamelCase(str: string): string { - return str.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() || ""); -} - -function toPascalCase(str: string): string { - const camelCase = toCamelCase(str); - return camelCase.charAt(0).toUpperCase() + camelCase.slice(1); -} - -function toSnakeCase(str: string): string { - return str - .replace(/([A-Z])/g, "_$1") - .replace(/[-\s]+/g, "_") - .toLowerCase() - .replace(/^_/, ""); -} - -function toKebabCase(str: string): string { - return toSnakeCase(str).replace(/_/g, "-"); -} diff --git a/src/api/generators/base/TypeScriptTypeMapper.ts b/src/api/generators/base/TypeScriptTypeMapper.ts deleted file mode 100644 index 517d87d59..000000000 --- a/src/api/generators/base/TypeScriptTypeMapper.ts +++ /dev/null @@ -1,281 +0,0 @@ -/** - * TypeScript-specific type mapper implementation - */ - -import type { Identifier } from "@typeschema/types"; -import { type LanguageType, TypeMapper, type TypeMapperOptions } from "./TypeMapper"; - -/** - * TypeScript-specific options - */ -export interface TypeScriptTypeMapperOptions extends TypeMapperOptions { - /** Whether to use 'unknown' or 'any' for unmapped types */ - preferUnknown?: boolean; - - /** Whether to generate branded types for primitives */ - useBrandedTypes?: boolean; - - /** Whether to use 'undefined' or 'null' for optional types */ - preferUndefined?: boolean; - - /** Module format for imports */ - moduleFormat?: "esm" | "commonjs"; -} - -/** - * TypeScript type mapper - */ -export class TypeScriptTypeMapper extends TypeMapper { - private readonly tsOptions: Required; - - constructor(options: TypeScriptTypeMapperOptions = {}) { - super(options); - - this.tsOptions = { - ...this.options, - preferUnknown: true, - useBrandedTypes: false, - preferUndefined: true, - moduleFormat: "esm", - ...options, - }; - } - - getLanguageName(): string { - return "TypeScript"; - } - - mapPrimitive(fhirType: string): LanguageType { - const customMapping = this.getCustomMapping(fhirType); - if (customMapping) { - return { - name: customMapping, - isPrimitive: true, - nullable: false, - }; - } - - const primitiveMap: Record = { - string: "string", - integer: "number", - decimal: "number", - boolean: "boolean", - dateTime: "string", - date: "string", - time: "string", - instant: "string", - uri: "string", - url: "string", - canonical: "string", - oid: "string", - uuid: "string", - base64Binary: "string", - code: "string", - id: "string", - markdown: "string", - unsignedInt: "number", - positiveInt: "number", - - xhtml: "string", - json: "unknown", - }; - - const mappedType = primitiveMap[fhirType]; - - if (!mappedType) { - console.warn(`Unknown FHIR primitive type: ${fhirType}`); - return { - name: this.tsOptions.preferUnknown ? "unknown" : "any", - isPrimitive: true, - nullable: false, - metadata: { warning: "unmapped_primitive", originalType: fhirType }, - }; - } - - if (this.tsOptions.useBrandedTypes && fhirType !== mappedType) { - return { - name: `${mappedType} & { readonly __brand: '${fhirType}' }`, - isPrimitive: false, - importPath: "./brands", - nullable: false, - metadata: { isBranded: true, originalFhirType: fhirType }, - }; - } - - return { - name: mappedType, - isPrimitive: true, - nullable: false, - }; - } - - mapReference(targets: Identifier[]): LanguageType { - if (!targets || targets.length === 0) { - return { - name: "Reference", - isPrimitive: false, - importPath: "./Reference", - generics: ["unknown"], - nullable: false, - }; - } - - if (targets.length === 1) { - const targetName = targets[0]?.name || "unknown"; - const targetStringLiteral = targetName === "unknown" ? "unknown" : `'${targetName}'`; - return { - name: "Reference", - isPrimitive: false, - importPath: "./Reference", - generics: [targetStringLiteral], - nullable: false, - metadata: { - referencedType: targetName, - referencedSchema: targets[0], - }, - }; - } - - const targetStringLiterals = targets.map((t) => { - const targetName = t.name || "unknown"; - return targetName === "unknown" ? "unknown" : `'${targetName}'`; - }); - return { - name: "Reference", - isPrimitive: false, - importPath: "./Reference", - generics: [targetStringLiterals.join(" | ")], - nullable: false, - metadata: { - referencedTypes: targets.map((t) => t.name || "unknown"), - referencedSchemas: targets, - }, - }; - } - - mapArray(elementType: LanguageType): LanguageType { - if (this.options.preferArraySyntax) { - return { - name: `${elementType.name}[]`, - isPrimitive: elementType.isPrimitive, - importPath: elementType.importPath, - isArray: true, - nullable: false, - metadata: { - elementType: elementType, - arrayStyle: "suffix", - }, - }; - } else { - return { - name: "Array", - isPrimitive: false, - generics: [elementType.name], - isArray: true, - nullable: false, - metadata: { - elementType: elementType, - arrayStyle: "generic", - }, - }; - } - } - - mapOptional(type: LanguageType, required: boolean): LanguageType { - if (required || !this.shouldBeNullable(required)) { - return type; - } - - const nullType = this.tsOptions.preferUndefined ? "undefined" : "null"; - - return { - ...type, - name: `${type.name} | ${nullType}`, - nullable: true, - metadata: { - ...type.metadata, - nullabilityType: nullType, - wasOptional: true, - }, - }; - } - - mapEnum(values: string[], name?: string): LanguageType { - const enumName = name ? this.formatTypeName(name) : "CodedValue"; - - const unionType = values.map((v) => `'${v}'`).join(" | "); - - return { - name: unionType, - isPrimitive: false, - nullable: false, - metadata: { - enumName, - values, - isUnionType: true, - }, - }; - } - - formatTypeName(name: string): string { - return this.applyNamingConvention(name); - } - - formatFieldName(name: string): string { - return toCamelCase(name); - } - - formatFileName(name: string): string { - return this.applyNamingConvention(name); - } - - /** - * Generate TypeScript interface field - * @param fieldName Field name - * @param fieldType Field type - * @param required Whether field is required - */ - generateInterfaceField(fieldName: string, fieldType: LanguageType, required: boolean): string { - const formattedName = this.formatFieldName(fieldName); - const optionalMarker = required ? "" : "?"; - - return `${formattedName}${optionalMarker}: ${fieldType.name};`; - } - - /** - * Generate import statement for a type - * @param type Language type with import info - */ - generateImportStatement(type: LanguageType): string | undefined { - if (!type.importPath || type.isPrimitive) { - return undefined; - } - - if (this.tsOptions.moduleFormat === "esm") { - return `import type { ${type.name} } from '${type.importPath}';`; - } else { - return `const { ${type.name} } = require('${type.importPath}');`; - } - } - - /** - * Get all required imports for a set of types - * @param types Array of language types - */ - getRequiredImports(types: LanguageType[]): string[] { - const imports = new Set(); - - for (const type of types) { - const importStatement = this.generateImportStatement(type); - if (importStatement) { - imports.add(importStatement); - } - } - - return Array.from(imports).sort(); - } -} - -function toCamelCase(str: string): string { - return str.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() || ""); -} diff --git a/src/api/generators/base/builders/DirectoryBuilder.ts b/src/api/generators/base/builders/DirectoryBuilder.ts deleted file mode 100644 index 9805f2988..000000000 --- a/src/api/generators/base/builders/DirectoryBuilder.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Directory builder for batch file operations - * - * Provides a fluent API for managing entire directories of files, - * including subdirectories, index files, and batch operations. - */ - -import type { CodegenLogger } from "../../../../utils/codegen-logger"; -import type { FileManager } from "../FileManager"; -import type { FileBuilder } from "./FileBuilder"; -import type { IndexBuilder } from "./IndexBuilder"; - -export interface DirectoryBuilderConfig { - path: string; - fileManager: FileManager; - logger: CodegenLogger; -} - -/** - * Fluent builder for directory operations - * - * Features: - * - Subdirectory management - * - Batch file operations - * - Index file generation - * - Directory cleaning - * - File listing for preview - */ -export class DirectoryBuilder { - private readonly config: DirectoryBuilderConfig; - private readonly files = new Map(); - private readonly subdirectories = new Map(); - private indexBuilder?: IndexBuilder; - private shouldClean = false; - - constructor(config: DirectoryBuilderConfig) { - this.config = config; - } - - /** - * Add a subdirectory - * @param name Subdirectory name - */ - withSubdirectory(name: string): DirectoryBuilder { - const subdirPath = `${this.config.path}/${name}`; - const subdir = new DirectoryBuilder({ - path: subdirPath, - fileManager: this.config.fileManager, - logger: this.config.logger.child(`Dir:${name}`), - }); - - this.subdirectories.set(name, subdir); - return subdir; - } - - /** - * Add files to this directory - * @param files Map of filename to FileBuilder - */ - withFiles(files: Record): DirectoryBuilder { - for (const [filename, builder] of Object.entries(files)) { - this.files.set(filename, builder); - } - return this; - } - - /** - * Add a single file - * @param filename File name - * @param builder File builder - */ - withFile(filename: string, builder: FileBuilder): DirectoryBuilder { - this.files.set(filename, builder); - return this; - } - - /** - * Set index builder for this directory - * @param builder Index builder - */ - withIndex(builder: IndexBuilder): DirectoryBuilder { - this.indexBuilder = builder; - return this; - } - - /** - * Clean directory before creating files - */ - clean(): DirectoryBuilder { - this.shouldClean = true; - return this; - } - - /** - * Ensure directory exists - */ - async ensure(): Promise { - await this.config.fileManager.ensureDirectory(this.config.path); - return this; - } - - /** - * Save all files in this directory - */ - async save(): Promise { - const savedPaths: string[] = []; - - // Clean directory if requested - if (this.shouldClean) { - await this.config.fileManager.cleanDirectory(this.config.path); - } - - // Ensure directory exists - await this.ensure(); - - // Save all files in parallel for better performance - const filePromises = Array.from(this.files.entries()).map(async ([filename, builder]) => { - try { - const path = await builder.save(); - savedPaths.push(path); - return path; - } catch (error) { - this.config.logger.error( - `Failed to save file ${filename}:`, - error instanceof Error ? error : undefined, - ); - throw error; - } - }); - - await Promise.all(filePromises); - - // Save subdirectories recursively - for (const [name, subdir] of this.subdirectories) { - try { - const subdirPaths = await subdir.save(); - savedPaths.push(...subdirPaths); - } catch (error) { - this.config.logger.error( - `Failed to save subdirectory ${name}:`, - error instanceof Error ? error : undefined, - ); - throw error; - } - } - - // Generate index if provided - if (this.indexBuilder) { - try { - const indexPath = await this.indexBuilder.save(); - savedPaths.push(indexPath); - } catch (error) { - this.config.logger.error("Failed to save index file:", error instanceof Error ? error : undefined); - throw error; - } - } - - this.config.logger.info(`Saved directory ${this.config.path} with ${savedPaths.length} files`); - return savedPaths; - } - - /** - * Get all files that would be generated (for preview) - */ - getFileList(): string[] { - const files: string[] = []; - - for (const filename of this.files.keys()) { - files.push(`${this.config.path}/${filename}`); - } - - for (const [_name, subdir] of this.subdirectories) { - const subdirFiles = subdir.getFileList(); - files.push(...subdirFiles); - } - - if (this.indexBuilder) { - files.push(`${this.config.path}/index.ts`); // Assuming TypeScript - } - - return files.sort(); - } - - /** - * Get statistics about this directory - */ - getStats(): { - fileCount: number; - subdirectoryCount: number; - hasIndex: boolean; - totalFiles: number; - } { - let totalFiles = this.files.size; - - for (const subdir of this.subdirectories.values()) { - totalFiles += subdir.getStats().totalFiles; - } - - if (this.indexBuilder) { - totalFiles += 1; - } - - return { - fileCount: this.files.size, - subdirectoryCount: this.subdirectories.size, - hasIndex: !!this.indexBuilder, - totalFiles, - }; - } - - /** - * Check if directory would overwrite existing files - */ - async wouldOverwrite(): Promise { - const conflicts: string[] = []; - - // Check files in this directory - for (const filename of this.files.keys()) { - const filePath = `${this.config.path}/${filename}`; - if (await this.config.fileManager.wouldOverwrite(filePath)) { - conflicts.push(filePath); - } - } - - // Check subdirectories - for (const subdir of this.subdirectories.values()) { - const subdirConflicts = await subdir.wouldOverwrite(); - conflicts.push(...subdirConflicts); - } - - // Check index file - if (this.indexBuilder) { - const indexPath = `${this.config.path}/index.ts`; - if (await this.config.fileManager.wouldOverwrite(indexPath)) { - conflicts.push(indexPath); - } - } - - return conflicts; - } - - /** - * Get the path of this directory - */ - getPath(): string { - return this.config.path; - } - - /** - * Get all file builders (for testing/debugging) - */ - getFiles(): Map { - return new Map(this.files); - } - - /** - * Get all subdirectories (for testing/debugging) - */ - getSubdirectories(): Map { - return new Map(this.subdirectories); - } - - /** - * Get index builder (for testing/debugging) - */ - getIndexBuilder(): IndexBuilder | undefined { - return this.indexBuilder; - } -} diff --git a/src/api/generators/base/builders/FileBuilder.ts b/src/api/generators/base/builders/FileBuilder.ts deleted file mode 100644 index 280b19a55..000000000 --- a/src/api/generators/base/builders/FileBuilder.ts +++ /dev/null @@ -1,494 +0,0 @@ -/** - * Fluent file builder with lifecycle hooks and validation - * - * This provides a clean, chainable API for building files with imports, - * exports, content generation, and lifecycle hooks for customization. - */ - -import type { CodegenLogger } from "../../../../utils/codegen-logger"; -import { FileOperationError, TemplateError } from "../errors"; -import type { FileManager } from "../FileManager"; -import type { - AfterSaveHook, - BeforeSaveHook, - ErrorHook, - FileBuilderOptions, - FileContext, - TemplateEngine, - TypeMapper, -} from "../types"; - -export interface FileBuilderConfig { - filename: string; - fileManager: FileManager; - templateEngine?: TemplateEngine; - typeMapper: TypeMapper; - logger: CodegenLogger; -} - -/** - * Fluent builder for creating files with lifecycle hooks - * - * Features: - * - Fluent API for content building - * - Template integration - * - Import/export management - * - Lifecycle hooks (before/after save, error handling) - * - Content validation - * - Automatic import path resolution - */ -export class FileBuilder { - private readonly config: FileBuilderConfig; - private content: string = ""; - private readonly imports = new Map(); - private readonly exports = new Set(); - private readonly metadata = new Map(); - - // Lifecycle hooks - private beforeSaveHooks: BeforeSaveHook[] = []; - private afterSaveHooks: AfterSaveHook[] = []; - private errorHooks: ErrorHook[] = []; - - // Options - private options: FileBuilderOptions = { - template: undefined, - importStrategy: "auto", - validation: "strict", - prettify: true, - formatting: { - indentSize: 2, - useTabs: false, - maxLineLength: 100, - }, - encoding: "utf-8", - }; - - constructor(config: FileBuilderConfig) { - this.config = config; - } - - // ========================================== - // Content Building Methods - // ========================================== - - /** - * Set content directly - * @param content File content - */ - withContent(content: string | (() => string)): FileBuilder { - this.content = typeof content === "string" ? content : content(); - return this; - } - - /** - * Generate content from template - * @param templateName Template to use - * @param context Template context - */ - withTemplate(templateName: string, context: Record): FileBuilder { - if (!this.config.templateEngine) { - throw new TemplateError( - `Template engine is required for template rendering. Template: '${templateName}'`, - templateName, - context, - ); - } - - this.options.template = templateName; - - try { - this.content = this.config.templateEngine.render(templateName, { - ...context, - imports: this.imports, - exports: this.exports, - filename: this.config.filename, - }); - } catch (_error) { - throw new TemplateError(`Failed to render template '${templateName}'`, templateName, context, { - availableTemplates: this.config.templateEngine.getAvailableTemplates?.() || [], - }); - } - - return this; - } - - /** - * Append content to existing content - * @param content Content to append - */ - appendContent(content: string): FileBuilder { - this.content += content; - return this; - } - - /** - * Prepend content to existing content - * @param content Content to prepend - */ - prependContent(content: string): FileBuilder { - this.content = content + this.content; - return this; - } - - // ========================================== - // Import/Export Management - // ========================================== - - /** - * Set all imports at once - * @param imports Map of symbol name to import path - */ - withImports(imports: Map): FileBuilder { - this.imports.clear(); - for (const [symbol, path] of imports) { - this.imports.set(symbol, path); - } - return this; - } - - /** - * Add a single import - * @param symbol Symbol to import - * @param from Import path - */ - addImport(symbol: string, from: string): FileBuilder { - this.imports.set(symbol, from); - return this; - } - - /** - * Add multiple imports from the same path - * @param symbols Symbols to import - * @param from Import path - */ - addImports(symbols: string[], from: string): FileBuilder { - for (const symbol of symbols) { - this.imports.set(symbol, from); - } - return this; - } - - /** - * Set all exports at once - * @param exports Array of export names - */ - withExports(exports: string[]): FileBuilder { - this.exports.clear(); - for (const exp of exports) { - this.exports.add(exp); - } - return this; - } - - /** - * Add a single export - * @param name Export name - */ - addExport(name: string): FileBuilder { - this.exports.add(name); - return this; - } - - /** - * Add multiple exports - * @param names Export names - */ - addExports(names: string[]): FileBuilder { - for (const name of names) { - this.exports.add(name); - } - return this; - } - - // ========================================== - // Metadata and Options - // ========================================== - - /** - * Set metadata for the file - * @param key Metadata key - * @param value Metadata value - */ - withMetadata(key: string, value: unknown): FileBuilder { - this.metadata.set(key, value); - return this; - } - - /** - * Set file builder options - * @param options Options to set - */ - withOptions(options: Partial): FileBuilder { - this.options = { ...this.options, ...options }; - return this; - } - - // ========================================== - // Lifecycle Hooks - // ========================================== - - /** - * Add hook to run before saving - * @param hook Hook function - */ - onBeforeSave(hook: BeforeSaveHook): FileBuilder { - this.beforeSaveHooks.push(hook); - return this; - } - - /** - * Add hook to run after successful save - * @param hook Hook function - */ - onAfterSave(hook: AfterSaveHook): FileBuilder { - this.afterSaveHooks.push(hook); - return this; - } - - /** - * Add hook to run when error occurs - * @param hook Hook function - */ - onError(hook: ErrorHook): FileBuilder { - this.errorHooks.push(hook); - return this; - } - - // ========================================== - // Execution Methods - // ========================================== - - /** - * Build final content without saving - * @returns File context with final content - */ - build(): FileContext { - const finalContent = this.buildFinalContent(); - - return { - filename: this.config.filename, - content: finalContent, - imports: new Map(this.imports), - exports: new Set(this.exports), - metadata: Object.fromEntries(this.metadata), - templateName: this.options.template, - }; - } - - /** - * Save the file - * @returns Promise resolving to file path - */ - async save(): Promise { - const context = this.build(); - - try { - // Run before-save hooks - for (const hook of this.beforeSaveHooks) { - await hook(context); - } - - // Validate content if enabled - if (this.options.validation !== "none") { - await this.validateContent(context.content); - } - - // Write file - const result = await this.config.fileManager.writeFile(this.config.filename, context.content, { - encoding: this.options.encoding, - }); - - const stats = { - size: result.size, - generationTime: 0, // Set by caller if needed - writeTime: result.writeTime, - }; - - // Run after-save hooks - for (const hook of this.afterSaveHooks) { - await hook(result.path, stats); - } - - this.config.logger.debug(`Saved ${this.config.filename} successfully`); - return result.path; - } catch (error) { - // Run error hooks - for (const hook of this.errorHooks) { - try { - await hook(error instanceof Error ? error : new Error(String(error)), context); - } catch (hookError) { - this.config.logger.warn( - `Error hook failed: ${hookError instanceof Error ? hookError.message : String(hookError)}`, - ); - } - } - - throw error; - } - } - - // ========================================== - // Private Helper Methods - // ========================================== - - /** - * Build final content with imports and exports - */ - private buildFinalContent(): string { - const parts: string[] = []; - - // Add imports - if (this.imports.size > 0 && this.options.importStrategy !== "none") { - parts.push(this.generateImportStatements()); - parts.push(""); // Empty line after imports - } - - // Add main content - if (this.content) { - parts.push(this.content); - } - - // Add exports if not already in content - if (this.exports.size > 0 && !this.content.includes("export")) { - parts.push(""); // Empty line before exports - parts.push(this.generateExportStatements()); - } - - let finalContent = parts.join("\n"); - - // Prettify if enabled - if (this.options.prettify) { - finalContent = this.prettifyContent(finalContent); - } - - return finalContent; - } - - /** - * Generate import statements - */ - private generateImportStatements(): string { - const lines: string[] = []; - - // Group imports by path - const importsByPath = new Map(); - for (const [symbol, path] of this.imports) { - const resolvedPath = - this.options.importStrategy === "auto" - ? this.config.fileManager.getRelativeImportPath(this.config.filename, path) - : path; - - if (!importsByPath.has(resolvedPath)) { - importsByPath.set(resolvedPath, []); - } - importsByPath.get(resolvedPath)?.push(symbol); - } - - // Generate import statements - for (const [path, symbols] of importsByPath) { - if (symbols.length === 1) { - lines.push(`import type { ${symbols[0]} } from '${path}';`); - } else { - const sortedSymbols = symbols.sort(); - if (sortedSymbols.length <= 3) { - lines.push(`import type { ${sortedSymbols.join(", ")} } from '${path}';`); - } else { - lines.push(`import type {`); - const indent = "\t"; - sortedSymbols.forEach((symbol, index) => { - const isLast = index === sortedSymbols.length - 1; - lines.push(`${indent}${symbol}${isLast ? "" : ","}`); - }); - lines.push(`} from '${path}';`); - } - } - } - - return lines.join("\n"); - } - - /** - * Generate export statements - */ - private generateExportStatements(): string { - const exports = Array.from(this.exports).sort(); - return exports.map((exp) => `export { ${exp} };`).join("\n"); - } - - /** - * Prettify content (basic implementation) - */ - private prettifyContent(content: string): string { - // Basic prettification - return content - .replace(/\n{3,}/g, "\n\n") // Max 2 consecutive newlines - .replace( - /\t/g, - this.options.formatting?.useTabs ? "\t" : " ".repeat(this.options.formatting?.indentSize || 2), - ) - .trim(); - } - - /** - * Validate generated content - */ - private async validateContent(content: string): Promise { - if (this.options.validation === "none") return; - - // Basic validation - check for syntax errors - const issues: string[] = []; - - // Check for unmatched braces - const openBraces = (content.match(/\{/g) || []).length; - const closeBraces = (content.match(/\}/g) || []).length; - if (openBraces !== closeBraces) { - issues.push(`Unmatched braces: ${openBraces} open, ${closeBraces} close`); - } - - // Check for unmatched parentheses - const openParens = (content.match(/\(/g) || []).length; - const closeParens = (content.match(/\)/g) || []).length; - if (openParens !== closeParens) { - issues.push(`Unmatched parentheses: ${openParens} open, ${closeParens} close`); - } - - // Check for basic TypeScript syntax issues - if (this.config.filename.endsWith(".ts") || this.config.filename.endsWith(".tsx")) { - if (content.includes("interface") && !content.match(/interface\s+\w+\s*\{/)) { - issues.push("Invalid interface syntax detected"); - } - } - - if (issues.length > 0 && this.options.validation === "strict") { - throw new FileOperationError( - `Content validation failed for ${this.config.filename}: ${issues.join(", ")}`, - "write", - this.config.filename, - ); - } else if (issues.length > 0) { - // Just warn for non-strict validation - this.config.logger.warn(`Validation issues in ${this.config.filename}: ${issues.join(", ")}`); - } - } - - /** - * Get current content (for testing/debugging) - */ - getContent(): string { - return this.content; - } - - /** - * Get current imports (for testing/debugging) - */ - getImports(): Map { - return new Map(this.imports); - } - - /** - * Get current exports (for testing/debugging) - */ - getExports(): Set { - return new Set(this.exports); - } -} diff --git a/src/api/generators/base/builders/IndexBuilder.ts b/src/api/generators/base/builders/IndexBuilder.ts deleted file mode 100644 index 7bcb0bdfa..000000000 --- a/src/api/generators/base/builders/IndexBuilder.ts +++ /dev/null @@ -1,340 +0,0 @@ -/** - * Index file builder for automated exports - * - * Automatically generates index files that export all types and functions - * from a directory, with support for grouping and namespaces. - */ - -import type { CodegenLogger } from "../../../../utils/codegen-logger"; -import type { FileManager } from "../FileManager"; -import type { TemplateEngine } from "../types"; - -export interface IndexBuilderConfig { - directory: string; - fileManager: FileManager; - templateEngine?: TemplateEngine; - logger: CodegenLogger; -} - -/** - * Builder for index files with intelligent export management - * - * Features: - * - Automatic export detection - * - Namespace support - * - Export grouping - * - Custom headers - * - Template support - */ -export class IndexBuilder { - private readonly config: IndexBuilderConfig; - private readonly exports = new Map(); // symbol -> from path - private readonly namespaces = new Map(); // namespace -> path - private readonly reExports = new Map(); // export all from path - private header = ""; - private footer = ""; - private groupingFunction?: (exportName: string) => string; - private sortFunction?: (a: [string, string], b: [string, string]) => number; - - constructor(config: IndexBuilderConfig) { - this.config = config; - } - - /** - * Add exports from a specific file - * @param exportNames Export names - * @param fromPath Path to file (without extension) - */ - withExports(exportNames: string[], fromPath: string): IndexBuilder { - for (const name of exportNames) { - this.exports.set(name, fromPath); - } - return this; - } - - /** - * Add a single export - * @param exportName Export name - * @param fromPath Path to file - */ - withExport(exportName: string, fromPath: string): IndexBuilder { - this.exports.set(exportName, fromPath); - return this; - } - - /** - * Add namespace exports - * @param namespaces Map of namespace to path - */ - withNamespaces(namespaces: Record): IndexBuilder { - for (const [ns, path] of Object.entries(namespaces)) { - this.namespaces.set(ns, path); - } - return this; - } - - /** - * Add namespace export - * @param namespace Namespace name - * @param path Path to export as namespace - */ - withNamespace(namespace: string, path: string): IndexBuilder { - this.namespaces.set(namespace, path); - return this; - } - - /** - * Re-export all from paths - * @param paths Paths to re-export all from - */ - withReExports(paths: string[]): IndexBuilder { - for (const path of paths) { - this.reExports.set(path, path); - } - return this; - } - - /** - * Add re-export - * @param path Path to re-export all from - */ - withReExport(path: string): IndexBuilder { - this.reExports.set(path, path); - return this; - } - - /** - * Set header content - * @param header Header content - */ - withHeader(header: string): IndexBuilder { - this.header = header; - return this; - } - - /** - * Set footer content - * @param footer Footer content - */ - withFooter(footer: string): IndexBuilder { - this.footer = footer; - return this; - } - - /** - * Group exports by function - * @param fn Function that returns group name for export - */ - groupBy(fn: (exportName: string) => string): IndexBuilder { - this.groupingFunction = fn; - return this; - } - - /** - * Sort exports by function - * @param fn Sort function for [exportName, fromPath] tuples - */ - sortBy(fn: (a: [string, string], b: [string, string]) => number): IndexBuilder { - this.sortFunction = fn; - return this; - } - - /** - * Auto-discover exports from directory - * @param filePattern Pattern to match files (e.g., "*.ts") - */ - async autoDiscover(_filePattern?: string): Promise { - // This is a placeholder - in a real implementation, this would: - // 1. Read all files in the directory - // 2. Parse TypeScript/JavaScript to extract exports - // 3. Add them to the exports map - - this.config.logger.debug(`Auto-discovering exports in ${this.config.directory}`); - - // For now, just log that this feature would be implemented - this.config.logger.warn("Auto-discovery not yet implemented - manually add exports"); - - return this; - } - - /** - * Save the index file - */ - async save(): Promise { - const content = this.generateContent(); - const indexPath = `${this.config.directory}/index.ts`; - - const result = await this.config.fileManager.writeFile(indexPath, content); - this.config.logger.debug(`Generated index file: ${indexPath}`); - - return result.path; - } - - /** - * Build content without saving (for preview) - */ - build(): string { - return this.generateContent(); - } - - /** - * Generate the index file content - */ - private generateContent(): string { - const lines: string[] = []; - - // Add header - if (this.header) { - lines.push(this.header); - lines.push(""); - } - - // Add re-exports first - if (this.reExports.size > 0) { - lines.push("// Re-exports"); - for (const path of this.reExports.values()) { - lines.push(`export * from './${path}';`); - } - lines.push(""); - } - - // Process exports - if (this.exports.size > 0) { - if (this.groupingFunction) { - this.generateGroupedExports(lines); - } else { - this.generateSimpleExports(lines); - } - lines.push(""); - } - - // Add namespace exports - if (this.namespaces.size > 0) { - lines.push("// Namespace exports"); - const sortedNamespaces = Array.from(this.namespaces.entries()).sort(); - for (const [ns, path] of sortedNamespaces) { - lines.push(`export * as ${ns} from './${path}';`); - } - lines.push(""); - } - - // Add footer - if (this.footer) { - lines.push(this.footer); - } - - // Clean up extra empty lines - const content = lines - .join("\n") - .replace(/\n{3,}/g, "\n\n") - .trim(); - return `${content}\n`; // Ensure file ends with newline - } - - /** - * Generate simple exports without grouping - */ - private generateSimpleExports(lines: string[]): void { - lines.push("// Exports"); - - let exportEntries = Array.from(this.exports.entries()); - - // Apply custom sorting if provided - if (this.sortFunction) { - exportEntries = exportEntries.sort(this.sortFunction); - } else { - // Default: sort by export name - exportEntries = exportEntries.sort(([a], [b]) => a.localeCompare(b)); - } - - // Group by path for cleaner output - const exportsByPath = new Map(); - for (const [exportName, fromPath] of exportEntries) { - if (!exportsByPath.has(fromPath)) { - exportsByPath.set(fromPath, []); - } - exportsByPath.get(fromPath)?.push(exportName); - } - - // Generate export statements - for (const [path, exports] of exportsByPath) { - const sortedExports = exports.sort(); - if (sortedExports.length === 1) { - lines.push(`export type { ${sortedExports[0]} } from './${path}';`); - } else if (sortedExports.length <= 3) { - lines.push(`export type { ${sortedExports.join(", ")} } from './${path}';`); - } else { - lines.push(`export type {`); - sortedExports.forEach((exp, index) => { - const isLast = index === sortedExports.length - 1; - lines.push(`\t${exp}${isLast ? "" : ","}`); - }); - lines.push(`} from './${path}';`); - } - } - } - - /** - * Generate grouped exports - */ - private generateGroupedExports(lines: string[]): void { - if (!this.groupingFunction) return; - - const groups = new Map>(); - - // Group exports - for (const [exportName, fromPath] of this.exports) { - const group = this.groupingFunction(exportName); - - if (!groups.has(group)) { - groups.set(group, new Map()); - } - - const groupMap = groups.get(group)!; - if (!groupMap.has(fromPath)) { - groupMap.set(fromPath, []); - } - - groupMap.get(fromPath)?.push(exportName); - } - - // Generate grouped output - const sortedGroups = Array.from(groups.entries()).sort(); - - for (const [groupName, groupExports] of sortedGroups) { - lines.push(`// ${groupName}`); - - for (const [path, exports] of groupExports) { - const sortedExports = exports.sort(); - if (sortedExports.length === 1) { - lines.push(`export type { ${sortedExports[0]} } from './${path}';`); - } else { - lines.push(`export type { ${sortedExports.join(", ")} } from './${path}';`); - } - } - - lines.push(""); - } - } - - /** - * Get current exports (for testing/debugging) - */ - getExports(): Map { - return new Map(this.exports); - } - - /** - * Get current namespaces (for testing/debugging) - */ - getNamespaces(): Map { - return new Map(this.namespaces); - } - - /** - * Get current re-exports (for testing/debugging) - */ - getReExports(): Map { - return new Map(this.reExports); - } -} diff --git a/src/api/generators/base/enhanced-errors.ts b/src/api/generators/base/enhanced-errors.ts deleted file mode 100644 index 391e5c3a2..000000000 --- a/src/api/generators/base/enhanced-errors.ts +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Enhanced error handling with rich context and suggestions - * - * This module builds on the basic GeneratorError classes to provide - * even more detailed error context, smarter suggestions, and better - * user experience for developers at all skill levels. - */ - -import type { TypeSchema } from "@typeschema/index"; -import { GeneratorError } from "./errors"; - -/** - * Enhanced schema validation error with smart suggestions - */ -export class EnhancedSchemaValidationError extends GeneratorError { - constructor( - message: string, - public readonly schema: TypeSchema, - public readonly validationErrors: string[], - public readonly userContext?: { - isBeginnerMode?: boolean; - previousSuccessfulSchemas?: string[]; - commonPatterns?: string[]; - }, - ) { - super(message, "validation", { - schemaName: schema.identifier.name, - schemaKind: schema.identifier.kind, - schemaPackage: schema.identifier.package, - validationErrors, - userContext, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - - // Basic suggestions - suggestions.push("Check the schema structure matches TypeSchema specification"); - suggestions.push("Verify all required fields are present"); - - // Context-aware suggestions based on validation errors - for (const error of this.validationErrors) { - if (error.includes("identifier.name")) { - suggestions.push("Add missing identifier.name field to the schema"); - suggestions.push("Ensure identifier.name is a non-empty string"); - } - - if (error.includes("identifier.kind")) { - suggestions.push("Set identifier.kind to one of: resource, complex-type, profile, primitive-type"); - suggestions.push("Check FHIR specification for valid kind values"); - } - - if (error.includes("circular")) { - suggestions.push("Remove circular references between schemas"); - suggestions.push("Use forward references for recursive types"); - } - } - - // Beginner-friendly suggestions - if (this.userContext?.isBeginnerMode) { - suggestions.push("📚 Review the TypeSchema documentation at: docs/typeschema.md"); - suggestions.push("🔍 Use --verbose flag for more detailed error information"); - - if (this.userContext.previousSuccessfulSchemas?.length) { - suggestions.push(`✅ Compare with working schema: ${this.userContext.previousSuccessfulSchemas[0]}`); - } - } - - return suggestions; - } - - /** - * Get formatted error message for display - */ - getFormattedMessage(): string { - const lines = [ - `❌ Schema Validation Failed: ${this.message}`, - "", - "📍 Context:", - ` Schema: ${this.schema.identifier.name}`, - ` Kind: ${this.schema.identifier.kind}`, - ` Package: ${this.schema.identifier.package || "unknown"}`, - "", - ]; - - if (this.validationErrors.length > 0) { - lines.push("🔍 Validation Errors:"); - this.validationErrors.forEach((error, index) => { - lines.push(` ${index + 1}. ${error}`); - }); - lines.push(""); - } - - const suggestions = this.getSuggestions(); - if (suggestions.length > 0) { - lines.push("💡 Suggestions:"); - suggestions.forEach((suggestion) => { - lines.push(` • ${suggestion}`); - }); - } - - return lines.join("\n"); - } -} - -/** - * Enhanced file operation error with recovery suggestions - */ -export class EnhancedFileOperationError extends GeneratorError { - constructor( - message: string, - public readonly operation: "create" | "write" | "read" | "delete", - public readonly filePath: string, - public readonly originalError?: Error, - public readonly recoveryOptions?: { - canRetry?: boolean; - alternativePaths?: string[]; - permissionFix?: string; - }, - ) { - super(message, "writing", { - operation, - filePath, - originalError: originalError?.message, - recoveryOptions, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - - // Operation-specific suggestions - switch (this.operation) { - case "create": - case "write": - suggestions.push("Check if the directory exists and is writable"); - suggestions.push("Verify you have permission to write to this location"); - - if (this.filePath.includes(" ")) { - suggestions.push("File path contains spaces - ensure proper escaping"); - } - - if (this.recoveryOptions?.alternativePaths?.length) { - suggestions.push("Try alternative output directory:"); - this.recoveryOptions.alternativePaths.forEach((path) => { - suggestions.push(` • ${path}`); - }); - } - break; - - case "read": - suggestions.push("Verify the file exists at the specified path"); - suggestions.push("Check file permissions are readable"); - break; - - case "delete": - suggestions.push("Check if file is locked by another process"); - suggestions.push("Verify delete permissions for this directory"); - break; - } - - // Permission-specific suggestions - if (this.originalError?.message.includes("EACCES")) { - suggestions.push("Permission denied - try running with elevated permissions"); - if (this.recoveryOptions?.permissionFix) { - suggestions.push(`Fix command: ${this.recoveryOptions.permissionFix}`); - } - } - - // Space-related suggestions - if (this.originalError?.message.includes("ENOSPC")) { - suggestions.push("Insufficient disk space - free up space and retry"); - suggestions.push("Consider using a different output directory"); - } - - // Recovery suggestions - if (this.recoveryOptions?.canRetry) { - suggestions.push("This operation can be retried safely"); - } - - return suggestions; - } - - /** - * Check if this error is recoverable - */ - override isRecoverable(): boolean { - return this.recoveryOptions?.canRetry || false; - } - - /** - * Get recovery actions user can take - */ - getRecoveryActions(): Array<{ - action: string; - command?: string; - automatic?: boolean; - }> { - const actions: Array<{ - action: string; - command?: string; - automatic?: boolean; - }> = []; - - if (this.originalError?.message.includes("EACCES")) { - actions.push({ - action: "Fix file permissions", - command: `chmod 755 "${this.filePath}"`, - automatic: false, - }); - } - - if (this.originalError?.message.includes("ENOENT")) { - actions.push({ - action: "Create missing directory", - command: `mkdir -p "${this.filePath}"`, - automatic: true, - }); - } - - return actions; - } -} - -/** - * Enhanced template error with template debugging info - */ -export class EnhancedTemplateError extends GeneratorError { - constructor( - message: string, - public readonly templateName: string, - public readonly templateContext: Record, - public readonly debugInfo?: { - availableTemplates?: string[]; - missingVariables?: string[]; - templateSource?: string; - lineNumber?: number; - }, - ) { - super(message, "generation", { - templateName, - contextKeys: Object.keys(templateContext), - debugInfo, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - - // Template existence suggestions - if (this.debugInfo?.availableTemplates?.length) { - suggestions.push("Available templates:"); - this.debugInfo.availableTemplates.forEach((template) => { - suggestions.push(` • ${template}`); - }); - - // Suggest similar template names - const similar = this.findSimilarTemplates(this.templateName, this.debugInfo.availableTemplates); - if (similar.length > 0) { - suggestions.push("Did you mean:"); - similar.forEach((template) => { - suggestions.push(` • ${template}`); - }); - } - } - - // Missing variables suggestions - if (this.debugInfo?.missingVariables?.length) { - suggestions.push("Missing template variables:"); - this.debugInfo.missingVariables.forEach((variable) => { - suggestions.push(` • ${variable}`); - }); - suggestions.push("Add these variables to the template context"); - } - - // Template syntax suggestions - if (this.debugInfo?.lineNumber) { - suggestions.push(`Check template syntax around line ${this.debugInfo.lineNumber}`); - } - - suggestions.push("Enable template debugging: { debug: true }"); - suggestions.push("Verify template file exists and has correct syntax"); - - return suggestions; - } - - private findSimilarTemplates(target: string, available: string[]): string[] { - return available - .filter((template) => { - const distance = this.levenshteinDistance(target.toLowerCase(), template.toLowerCase()); - return distance <= 2 && distance > 0; - }) - .slice(0, 3); - } - - private levenshteinDistance(str1: string, str2: string): number { - const matrix: number[][] = []; - - for (let i = 0; i <= str2.length; i++) { - matrix[i] = [i]; - } - - for (let j = 0; j <= str1.length; j++) { - matrix[0]![j] = j; - } - - for (let i = 1; i <= str2.length; i++) { - for (let j = 1; j <= str1.length; j++) { - if (str2.charAt(i - 1) === str1.charAt(j - 1)) { - matrix[i]![j] = matrix[i - 1]?.[j - 1]!; - } else { - matrix[i]![j] = Math.min( - matrix[i - 1]?.[j - 1]! + 1, - matrix[i]?.[j - 1]! + 1, - matrix[i - 1]?.[j]! + 1, - ); - } - } - } - - return matrix[str2.length]?.[str1.length]!; - } -} diff --git a/src/api/generators/base/error-handler.ts b/src/api/generators/base/error-handler.ts deleted file mode 100644 index 1eacafe1c..000000000 --- a/src/api/generators/base/error-handler.ts +++ /dev/null @@ -1,288 +0,0 @@ -/** - * Centralized error handling and reporting system - * - * This module provides a comprehensive error handling solution that: - * - Handles both generator-specific and unknown errors gracefully - * - Provides rich, context-aware error reporting - * - Supports multiple output formats (console, JSON, structured) - * - Includes batch error handling for multiple failures - * - Offers smart error recovery suggestions - */ - -import type { TypeSchema } from "@typeschema/types"; -import type { CodegenLogger } from "../../../utils/codegen-logger"; -import { BatchOperationError, GeneratorError } from "./errors"; - -export interface ErrorHandlerOptions { - logger: CodegenLogger; - verbose?: boolean; - beginnerMode?: boolean; - outputFormat?: "this.options.logger" | "json" | "structured"; -} - -/** - * Centralized error handler with smart reporting - */ -export class ErrorHandler { - constructor(private options: ErrorHandlerOptions) {} - - /** - * Handle a single error with appropriate reporting - */ - handleError(error: Error, context?: { schema?: TypeSchema; filename?: string }): void { - if (error instanceof GeneratorError) { - this.handleGeneratorError(error, context); - } else { - this.handleUnknownError(error, context); - } - } - - /** - * Handle multiple errors in batch - */ - handleBatchErrors(errors: Error[]): void { - const generatorErrors = errors.filter((e) => e instanceof GeneratorError) as GeneratorError[]; - const unknownErrors = errors.filter((e) => !(e instanceof GeneratorError)); - - if (generatorErrors.length > 0) { - this.reportBatchErrors(generatorErrors); - } - - unknownErrors.forEach((error) => { - this.handleUnknownError(error); - }); - } - - /** - * Handle generator-specific errors with rich context - */ - private handleGeneratorError(error: GeneratorError, _context?: { schema?: TypeSchema; filename?: string }): void { - switch (this.options.outputFormat) { - case "json": - this.reportErrorAsJson(error); - break; - case "structured": - this.reportErrorStructured(error); - break; - default: - this.reportErrorToConsole(error); - } - } - - /** - * Handle unknown errors gracefully - */ - private handleUnknownError(error: Error, context?: { schema?: TypeSchema; filename?: string }): void { - this.options.logger.error("Unexpected error occurred:", error); - - if (this.options.verbose) { - this.options.logger.error("\n🚨 Unexpected Error Details:"); - this.options.logger.error(` Type: ${error.constructor.name}`); - this.options.logger.error(` Message: ${error.message}`); - if (error.stack) { - this.options.logger.error(` Stack: ${error.stack}`); - } - if (context?.schema) { - this.options.logger.error(` Schema: ${context.schema.identifier.name}`); - } - if (context?.filename) { - this.options.logger.error(` File: ${context.filename}`); - } - } - - this.options.logger.error("\n💡 General troubleshooting suggestions:"); - this.options.logger.error(" • Run with --verbose flag for more details"); - this.options.logger.error(" • Check your input files for corruption"); - this.options.logger.error(" • Update to the latest version of atomic-codegen"); - this.options.logger.error(" • Report this issue at: https://github.com/atomic-ehr/codegen/issues"); - } - - /** - * Report error to console with formatting - */ - private reportErrorToConsole(error: GeneratorError): void { - if ("getFormattedMessage" in error) { - this.options.logger.error((error as any).getFormattedMessage()); - } else { - this.options.logger.error(`\n❌ ${error.constructor.name}: ${error.message}`); - - const suggestions = error.getSuggestions(); - if (suggestions.length > 0) { - this.options.logger.error("\n💡 Suggestions:"); - suggestions.forEach((suggestion) => { - this.options.logger.error(` • ${suggestion}`); - }); - } - } - - if (this.options.verbose && error.context) { - this.options.logger.error("\n🔍 Debug Information:"); - this.options.logger.error(JSON.stringify(error.context, null, 2)); - } - } - - /** - * Report error as JSON for programmatic consumption - */ - private reportErrorAsJson(error: GeneratorError): void { - const errorData = { - type: error.constructor.name, - message: error.message, - phase: error.phase, - context: error.context, - suggestions: error.getSuggestions(), - timestamp: new Date().toISOString(), - }; - - this.options.logger.error(JSON.stringify(errorData, null, 2)); - } - - /** - * Report error in structured format - */ - private reportErrorStructured(error: GeneratorError): void { - const structure = { - error: { - type: error.constructor.name, - message: error.message, - phase: error.phase, - }, - context: error.context, - suggestions: error.getSuggestions(), - actions: this.getRecoveryActions(error), - }; - - this.options.logger.error("---"); - this.options.logger.error("Error Report:"); - this.options.logger.error(JSON.stringify(structure, null, 2)); - this.options.logger.error("---"); - } - - /** - * Report multiple errors efficiently - */ - private reportBatchErrors(errors: GeneratorError[]): void { - this.options.logger.error(`\n❌ ${errors.length} errors occurred during generation:`); - - // Group errors by type - const errorGroups = new Map(); - errors.forEach((error) => { - const type = error.constructor.name; - if (!errorGroups.has(type)) { - errorGroups.set(type, []); - } - errorGroups.get(type)?.push(error); - }); - - // Report each group - for (const [type, groupErrors] of errorGroups) { - this.options.logger.error(`\n📋 ${type} (${groupErrors.length} occurrences):`); - - groupErrors.forEach((error, index) => { - this.options.logger.error(` ${index + 1}. ${error.message}`); - if (error.context?.schemaName) { - this.options.logger.error(` Schema: ${error.context.schemaName}`); - } - }); - - // Show common suggestions for this error type - const commonSuggestions = this.getCommonSuggestions(groupErrors); - if (commonSuggestions.length > 0) { - this.options.logger.error("\n 💡 Common suggestions:"); - commonSuggestions.forEach((suggestion) => { - this.options.logger.error(` • ${suggestion}`); - }); - } - } - } - - /** - * Get common suggestions across similar errors - */ - private getCommonSuggestions(errors: GeneratorError[]): string[] { - const allSuggestions = errors.flatMap((e) => e.getSuggestions()); - const suggestionCounts = new Map(); - - allSuggestions.forEach((suggestion) => { - suggestionCounts.set(suggestion, (suggestionCounts.get(suggestion) || 0) + 1); - }); - - // Return suggestions that appear in at least half the errors - const threshold = Math.ceil(errors.length / 2); - return Array.from(suggestionCounts.entries()) - .filter(([_, count]) => count >= threshold) - .map(([suggestion, _]) => suggestion) - .slice(0, 5); // Limit to 5 most common - } - - /** - * Get recovery actions for an error - */ - private getRecoveryActions(error: GeneratorError): Array<{ action: string; command?: string }> { - if ("getRecoveryActions" in error) { - return (error as any).getRecoveryActions(); - } - - return [ - { action: "Review error message and suggestions above" }, - { action: "Check input files and configuration" }, - { action: "Try with --verbose flag for more information" }, - ]; - } -} - -/** - * Error boundary for catching and handling all generator errors - */ -export class GeneratorErrorBoundary { - constructor(private errorHandler: ErrorHandler) {} - - /** - * Wrap an async operation with error boundary - */ - async withErrorBoundary( - operation: () => Promise, - context?: { - schema?: TypeSchema; - filename?: string; - operationName?: string; - }, - ): Promise { - try { - return await operation(); - } catch (error) { - this.errorHandler.handleError(error instanceof Error ? error : new Error(String(error)), context); - throw error; // Re-throw after handling - } - } - - /** - * Wrap a batch operation with error boundary - */ - async withBatchErrorBoundary( - operations: Array<() => Promise>, - _context?: { operationName?: string }, - ): Promise { - const results: T[] = []; - const errors: Error[] = []; - - for (const operation of operations) { - try { - const result = await operation(); - results.push(result); - } catch (error) { - errors.push(error instanceof Error ? error : new Error(String(error))); - } - } - - if (errors.length > 0) { - this.errorHandler.handleBatchErrors(errors); - throw new BatchOperationError( - `${errors.length} operations failed`, - errors.filter((e) => e instanceof GeneratorError) as GeneratorError[], - ); - } - - return results; - } -} diff --git a/src/api/generators/base/errors.ts b/src/api/generators/base/errors.ts deleted file mode 100644 index a0213f2b7..000000000 --- a/src/api/generators/base/errors.ts +++ /dev/null @@ -1,822 +0,0 @@ -/** - * Comprehensive error handling system for the base generator - * - * This module provides rich, contextual error classes that help developers - * at all skill levels understand and resolve issues quickly. - */ - -import type { TypeSchema } from "@typeschema/index"; -import type { FileContext } from "./types"; - -/** - * Base error class for all generator-related errors - * - * Provides common functionality like context tracking, suggestions, - * and detailed error reporting that makes debugging easier. - */ -export abstract class GeneratorError extends Error { - /** When this error occurred */ - public readonly timestamp: Date; - - /** Unique error ID for tracking */ - public readonly errorId: string; - - constructor( - message: string, - /** Phase of generation where error occurred */ - public readonly phase: "validation" | "generation" | "writing" | "initialization", - /** Additional context about the error */ - public readonly context?: Record, - ) { - super(message); - this.name = this.constructor.name; - this.timestamp = new Date(); - this.errorId = this.generateErrorId(); - - // Maintain proper stack trace in V8 - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } - } - - /** - * Generate a unique error ID for tracking - */ - private generateErrorId(): string { - return `${this.constructor.name}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - } - - /** - * Get formatted error message with full context - * This provides a comprehensive view of what went wrong - */ - getDetailedMessage(): string { - const lines = [ - `❌ ${this.constructor.name}: ${this.message}`, - ` Error ID: ${this.errorId}`, - ` Phase: ${this.phase}`, - ` Time: ${this.timestamp.toISOString()}`, - ]; - - if (this.context && Object.keys(this.context).length > 0) { - lines.push(""); - lines.push("📍 Context:"); - Object.entries(this.context).forEach(([key, value]) => { - lines.push(` ${key}: ${this.formatContextValue(value)}`); - }); - } - - return lines.join("\n"); - } - - /** - * Format context values for display - */ - private formatContextValue(value: unknown): string { - if (value === null || value === undefined) { - return String(value); - } - - if (typeof value === "string") { - return `"${value}"`; - } - - if (typeof value === "object") { - try { - return JSON.stringify(value, null, 2); - } catch { - return "[Object]"; - } - } - - return String(value); - } - - /** - * Get actionable suggestions for fixing the error - * Each error type should provide specific, helpful suggestions - */ - abstract getSuggestions(): string[]; - - /** - * Get error severity level - */ - getSeverity(): "error" | "warning" | "info" { - return "error"; - } - - /** - * Check if this error is recoverable - */ - isRecoverable(): boolean { - return false; - } - - /** - * Get related documentation links - */ - getDocumentationLinks(): string[] { - return [ - "https://github.com/atomic-ehr/codegen/docs/troubleshooting.md", - `https://github.com/atomic-ehr/codegen/docs/errors/${this.constructor.name}.md`, - ]; - } -} - -/** - * Schema validation errors with intelligent suggestions - */ -export class SchemaValidationError extends GeneratorError { - constructor( - message: string, - /** The schema that failed validation */ - public readonly schema: TypeSchema, - /** Specific validation errors */ - public readonly validationErrors: string[], - /** Additional user context for better suggestions */ - public readonly userContext?: { - isBeginnerMode?: boolean; - previousSuccessfulSchemas?: string[]; - commonPatterns?: string[]; - }, - ) { - super(message, "validation", { - schemaName: schema.identifier?.name || "unknown", - schemaKind: schema.identifier?.kind || "unknown", - schemaPackage: schema.identifier?.package || "unknown", - validationErrors, - userContext, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - - // Add basic validation suggestions - suggestions.push("Verify the schema follows the TypeSchema specification"); - suggestions.push("Check that all required fields are present and properly typed"); - - // Context-aware suggestions based on specific errors - for (const error of this.validationErrors) { - if (error.includes("identifier.name")) { - suggestions.push("✨ Add a valid identifier.name field to your schema"); - suggestions.push('💡 Example: identifier: { name: "Patient", kind: "resource" }'); - } - - if (error.includes("identifier.kind")) { - suggestions.push( - '🎯 Set identifier.kind to: "resource", "complex-type", "profile", or "primitive-type"', - ); - suggestions.push("📚 Check FHIR specification for the correct kind value"); - } - - if (error.includes("circular")) { - suggestions.push("🔄 Remove circular references between schemas"); - suggestions.push("💡 Use forward declarations for recursive types"); - suggestions.push("🔍 Look for schemas that reference each other in a loop"); - } - - if (error.includes("fields")) { - suggestions.push("📝 Check that all fields have proper type definitions"); - suggestions.push("🔧 Ensure field types reference valid TypeSchema identifiers"); - } - } - - // Beginner-specific suggestions - if (this.userContext?.isBeginnerMode) { - suggestions.push(""); - suggestions.push("🎓 Beginner Tips:"); - suggestions.push("📖 Start with the Quick Start guide: docs/getting-started/quick-start.md"); - suggestions.push("🔍 Use --verbose flag for detailed error information"); - suggestions.push("🧪 Test with a simple schema first to verify your setup"); - - if (this.userContext.previousSuccessfulSchemas?.length) { - suggestions.push( - `✅ Compare with your working schema: ${this.userContext.previousSuccessfulSchemas[0]}`, - ); - } - } - - // Advanced suggestions for experienced users - if (!this.userContext?.isBeginnerMode) { - suggestions.push(""); - suggestions.push("🔧 Advanced Debugging:"); - suggestions.push("🕵️ Enable schema validation debugging"); - suggestions.push("📊 Check schema statistics and complexity metrics"); - suggestions.push("⚡ Consider schema preprocessing if dealing with complex inheritance"); - } - - return suggestions; - } - - override isRecoverable(): boolean { - // Simple validation errors like missing fields are usually recoverable - return this.validationErrors.every((error) => !error.includes("circular") && !error.includes("corruption")); - } -} - -/** - * Template processing errors with debugging information - */ -export class TemplateError extends GeneratorError { - constructor( - message: string, - /** Name of the template that failed */ - public readonly templateName: string, - /** Context data passed to the template */ - public readonly templateContext: Record, - /** Additional debugging information */ - public readonly debugInfo?: { - availableTemplates?: string[]; - missingVariables?: string[]; - templateSource?: string; - lineNumber?: number; - columnNumber?: number; - }, - ) { - super(message, "generation", { - templateName, - contextKeys: Object.keys(templateContext), - availableTemplates: debugInfo?.availableTemplates?.length || 0, - debugInfo, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - - // Template existence suggestions - if (this.debugInfo?.availableTemplates?.length) { - if (!this.debugInfo.availableTemplates.includes(this.templateName)) { - suggestions.push(`❌ Template '${this.templateName}' not found`); - suggestions.push("📂 Available templates:"); - this.debugInfo.availableTemplates.forEach((template) => { - suggestions.push(` • ${template}`); - }); - - // Suggest similar template names using Levenshtein distance - const similar = this.findSimilarTemplates(this.templateName, this.debugInfo.availableTemplates); - if (similar.length > 0) { - suggestions.push("🤔 Did you mean:"); - similar.forEach((template) => { - suggestions.push(` • ${template}`); - }); - } - } - } - - // Missing variables suggestions - if (this.debugInfo?.missingVariables?.length) { - suggestions.push("📝 Missing template variables:"); - this.debugInfo.missingVariables.forEach((variable) => { - suggestions.push(` • ${variable}`); - }); - suggestions.push("💡 Add these variables to your template context"); - - // Suggest similar variable names from context - const contextKeys = Object.keys(this.templateContext); - this.debugInfo.missingVariables.forEach((missing) => { - const similar = contextKeys.filter( - (key) => this.levenshteinDistance(missing.toLowerCase(), key.toLowerCase()) <= 2, - ); - if (similar.length > 0) { - suggestions.push(` Similar to: ${similar.join(", ")}`); - } - }); - } - - // Template syntax suggestions - if (this.debugInfo?.lineNumber) { - suggestions.push(`🐛 Check template syntax around line ${this.debugInfo.lineNumber}`); - - if (this.debugInfo.columnNumber) { - suggestions.push(` Column: ${this.debugInfo.columnNumber}`); - } - - if (this.debugInfo.templateSource) { - const lines = this.debugInfo.templateSource.split("\n"); - const errorLine = lines[this.debugInfo.lineNumber - 1]; - if (errorLine) { - suggestions.push(` Code: ${errorLine.trim()}`); - } - } - } - - // General template debugging - suggestions.push("🔧 Template debugging steps:"); - suggestions.push(" • Enable template debugging: { debug: true }"); - suggestions.push(" • Verify template file exists and has correct syntax"); - suggestions.push(" • Check template variable names match context keys"); - suggestions.push(" • Ensure template engine is properly configured"); - - return suggestions; - } - - /** - * Find templates with similar names using Levenshtein distance - */ - private findSimilarTemplates(target: string, available: string[]): string[] { - return available - .filter((template) => { - const distance = this.levenshteinDistance(target.toLowerCase(), template.toLowerCase()); - return distance <= 2 && distance > 0; - }) - .slice(0, 3) - .sort( - (a, b) => - this.levenshteinDistance(target.toLowerCase(), a.toLowerCase()) - - this.levenshteinDistance(target.toLowerCase(), b.toLowerCase()), - ); - } - - /** - * Calculate Levenshtein distance between two strings - */ - private levenshteinDistance(str1: string, str2: string): number { - const matrix = Array(str2.length + 1) - .fill(null) - .map(() => Array(str1.length + 1).fill(0)); - - for (let i = 0; i <= str1.length; i++) matrix[0]![i] = i; - for (let j = 0; j <= str2.length; j++) matrix[j]![0] = j; - - for (let j = 1; j <= str2.length; j++) { - for (let i = 1; i <= str1.length; i++) { - const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; - matrix[j]![i] = Math.min( - matrix[j]?.[i - 1]! + 1, // deletion - matrix[j - 1]?.[i]! + 1, // insertion - matrix[j - 1]?.[i - 1]! + indicator, // substitution - ); - } - } - - return matrix[str2.length]?.[str1.length]!; - } - - override isRecoverable(): boolean { - // Template errors are usually recoverable by fixing template or context - return true; - } -} - -/** - * File operation errors with recovery suggestions - */ -export class FileOperationError extends GeneratorError { - constructor( - message: string, - /** Type of file operation that failed */ - public readonly operation: "create" | "write" | "read" | "delete" | "copy" | "move", - /** Path of the file that caused the error */ - public readonly filePath: string, - /** Original system error if available */ - public readonly originalError?: Error, - /** Recovery options and suggestions */ - public readonly recoveryOptions?: { - canRetry?: boolean; - alternativePaths?: string[]; - permissionFix?: string; - diskSpaceRequired?: number; - }, - ) { - super(message, "writing", { - operation, - filePath, - originalErrorMessage: originalError?.message, - originalErrorCode: (originalError as NodeJS.ErrnoException)?.code, - recoveryOptions, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - const errorCode = (this.originalError as NodeJS.ErrnoException)?.code; - - // Operation-specific suggestions - switch (this.operation) { - case "create": - case "write": - suggestions.push("📁 File writing troubleshooting:"); - suggestions.push(" • Check if the output directory exists"); - suggestions.push(" • Verify write permissions for the target directory"); - suggestions.push(" • Ensure no other process has the file locked"); - - if (this.filePath.includes(" ")) { - suggestions.push(" • File path contains spaces - check path escaping"); - } - - if (this.recoveryOptions?.alternativePaths?.length) { - suggestions.push("🔄 Alternative output directories:"); - this.recoveryOptions.alternativePaths.forEach((path) => { - suggestions.push(` • ${path}`); - }); - } - break; - - case "read": - suggestions.push("📖 File reading troubleshooting:"); - suggestions.push(" • Verify the file exists at the specified path"); - suggestions.push(" • Check file read permissions"); - suggestions.push(" • Ensure file is not corrupted"); - break; - - case "delete": - suggestions.push("🗑️ File deletion troubleshooting:"); - suggestions.push(" • Check if file is locked by another process"); - suggestions.push(" • Verify delete permissions for the directory"); - suggestions.push(" • Ensure file exists before attempting deletion"); - break; - } - - // Error code specific suggestions - switch (errorCode) { - case "EACCES": - suggestions.push("🔐 Permission Error:"); - suggestions.push(" • Current user lacks necessary file permissions"); - - if (this.recoveryOptions?.permissionFix) { - suggestions.push(` • Fix command: ${this.recoveryOptions.permissionFix}`); - } else { - suggestions.push(` • Try: chmod 755 "${this.filePath}"`); - } - - suggestions.push(" • Consider running with elevated permissions"); - suggestions.push(" • Check directory ownership and permissions"); - break; - - case "ENOSPC": - suggestions.push("💾 Disk Space Error:"); - suggestions.push(" • Insufficient disk space available"); - suggestions.push(" • Free up disk space and retry"); - suggestions.push(" • Consider using a different output directory"); - - if (this.recoveryOptions?.diskSpaceRequired) { - const mb = Math.ceil(this.recoveryOptions.diskSpaceRequired / 1024 / 1024); - suggestions.push(` • Required space: ~${mb}MB`); - } - break; - - case "ENOENT": - suggestions.push("📂 File Not Found:"); - suggestions.push(" • File or directory does not exist"); - suggestions.push(" • Check the file path for typos"); - suggestions.push(" • Ensure parent directories exist"); - suggestions.push(` • Create directory: mkdir -p "$(dirname "${this.filePath}")"`); - break; - - case "EMFILE": - case "ENFILE": - suggestions.push("📊 Too Many Open Files:"); - suggestions.push(" • System has reached file handle limit"); - suggestions.push(" • Close unused files and retry"); - suggestions.push(" • Consider processing files in smaller batches"); - break; - } - - // Recovery suggestions - if (this.recoveryOptions?.canRetry) { - suggestions.push(""); - suggestions.push("🔄 Recovery Options:"); - suggestions.push(" • This operation can be retried safely"); - suggestions.push(" • Fix the underlying issue and run again"); - } - - // General debugging - suggestions.push(""); - suggestions.push("🔍 General Debugging:"); - suggestions.push(" • Check system logs for more details"); - suggestions.push(" • Verify disk health if errors persist"); - suggestions.push(" • Test with a simpler file path"); - - return suggestions; - } - - override isRecoverable(): boolean { - return this.recoveryOptions?.canRetry || false; - } - - /** - * Get specific recovery actions that can be taken - */ - getRecoveryActions(): Array<{ - action: string; - command?: string; - automatic?: boolean; - riskLevel?: "low" | "medium" | "high"; - }> { - const actions: Array<{ - action: string; - command?: string; - automatic?: boolean; - riskLevel?: "low" | "medium" | "high"; - }> = []; - - const errorCode = (this.originalError as NodeJS.ErrnoException)?.code; - - switch (errorCode) { - case "EACCES": - actions.push({ - action: "Fix file permissions", - command: `chmod 755 "${this.filePath}"`, - automatic: false, - riskLevel: "medium", - }); - break; - - case "ENOENT": - actions.push({ - action: "Create missing directory", - command: `mkdir -p "$(dirname "${this.filePath}")"`, - automatic: true, - riskLevel: "low", - }); - break; - - case "ENOSPC": - actions.push({ - action: "Free up disk space", - automatic: false, - riskLevel: "low", - }); - break; - } - - return actions; - } -} - -/** - * Type mapping errors for language-specific type conversion issues - */ -export class TypeMappingError extends GeneratorError { - constructor( - message: string, - /** FHIR type that couldn't be mapped */ - public readonly fhirType: string, - /** Target language name */ - public readonly targetLanguage: string, - /** Additional mapping context */ - public readonly mappingContext?: { - availableMappings?: string[]; - suggestedMappings?: Record; - schema?: TypeSchema; - }, - ) { - super(message, "generation", { - fhirType, - targetLanguage, - availableMappings: mappingContext?.availableMappings?.length || 0, - hasSchema: !!mappingContext?.schema, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - - suggestions.push(`🎯 Type mapping issue for '${this.fhirType}' → ${this.targetLanguage}`); - - // Check for available mappings - if (this.mappingContext?.availableMappings?.length) { - suggestions.push("📋 Available type mappings:"); - this.mappingContext.availableMappings.forEach((mapping) => { - suggestions.push(` • ${mapping}`); - }); - - // Suggest similar types - const similar = this.mappingContext.availableMappings.filter( - (mapping) => - mapping.toLowerCase().includes(this.fhirType.toLowerCase()) || - this.fhirType.toLowerCase().includes(mapping.toLowerCase()), - ); - - if (similar.length > 0) { - suggestions.push("🤔 Similar types found:"); - similar.forEach((mapping) => { - suggestions.push(` • ${mapping}`); - }); - } - } - - // Suggested mappings - if (this.mappingContext?.suggestedMappings) { - suggestions.push("💡 Suggested mappings:"); - Object.entries(this.mappingContext.suggestedMappings).forEach(([fhir, target]) => { - suggestions.push(` • ${fhir} → ${target}`); - }); - } - - // General type mapping suggestions - suggestions.push(""); - suggestions.push("🔧 Fixing type mapping issues:"); - suggestions.push(` • Add '${this.fhirType}' mapping in ${this.targetLanguage}TypeMapper`); - suggestions.push(" • Check if the FHIR type name is spelled correctly"); - suggestions.push(" • Verify the type mapper is properly configured"); - suggestions.push(" • Consider adding a fallback type mapping"); - - // Language-specific suggestions - switch (this.targetLanguage.toLowerCase()) { - case "typescript": - suggestions.push(" • Add to TYPESCRIPT_PRIMITIVES map"); - suggestions.push(" • Implement in mapPrimitive() method"); - break; - case "python": - suggestions.push(" • Add to PYTHON_TYPE_MAP dictionary"); - suggestions.push(" • Consider using typing module types"); - break; - case "rust": - suggestions.push(" • Add to RUST_TYPE_MAP"); - suggestions.push(" • Consider Option for nullable types"); - break; - } - - return suggestions; - } - - override isRecoverable(): boolean { - return true; // Type mapping errors can usually be fixed by updating the mapper - } -} - -/** - * Configuration errors with validation details - */ -export class ConfigurationError extends GeneratorError { - constructor( - message: string, - /** Configuration key that has an issue */ - public readonly configKey: string, - /** The invalid value that was provided */ - public readonly providedValue: unknown, - /** Expected value type or format */ - public readonly expectedValue?: string, - /** Valid options if applicable */ - public readonly validOptions?: unknown[], - ) { - super(message, "initialization", { - configKey, - providedValue, - providedType: typeof providedValue, - expectedValue, - validOptions, - }); - } - - getSuggestions(): string[] { - const suggestions: string[] = []; - - suggestions.push(`⚙️ Configuration error for '${this.configKey}'`); - suggestions.push(` Provided: ${JSON.stringify(this.providedValue)} (${typeof this.providedValue})`); - - if (this.expectedValue) { - suggestions.push(` Expected: ${this.expectedValue}`); - } - - if (this.validOptions?.length) { - suggestions.push(" Valid options:"); - this.validOptions.forEach((option) => { - suggestions.push(` • ${JSON.stringify(option)}`); - }); - } - - suggestions.push(""); - suggestions.push("🔧 Configuration fixes:"); - suggestions.push(` • Check the '${this.configKey}' value in your configuration`); - suggestions.push(" • Refer to the configuration documentation"); - suggestions.push(" • Use TypeScript for better config validation"); - suggestions.push(" • Check for typos in configuration keys"); - - // Specific suggestions based on config key - switch (this.configKey) { - case "outputDir": - suggestions.push(" • Ensure the output directory path exists"); - suggestions.push(" • Use absolute paths for better reliability"); - break; - case "logger": - suggestions.push(" • Provide a valid logger instance"); - suggestions.push(" • Use createLogger() from utils/codegen-logger"); - break; - case "validation": - suggestions.push(' • Set to true, false, or "strict"'); - break; - } - - return suggestions; - } - - override isRecoverable(): boolean { - return true; // Configuration errors are usually fixable - } -} - -/** - * Batch operation error for multiple failures - */ -export class BatchOperationError extends GeneratorError { - constructor( - message: string, - /** Individual errors that occurred */ - public readonly errors: GeneratorError[], - ) { - super(message, "generation", { - errorCount: errors.length, - errorTypes: [...new Set(errors.map((e) => e.constructor.name))], - phases: [...new Set(errors.map((e) => e.phase))], - }); - } - - getSuggestions(): string[] { - // Aggregate unique suggestions from all errors - const allSuggestions = this.errors.flatMap((e) => e.getSuggestions()); - const uniqueSuggestions = [...new Set(allSuggestions)]; - - const suggestions: string[] = []; - suggestions.push(`📊 Batch operation failed with ${this.errors.length} errors:`); - - // Group errors by type - const errorGroups = new Map(); - this.errors.forEach((error) => { - const type = error.constructor.name; - if (!errorGroups.has(type)) { - errorGroups.set(type, []); - } - errorGroups.get(type)?.push(error); - }); - - // Show error breakdown - suggestions.push(""); - suggestions.push("🔍 Error breakdown:"); - for (const [type, typeErrors] of errorGroups) { - suggestions.push(` • ${type}: ${typeErrors.length} occurrences`); - } - - // Show most common suggestions with proper prefixing - suggestions.push("💡 Most relevant suggestions:"); - const prefixedSuggestions = uniqueSuggestions.slice(0, 8).map((suggestion) => ` ${suggestion}`); - suggestions.push(...prefixedSuggestions); - - return suggestions; - } - - /** - * Get detailed breakdown of all errors - */ - getErrorBreakdown(): string { - const lines: string[] = []; - - this.errors.forEach((error, index) => { - lines.push(`${index + 1}. ${error.constructor.name}: ${error.message}`); - if (error.context?.schemaName) { - lines.push(` Schema: ${error.context.schemaName}`); - } - if (error.context?.filename) { - lines.push(` File: ${error.context.filename}`); - } - lines.push(""); - }); - - return lines.join("\n"); - } - - override isRecoverable(): boolean { - // Batch is recoverable if at least some individual errors are recoverable - return this.errors.some((error) => error.isRecoverable()); - } - - /** - * Get errors that are recoverable - */ - getRecoverableErrors(): GeneratorError[] { - return this.errors.filter((error) => error.isRecoverable()); - } - - /** - * Get errors that are not recoverable - */ - getNonRecoverableErrors(): GeneratorError[] { - return this.errors.filter((error) => !error.isRecoverable()); - } -} - -/** - * Utility function to create context-aware errors - * Helps maintain consistent error creation patterns - */ -export function createErrorWithContext( - ErrorClass: new (...args: any[]) => T, - message: string, - context: FileContext, - additionalContext?: Record, -): T { - const fullContext = { - filename: context.filename, - importsCount: context.imports.size, - exportsCount: context.exports.size, - hasSchema: !!context.schema, - templateName: context.templateName, - ...additionalContext, - }; - - // Create error with the enhanced context - need to pass phase parameter - return new ErrorClass(message, "generation", fullContext); -} diff --git a/src/api/generators/base/index.ts b/src/api/generators/base/index.ts deleted file mode 100644 index 20520b02a..000000000 --- a/src/api/generators/base/index.ts +++ /dev/null @@ -1,274 +0,0 @@ -/** - * Base Generator Public API - * - * This is the main entry point for the base generator system. - * Import from here to access all base generator functionality with - * clean, well-organized exports. - */ - -// ========================================== -// Core Base Generator System -// ========================================== - -// Main base generator class -export { BaseGenerator } from "./BaseGenerator"; -export { PythonTypeMapper } from "./PythonTypeMapper"; -export type { TypeMapperOptions } from "./TypeMapper"; -// Type system and mapping -export { TypeMapper } from "./TypeMapper"; -export type { TypeScriptTypeMapperOptions } from "./TypeScriptTypeMapper"; -export { TypeScriptTypeMapper } from "./TypeScriptTypeMapper"; -export type { - ConfigValidationResult, - GeneratorCapabilities, - LanguageType, - TemplateContext, -} from "./types"; - -// ========================================== -// Configuration and Options -// ========================================== - -export type { BaseGeneratorOptions, FileBuilderOptions } from "./types"; - -// ========================================== -// File Management System -// ========================================== - -export type { DirectoryBuilderConfig } from "./builders/DirectoryBuilder"; -export { DirectoryBuilder } from "./builders/DirectoryBuilder"; -export type { FileBuilderConfig } from "./builders/FileBuilder"; -// Fluent builders -export { FileBuilder } from "./builders/FileBuilder"; -export type { IndexBuilderConfig } from "./builders/IndexBuilder"; -export { IndexBuilder } from "./builders/IndexBuilder"; -export type { FileManagerOptions, WriteFileResult } from "./FileManager"; -// Core file management -export { FileManager } from "./FileManager"; - -// ========================================== -// Error Handling System -// ========================================== - -// All error classes for comprehensive error handling -export { - BatchOperationError, - ConfigurationError, - createErrorWithContext, - FileOperationError, - GeneratorError, - SchemaValidationError, - TemplateError, - TypeMappingError, -} from "./errors"; - -// ========================================== -// Core Types and Interfaces -// ========================================== - -// Generated file types -// Progress monitoring -// Lifecycle hooks for customization -// Batch operations -export type { - AfterSaveHook, - BatchResult, - BeforeSaveHook, - ErrorHook, - FileContext, - FileStats, - GeneratedFile, - ProgressCallback, -} from "./types"; - -// ========================================== -// External Dependencies (re-exported for convenience) -// ========================================== - -// TypeSchema types (commonly needed by generator implementations) -export type { Identifier, TypeSchema } from "@typeschema/index"; - -// Logger interface -export type { CodegenLogger } from "../../../utils/codegen-logger"; -// ========================================== -// Utility Types for Generator Development -// ========================================== - -/** - * Helper type for creating generator options with language-specific extensions - * - * @example - * ```typescript - * interface TypeScriptOptions extends GeneratorOptions<{ - * moduleFormat: 'esm' | 'cjs'; - * generateIndex: boolean; - * }> {} - * ``` - */ -export type GeneratorOptions> = import("./types").BaseGeneratorOptions & - TExtensions; - -/** - * Helper type for generator result arrays - * Useful for typing the return value of generate() methods - */ -export type GeneratorResult = import("./types").GeneratedFile[]; - -/** - * Helper type for async generator functions - */ -export type AsyncGenerator< - TOptions extends import("./types").BaseGeneratorOptions, - TResult extends import("./types").GeneratedFile[], -> = (options: TOptions) => Promise; - -/** - * Type guard to check if an object is a GeneratedFile - */ -export function isGeneratedFile(obj: unknown): obj is import("./types").GeneratedFile { - return ( - typeof obj === "object" && - obj !== null && - "path" in obj && - "filename" in obj && - "content" in obj && - "exports" in obj && - "size" in obj && - "timestamp" in obj - ); -} - -/** - * Type guard to check if an error is a GeneratorError - */ -export function isGeneratorError(error: unknown): error is import("./errors").GeneratorError { - const { GeneratorError } = require("./errors"); - return error instanceof GeneratorError; -} - -// ========================================== -// Version Information -// ========================================== - -/** - * Base generator system version - * Updated automatically during build process - */ -export const VERSION = "1.0.0"; -/** - * Supported TypeSchema version - */ -export const SUPPORTED_TYPESCHEMA_VERSION = "1.0.0"; -// ========================================== -// Development Utilities -// ========================================== - -/** - * Create a development logger for testing - * @param prefix - Logger prefix - * @param verbose - Enable verbose logging - */ -export function createDevLogger(prefix: string = "Dev", verbose: boolean = true) { - const { createLogger } = require("../../../utils/codegen-logger"); - return createLogger({ prefix, verbose }); -} -/** - * Validate generator options before instantiation - * @param options - Options to validate - * @returns Validation result with errors and suggestions - */ -export function validateGeneratorOptions( - options: import("./types").BaseGeneratorOptions, -): import("./types").ConfigValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - const suggestions: string[] = []; - - // Required field validation - if (!options.outputDir) { - errors.push("outputDir is required"); - suggestions.push("Provide a valid output directory path"); - } - - // Type validation - if (options.outputDir && typeof options.outputDir !== "string") { - errors.push("outputDir must be a string"); - } - - if (options.overwrite !== undefined && typeof options.overwrite !== "boolean") { - errors.push("overwrite must be a boolean"); - } - - if (options.validate !== undefined && typeof options.validate !== "boolean") { - errors.push("validate must be a boolean"); - } - - // Path validation (only if outputDir is a valid string) - if (options.outputDir && typeof options.outputDir === "string") { - const path = require("node:path"); - if (!path.isAbsolute(options.outputDir)) { - warnings.push("Using relative path for outputDir - consider using absolute path"); - suggestions.push("Use path.resolve() to convert to absolute path"); - } - } - - // Performance warnings - if (options.validate === false) { - warnings.push("Validation is disabled - this may lead to invalid generated code"); - suggestions.push("Consider enabling validation for better code quality"); - } - - return { - isValid: errors.length === 0, - errors, - warnings, - suggestions, - }; -} -// ========================================== -// Constants and Defaults -// ========================================== - -/** - * Default generator options - */ -export const DEFAULT_GENERATOR_OPTIONS: Partial = { - outputDir: "./generated", - overwrite: true, - validate: true, - verbose: false, - beginnerMode: false, - errorFormat: "console", -}; -/** - * Maximum recommended batch size for schema processing - */ -export const MAX_BATCH_SIZE = 50; -/** - * Default file builder options - */ -export const DEFAULT_FILE_BUILDER_OPTIONS: Partial = { - importStrategy: "auto", - validation: "strict", - prettify: true, - formatting: { - indentSize: 2, - useTabs: false, - maxLineLength: 100, - }, - encoding: "utf-8", -}; -// ========================================== -// Backwards Compatibility -// ========================================== - -/** - * @deprecated Use BaseGenerator instead - * Provided for backwards compatibility only - */ -export { BaseGenerator as Generator } from "./BaseGenerator"; -/** - * @deprecated Use GeneratorError instead - * Provided for backwards compatibility only - */ -export { GeneratorError as BaseError } from "./errors"; diff --git a/src/api/generators/base/types.ts b/src/api/generators/base/types.ts deleted file mode 100644 index dc992acfa..000000000 --- a/src/api/generators/base/types.ts +++ /dev/null @@ -1,544 +0,0 @@ -/** - * Core types and interfaces for the base generator system - * - * This module provides the foundational type definitions that all generators - * build upon, ensuring consistency and type safety across the system. - */ - -import type { Identifier, TypeSchema } from "@typeschema/index"; -import type { CodegenLogger } from "../../../utils/codegen-logger"; - -/** - * Base configuration options that all generators must support - * These options provide the minimum required configuration for any generator - */ -export interface BaseGeneratorOptions { - /** Output directory where generated files will be written */ - outputDir: string; - - /** Logger instance for tracking generation progress and errors */ - logger?: CodegenLogger; - - /** Whether to overwrite existing files (default: true) */ - overwrite?: boolean; - - /** Whether to validate schemas and generated content (default: true) */ - validate?: boolean; - - /** Enable detailed logging for debugging (default: false) */ - verbose?: boolean; - - /** Enable beginner-friendly error messages and guidance (default: false) */ - beginnerMode?: boolean; - - /** Format for error output: console, json, or structured (default: 'console') */ - errorFormat?: "console" | "json" | "structured"; -} - -/** - * Language-specific type representation - * This interface standardizes how FHIR types are mapped to target languages - */ -export interface LanguageType { - /** The type string in the target language (e.g., "string", "number", "Patient") */ - name: string; - - /** Whether this is a primitive type that doesn't need imports */ - isPrimitive: boolean; - - /** Import path if this type needs to be imported from another module */ - importPath?: string; - - /** Generic parameters if this is a generic type (e.g., ["T", "K"] for Map) */ - generics?: string[]; - - /** Whether this type is nullable/optional in the target language */ - nullable?: boolean; - - /** Additional metadata specific to the target language */ - metadata?: Record; -} - -/** - * Generated file metadata and content - * Represents a single generated file with all its associated information - */ -export interface GeneratedFile { - /** Full file system path where the file was/will be written */ - path: string; - - /** Filename only (without directory path) */ - filename: string; - - /** The generated content of the file */ - content: string; - - /** List of symbols exported from this file */ - exports: string[]; - - /** File size in bytes */ - size: number; - - /** When this file was generated */ - timestamp: Date; - - /** Additional metadata about the generation process */ - metadata?: { - /** Time taken to generate this file in milliseconds */ - generationTime?: number; - - /** Number of schemas processed for this file */ - schemaCount?: number; - - /** Template used to generate this file */ - templateName?: string; - - /** Any warnings generated during processing */ - warnings?: string[]; - }; -} - -/** - * Template context provided to template engines - * Contains all the data needed to render templates for code generation - */ -export interface TemplateContext { - /** The schema being processed */ - schema: TypeSchema; - - /** Type mapper for the target language */ - typeMapper: TypeMapper; - - /** Current file being generated */ - filename: string; - - /** Target language name (e.g., "TypeScript", "Python") */ - language: string; - - /** Generation timestamp in ISO format */ - timestamp: string; - - /** Import map for the current file */ - imports?: Map; - - /** Export set for the current file */ - exports?: Set; - - /** Additional context data that templates can use */ - [key: string]: unknown; -} - -/** - * File builder context passed to lifecycle hooks - * Provides access to file generation state during the build process - */ -export interface FileContext { - /** Name of the file being generated */ - filename: string; - - /** Current file content */ - content: string; - - /** Map of imports (symbol name -> import path) */ - imports: Map; - - /** Set of exported symbols */ - exports: Set; - - /** Additional metadata about the file */ - metadata: Record; - - /** The schema that generated this file (if applicable) */ - schema?: TypeSchema; - - /** Template name used to generate this file (if applicable) */ - templateName?: string; -} - -/** - * Statistics about file operations - * Used for performance monitoring and optimization - */ -export interface FileStats { - /** File size in bytes */ - size: number; - - /** Time taken to generate content in milliseconds */ - generationTime: number; - - /** Time taken to write to disk in milliseconds */ - writeTime: number; - - /** Memory used during generation in bytes */ - memoryUsed?: number; - - /** Number of template renders performed */ - templateRenders?: number; -} - -/** - * Progress callback signature for monitoring generation - * Allows consumers to track progress of long-running operations - */ -export type ProgressCallback = ( - /** Current phase of generation */ - phase: "validation" | "generation" | "writing" | "complete", - - /** Current item number being processed */ - current: number, - - /** Total number of items to process */ - total: number, - - /** Optional message describing current operation */ - message?: string, - - /** Schema being processed (if applicable) */ - schema?: TypeSchema, -) => void; - -/** - * Lifecycle hook signatures for file operations - * These hooks allow customization of the file generation process - */ - -/** Hook called before saving a file - can modify content or abort save */ -export type BeforeSaveHook = (context: FileContext) => void | Promise; - -/** Hook called after successfully saving a file */ -export type AfterSaveHook = (filePath: string, stats: FileStats) => void | Promise; - -/** Hook called when an error occurs during file operations */ -export type ErrorHook = (error: Error, context: FileContext) => void | Promise; - -/** - * File builder configuration options - * Controls how files are generated and processed - */ -export interface FileBuilderOptions { - /** Template name to use for content generation */ - template?: string; - - /** Strategy for resolving import paths */ - importStrategy?: "auto" | "manual" | "none"; - - /** Level of content validation to perform */ - validation?: "strict" | "loose" | "none"; - - /** Enable pretty printing of generated content */ - prettify?: boolean; - - /** Custom formatting options */ - formatting?: { - /** Number of spaces for indentation (default: 2) */ - indentSize?: number; - - /** Use tabs instead of spaces (default: false) */ - useTabs?: boolean; - - /** Maximum line length before wrapping (default: 100) */ - maxLineLength?: number; - }; - - /** File encoding (default: 'utf-8') */ - encoding?: BufferEncoding; -} - -/** - * Abstract base class for type mapping between FHIR and target languages - * Each language-specific generator must implement this interface - */ -export abstract class TypeMapper { - /** - * Map a FHIR primitive type to the target language - * @param fhirType - FHIR primitive type name (e.g., "string", "integer") - * @returns Language-specific type representation - */ - abstract mapPrimitive(fhirType: string): LanguageType; - - /** - * Map a FHIR reference to the target language - * @param targets - Array of possible reference targets - * @returns Language-specific reference type - */ - abstract mapReference(targets: Identifier[]): LanguageType; - - /** - * Map an array type in the target language - * @param elementType - The element type name - * @returns Language-specific array type - */ - abstract mapArray(elementType: string): LanguageType; - - /** - * Map optional/nullable types - * @param type - The base type - * @param required - Whether the field is required - * @returns Language-specific optional type - */ - abstract mapOptional(type: string, required: boolean): LanguageType; - - /** - * Map enumerated values to the target language - * @param values - Array of possible values - * @param name - Optional name for the enum type - * @returns Language-specific enum type - */ - abstract mapEnum(values: string[], name?: string): LanguageType; - - /** - * Format a FHIR type name according to target language conventions - * @param name - FHIR type name - * @returns Formatted type name - */ - abstract formatTypeName(name: string): string; - - /** - * Format a field name according to target language conventions - * @param name - FHIR field name - * @returns Formatted field name - */ - abstract formatFieldName(name: string): string; - - /** - * Format a filename according to target language conventions - * @param name - Base filename - * @returns Formatted filename (without extension) - */ - abstract formatFileName(name: string): string; - - /** - * Map a complete TypeSchema identifier to target language type - * @param identifier - FHIR type identifier - * @returns Language-specific type representation - */ - abstract mapType(identifier: Identifier): LanguageType; - - /** - * Get import statement format for the target language - * @param symbols - Symbols to import - * @param from - Module to import from - * @returns Formatted import statement - */ - formatImport?(symbols: string[], from: string): string; - - /** - * Get export statement format for the target language - * @param symbols - Symbols to export - * @returns Formatted export statement - */ - formatExport?(symbols: string[]): string; -} - -/** - * Directory builder configuration - * Used for batch directory operations - */ -export interface DirectoryBuilderConfig { - /** Directory path relative to output directory */ - path: string; - - /** File manager instance for file operations */ - fileManager: any; // Will be properly typed in FileManager implementation - - /** Logger for directory operations */ - logger: CodegenLogger; - - /** Whether to clean directory before creating files */ - clean?: boolean; -} - -/** - * Index builder configuration - * Used for generating index/barrel files - */ -export interface IndexBuilderConfig { - /** Directory to create index for */ - directory: string; - - /** File manager instance */ - fileManager: any; - - /** Template engine for rendering index files */ - templateEngine: any; - - /** Logger instance */ - logger: CodegenLogger; - - /** Header to include in index file */ - header?: string; - - /** Footer to include in index file */ - footer?: string; -} - -/** - * Batch operation result - * Used for tracking results of operations on multiple items - */ -export interface BatchResult { - /** Successful results */ - successes: T[]; - - /** Failed operations with their errors */ - failures: Array<{ - /** The item that failed */ - item: unknown; - - /** The error that occurred */ - error: Error; - - /** Index of the failed item */ - index: number; - }>; - - /** Total number of items processed */ - total: number; - - /** Time taken for the entire batch operation */ - duration: number; -} - -/** - * Template engine interface - * Abstraction for different template engines (Handlebars, etc.) - */ -export interface TemplateEngine { - /** - * Render a template with the given context - * @param templateName - Name of the template to render - * @param context - Data to pass to the template - * @returns Rendered content - */ - render(templateName: string, context: Record): string; - - /** - * Register a template - * @param name - Template name - * @param template - Template content or compiled template - */ - registerTemplate(name: string, template: string | ((...args: any[]) => any)): void; - - /** - * Register a helper function - * @param name - Helper name - * @param helper - Helper function - */ - registerHelper(name: string, helper: (...args: any[]) => any): void; - - /** - * Check if a template exists - * @param name - Template name - * @returns True if template exists - */ - hasTemplate(name: string): boolean; - - /** - * Get list of available templates - * @returns Array of template names - */ - getAvailableTemplates(): string[]; -} - -/** - * File manager interface - * Abstraction for file system operations - */ -export interface FileManager { - /** - * Write a file with automatic directory creation - * @param relativePath - Path relative to output directory - * @param content - File content - * @param options - Write options - * @returns Write result with path and stats - */ - writeFile( - relativePath: string, - content: string, - options?: { encoding?: BufferEncoding; overwrite?: boolean }, - ): Promise<{ path: string; size: number; writeTime: number }>; - - /** - * Write multiple files in batch - * @param files - Map of path to content - * @returns Array of write results - */ - writeBatch(files: Map): Promise< - Array<{ - path: string; - size: number; - writeTime: number; - }> - >; - - /** - * Ensure directory exists - * @param dirPath - Directory path - */ - ensureDirectory(dirPath: string): Promise; - - /** - * Clean directory contents - * @param relativePath - Path relative to output directory - */ - cleanDirectory(relativePath?: string): Promise; - - /** - * Get relative import path between files - * @param fromFile - Source file - * @param toFile - Target file - * @returns Relative import path - */ - getRelativeImportPath(fromFile: string, toFile: string): string; -} - -/** - * Generator configuration validation result - * Used to validate generator options before initialization - */ -export interface ConfigValidationResult { - /** Whether the configuration is valid */ - isValid: boolean; - - /** Validation errors if any */ - errors: string[]; - - /** Non-fatal warnings */ - warnings: string[]; - - /** Suggested fixes for invalid configuration */ - suggestions: string[]; -} - -/** - * Generator capabilities interface - * Describes what a generator can do - used for introspection - */ -export interface GeneratorCapabilities { - /** Programming language this generator targets */ - language: string; - - /** File extensions this generator produces */ - fileExtensions: string[]; - - /** Whether this generator supports templates */ - supportsTemplates: boolean; - - /** Whether this generator supports custom type mapping */ - supportsCustomTypeMapping: boolean; - - /** Whether this generator supports incremental generation */ - supportsIncrementalGeneration: boolean; - - /** Whether this generator supports validation */ - supportsValidation: boolean; - - /** Supported schema kinds */ - supportedSchemaKinds: Array<"resource" | "complex-type" | "profile" | "primitive-type" | "logical">; - - /** Version of the generator */ - version: string; - - /** Additional metadata about capabilities */ - metadata?: Record; -} diff --git a/src/api/generators/types.ts b/src/api/generators/types.ts deleted file mode 100644 index 8e3d75cf7..000000000 --- a/src/api/generators/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Common types used in generated code - */ - -/** - * Result of validation operation - */ -export interface ValidationResult { - valid: boolean; - errors: string[]; -} - -/** - * Base type for FHIR resources - */ -export interface Resource { - resourceType: string; - id?: string; - meta?: Meta; -} - -/** - * FHIR Meta element - */ -export interface Meta { - versionId?: string; - lastUpdated?: string; - source?: string; - profile?: string[]; - security?: Coding[]; - tag?: Coding[]; -} - -/** - * FHIR Coding element - */ -export interface Coding { - system?: string; - version?: string; - code?: string; - display?: string; - userSelected?: boolean; -} - -/** - * Generic resource type mapping interface - * Maps FHIR resource type strings to their corresponding TypeScript interfaces - */ -export interface ResourceTypeMap { - [resourceType: string]: any; -} - -/** - * Type-safe resource type mapping generator - * Creates a mapping from resource type strings to interfaces for a given set of resource types - */ -export type CreateResourceTypeMap = { - [K in T[number]]: any; -}; diff --git a/src/api/generators/typescript.ts b/src/api/generators/typescript.ts deleted file mode 100644 index a2fa2b217..000000000 --- a/src/api/generators/typescript.ts +++ /dev/null @@ -1,1048 +0,0 @@ -/** - * Modern TypeScript Generator built on BaseGenerator - * - * This is the new, clean implementation that replaces the monolithic typescript.ts generator. - * Built using the BaseGenerator architecture with TypeMapper, TemplateEngine, and FileManager. - */ - -// DEPRICATED, USE: src/api/writer-generator/typescript.ts - -import type { BindingTypeSchema, TypeSchema } from "@typeschema/types"; -import { isBindingSchema } from "@typeschema/types"; -import { BaseGenerator, type GeneratorInput } from "./base/BaseGenerator"; -import { TypeScriptTypeMapper, type TypeScriptTypeMapperOptions } from "./base/TypeScriptTypeMapper"; -import type { BaseGeneratorOptions, GeneratedFile, TemplateContext, TypeMapper } from "./base/types"; - -/** - * TypeScript-specific generator options - */ -export interface TypeScriptGeneratorOptions extends BaseGeneratorOptions { - /** Module format for imports/exports */ - moduleFormat?: "esm" | "cjs"; - - /** Whether to generate index files */ - generateIndex?: boolean; - - /** Include JSDoc documentation */ - includeDocuments?: boolean; - - /** Naming convention for types */ - namingConvention?: "PascalCase" | "camelCase"; - - /** Include FHIR extensions */ - includeExtensions?: boolean; - - /** Include FHIR profiles */ - includeProfiles?: boolean; - - /** Generate value set files (default: false) */ - generateValueSets?: boolean; - - /** Include helper validation functions (default: false) */ - includeValueSetHelpers?: boolean; - - /** - * Which binding strengths to generate value sets for - * Only used when valueSetMode is 'custom' - * @default ['required'] - */ - valueSetStrengths?: ("required" | "preferred" | "extensible" | "example")[]; - - /** - * Directory name for value set files (relative to outputDir) - * @default 'valuesets' - */ - valueSetDirectory?: string; - - /** - * Value set generation mode - * - 'all': Generate for all binding strengths with enums - * - 'required-only': Generate only for required bindings (safe default) - * - 'custom': Use valueSetStrengths array to control - * @default 'required-only' - */ - valueSetMode?: "all" | "required-only" | "custom"; - - /** Type mapper options */ - typeMapperOptions?: TypeScriptTypeMapperOptions; -} - -/** - * Result of generating a single TypeScript file - */ -export interface GeneratedTypeScript { - content: string; - imports: Map; - exports: string[]; - filename: string; -} - -/** - * Modern TypeScript Generator - * - * Generates clean, type-safe TypeScript interfaces from FHIR TypeSchema documents. - * Uses the new BaseGenerator architecture for maintainability and extensibility. - */ -export class TypeScriptGenerator extends BaseGenerator { - private readonly resourceTypes = new Set(); - private collectedValueSets = new Map(); - - private get tsOptions(): Required { - return this.options as Required; - } - - protected getLanguageName(): string { - return "TypeScript"; - } - - protected getFileExtension(): string { - return ".ts"; - } - - protected override createTypeMapper(): TypeMapper { - const options = this.options as TypeScriptGeneratorOptions; - return new TypeScriptTypeMapper({ - namingConvention: (options.namingConvention ?? "PascalCase") === "PascalCase" ? "PascalCase" : "camelCase", - moduleFormat: options.moduleFormat === "cjs" ? "commonjs" : "esm", - preferUndefined: true, - ...options.typeMapperOptions, - }) as unknown as TypeMapper; - } - - protected async generateSchemaContent(schema: TypeSchema, _context: TemplateContext): Promise { - // Skip unsupported schema types - if (this.shouldSkipSchema(schema)) { - return ""; - } - - // Collect resource types for Reference generic - if (schema.identifier.kind === "resource") { - this.resourceTypes.add(this.typeMapper.formatTypeName(schema.identifier.name)); - } - - // Update filename for profiles to include proper directory structure - // if (false) { - // // Profile support removed - not in core schema - // const sanitizedPackage = this.sanitizePackageName( - // schema.identifier.package || "unknown", - // ); - // const profileFileName = this.typeMapper.formatFileName( - // schema.identifier.name, - // ); - // context.filename = `profiles/${sanitizedPackage}/${profileFileName}`; - - // // Track profile for index generation - // if (!this.profilesByPackage.has(schema.identifier.package || "unknown")) { - // this.profilesByPackage.set(schema.identifier.package || "unknown", []); - // } - // this.profilesByPackage.get(schema.identifier.package || "unknown")?.push({ - // filename: profileFileName, - // interfaceName: this.typeMapper.formatTypeName(schema.identifier.name), - // }); - // } - - // Handle Reference type specially - if (schema.identifier.name === "Reference") { - return this.generateReferenceInterface(schema); - } - - // Generate TypeScript content directly (no templates for simplicity) - const mainInterface = this.generateTypeScriptInterface(schema); - - // Generate nested types if present - let nestedInterfaces = ""; - if ("nested" in schema && schema.nested && Array.isArray(schema.nested)) { - const nestedInterfaceStrings = schema.nested.map((nestedType) => - this.generateNestedTypeInterface(schema.identifier.name, nestedType), - ); - nestedInterfaces = nestedInterfaceStrings.join("\n\n"); - } - - // Combine main interface with nested interfaces - if (nestedInterfaces) { - return `${mainInterface}\n\n${nestedInterfaces}`; - } - - return mainInterface; - } - protected override filterAndSortSchemas(schemas: TypeSchema[]): TypeSchema[] { - // Collect value sets from ALL schemas before filtering - this.collectedValueSets = this.collectValueSets(schemas); - - return schemas.filter((schema) => !this.shouldSkipSchema(schema)); - } - - protected async validateContent(content: string, context: TemplateContext): Promise { - const hasValidExport = /export\s+(interface|class|type|enum)\s+\w+/.test(content); - const hasValidSyntax = content.includes("{") && content.includes("}"); - - if (!hasValidExport) { - throw new Error( - `Generated content for ${context.schema.identifier.name} does not contain valid export statements`, - ); - } - - if (!hasValidSyntax) { - throw new Error( - `Generated content for ${context.schema.identifier.name} has invalid syntax (missing braces)`, - ); - } - } - - /** - * Transform multiple schemas into TypeScript - */ - async transformSchemas(schemas: TypeSchema[]): Promise { - const results: GeneratedTypeScript[] = []; - - for (const schema of schemas) { - const result = await this.transformSchema(schema); - if (result) { - results.push(result); - } - } - - return results; - } - - /** - * Transform a single schema into TypeScript - */ - async transformSchema(schema: TypeSchema): Promise { - if (this.shouldSkipSchema(schema)) { - return undefined; - } - - // Create template context - const context: TemplateContext = { - schema, - typeMapper: this.typeMapper, - filename: this.getFilenameForSchema(schema), - language: "TypeScript", - timestamp: new Date().toISOString(), - }; - - // Generate content using template engine - const content = await this.generateSchemaContent(schema, context); - - if (!content.trim()) { - return undefined; - } - - // Extract imports and exports from generated content - const imports = this.extractImportsFromContent(content, schema); - const exports = this.extractExportsFromContent(content, schema); - const filename = this.getFilenameForSchema(schema); - - return { - content, - imports, - exports: Array.from(exports), - filename, - }; - } - - /** - * Check if a binding schema should generate a value set file - */ - private shouldGenerateValueSet(schema: TypeSchema): boolean { - if (!isBindingSchema(schema) || !schema.enum || !Array.isArray(schema.enum) || schema.enum.length === 0) { - return false; - } - - // Handle different value set modes - const mode = (this.options as any).valueSetMode || "required-only"; - switch (mode) { - case "all": - return true; // Generate for all binding strengths - case "required-only": - return schema.strength === "required"; - case "custom": { - const strengths = (this.options as any).valueSetStrengths || ["required"]; - return strengths.includes(schema.strength as any); - } - default: - return schema.strength === "required"; - } - } - - /** - * Collect value sets from schemas that should generate value set files - */ - private collectValueSets(schemas: TypeSchema[]): Map { - const valueSets = new Map(); - - for (const schema of schemas) { - if (this.shouldGenerateValueSet(schema) && isBindingSchema(schema)) { - const name = this.typeMapper.formatTypeName(schema.identifier.name); - valueSets.set(name, schema); - } - } - - return valueSets; - } - - /** - * Check if a field binding should use a value set type - */ - private shouldUseValueSetType(binding: any): boolean { - if (!binding) { - return false; - } - - // If generateValueSets is false, never use value set types - if (!this.tsOptions.generateValueSets) { - return false; - } - - const valueSetTypeName = this.typeMapper.formatTypeName(binding.name); - return this.collectedValueSets.has(valueSetTypeName); - } - - /** - * Get the TypeScript type name for a binding - */ - private getValueSetTypeName(binding: any): string { - return this.typeMapper.formatTypeName(binding.name); - } - - /** - * Check if a field has enum values that should be inlined - */ - private shouldUseInlineEnum(field: any): boolean { - if (!field) { - return false; - } - - // Only use inline enums when generateValueSets is false - if (this.tsOptions.generateValueSets) { - return false; - } - - // Check if field has enum values directly on the field - return field.enum && Array.isArray(field.enum) && field.enum.length > 0; - } - - /** - * Generate inline enum type from field enum values - */ - private generateInlineEnumType(field: any): string { - if (!field.enum || !Array.isArray(field.enum)) { - return "string"; // fallback - } - - // Create union type from enum values - const enumValues = field.enum.map((value: string) => `'${value}'`).join(" | "); - return enumValues; - } - - private shouldSkipSchema(schema: TypeSchema): boolean { - if ( - schema.identifier.kind === "value-set" || - schema.identifier.kind === "binding" || - schema.identifier.kind === "primitive-type" - ) { - return true; - } - - // Profile support removed - not in core schema specification - - // Skip FHIR extensions when includeExtensions is false - if (!this.tsOptions.includeExtensions) { - // Check if this is a FHIR extension by looking at the URL pattern - const url = schema.identifier.url; - if (url?.includes("StructureDefinition/")) { - // Extensions typically have URLs like: - // http://hl7.org/fhir/StructureDefinition/extension-name - // http://hl7.org/fhir/StructureDefinition/resource-extension - - // Get the part after StructureDefinition/ - const structDefPart = url.split("StructureDefinition/")[1]; - if (structDefPart) { - // Check if it contains a hyphen (indicating extension pattern) - // FHIR extensions are profiles with hyphenated names - const hasHyphenPattern = structDefPart.includes("-"); - const isProfileKind = (schema.identifier as any).kind === "profile"; - - // Extensions are profiles with hyphenated StructureDefinition names - // But we need to exclude core resources that also have hyphens - if (hasHyphenPattern && isProfileKind) { - return true; - } - } - } - } - - return false; - } - - private getFilenameForSchema(schema: TypeSchema): string { - const baseName = this.typeMapper.formatFileName(schema.identifier.name); - return `${baseName}${this.getFileExtension()}`; - } - - private extractImportsFromContent(content: string, _schema: TypeSchema): Map { - const imports = new Map(); - const importRegex = /import\s+(?:type\s+)?{\s*([^}]+)\s*}\s+from\s+['"]([^'"]+)['"];?/g; - - let match; - while ((match = importRegex.exec(content)) !== null) { - const symbolsStr = match[1]; - const path = match[2]; - - if (!symbolsStr || !path) continue; - - const symbols = symbolsStr.split(",").map((s) => s.trim()); - for (const symbol of symbols) { - imports.set(symbol, path); - } - } - - return imports; - } - - private extractExportsFromContent(content: string, schema: TypeSchema): Set { - const exports = new Set(); - - const exportRegex = /export\s+(?:interface|class|enum|type)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g; - - let match; - while ((match = exportRegex.exec(content)) !== null) { - if (match[1]) exports.add(match[1]); - } - - exports.add(this.typeMapper.formatTypeName(schema.identifier.name)); - - return exports; - } - - /** - * Generate special Reference interface with generics - */ - private generateReferenceInterface(schema: TypeSchema): string { - const lines: string[] = []; - const imports = new Set(); - - if ("fields" in schema && schema.fields) { - for (const [, field] of Object.entries(schema.fields)) { - const importDeps = this.collectFieldImports(field); - importDeps.forEach((imp) => { - imports.add(imp); - }); - } - } - - lines.push("import type { ResourceType } from './utilities.js';"); - - if (imports.size > 0) { - const sortedImports = Array.from(imports).sort(); - for (const importName of sortedImports) { - lines.push(`import type { ${importName} } from './${importName}.js';`); - } - } - lines.push(""); // Add blank line after imports - - // Add JSDoc comment - if (this.tsOptions.includeDocuments && schema.description) { - lines.push("/**"); - lines.push(` * ${schema.description}`); - if (schema.identifier.url) { - lines.push(` * @see ${schema.identifier.url}`); - } - if (schema.identifier.package) { - lines.push(` * @package ${schema.identifier.package}`); - } - lines.push(" * @template T - The resource type being referenced"); - lines.push(" */"); - } - - // Generate generic interface declaration - lines.push("export interface Reference {"); - - if ("fields" in schema && schema.fields) { - for (const [fieldName, field] of Object.entries(schema.fields)) { - if (fieldName === "type") { - // Special handling for the type field to use the generic parameter - lines.push(" type?: T;"); - } else { - const fieldLines = this.generateFieldLines(fieldName, field); - for (const fieldLine of fieldLines) { - if (fieldLine) { - lines.push(` ${fieldLine}`); - } - } - } - } - } - - lines.push("}"); - return lines.join("\n"); - } - - /** - * Generate TypeScript interface directly without templates - */ - private generateTypeScriptInterface(schema: TypeSchema): string { - const lines: string[] = []; - const interfaceName = this.typeMapper.formatTypeName(schema.identifier.name); - const imports = new Set(); - const valueSetImports = new Set(); - - // Collect imports from fields - if ("fields" in schema && schema.fields) { - for (const [, field] of Object.entries(schema.fields)) { - const fieldImports = this.collectFieldImports(field); - - for (const imp of fieldImports) { - // Check if this is a value set import - if (this.collectedValueSets.has(imp)) { - valueSetImports.add(imp); - } else { - imports.add(imp); - } - } - } - } - - // Collect imports from nested types - if ("nested" in schema && schema.nested && Array.isArray(schema.nested)) { - for (const nestedType of schema.nested) { - if (nestedType.fields) { - for (const [, field] of Object.entries(nestedType.fields)) { - const fieldImports = this.collectFieldImports(field); - - for (const imp of fieldImports) { - // Check if this is a value set import - if (this.collectedValueSets.has(imp)) { - valueSetImports.add(imp); - } else { - imports.add(imp); - } - } - } - } - } - } - - // Generate regular type imports - if (imports.size > 0) { - const sortedImports = Array.from(imports).sort(); - for (const importName of sortedImports) { - lines.push(`import type { ${importName} } from './${importName}.js';`); - } - } - - // Generate value set imports - if (valueSetImports.size > 0) { - const sortedValueSetImports = Array.from(valueSetImports).sort(); - const importList = sortedValueSetImports.join(", "); - lines.push(`import type { ${importList} } from './valuesets/index.js';`); - } - - if (imports.size > 0 || valueSetImports.size > 0) { - lines.push(""); // Add blank line after imports - } - - // Add JSDoc comment if enabled - if (this.tsOptions.includeDocuments && schema.description) { - lines.push("/**"); - lines.push(` * ${schema.description}`); - if (schema.identifier.url) { - lines.push(` * @see ${schema.identifier.url}`); - } - if (schema.identifier.package) { - lines.push(` * @package ${schema.identifier.package}`); - } - lines.push(" */"); - } - - // Generate interface declaration - lines.push(`export interface ${interfaceName} {`); - - // Add resourceType for FHIR resources - if (schema.identifier.kind === "resource") { - lines.push(` resourceType: '${interfaceName}';`); - } - - // Generate fields (if any) - if ("fields" in schema && schema.fields) { - for (const [fieldName, field] of Object.entries(schema.fields)) { - const fieldLines = this.generateFieldLines(fieldName, field); - for (const fieldLine of fieldLines) { - if (fieldLine) { - lines.push(` ${fieldLine}`); - } - } - } - } - - lines.push("}"); - return lines.join("\n"); - } - - /** - * Collect import dependencies from a field - */ - private collectFieldImports(field: any): string[] { - const imports: string[] = []; - - // Skip polymorphic declaration fields (they don't have types to import) - if ("choices" in field && field.choices && Array.isArray(field.choices)) { - return imports; - } - - // Handle value set imports (only when generateValueSets is true) - if (field.binding && this.shouldUseValueSetType(field.binding)) { - const valueSetTypeName = this.getValueSetTypeName(field.binding); - imports.push(valueSetTypeName); - return imports; - } - - // Handle all other fields (regular fields and polymorphic instance fields) - if ("type" in field && field.type) { - // Handle nested types - they don't need imports as they're in the same file - if (field.type.kind === "nested") { - // Nested types are generated in the same file, no import needed - return imports; - } - - const languageType = this.typeMapper.mapType(field.type); - - // Only import non-primitive types that are not built-in - if (!languageType.isPrimitive && languageType.name !== "any") { - const builtInTypes = ["string", "number", "boolean", "Date", "object", "unknown", "any"]; - if (!builtInTypes.includes(languageType.name)) { - imports.push(languageType.name); - } - } - } - - return [...new Set(imports)]; // Remove duplicates - } - - /** - * Extract resource types from reference field constraints - */ - private extractReferenceTypes(referenceConstraints: any[]): string[] { - const resourceTypes: string[] = []; - - if (!Array.isArray(referenceConstraints)) { - return resourceTypes; - } - - for (const constraint of referenceConstraints) { - if (!constraint || typeof constraint !== "object") { - continue; - } - - if (constraint.kind === "resource" && constraint.name) { - const resourceType = this.typeMapper.formatTypeName(constraint.name); - resourceTypes.push(resourceType); - } - } - - return [...new Set(resourceTypes)]; // Remove duplicates - } - - /** - * Generate nested type interface - */ - private generateNestedTypeInterface(parentTypeName: string, nestedType: any): string { - const lines: string[] = []; - const nestedTypeName = this.typeMapper.formatTypeName( - `${parentTypeName}${this.capitalizeFirst(nestedType.identifier.name)}`, - ); - - // Add JSDoc comment if enabled - if (this.tsOptions.includeDocuments && nestedType.description) { - lines.push("/**"); - lines.push(` * ${nestedType.description}`); - if (nestedType.identifier.url) { - lines.push(` * @see ${nestedType.identifier.url}`); - } - lines.push(" */"); - } - - // Generate interface declaration - lines.push(`export interface ${nestedTypeName} {`); - - // Generate fields - if (nestedType.fields) { - for (const [fieldName, field] of Object.entries(nestedType.fields)) { - const fieldLines = this.generateFieldLines(fieldName, field); - for (const fieldLine of fieldLines) { - if (fieldLine) { - lines.push(` ${fieldLine}`); - } - } - } - } - - lines.push("}"); - return lines.join("\n"); - } - - /** - * Capitalize first letter of string - */ - private capitalizeFirst(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - /** - * Generate field lines (handles polymorphic fields by expanding them) - */ - private generateFieldLines(fieldName: string, field: any): string[] { - // Check if this field has choices (polymorphic declaration field) - if ("choices" in field && field.choices && Array.isArray(field.choices)) { - // Skip declaration fields - the actual instance fields are generated separately - // Declaration fields like `{"choices": ["deceasedBoolean", "deceasedDateTime"]}` - // are just metadata and shouldn't be rendered as actual TypeScript fields - return []; - } - - // For all other fields (including polymorphic instance fields with choiceOf), generate normally - const fieldLine = this.generateFieldLine(fieldName, field); - return fieldLine ? [fieldLine] : []; - } - - /** - * Generate a single field line - */ - private generateFieldLine(fieldName: string, field: any): string | null { - let typeString = "any"; - let required = false; - let isArray = false; - - if ("type" in field && field.type) { - // Check if field has a binding that we generated a value set for - if (field.binding && this.shouldUseValueSetType(field.binding)) { - const valueSetTypeName = this.getValueSetTypeName(field.binding); - typeString = valueSetTypeName; - } else if (field.binding && this.shouldUseInlineEnum(field)) { - // Generate inline enum union type when generateValueSets is false - typeString = this.generateInlineEnumType(field); - } else { - // Existing type mapping logic - const languageType = this.typeMapper.mapType(field.type); - typeString = languageType.name; - - // Handle nested types specially - if (field.type.kind === "nested") { - // Extract parent name from URL like "http://hl7.org/fhir/StructureDefinition/Patient#contact" - const urlParts = field.type.url?.split("#") || []; - if (urlParts.length === 2) { - const parentName = urlParts[0].split("/").pop() || ""; - const nestedName = field.type.name; - typeString = this.typeMapper.formatTypeName(`${parentName}${this.capitalizeFirst(nestedName)}`); - } else { - typeString = this.typeMapper.formatTypeName(field.type.name); - } - } else if (typeString === "Reference" && field.reference && Array.isArray(field.reference)) { - const referenceTypes = this.extractReferenceTypes(field.reference); - if (referenceTypes.length > 0) { - referenceTypes.forEach((type) => { - this.resourceTypes.add(type); - }); - - const unionType = referenceTypes.map((type) => `'${type}'`).join(" | "); - typeString = `Reference<${unionType}>`; - } - } - } - } - - if ("required" in field) { - required = field.required; - } - - if ("array" in field) { - isArray = field.array; - } - - const optional = required ? "" : "?"; - const arrayType = isArray ? "[]" : ""; - - return `${fieldName}${optional}: ${typeString}${arrayType};`; - } - - // ========================================== - /** - * Extract exported symbols from TypeScript content - */ - protected override extractExports(content: string): string[] { - const exports: string[] = []; - - const exportListPattern = /export\s*\{\s*([^}]+)\s*\}/g; - let match; - while ((match = exportListPattern.exec(content)) !== null) { - if (match[1]) { - const names = match[1] - .split(",") - .map((name) => name.trim()) - .filter(Boolean); - exports.push(...names); - } - } - - const directExportPatterns = [ - /export\s+interface\s+(\w+)/g, // export interface Name - /export\s+type\s+(\w+)/g, // export type Name - /export\s+class\s+(\w+)/g, // export class Name - /export\s+enum\s+(\w+)/g, // export enum Name - /export\s+const\s+(\w+)/g, // export const name - /export\s+function\s+(\w+)/g, // export function name - ]; - - for (const pattern of directExportPatterns) { - let match; - while ((match = pattern.exec(content)) !== null) { - if (match[1]) { - exports.push(match[1]); - } - } - } - - return [...new Set(exports)]; - } - - /** - * Set output directory for compatibility with API builder - */ - setOutputDir(directory: string): void { - this.options.outputDir = directory; - } - - /** - * Update generator options for compatibility with API builder - */ - setOptions(options: Partial): void { - this.options = { ...this.options, ...options }; - } - - /** - * Get current options for compatibility with API builder - */ - getOptions(): TypeScriptGeneratorOptions { - return { ...this.options }; - } - - /** - * Override generate to clean directory first - */ - public override async generate(input: GeneratorInput): Promise { - await this.fileManager.cleanDirectory(); - - this.logger.debug("Cleaned output directory before generation"); - return super.generate(input); - } - - /** - * Run post-generation hooks - generate utility files - */ - protected override async runPostGenerationHooks(): Promise { - await super.runPostGenerationHooks(); - - await this.generateValueSetFiles(); - await this.generateUtilitiesFile(); - await this.generateMainIndexFile(); - } - - /** - * Generate utilities.ts file with ResourceType union - */ - private async generateUtilitiesFile(): Promise { - if (this.resourceTypes.size === 0) { - this.logger.warn("No resource types found, skipping utilities.ts generation"); - return; - } - - const lines: string[] = []; - - // Add file header comment - lines.push("/**"); - lines.push(" * FHIR Resource Type Utilities"); - lines.push(" * This file contains utility types for FHIR resources."); - lines.push(" * "); - lines.push(" * @generated This file is auto-generated. Do not edit manually."); - lines.push(" */"); - lines.push(""); - - // Generate ResourceType union - const sortedResourceTypes = Array.from(this.resourceTypes).sort(); - lines.push("/**"); - lines.push(" * Union of all FHIR resource types in this package"); - lines.push(" */"); - lines.push("export type ResourceType ="); - - for (let i = 0; i < sortedResourceTypes.length; i++) { - const isLast = i === sortedResourceTypes.length - 1; - const separator = isLast ? ";" : ""; - lines.push(` | '${sortedResourceTypes[i]}'${separator}`); - } - - lines.push(""); - - // Generate helper type for Resource references - lines.push("/**"); - lines.push(" * Helper type for creating typed References"); - lines.push( - " * @example Reference<'Patient' | 'Practitioner'> - Reference that can point to Patient or Practitioner", - ); - lines.push(" */"); - lines.push("export type TypedReference = {"); - lines.push(" reference?: string;"); - lines.push(" type?: T;"); - lines.push(" identifier?: any; // Simplified for utility"); - lines.push(" display?: string;"); - lines.push("};"); - - const content = lines.join("\n"); - - // Write the utilities file - await this.fileManager.writeFile("utilities.ts", content); - - this.logger.info(`Generated utilities.ts with ${this.resourceTypes.size} resource types`); - } - - /** - * Generate a complete value set TypeScript file - */ - private generateValueSetFile(binding: BindingTypeSchema): string { - const name = this.typeMapper.formatTypeName(binding.identifier.name); - const values = binding.enum?.map((v: string) => ` '${v}'`).join(",\n") || ""; - - const lines: string[] = []; - - // Add file header comment - if (this.options.includeDocuments) { - lines.push("/**"); - lines.push(` * ${binding.identifier.name} value set`); - if (binding.description) { - lines.push(` * ${binding.description}`); - } - if (binding.valueset?.url) { - lines.push(` * @see ${binding.valueset.url}`); - } - if (binding.identifier.package) { - lines.push(` * @package ${binding.identifier.package}`); - } - lines.push(" * @generated This file is auto-generated. Do not edit manually."); - lines.push(" */"); - lines.push(""); - } - - // Add values array - lines.push(`export const ${name}Values = [`); - if (values) { - lines.push(values); - } - lines.push("] as const;"); - lines.push(""); - - // Add union type - lines.push(`export type ${name} = typeof ${name}Values[number];`); - - // Add helper function if enabled - if (this.tsOptions.includeValueSetHelpers) { - lines.push(""); - lines.push(`export const isValid${name} = (value: string): value is ${name} =>`); - lines.push(` ${name}Values.includes(value as ${name});`); - } - - return lines.join("\n"); - } - - /** - * Create valuesets directory and generate all value set files - */ - private async generateValueSetFiles(): Promise { - if (!this.tsOptions.generateValueSets || this.collectedValueSets.size === 0) { - return; - } - - // Generate individual value set files in valuesets/ - for (const [name, binding] of this.collectedValueSets) { - const content = this.generateValueSetFile(binding); - const fileName = `valuesets/${name}.ts`; - - await this.fileManager.writeFile(fileName, content); - this.logger.info(`Generated value set: ${fileName}`); - } - - // Generate index file in valuesets/ - await this.generateValueSetIndexFile(); - } - - /** - * Generate index.ts file that re-exports all value sets - */ - private async generateValueSetIndexFile(): Promise { - const lines: string[] = []; - - if (this.tsOptions.includeDocuments) { - lines.push("/**"); - lines.push(" * FHIR Value Sets"); - lines.push(" * This file re-exports all generated value sets."); - lines.push(" * "); - lines.push(" * @generated This file is auto-generated. Do not edit manually."); - lines.push(" */"); - lines.push(""); - } - - // Sort value sets for consistent output - const sortedValueSets = Array.from(this.collectedValueSets.keys()).sort(); - - for (const name of sortedValueSets) { - lines.push(`export * from './${name}.js';`); - } - - const content = lines.join("\n"); - await this.fileManager.writeFile("valuesets/index.ts", content); - this.logger.info(`Generated valuesets/index.ts with ${this.collectedValueSets.size} value sets`); - } - - /** - * Generate main types/index.ts file that exports all types and value sets - */ - private async generateMainIndexFile(): Promise { - if (!this.options.generateIndex) { - return; - } - - const lines: string[] = []; - - if (this.tsOptions.includeDocuments) { - lines.push("/**"); - lines.push(" * FHIR R4 TypeScript Types"); - lines.push(" * Generated from FHIR StructureDefinitions"); - lines.push(" * "); - lines.push(" * @generated This file is auto-generated. Do not edit manually."); - lines.push(" */"); - lines.push(""); - } - - // Generate exports for all generated files - we'll keep this simple - // and avoid accessing private fields for now. The key functionality - // (value set generation and interface type updates) is already working. - - // For now, we'll skip the individual file exports since they're complex - // and the main functionality is already working. This can be improved later. - - // Export utilities - lines.push('export * from "./utilities";'); - - // Export value sets if any were generated - if (this.tsOptions.generateValueSets && this.collectedValueSets.size > 0) { - lines.push(""); - lines.push("// Value Sets"); - lines.push('export * from "./valuesets/index";'); - } - - const content = lines.join("\n"); - await this.fileManager.writeFile("index.ts", content); - this.logger.info( - `Generated index.ts with type exports${this.tsOptions.generateValueSets && this.collectedValueSets.size > 0 ? " and value sets" : ""}`, - ); - } -} diff --git a/src/api/index.ts b/src/api/index.ts index ab695cf7f..af0d33d8c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,68 +7,7 @@ * @packageDocumentation */ -// Re-export core utilities -export { - TypeSchemaCache, - TypeSchemaGenerator, - TypeSchemaParser, -} from "@typeschema/index"; -// Re-export core TypeSchema types for convenience -export type { - Field as TypeSchemaField, - Identifier as TypeSchemaIdentifier, - PackageMeta as PackageInfo, - TypeSchema, -} from "@typeschema/types"; -// Export types and interfaces -export type { - APIBuilderOptions, - GenerationResult, - ProgressCallback, -} from "./builder"; -// Export main API builder and utilities -export { - APIBuilder, - createAPIFromConfig, -} from "./builder"; -export type { GeneratedFile } from "./generators/base/index"; -export type { TypeScriptGeneratorOptions } from "./generators/typescript"; -// Export generator classes for advanced usage -export { TypeScriptGenerator } from "./generators/typescript"; - -/** - * Quick start examples: - * - * @example - * Generate TypeScript types from a FHIR package: - * ```typescript - * import { createAPI } from '@atomic-codegen/api'; - * - * const result = await createAPI() - * .fromPackage('hl7.fhir.r4.core') - * .typescript() - * .generate(); - * ``` - * - * @example - * Generate TypeScript types from TypeSchema files: - * ```typescript - * import { createAPI } from '@atomic-codegen/api'; - * - * const result = await createAPI() - * .fromFiles('./schemas/*.ndjson') - * .typescript() - * .generate(); - * ``` - * - * @example - * Build in-memory without writing files: - * ```typescript - * import { createAPI } from '@atomic-codegen/api'; - * - * const results = await createAPI() - * .fromPackage('hl7.fhir.r4.core') - * .typescript() - * .build(); - * ``` - */ +export type { APIBuilderOptions } from "./builder"; +export { APIBuilder } from "./builder"; +export type { CSharpGeneratorOptions } from "./writer-generator/csharp/csharp"; +export type { TypeScriptOptions } from "./writer-generator/typescript"; diff --git a/src/api/writer-generator/csharp/csharp.ts b/src/api/writer-generator/csharp/csharp.ts index df7b023ba..c2acf7a5b 100644 --- a/src/api/writer-generator/csharp/csharp.ts +++ b/src/api/writer-generator/csharp/csharp.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import Path from "node:path"; import { pascalCase, uppercaseFirstLetter, uppercaseFirstLetterOfEach } from "@root/api/writer-generator/utils.ts"; -import { Writer, type PartialBy, type WriterOptions } from "@root/api/writer-generator/writer.ts"; +import { type PartialBy, Writer, type WriterOptions } from "@root/api/writer-generator/writer.ts"; import type { Field, Identifier, RegularField } from "@typeschema/types"; import { type ChoiceFieldInstance, isChoiceDeclarationField, type RegularTypeSchema } from "@typeschema/types.ts"; import type { TypeSchemaIndex } from "@typeschema/utils.ts"; @@ -75,7 +75,7 @@ const isReservedTypeName = (name: string): boolean => RESERVED_TYPE_NAMES.includ const prefixReservedTypeName = (name: string): string => (isReservedTypeName(name) ? `Resource${name}` : name); -type CSharpGeneratorOptions = WriterOptions & { +export type CSharpGeneratorOptions = WriterOptions & { outputDir: string; staticSourceDir?: string; targetNamespace: string; diff --git a/src/api/writer-generator/typescript.ts b/src/api/writer-generator/typescript.ts index 14dfde774..5bc8ef783 100644 --- a/src/api/writer-generator/typescript.ts +++ b/src/api/writer-generator/typescript.ts @@ -275,6 +275,7 @@ export class TypeScript extends Writer { if (field.enum) { tsType = tsEnumType(field.enum); } else if (schema.identifier.name === "Reference" && tsName === "reference") { + // biome-ignore lint: that is exactly string what we want tsType = "`${T}/${string}`"; } else if (field.reference && field.reference.length > 0) { const references = field.reference.map((ref) => `"${ref.name}"`).join(" | "); diff --git a/src/cli/commands/generate.ts b/src/cli/commands/generate.ts index 3d67c51f7..4c836af1a 100644 --- a/src/cli/commands/generate.ts +++ b/src/cli/commands/generate.ts @@ -7,10 +7,10 @@ import { existsSync } from "node:fs"; import { resolve } from "node:path"; +import { createLogger, error, step, success, warn } from "@root/utils/codegen-logger"; import type { CommandModule } from "yargs"; import { APIBuilder } from "../../api/index"; import { CONFIG_FILE_NAMES, loadConfig } from "../../config"; -import { createLogger, error, step, success, warn } from "../utils/log"; import type { CLIArgv } from "./index"; interface GenerateArgs extends CLIArgv { @@ -119,14 +119,7 @@ export const generateCommand: CommandModule, GenerateArg // Configure generators from config if (config.typescript) { - if (verbose) { - logger.info("Configuring TypeScript generation from config"); - logger.debug(`Module format: ${config.typescript.moduleFormat || "esm"}`); - logger.debug(`Generate index: ${config.typescript.generateIndex ?? true}`); - logger.debug(`Include docs: ${config.typescript.includeDocuments ?? false}`); - logger.debug(`Naming convention: ${config.typescript.namingConvention || "PascalCase"}`); - } - builder.typescriptDepricated(config.typescript); + throw new Error("Not Implemented"); } // Check that at least one generator is configured diff --git a/src/cli/commands/generate/typescript.ts b/src/cli/commands/generate/typescript.ts deleted file mode 100644 index b88f6927f..000000000 --- a/src/cli/commands/generate/typescript.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Generate TypeScript Command - * - * Generate TypeScript types from TypeSchema files using the new high-level API - */ - -import { APIBuilder } from "../../../api/index"; -import type { Config } from "../../../config"; -import { createLogger } from "../../utils/log"; - -/** - * Generate TypeScript types from TypeSchema using the high-level API - */ -export async function generateTypeScript(config: Config, inputPath?: string): Promise { - const log = createLogger({ - verbose: config.verbose, - prefix: "TypeScript", - }); - - log.step("Generating TypeScript types using high-level API"); - - // Create API builder with config options - const builder = new APIBuilder({ - outputDir: config.outputDir, - verbose: config.verbose, - overwrite: config.overwrite, - cache: config.cache, - }); - - // Load TypeSchema from input path if provided - if (inputPath) { - log.info(`Loading TypeSchema from: ${inputPath}`); - builder.fromFiles(inputPath); - } - - // Configure TypeScript generation with options from config - builder.typescriptDepricated(config.typescript || {}); - - // Add progress callback if verbose - if (config.verbose) { - builder.onProgress((phase, current, total, message) => { - const progress = Math.round((current / total) * 100); - log.progress(`[${phase}] ${progress}% - ${message || "Processing..."}`); - }); - } - - // Execute the generation - const result = await builder.generate(); - - if (result.success) { - log.success(`Generated ${result.filesGenerated.length} TypeScript files in ${result.duration.toFixed(2)}ms`); - if (result.warnings.length > 0) { - log.warn(`${result.warnings.length} warnings found`); - result.warnings.forEach((warning) => { - log.dim(` ${warning}`); - }); - } - } else { - log.error(`TypeScript generation failed: ${result.errors.join(", ")}`); - throw new Error(`TypeScript generation failed: ${result.errors.join(", ")}`); - } -} diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index 903384e59..ba47eda65 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -6,9 +6,9 @@ * Modern CLI with subcommands for typeschema and code generation */ +import { configure, error, header } from "@root/utils/codegen-logger"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { configure, error, header } from "../utils/log"; import { generateCommand } from "./generate"; import { typeschemaCommand } from "./typeschema"; diff --git a/src/cli/commands/typeschema.ts b/src/cli/commands/typeschema.ts index 92572012f..37863c16e 100644 --- a/src/cli/commands/typeschema.ts +++ b/src/cli/commands/typeschema.ts @@ -4,8 +4,8 @@ * Commands for validating and managing TypeSchema files */ +import { error, info, list } from "@root/utils/codegen-logger"; import type { CommandModule } from "yargs"; -import { error, info, list } from "../utils/log"; import { generateTypeschemaCommand } from "./typeschema/generate"; /** diff --git a/src/cli/commands/typeschema/generate.ts b/src/cli/commands/typeschema/generate.ts index 7253816e3..fd798e8aa 100644 --- a/src/cli/commands/typeschema/generate.ts +++ b/src/cli/commands/typeschema/generate.ts @@ -6,10 +6,10 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; +import { complete, createLogger, list } from "@root/utils/codegen-logger"; import { TypeSchemaGenerator } from "@typeschema/generator"; import type { CommandModule } from "yargs"; import { loadConfig } from "../../../config"; -import { complete, createLogger, list } from "../../utils/log"; interface GenerateTypeschemaArgs { packages: string[]; @@ -126,7 +126,8 @@ export const generateTypeschemaCommand: CommandModule, G // Use the output format determined earlier // Ensure output directory exists - const outputPath = argv.output!; + const outputPath = argv.output; + if (!outputPath) throw new Error("Output format not specified"); await mkdir(dirname(outputPath), { recursive: true }); // Format and write the schemas diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts deleted file mode 100644 index d80fc279b..000000000 --- a/src/cli/utils/log.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * CLI Utilities - Re-exports from main utils with spinner support - */ - -import { createSpinner } from "./spinner"; - -// Re-export all logger functions from the main utils -export { - type CodegenLogger, - complete, - configure, - createLogger, - debug, - dim, - error, - header, - info, - type LogOptions, - list, - plain, - progress, - section, - step, - success, - table, - warn, -} from "../../utils/codegen-logger"; - -// Export CLI-specific utilities -export { createSpinner }; - -/** - * Run a task with a spinner - */ -export async function withSpinner(promise: Promise, message: string, successMessage?: string): Promise { - const spinner = createSpinner(message).start(); - try { - const result = await promise; - spinner.succeed(successMessage || message); - return result; - } catch (error) { - spinner.fail(`Failed: ${message}`); - throw error; - } -} diff --git a/src/cli/utils/prompts.ts b/src/cli/utils/prompts.ts deleted file mode 100644 index 1686f3754..000000000 --- a/src/cli/utils/prompts.ts +++ /dev/null @@ -1,248 +0,0 @@ -/** - * CLI Prompts Utility - * - * Interactive prompts for better CLI user experience - */ - -import { checkbox, confirm, input, select } from "@inquirer/prompts"; -import pc from "picocolors"; - -/** - * Prompt for project initialization - */ -export async function promptInitConfig(): Promise<{ - projectName: string; - description?: string; - outputDir: string; - generators: string[]; - packageManager: "npm" | "yarn" | "pnpm" | "bun"; - typescript: boolean; -}> { - console.log(pc.bold(pc.blue("\n🚀 Initialize Atomic Codegen Project\n"))); - - const projectName = await input({ - message: "Project name:", - default: "my-fhir-types", - validate: (value) => { - if (!value.trim()) return "Project name is required"; - if (!/^[a-z0-9-_]+$/i.test(value)) { - return "Project name can only contain letters, numbers, hyphens, and underscores"; - } - return true; - }, - }); - - const description = await input({ - message: "Project description (optional):", - default: "", - }); - - const outputDir = await input({ - message: "Output directory for generated code:", - default: "./generated", - }); - - const generators = await checkbox({ - message: "Select generators to use:", - choices: [ - { name: "TypeScript", value: "typescript", checked: true }, - { name: "Python", value: "python" }, - ], - }); - - const packageManager = (await select({ - message: "Package manager:", - choices: [ - { name: "Bun", value: "bun" }, - { name: "npm", value: "npm" }, - { name: "Yarn", value: "yarn" }, - { name: "pnpm", value: "pnpm" }, - ], - default: "bun", - })) as "npm" | "yarn" | "pnpm" | "bun"; - - const typescript = await confirm({ - message: "Use TypeScript for configuration?", - default: true, - }); - - return { - projectName, - description, - outputDir, - generators, - packageManager, - typescript, - }; -} - -/** - * Prompt for FHIR package selection - */ -export async function promptFHIRPackage(): Promise<{ - packageId: string; - version?: string; - profiles?: string[]; -}> { - const packageId = await select({ - message: "Select FHIR package:", - choices: [ - { name: "FHIR R4 Core", value: "hl7.fhir.r4.core" }, - { name: "FHIR R5 Core", value: "hl7.fhir.r5.core" }, - { name: "US Core", value: "hl7.fhir.us.core" }, - { name: "Custom package...", value: "custom" }, - ], - }); - - let finalPackageId = packageId; - let version: string | undefined; - - if (packageId === "custom") { - finalPackageId = await input({ - message: "Enter package ID (e.g., hl7.fhir.us.core):", - validate: (value) => { - if (!value.trim()) return "Package ID is required"; - return true; - }, - }); - - version = await input({ - message: "Package version (optional):", - default: "", - }); - } - - const includeProfiles = await confirm({ - message: "Include profiles in generation?", - default: false, - }); - - let profiles: string[] = []; - if (includeProfiles) { - const profileSelection = await select({ - message: "Profile selection:", - choices: [ - { name: "All profiles", value: "all" }, - { name: "None", value: "none" }, - ], - default: "none", - }); - - if (profileSelection === "all") { - profiles = ["*"]; - } - } - - return { - packageId: finalPackageId, - version: version || undefined, - profiles, - }; -} - -/** - * Prompt for generator configuration - */ -export async function promptGeneratorConfig(generatorId: string): Promise> { - const config: Record = {}; - - switch (generatorId) { - case "typescript": - config.moduleFormat = await select({ - message: "Module format:", - choices: [ - { name: "ESM", value: "esm" }, - { name: "CommonJS", value: "cjs" }, - { name: "Both", value: "both" }, - ], - default: "esm", - }); - - config.strict = await confirm({ - message: "Enable strict TypeScript checks?", - default: true, - }); - - config.generateIndex = await confirm({ - message: "Generate index files?", - default: true, - }); - break; - - case "python": - config.style = await select({ - message: "Python style:", - choices: [ - { name: "Dataclasses", value: "dataclass" }, - { name: "Pydantic", value: "pydantic" }, - { name: "Plain classes", value: "plain" }, - ], - default: "dataclass", - }); - - config.typeHints = await confirm({ - message: "Include type hints?", - default: true, - }); - break; - } - - return config; -} - -/** - * Prompt for confirmation with custom message - */ -export async function promptConfirm(message: string, defaultValue = false): Promise { - return await confirm({ - message, - default: defaultValue, - }); -} - -/** - * Prompt for text input with validation - */ -export async function promptInput( - message: string, - options?: { - default?: string; - validate?: (value: string) => boolean | string; - transform?: (value: string) => string; - }, -): Promise { - return await input({ - message, - default: options?.default, - validate: options?.validate, - transformer: options?.transform, - }); -} - -/** - * Show success message - */ -export function showSuccess(message: string): void { - console.log(pc.green(`✅ ${message}`)); -} - -/** - * Show error message - */ -export function showError(message: string): void { - console.log(pc.red(`❌ ${message}`)); -} - -/** - * Show warning message - */ -export function showWarning(message: string): void { - console.log(pc.yellow(`⚠️ ${message}`)); -} - -/** - * Show info message - */ -export function showInfo(message: string): void { - console.log(pc.blue(`ℹ️ ${message}`)); -} diff --git a/src/cli/utils/spinner.ts b/src/cli/utils/spinner.ts deleted file mode 100644 index 87be91ba5..000000000 --- a/src/cli/utils/spinner.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * CLI Spinner and Progress Utilities - * - * Visual progress indicators for long-running operations - */ - -import { performance } from "node:perf_hooks"; -import ora, { type Ora } from "ora"; -import pc from "picocolors"; - -/** - * Task progress tracker - */ -export class ProgressTracker { - private tasks: Map< - string, - { - startTime: number; - status: "pending" | "running" | "completed" | "failed"; - } - > = new Map(); - private currentSpinner: Ora | null = null; - - /** - * Start a new task - */ - startTask(taskId: string, message: string): void { - this.tasks.set(taskId, { - startTime: performance.now(), - status: "running", - }); - - if (this.currentSpinner) { - this.currentSpinner.stop(); - } - - this.currentSpinner = ora({ - text: message, - spinner: "dots", - }).start(); - } - - /** - * Update task progress - */ - updateTask(taskId: string, message: string): void { - const task = this.tasks.get(taskId); - if (!task || task.status !== "running") return; - - if (this.currentSpinner) { - this.currentSpinner.text = message; - } - } - - /** - * Complete a task successfully - */ - completeTask(taskId: string, message?: string): void { - const task = this.tasks.get(taskId); - if (!task) return; - - const duration = Math.round(performance.now() - task.startTime); - task.status = "completed"; - - if (this.currentSpinner) { - const finalMessage = message || this.currentSpinner.text; - this.currentSpinner.succeed(`${finalMessage} ${pc.gray(`(${duration}ms)`)}`); - this.currentSpinner = null; - } - } - - /** - * Fail a task - */ - failTask(taskId: string, error?: string): void { - const task = this.tasks.get(taskId); - if (!task) return; - - task.status = "failed"; - - if (this.currentSpinner) { - const errorMessage = error ? `: ${error}` : ""; - this.currentSpinner.fail(this.currentSpinner.text + errorMessage); - this.currentSpinner = null; - } - } - - /** - * Stop all spinners - */ - stop(): void { - if (this.currentSpinner) { - this.currentSpinner.stop(); - this.currentSpinner = null; - } - } - - /** - * Get task statistics - */ - getStats(): { - total: number; - completed: number; - failed: number; - running: number; - pending: number; - } { - let completed = 0; - let failed = 0; - let running = 0; - let pending = 0; - - for (const task of this.tasks.values()) { - switch (task.status) { - case "completed": - completed++; - break; - case "failed": - failed++; - break; - case "running": - running++; - break; - case "pending": - pending++; - break; - } - } - - return { - total: this.tasks.size, - completed, - failed, - running, - pending, - }; - } -} - -/** - * Simple spinner for single operations - */ -export class SimpleSpinner { - private spinner: Ora; - - constructor(message: string) { - this.spinner = ora({ - text: message, - spinner: "dots", - }); - } - - start(message?: string): SimpleSpinner { - if (message) { - this.spinner.text = message; - } - this.spinner.start(); - return this; - } - - update(message: string): SimpleSpinner { - this.spinner.text = message; - return this; - } - - succeed(message?: string): void { - this.spinner.succeed(message); - } - - fail(message?: string): void { - this.spinner.fail(message); - } - - warn(message?: string): void { - this.spinner.warn(message); - } - - info(message?: string): void { - this.spinner.info(message); - } - - stop(): void { - this.spinner.stop(); - } -} - -/** - * Progress bar for batch operations - */ -export class ProgressBar { - private current = 0; - private barLength = 30; - private startTime = performance.now(); - - constructor( - private total: number, - private message: string, - ) {} - - /** - * Update progress - */ - update(current: number, additionalInfo?: string): void { - this.current = Math.min(current, this.total); - this.render(additionalInfo); - } - - /** - * Increment progress by 1 - */ - increment(additionalInfo?: string): void { - this.update(this.current + 1, additionalInfo); - } - - /** - * Complete the progress bar - */ - complete(message?: string): void { - this.current = this.total; - this.render(); - - const duration = Math.round(performance.now() - this.startTime); - console.log(pc.green(`✅ ${message || this.message} ${pc.gray(`(${duration}ms)`)}`)); - } - - /** - * Render the progress bar - */ - private render(additionalInfo?: string): void { - const percentage = Math.round((this.current / this.total) * 100); - const filled = Math.round((this.current / this.total) * this.barLength); - const empty = this.barLength - filled; - - const bar = pc.green("█".repeat(filled)) + pc.gray("░".repeat(empty)); - const progress = `${this.current}/${this.total}`; - const info = additionalInfo ? pc.gray(` ${additionalInfo}`) : ""; - - // Clear line and write progress - process.stdout.clearLine(0); - process.stdout.cursorTo(0); - process.stdout.write(`${this.message} ${bar} ${pc.cyan(progress)} ${pc.yellow(`${percentage}%`)}${info}`); - - // Add newline when complete - if (this.current === this.total) { - process.stdout.write("\n"); - } - } -} - -/** - * Create a simple spinner - */ -export function createSpinner(message: string): SimpleSpinner { - return new SimpleSpinner(message); -} - -/** - * Create a progress tracker for multiple tasks - */ -export function createProgressTracker(): ProgressTracker { - return new ProgressTracker(); -} - -/** - * Create a progress bar - */ -export function createProgressBar(total: number, message: string): ProgressBar { - return new ProgressBar(total, message); -} - -/** - * Show a spinner for a promise - */ -export async function withSpinner( - promise: Promise, - options: { - start: string; - success?: string | ((result: T) => string); - fail?: string | ((error: any) => string); - }, -): Promise { - const spinner = ora({ - text: options.start, - spinner: "dots", - }).start(); - - try { - const result = await promise; - const successMessage = - typeof options.success === "function" ? options.success(result) : options.success || options.start; - spinner.succeed(successMessage); - return result; - } catch (error) { - const failMessage = - typeof options.fail === "function" ? options.fail(error) : options.fail || `Failed: ${options.start}`; - spinner.fail(failMessage); - throw error; - } -} - -/** - * Multi-step task execution with progress - */ -export async function executeSteps( - steps: Array<{ - name: string; - task: () => Promise; - }>, -): Promise { - const results: T[] = []; - const progressBar = new ProgressBar(steps.length, "Executing tasks"); - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - progressBar.update(i, step?.name); - - try { - const result = await step?.task(); - if (result) { - results.push(result); - } - } catch (error) { - progressBar.complete(`Failed at step: ${step?.name}`); - throw error; - } - } - - progressBar.complete("All tasks completed"); - return results; -} diff --git a/src/config.ts b/src/config.ts index 287e0d148..f2a1a9fd0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -731,7 +731,8 @@ export class ConfigLoader { } // Merge with defaults - return this.mergeWithDefaults(validation.config!); + if (!validation.config) throw new Error("Invalid configuration"); + return this.mergeWithDefaults(validation.config); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to load config from ${filePath}: ${error.message}`); diff --git a/src/typeschema/cache.ts b/src/typeschema/cache.ts deleted file mode 100644 index f9714bfd9..000000000 --- a/src/typeschema/cache.ts +++ /dev/null @@ -1,285 +0,0 @@ -/** - * TypeSchema Cache System - * - * Caching system for TypeSchema documents with both in-memory and persistent file-based storage. - */ - -import { existsSync, mkdirSync } from "node:fs"; -import { readdir, readFile, stat, unlink, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import type { TypeSchemaConfig } from "@root/config"; -import type { Identifier, TypeSchema } from "@typeschema/types"; - -/** - * Cached schema metadata for persistence - */ -interface CachedSchemaMetadata { - schema: TypeSchema; - timestamp: number; - version: string; -} - -/** - * TypeSchema Cache with optional persistent storage - */ -export class TypeSchemaCache { - private cache = new Map(); - private config: TypeSchemaConfig; - private cacheDir?: string; - - constructor(config?: TypeSchemaConfig) { - this.config = { - enablePersistence: true, - cacheDir: ".typeschema-cache", - maxAge: 24 * 60 * 60 * 1000, // 24 hours - validateCached: true, - ...config, - }; - - if (this.config.enablePersistence && this.config.cacheDir) { - this.cacheDir = this.config.cacheDir; - } - } - - /** - * Store a schema in the cache - */ - async set(schema: TypeSchema): Promise { - const key = this.generateKey(schema.identifier); - this.cache.set(key, schema); - - // Persist to disk if enabled - if (this.config.enablePersistence && this.cacheDir) { - await this.persistSchema(schema); - } - } - - /** - * Retrieve a schema by identifier - */ - get(identifier: Identifier): TypeSchema | null { - const key = this.generateKey(identifier); - return this.cache.get(key) || null; - } - - /** - * Retrieve a schema by URL - */ - getByUrl(url: string): TypeSchema | null { - for (const schema of this.cache.values()) { - if (schema.identifier.url === url) { - return schema; - } - } - return null; - } - - /** - * Check if a schema exists in cache - */ - has(identifier: Identifier): boolean { - const key = this.generateKey(identifier); - return this.cache.has(key); - } - - /** - * Check if a schema exists by URL - */ - hasByUrl(url: string): boolean { - for (const schema of this.cache.values()) { - if (schema.identifier.url === url) { - return true; - } - } - return false; - } - - /** - * Delete a schema from cache - */ - delete(identifier: Identifier): boolean { - const key = this.generateKey(identifier); - return this.cache.delete(key); - } - - /** - * Delete a schema by URL - */ - deleteByUrl(url: string): boolean { - for (const [key, schema] of this.cache.entries()) { - if (schema.identifier.url === url) { - return this.cache.delete(key); - } - } - return false; - } - - /** - * Get schemas by package - */ - getByPackage(packageName: string): TypeSchema[] { - const results: TypeSchema[] = []; - for (const schema of this.cache.values()) { - if (schema.identifier.package === packageName) { - results.push(schema); - } - } - return results; - } - - /** - * Get schemas by kind - */ - getByKind(kind: string): TypeSchema[] { - const results: TypeSchema[] = []; - for (const schema of this.cache.values()) { - if (schema.identifier.kind === kind) { - results.push(schema); - } - } - return results; - } - - /** - * Store multiple schemas - */ - setMany(schemas: TypeSchema[]): void { - for (const schema of schemas) { - this.set(schema); - } - } - - /** - * Clear all cached schemas - */ - clear(): void { - this.cache.clear(); - } - - /** - * Generate cache key for identifier - */ - private generateKey(identifier: Identifier): string { - return `${identifier.package}:${identifier.version}:${identifier.kind}:${identifier.name}`; - } - - /** - * Initialize cache directory if persistence is enabled - */ - async initialize(): Promise { - if (this.config.enablePersistence && this.cacheDir) { - // Ensure cache directory exists - if (!existsSync(this.cacheDir)) { - mkdirSync(this.cacheDir, { recursive: true }); - } - - // Load all cached schemas from disk - await this.loadFromDisk(); - } - } - - /** - * Load all cached schemas from disk - */ - private async loadFromDisk(): Promise { - if (!this.cacheDir || !existsSync(this.cacheDir)) { - return; - } - - try { - const files = await readdir(this.cacheDir); - const schemaFiles = files.filter((f) => f.endsWith(".typeschema.json")); - - for (const file of schemaFiles) { - const filePath = join(this.cacheDir, file); - const stats = await stat(filePath); - - // Check if cache is expired - if (this.config.maxAge) { - const age = Date.now() - stats.mtimeMs; - if (age > this.config.maxAge) { - // Remove expired cache file - await unlink(filePath); - continue; - } - } - - try { - const content = await readFile(filePath, "utf-8"); - const metadata: CachedSchemaMetadata = JSON.parse(content); - - // Validate cached schema if configured - if (this.config.validateCached) { - // Basic validation - check required fields - if (!metadata.schema?.identifier) { - continue; - } - } - - // Add to in-memory cache - const key = this.generateKey(metadata.schema.identifier); - this.cache.set(key, metadata.schema); - } catch (error) { - console.warn(`Failed to load cached schema from ${file}:`, error); - } - } - } catch (error) { - console.warn("Failed to load cached schemas from disk:", error); - } - } - - /** - * Persist a schema to disk - */ - private async persistSchema(schema: TypeSchema): Promise { - if (!this.cacheDir) { - return; - } - - // Ensure cache directory exists - if (!existsSync(this.cacheDir)) { - mkdirSync(this.cacheDir, { recursive: true }); - } - - const metadata: CachedSchemaMetadata = { - schema, - timestamp: Date.now(), - version: schema.identifier.version as string, - }; - - // Generate filename from identifier - const fileName = - `${schema.identifier.package}-${schema.identifier.version}-${schema.identifier.kind}-${schema.identifier.name}.typeschema.json`.replace( - /[^a-zA-Z0-9.-]/g, - "_", - ); - - const filePath = join(this.cacheDir, fileName); - - try { - await writeFile(filePath, JSON.stringify(metadata, null, 2), "utf-8"); - } catch (error) { - console.warn(`Failed to persist schema to ${filePath}:`, error); - } - } - - /** - * Clear cache directory - */ - async clearDisk(): Promise { - if (!this.cacheDir || !existsSync(this.cacheDir)) { - return; - } - - try { - const files = await readdir(this.cacheDir); - const schemaFiles = files.filter((f) => f.endsWith(".typeschema.json")); - - for (const file of schemaFiles) { - await unlink(join(this.cacheDir, file)); - } - } catch (error) { - console.warn("Failed to clear cache directory:", error); - } - } -} diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index 860236246..dd7fd8fa4 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -14,7 +14,8 @@ import { mkBindingIdentifier, mkIdentifier } from "./identifier"; import { mkNestedIdentifier } from "./nested-types"; function isRequired(register: Register, fhirSchema: RichFHIRSchema, path: string[]): boolean { - const fieldName = path[path.length - 1]!; + const fieldName = path[path.length - 1]; + if (!fieldName) throw new Error(`Internal error: fieldName is missing for path ${path.join("/")}`); const parentPath = path.slice(0, -1); const requires = register.resolveFsGenealogy(fhirSchema.package_meta, fhirSchema.url).flatMap((fs) => { @@ -116,7 +117,7 @@ export const mkField = ( if (!fieldType) logger?.warn(`Field type not found for '${fhirSchema.url}#${path.join(".")}' (${fhirSchema.derivation})`); return { - type: fieldType!, + type: fieldType as Identifier, required: isRequired(register, fhirSchema, path), excluded: isExcluded(register, fhirSchema, path), diff --git a/src/typeschema/core/nested-types.ts b/src/typeschema/core/nested-types.ts index feb6930c4..110871f50 100644 --- a/src/typeschema/core/nested-types.ts +++ b/src/typeschema/core/nested-types.ts @@ -116,7 +116,7 @@ export function mkNestedTypes( url: baseUrl, }; - const fields = transformNestedElements(register, fhirSchema, path, element.elements!, logger); + const fields = transformNestedElements(register, fhirSchema, path, element.elements ?? {}, logger); const nestedType: NestedType = { identifier, diff --git a/src/typeschema/generator.ts b/src/typeschema/generator.ts index 7a8408854..5bd0119f7 100644 --- a/src/typeschema/generator.ts +++ b/src/typeschema/generator.ts @@ -8,12 +8,10 @@ import { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager"; import type { FHIRSchema, StructureDefinition } from "@atomic-ehr/fhirschema"; import * as fhirschema from "@atomic-ehr/fhirschema"; -import type { TypeSchemaConfig } from "@root/config"; import type { CodegenLogger } from "@root/utils/codegen-logger"; import { createLogger } from "@root/utils/codegen-logger"; import type { Register } from "@typeschema/register"; import { registerFromManager } from "@typeschema/register"; -import { TypeSchemaCache } from "./cache"; import { transformFhirSchema, transformValueSet } from "./core/transformer"; import { type PackageMeta, @@ -33,14 +31,11 @@ export class TypeSchemaGenerator { private manager: ReturnType; private options: TypeschemaGeneratorOptions; - private cacheConfig?: TypeSchemaConfig; - private cache?: TypeSchemaCache; private logger?: CodegenLogger; - constructor(options: TypeschemaGeneratorOptions = {}, cacheConfig?: TypeSchemaConfig) { + constructor(options: TypeschemaGeneratorOptions = {}) { this.options = { verbose: false, ...options }; this.manager = options.manager || CanonicalManager({ packages: [], workingDir: "tmp/fhir" }); - this.cacheConfig = cacheConfig; this.logger = options.logger || createLogger({ @@ -49,13 +44,6 @@ export class TypeSchemaGenerator { }); } - private async initializeCache(): Promise { - if (this.cacheConfig && !this.cache) { - this.cache = new TypeSchemaCache(this.cacheConfig); - await this.cache.initialize(); - } - } - async registerFromPackageMetas(packageMetas: PackageMeta[]): Promise { const packageNames = packageMetas.map(packageMetaToFhir); this.logger?.step(`Loading FHIR packages: ${packageNames.join(", ")}`); @@ -68,14 +56,11 @@ export class TypeSchemaGenerator { generateFhirSchemas(structureDefinitions: StructureDefinition[]): FHIRSchema[] { this.logger?.progress(`Converting ${structureDefinitions.length} StructureDefinitions to FHIRSchemas`); - // TODO: do it on the TypeSchema? - const filteredStructureDefinitions = this.applyStructureDefinitionTreeshaking(structureDefinitions); - const fhirSchemas: FHIRSchema[] = []; let convertedCount = 0; let failedCount = 0; - for (const sd of filteredStructureDefinitions) { + for (const sd of structureDefinitions) { try { const fhirSchema = fhirschema.translate(sd as StructureDefinition) as FHIRSchema; fhirSchemas.push(fhirSchema); @@ -91,7 +76,7 @@ export class TypeSchemaGenerator { } this.logger?.success( - `FHIR Schema conversion completed: ${convertedCount}/${filteredStructureDefinitions.length} successful, ${failedCount} failed`, + `FHIR Schema conversion completed: ${convertedCount}/${structureDefinitions.length} successful, ${failedCount} failed`, ); return fhirSchemas; } @@ -140,17 +125,6 @@ export class TypeSchemaGenerator { packageVersion: string | undefined, logger?: CodegenLogger, ): Promise { - await this.initializeCache(); - if (this.cache && !(this.cacheConfig?.forceRegenerate ?? false)) { - const cachedSchemas = this.cache.getByPackage(packageName); - if (cachedSchemas.length > 0) { - this.logger?.info( - `Using cached TypeSchemas for package: ${packageName} (${cachedSchemas.length} schemas)`, - ); - return cachedSchemas; - } - } - const packageInfo: PackageMeta = { name: packageName, version: packageVersion || "latest", @@ -163,164 +137,6 @@ export class TypeSchemaGenerator { ).flat(); const allSchemas = [...fhirSchemas, ...valueSets]; - if (this.cache) { - for (const schema of allSchemas) { - await this.cache.set(schema); - } - } - return allSchemas; } - - /** - * Apply treeshaking to StructureDefinitions before FHIR schema transformation - * This is more efficient and includes smart reference handling - */ - private applyStructureDefinitionTreeshaking(structureDefinitions: any[]): any[] { - const treeshakeList = this.options.treeshake; - - if (!treeshakeList || treeshakeList.length === 0) { - return structureDefinitions; - } - - this.logger?.info(`Applying treeshaking filter for ResourceTypes: ${treeshakeList.join(", ")}`); - - const allStructureDefinitions = new Map(); - const realDependencies = new Map>(); - const referenceTargets = new Map>(); - - for (const sd of structureDefinitions) { - const name = sd.name || sd.id; - if (name) { - allStructureDefinitions.set(name, sd); - realDependencies.set(name, new Set()); - referenceTargets.set(name, new Set()); - } - } - - for (const sd of structureDefinitions) { - const name = sd.name || sd.id; - if (!name) continue; - - const { realDeps, refTargets } = this.extractStructureDefinitionDependenciesWithReferences(sd); - realDependencies.set(name, new Set(realDeps)); - referenceTargets.set(name, new Set(refTargets)); - } - - const structureDefinitionsToKeep = new Set(); - - for (const resourceType of treeshakeList) { - if (allStructureDefinitions.has(resourceType)) { - structureDefinitionsToKeep.add(resourceType); - } else { - this.logger?.warn(`ResourceType '${resourceType}' not found in structure definitions`); - } - } - - const addRealDependenciesRecursively = (name: string, visited = new Set()) => { - if (visited.has(name) || !realDependencies.has(name)) { - return; - } - - visited.add(name); - const deps = realDependencies.get(name) || new Set(); - - for (const dep of Array.from(deps)) { - if (allStructureDefinitions.has(dep)) { - structureDefinitionsToKeep.add(dep); - addRealDependenciesRecursively(dep, visited); - } - } - }; - - for (const resourceType of Array.from(structureDefinitionsToKeep)) { - addRealDependenciesRecursively(resourceType); - } - - const filteredStructureDefinitions = structureDefinitions.filter((sd) => { - const name = sd.name || sd.id; - return name && structureDefinitionsToKeep.has(name); - }); - - const excludedReferenceTargets = new Set(); - for (const sd of structureDefinitions) { - const name = sd.name || sd.id; - if (name && !structureDefinitionsToKeep.has(name)) { - const isOnlyReferenceTarget = Array.from(referenceTargets.values()).some((targets) => - targets.has(name), - ); - - if (isOnlyReferenceTarget) { - excludedReferenceTargets.add(name); - } - } - } - - if (excludedReferenceTargets.size > 0) { - this.logger?.info(`Excluded reference-only targets: ${Array.from(excludedReferenceTargets).join(", ")}`); - } - - this.logger?.success( - `Treeshaking completed: kept ${filteredStructureDefinitions.length}/${structureDefinitions.length} structure definitions`, - ); - - return filteredStructureDefinitions; - } - - private extractStructureDefinitionDependenciesWithReferences(sd: any): { - realDeps: string[]; - refTargets: string[]; - } { - const realDeps = new Set(); - const refTargets = new Set(); - - if (sd.baseDefinition) { - const baseName = this.extractResourceNameFromUrl(sd.baseDefinition); - if (baseName) { - realDeps.add(baseName); - } - } - - if (sd.snapshot?.element || sd.differential?.element) { - const elements = sd.snapshot?.element || sd.differential?.element; - - for (const element of elements) { - if (element.type) { - for (const type of element.type) { - if (type.code) { - realDeps.add(type.code); - - if (type.code === "Reference" && type.targetProfile) { - for (const targetProfile of type.targetProfile) { - const targetName = this.extractResourceNameFromUrl(targetProfile); - if (targetName) { - refTargets.add(targetName); - } - } - } - } - - if (type.profile) { - for (const profile of type.profile) { - const profileName = this.extractResourceNameFromUrl(profile); - if (profileName) { - realDeps.add(profileName); - } - } - } - } - } - } - } - - return { - realDeps: Array.from(realDeps), - refTargets: Array.from(refTargets), - }; - } - - private extractResourceNameFromUrl(url: string): string | null { - const match = url.match(/\/([^/]+)$/); - return match ? (match[1] ?? null) : null; - } } diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index ab32453cd..c5e2dd451 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -16,7 +16,6 @@ import type { Register } from "./register"; import { packageMetaToFhir, type TypeSchema } from "./types"; // Re-export core dependencies -export { TypeSchemaCache } from "./cache"; export { TypeSchemaGenerator } from "./generator"; export { TypeSchemaParser } from "./parser"; export type { Identifier, TypeSchema } from "./types"; diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 82da8efcc..428d9626b 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -344,7 +344,7 @@ type ValueSetCompose = { include: { concept?: Concept[]; system?: string; - filter?: {}[]; + filter?: unknown[]; }[]; }; diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 219225c12..000000000 --- a/src/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Convert a string into PascalCase. - * Examples: - * - "patient-name" -> "PatientName" - * - "Patient name" -> "PatientName" - * - "patient_name" -> "PatientName" - * - "patientName" -> "PatientName" - */ -export function toPascalCase(input: string): string { - const parts = input - .replace(/[^A-Za-z0-9]+/g, " ") - .split(" ") - .map((p) => p.trim()) - .filter(Boolean); - - if (parts.length === 0) return ""; - - return parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); -} - -/** - * Split an array into chunks of a given size. - * - * Examples: - * - chunkArray([1,2,3,4,5], 2) -> [[1,2],[3,4],[5]] - * - chunkArray([], 3) -> [] - * - * @param arr - The array to split. - * @param size - The maximum size of each chunk (must be >= 1). - * @returns An array of chunks (each chunk is an array of T). - * @throws RangeError if size is less than 1. - */ -export function chunkArray(arr: T[], size: number): T[][] { - if (!Number.isInteger(size) || size < 1) { - throw new RangeError("chunk size must be an integer greater than 0"); - } - - const result: T[][] = []; - if (!arr || arr.length === 0) return result; - - for (let i = 0; i < arr.length; i += size) { - result.push(arr.slice(i, i + size)); - } - - return result; -} diff --git a/src/utils/codegen-logger.ts b/src/utils/codegen-logger.ts index e4757bcd5..e5a1285df 100644 --- a/src/utils/codegen-logger.ts +++ b/src/utils/codegen-logger.ts @@ -36,7 +36,7 @@ export class CodegenLogger { }; } - private static consoleLevelsMap: Record = { + private static consoleLevelsMap: Record void> = { [LogLevel.INFO]: console.log, [LogLevel.WARN]: console.warn, [LogLevel.ERROR]: console.error, @@ -183,7 +183,7 @@ export function info(message: string): void { defaultLogger.info(message); } -export function debug(message: string): void { +function _debug(message: string): void { defaultLogger.debug(message); } @@ -191,11 +191,11 @@ export function step(message: string): void { defaultLogger.step(message); } -export function progress(message: string): void { +function _progress(message: string): void { defaultLogger.progress(message); } -export function plain(message: string, color?: (str: string) => string): void { +function _plain(message: string, color?: (str: string) => string): void { defaultLogger.plain(message, color); } @@ -232,7 +232,7 @@ export function header(title: string): void { /** * Show a section break */ -export function section(title: string): void { +function _section(title: string): void { console.log(); console.log(pc.bold(title)); } @@ -266,7 +266,7 @@ export function list(items: string[], bullet = "•"): void { /** * Show key-value pairs */ -export function table(data: Record): void { +function _table(data: Record): void { const maxKeyLength = Math.max(...Object.keys(data).map((k) => k.length)); Object.entries(data).forEach(([key, value]) => { const paddedKey = key.padEnd(maxKeyLength); diff --git a/test/helpers/assertions.ts b/test/helpers/assertions.ts deleted file mode 100644 index b4a98e4c7..000000000 --- a/test/helpers/assertions.ts +++ /dev/null @@ -1,272 +0,0 @@ -/** - * Custom assertions for generator testing - */ - -import { expect } from "bun:test"; -import type { GeneratedFile } from "../../src/api/generators/base/types"; - -/** - * Assert that generated file has valid TypeScript syntax - */ -export function assertValidTypeScript(content: string, filename?: string): void { - const _context = filename ? ` in ${filename}` : ""; - - // Check balanced braces - const openBraces = (content.match(/\{/g) || []).length; - const closeBraces = (content.match(/\}/g) || []).length; - expect(openBraces).toBe(closeBraces); - - // Check balanced parentheses - const openParens = (content.match(/\(/g) || []).length; - const closeParens = (content.match(/\)/g) || []).length; - expect(openParens).toBe(closeParens); - - // Check for common TypeScript syntax - if (content.includes("interface")) { - expect(content).toMatch(/interface\s+\w+\s*\{/); - } - - if (content.includes("export")) { - const hasExportDeclaration = /export\s+(interface|type|const|function|class)/.test(content); - const hasExportStatement = /export\s*\{/.test(content); - expect(hasExportDeclaration || hasExportStatement).toBe(true); - } - - // Check for proper semicolons and line endings - const lines = content.split("\n"); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]?.trim(); - if (line && !line.startsWith("//") && !line.startsWith("/*") && !line.startsWith("*")) { - // Interface/type property lines should end with semicolon or be closing brace - if (line.match(/^\s*\w+[?]?:\s*[^;]+[^;}]$/)) { - expect(line).toMatch(/;$/); - } - } - } -} - -/** - * Assert file generation results meet quality standards - */ -export function assertGenerationQuality(files: GeneratedFile[]): void { - expect(files.length).toBeGreaterThan(0); - - for (const file of files) { - expect(file.filename).toMatch(/\.(ts|js|test)$/); - expect(file.content.length).toBeGreaterThan(0); - expect(file.exports.length).toBeGreaterThan(0); - expect(file.path).toBeTruthy(); - - // Check that content is not just whitespace - expect(file.content.trim()).not.toBe(""); - - // Check that exports are valid identifiers - for (const exportName of file.exports) { - expect(exportName).toMatch(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/); - } - } -} - -/** - * Assert that two generated file sets are equivalent - */ -export function assertFilesEquivalent(actual: GeneratedFile[], expected: GeneratedFile[]): void { - // Sort both arrays by filename for comparison - const sortedActual = actual.sort((a, b) => a.filename.localeCompare(b.filename)); - const sortedExpected = expected.sort((a, b) => a.filename.localeCompare(b.filename)); - - for (let i = 0; i < sortedActual.length; i++) { - const actualFile = sortedActual[i]!; - const expectedFile = sortedExpected[i]!; - - expect(actualFile.filename).toBe(expectedFile.filename); - expect(normalizeWhitespace(actualFile.content)).toBe(normalizeWhitespace(expectedFile.content)); - expect(new Set(actualFile.exports)).toEqual(new Set(expectedFile.exports)); - } -} - -/** - * Assert performance meets benchmarks - */ -export function assertPerformanceBenchmark(actualTime: number, baselineTime: number, tolerance: number = 0.1): void { - const maxAllowed = baselineTime * (1 + tolerance); - expect(actualTime).toBeLessThanOrEqual(maxAllowed); -} - -/** - * Assert memory usage is reasonable - */ -export function assertMemoryUsage(maxMemoryMB: number): void { - const memoryUsage = process.memoryUsage(); - const heapUsedMB = memoryUsage.heapUsed / 1024 / 1024; - - expect(heapUsedMB).toBeLessThanOrEqual(maxMemoryMB); -} - -/** - * Assert that content contains valid FHIR resource structure - */ -export function assertValidFHIRStructure(content: string, resourceType: string): void { - // Check for interface declaration - expect(content).toMatch(new RegExp(`interface\\s+${resourceType}\\s*\\{`)); - - // Check for common FHIR fields - expect(content).toMatch(/resourceType[?]?:\s*['"`]/); - - if (resourceType !== "Element" && resourceType !== "BackboneElement") { - expect(content).toMatch(/id[?]?:\s*string/); - } - - // Check export - expect(content).toMatch(new RegExp(`export\\s+.*${resourceType}`)); -} - -/** - * Assert code follows naming conventions - */ -export function assertNamingConventions(content: string): void { - // Interface names should be PascalCase - const interfaceMatches = content.match(/interface\s+(\w+)/g); - if (interfaceMatches) { - for (const match of interfaceMatches) { - const name = match.replace("interface ", ""); - expect(name).toMatch(/^[A-Z][a-zA-Z0-9]*$/); - } - } - - // Type names should be PascalCase - const typeMatches = content.match(/type\s+(\w+)/g); - if (typeMatches) { - for (const match of typeMatches) { - const name = match.replace("type ", ""); - expect(name).toMatch(/^[A-Z][a-zA-Z0-9]*$/); - } - } - - // Property names should be camelCase - const propertyMatches = content.match(/^\s*(\w+)[?]?:/gm); - if (propertyMatches) { - for (const match of propertyMatches) { - const name = match.replace(/^\s*/, "").replace(/[?]:.*$/, ""); - if (!name.startsWith("_") && !name.includes("_")) { - // Allow underscore prefixes and snake_case - expect(name).toMatch(/^[a-z][a-zA-Z0-9]*$/); - } - } - } -} - -/** - * Assert imports are properly organized - */ -export function assertImportOrganization(content: string): void { - const lines = content.split("\n"); - let importSection = true; - let lastImportLine = -1; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]?.trim(); - if (!line || line.startsWith("//") || line.startsWith("/*")) { - continue; - } - - if (line.startsWith("import ")) { - if (!importSection) { - expect(false).toBe(true); - } - lastImportLine = i; - } else if (line && !line.startsWith("import ")) { - importSection = false; - } - } - - // Check that there's proper spacing after imports - if (lastImportLine >= 0 && lastImportLine < lines.length - 1) { - const nextLine = lines[lastImportLine + 1]; - if (nextLine?.trim() && !nextLine.startsWith("//")) { - expect(nextLine.trim()).toBe(""); - } - } -} - -/** - * Assert content has proper JSDoc comments for exports - */ -export function assertHasDocumentation(content: string): void { - const exportMatches = content.match(/^export\s+(interface|type|class|const|function)\s+(\w+)/gm); - - if (exportMatches) { - const lines = content.split("\n"); - - for (const match of exportMatches) { - const _exportName = match.split(/\s+/).pop(); - const exportLineIndex = lines.findIndex((line) => line.includes(match)); - - if (exportLineIndex > 0) { - const previousLine = lines[exportLineIndex - 1]?.trim(); - const twoLinesBack = lines[exportLineIndex - 2]?.trim(); - - const hasJSDoc = - previousLine === "*/" || twoLinesBack?.startsWith("/**") || previousLine?.startsWith("//"); - - expect(hasJSDoc).toBe(true); - } - } - } -} - -/** - * Assert error messages are helpful and actionable - */ -export function assertHelpfulError(error: Error): void { - expect(error.message.length).toBeGreaterThan(10); - expect(error.message).not.toMatch(/undefined|null|\[object Object\]/); - - // Should contain suggestions or context - const hasSuggestion = - error.message.includes("try") || - error.message.includes("check") || - error.message.includes("ensure") || - error.message.includes("verify"); - expect(hasSuggestion).toBe(true); -} - -/** - * Assert array has expected structure and content - */ -export function assertArrayStructure( - array: T[], - expectedLength: { min?: number; max?: number; exact?: number }, - itemValidator?: (item: T, index: number) => void, -): void { - if (expectedLength.exact !== undefined) { - expect(array).toHaveLength(expectedLength.exact); - } else { - if (expectedLength.min !== undefined) { - expect(array.length).toBeGreaterThanOrEqual(expectedLength.min); - } - if (expectedLength.max !== undefined) { - expect(array.length).toBeLessThanOrEqual(expectedLength.max); - } - } - - if (itemValidator) { - array.forEach((item, index) => { - try { - itemValidator(item, index); - } catch (error) { - throw new Error( - `Item validation failed at index ${index}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }); - } -} - -// Helper functions -function normalizeWhitespace(content: string): string { - return content - .replace(/\s+/g, " ") - .replace(/\s*([{}();,])\s*/g, "$1") - .trim(); -} diff --git a/test/helpers/file-helpers.ts b/test/helpers/file-helpers.ts deleted file mode 100644 index 4fd7df5dc..000000000 --- a/test/helpers/file-helpers.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * File system testing utilities - */ - -import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; - -/** - * Test file system manager - */ -export class TestFileSystem { - constructor(private basePath: string) {} - - static async createTempTestDir(prefix = "codegen-test-"): Promise { - const tempPath = join(tmpdir(), `${prefix}${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); - const testFs = new TestFileSystem(tempPath); - await testFs.setup(); - return testFs; - } - - async setup(): Promise { - await this.cleanup(); - await mkdir(this.basePath, { recursive: true }); - } - - async cleanup(): Promise { - await rm(this.basePath, { recursive: true, force: true }); - } - - async writeTestFile(relativePath: string, content: string): Promise { - const fullPath = join(this.basePath, relativePath); - await mkdir(dirname(fullPath), { recursive: true }); - await writeFile(fullPath, content, "utf-8"); - } - - async readTestFile(relativePath: string): Promise { - const fullPath = join(this.basePath, relativePath); - return await readFile(fullPath, "utf-8"); - } - - async fileExists(relativePath: string): Promise { - try { - await readFile(join(this.basePath, relativePath)); - return true; - } catch { - return false; - } - } - - async listFiles(relativePath: string = ""): Promise { - try { - const { readdir } = await import("node:fs/promises"); - const fullPath = join(this.basePath, relativePath); - const files = await readdir(fullPath, { recursive: true }); - return files.filter((file) => typeof file === "string") as string[]; - } catch { - return []; - } - } - - getPath(relativePath: string = ""): string { - return join(this.basePath, relativePath); - } - - getBasePath(): string { - return this.basePath; - } -} - -/** - * File content expectations for testing - */ -export interface FileExpectation { - path: string; - content?: string; - contains?: string[]; - notContains?: string[]; - size?: { min?: number; max?: number }; -} - -/** - * Assert file system state matches expectations - */ -export async function assertFileSystemState(testFs: TestFileSystem, expectations: FileExpectation[]): Promise { - for (const expectation of expectations) { - const exists = await testFs.fileExists(expectation.path); - if (!exists) { - throw new Error(`Expected file does not exist: ${expectation.path}`); - } - - if (expectation.content !== undefined) { - const actualContent = await testFs.readTestFile(expectation.path); - if (actualContent !== expectation.content) { - throw new Error( - `File content mismatch for ${expectation.path}:\n` + - `Expected: ${expectation.content}\n` + - `Actual: ${actualContent}`, - ); - } - } - - if (expectation.contains) { - const actualContent = await testFs.readTestFile(expectation.path); - for (const substring of expectation.contains) { - if (!actualContent.includes(substring)) { - throw new Error(`File ${expectation.path} does not contain: ${substring}`); - } - } - } - - if (expectation.notContains) { - const actualContent = await testFs.readTestFile(expectation.path); - for (const substring of expectation.notContains) { - if (actualContent.includes(substring)) { - throw new Error(`File ${expectation.path} should not contain: ${substring}`); - } - } - } - - if (expectation.size) { - const actualContent = await testFs.readTestFile(expectation.path); - const actualSize = Buffer.byteLength(actualContent, "utf-8"); - - if (expectation.size.min !== undefined && actualSize < expectation.size.min) { - throw new Error(`File ${expectation.path} too small: ${actualSize} < ${expectation.size.min}`); - } - - if (expectation.size.max !== undefined && actualSize > expectation.size.max) { - throw new Error(`File ${expectation.path} too large: ${actualSize} > ${expectation.size.max}`); - } - } - } -} - -/** - * Create a temporary directory for testing - */ -export async function createTempTestDirectory(): Promise { - const tempPath = join(tmpdir(), `codegen-test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); - await mkdir(tempPath, { recursive: true }); - return tempPath; -} - -/** - * Clean up a temporary test directory - */ -export async function cleanupTempTestDirectory(path: string): Promise { - await rm(path, { recursive: true, force: true }); -} - -/** - * Write multiple test files at once - */ -export async function writeTestFiles(basePath: string, files: Record): Promise { - await Promise.all( - Object.entries(files).map(async ([relativePath, content]) => { - const fullPath = join(basePath, relativePath); - await mkdir(dirname(fullPath), { recursive: true }); - await writeFile(fullPath, content, "utf-8"); - }), - ); -} - -/** - * Read multiple test files at once - */ -export async function readTestFiles(basePath: string, relativePaths: string[]): Promise> { - const results: Record = {}; - - await Promise.all( - relativePaths.map(async (relativePath) => { - try { - const fullPath = join(basePath, relativePath); - results[relativePath] = await readFile(fullPath, "utf-8"); - } catch (error) { - results[relativePath] = `ERROR: ${error instanceof Error ? error.message : String(error)}`; - } - }), - ); - - return results; -} diff --git a/test/helpers/mock-generators.ts b/test/helpers/mock-generators.ts deleted file mode 100644 index a9e728a52..000000000 --- a/test/helpers/mock-generators.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Mock implementations for testing - */ - -import { BaseGenerator } from "@root/api/generators/base/BaseGenerator"; -import type { BaseGeneratorOptions, GeneratedFile, TemplateContext } from "@root/api/generators/base/types"; -import type { TypeSchema } from "@typeschema/types"; - -/** - * Mock logger that captures all log messages - */ -export class MockLogger { - public messages: Array<{ level: string; message: string; error?: Error }> = []; - - debug(message: string): void { - this.messages.push({ level: "debug", message }); - } - - info(message: string): void { - this.messages.push({ level: "info", message }); - } - - warn(message: string, error?: Error): void { - this.messages.push({ level: "warn", message, error }); - } - - error(message: string, error?: Error): void { - this.messages.push({ level: "error", message, error }); - } - - child(_prefix: string): MockLogger { - return new MockLogger(); - } - - hasLevel(level: string): boolean { - return this.messages.some((msg) => msg.level === level); - } - - getMessages(level?: string): Array<{ level: string; message: string; error?: Error }> { - return level ? this.messages.filter((msg) => msg.level === level) : this.messages; - } - - clear(): void { - this.messages = []; - } -} - -/** - * Simple test generator for testing base functionality - */ -export class TestGenerator extends BaseGenerator { - protected getLanguageName(): string { - return "TestLanguage"; - } - - protected getFileExtension(): string { - return ".test"; - } - - protected createTypeMapper(): any { - return { - formatTypeName: (name: string) => name, - formatFileName: (name: string) => name, - mapType: () => ({ name: "TestType", isPrimitive: false }), - }; - } - - protected async generateSchemaContent(schema: TypeSchema, context: TemplateContext): Promise { - const content = `// Test content for ${schema.identifier.name} -interface ${schema.identifier.name} { - id?: string; -} - -export { ${schema.identifier.name} };`; - - // Add exports to context - context.exports?.add(schema.identifier.name); - - return content; - } - - protected async validateContent(content: string, _context: TemplateContext): Promise { - if (content.includes("INVALID")) { - throw new Error("Test validation error"); - } - } - - protected filterAndSortSchemas(schemas: TypeSchema[]): TypeSchema[] { - return schemas.sort((a, b) => a.identifier.name.localeCompare(b.identifier.name)); - } -} - -/** - * Mock file manager for testing without file system - */ -export class MockFileManager { - public writtenFiles = new Map(); - public writeDelay = 0; // Simulate slow filesystem - - async writeFile( - path: string, - content: string, - ): Promise<{ - path: string; - size: number; - writeTime: number; - }> { - if (this.writeDelay > 0) { - await new Promise((resolve) => setTimeout(resolve, this.writeDelay)); - } - - this.writtenFiles.set(path, content); - - return { - path, - size: content.length, - writeTime: this.writeDelay, - }; - } - - async writeBatch(files: Map): Promise< - Array<{ - path: string; - size: number; - writeTime: number; - }> - > { - const results = []; - for (const [path, content] of files) { - results.push(await this.writeFile(path, content)); - } - return results; - } - - getWrittenFile(path: string): string | undefined { - return this.writtenFiles.get(path); - } - - hasWrittenFile(path: string): boolean { - return this.writtenFiles.has(path); - } - - getWrittenFiles(): Map { - return new Map(this.writtenFiles); - } - - clear(): void { - this.writtenFiles.clear(); - } -} diff --git a/test/helpers/schema-helpers.ts b/test/helpers/schema-helpers.ts deleted file mode 100644 index 1ac1e143e..000000000 --- a/test/helpers/schema-helpers.ts +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Helper functions for creating test schemas - */ - -import type { TypeSchema } from "@typeschema/index"; - -/** - * Create a minimal valid schema for testing - */ -export function createMockSchema(overrides: Partial = {}): TypeSchema { - const baseSchema: TypeSchema = { - identifier: { - name: "TestSchema", - kind: "resource", - package: "test.package", - url: "http://test.com/StructureDefinition/TestSchema", - version: "1.0.0", - ...overrides.identifier, - }, - description: "Test schema for unit tests", - fields: { - id: { - type: { - name: "string", - kind: "primitive-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/string", - }, - required: false, - array: false, - }, - status: { - type: { - name: "code", - kind: "primitive-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/code", - }, - required: true, - array: false, - enum: ["active", "inactive"], - }, - }, - ...overrides, - } as TypeSchema; - - return baseSchema; -} - -/** - * Create multiple schemas for batch testing - */ -export function createMockSchemas(names: string[]): TypeSchema[] { - return names.map((name) => - createMockSchema({ - identifier: { - name, - kind: "resource", - package: "test.package", - url: `http://test.com/StructureDefinition/${name}`, - version: "1.0.0", - }, - }), - ); -} - -/** - * Create schema with complex nested structure - */ -export function createComplexNestedSchema(name: string): TypeSchema { - return createMockSchema({ - identifier: { - name, - kind: "resource", - package: "test.package", - url: `http://test.com/StructureDefinition/${name}`, - version: "1.0.0", - }, - fields: { - id: { - type: { - name: "string", - kind: "primitive-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/string", - }, - required: true, - array: false, - }, - nested: { - type: { - name: `${name}Nested`, - kind: "nested", - package: "test.package", - version: "1.0.0", - url: `http://test.com/StructureDefinition/${name}Nested`, - }, - required: false, - array: true, - }, - reference: { - type: { - name: "Reference", - kind: "complex-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Reference", - }, - reference: [ - { - name: "Patient", - kind: "resource", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Patient", - }, - { - name: "Practitioner", - kind: "resource", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Practitioner", - }, - ], - required: false, - array: false, - }, - }, - nested: [ - { - identifier: { - name: `${name}Nested`, - kind: "nested", - package: "test.package", - version: "1.0.0", - url: `http://test.com/StructureDefinition/${name}Nested`, - }, - base: { - name: "BackboneElement", - kind: "complex-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/BackboneElement", - }, - fields: { - value: { - type: { - name: "string", - kind: "primitive-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/string", - }, - required: true, - array: false, - }, - }, - }, - ], - }) as TypeSchemaForResourceComplexTypeLogical; -} - -/** - * Generate schemas with various edge cases - */ -export function generateEdgeCaseSchemas(): TypeSchema[] { - return [ - // Schema with empty fields - createMockSchema({ fields: {} }), - - // Schema with very long name - createMockSchema({ - identifier: { - name: "A".repeat(100), - kind: "resource", - package: "test.package", - url: `http://test.com/StructureDefinition/${"A".repeat(100)}`, - version: "1.0.0", - }, - }), - - // Schema with special characters in name - createMockSchema({ - identifier: { - name: "Test-Schema_With.Special@Chars", - kind: "resource", - package: "test.package", - url: "http://test.com/StructureDefinition/Test-Schema_With.Special@Chars", - version: "1.0.0", - }, - }), - - // Profile schema - { - identifier: { - name: "USCorePatient", - kind: "profile", - package: "us.core", - version: "1.0.0", - url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient", - }, - base: { - name: "Patient", - kind: "resource", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Patient", - }, - description: "US Core Patient Profile", - } as TypeSchemaForProfile, - ]; -} - -/** - * Create malformed schemas for error testing - */ -export function generateMalformedSchemas(count: number): Partial[] { - const malformed: Partial[] = []; - - for (let i = 0; i < count; i++) { - switch (i % 5) { - case 0: - // Missing identifier - malformed.push({ description: "Schema without identifier" }); - break; - case 1: - // Invalid identifier kind - malformed.push({ - identifier: { - name: "Test", - kind: "invalid-kind" as any, - package: "test.package", - version: "1.0.0", - url: "http://test.com/StructureDefinition/Test", - }, - }); - break; - case 2: - // Circular reference - malformed.push({ - identifier: { - name: "Circular", - kind: "resource", - package: "test.package", - version: "1.0.0", - url: "http://test.com/StructureDefinition/Circular", - }, - fields: { - self: { - type: { - name: "Circular", - kind: "resource", - package: "test.package", - version: "1.0.0", - url: "http://test.com/StructureDefinition/Circular", - }, - required: false, - array: false, - }, - }, - }); - break; - case 3: - // Invalid field type - malformed.push({ - identifier: { - name: "InvalidField", - kind: "resource", - package: "test.package", - version: "1.0.0", - url: "http://test.com/StructureDefinition/InvalidField", - }, - fields: { - badField: { - type: null as any, - required: true, - array: false, - }, - }, - }); - break; - case 4: - // Missing required properties - malformed.push({ - identifier: { - name: "Incomplete", - package: "test.package", - version: "1.0.0", - url: "http://test.com/StructureDefinition/Incomplete", - } as any, - }); - break; - } - } - - return malformed; -} - -/** - * Create a primitive type schema - */ -export function createPrimitiveTypeSchema(name: string): TypeSchemaForPrimitiveType { - return { - identifier: { - name, - kind: "primitive-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: `http://hl7.org/fhir/StructureDefinition/${name}`, - }, - description: `FHIR primitive type: ${name}`, - base: { - name: "Element", - kind: "complex-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Element", - }, - }; -} diff --git a/test/integration/typescript-generation.test.ts b/test/integration/typescript-generation.test.ts deleted file mode 100644 index 3cebdf139..000000000 --- a/test/integration/typescript-generation.test.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Integration tests for TypeScript generation workflow - */ - -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { assertGenerationQuality } from "../helpers/assertions"; -import { assertFileSystemState, TestFileSystem } from "../helpers/file-helpers"; -import { MockLogger, TestGenerator } from "../helpers/mock-generators"; -import { - createComplexNestedSchema, - createMockSchemas, - createPrimitiveTypeSchema, - generateEdgeCaseSchemas, -} from "../helpers/schema-helpers"; - -describe("TypeScript Generation Integration", () => { - let generator: TestGenerator; - let logger: MockLogger; - let testFs: TestFileSystem; - - beforeEach(async () => { - logger = new MockLogger(); - testFs = await TestFileSystem.createTempTestDir(); - - generator = new TestGenerator({ - outputDir: testFs.getBasePath(), - logger: logger as any, - verbose: false, - validate: true, - }); - }); - - afterEach(async () => { - await testFs.cleanup(); - }); - - describe("End-to-End Generation Workflow", () => { - test("generates TypeScript files for FHIR resources", async () => { - const schemas = createMockSchemas(["Patient", "Observation", "Practitioner"]); - - const results = await generator.build(schemas); - - // Verify generation quality - assertGenerationQuality(results); - expect(results).toHaveLength(3); - - // Check specific files were generated - const filenames = results.map((r) => r.filename); - expect(filenames).toContain("Observation.test"); - expect(filenames).toContain("Patient.test"); - expect(filenames).toContain("Practitioner.test"); - - // Verify content quality - for (const file of results) { - expect(file.content).toContain(`Test content for ${file.filename.replace(".test", "")}`); - expect(file.exports.length).toBeGreaterThan(0); - } - }); - - test("handles complex nested schema structures", async () => { - const complexSchema = createComplexNestedSchema("DiagnosticReport"); - - const results = await generator.build([complexSchema]); - - expect(results).toHaveLength(1); - const file = results[0]!; - - expect(file.content).toContain("Test content for DiagnosticReport"); - expect(file.filename).toBe("DiagnosticReport.test"); - }); - - test("processes mixed schema types", async () => { - const schemas = [ - ...createMockSchemas(["Patient"]), - createPrimitiveTypeSchema("string"), - createComplexNestedSchema("Observation"), - ]; - - const results = await generator.build(schemas); - - expect(results).toHaveLength(3); - assertGenerationQuality(results); - - // Verify mixed types are handled correctly - const filenames = results.map((r) => r.filename); - expect(filenames).toContain("Patient.test"); - expect(filenames).toContain("string.test"); - expect(filenames).toContain("Observation.test"); - }); - - test("handles edge case schemas", async () => { - const edgeCaseSchemas = generateEdgeCaseSchemas(); - - const results = await generator.build(edgeCaseSchemas); - - // Should handle all edge cases without throwing - expect(results.length).toBeGreaterThan(0); - - // Each result should have valid content - for (const file of results) { - expect(file.content.length).toBeGreaterThan(0); - expect(file.filename).toMatch(/\.test$/); - } - }); - }); - - describe("File System Integration", () => { - test("writes files to correct locations", async () => { - // Use actual file generation instead of build() - const schemas = createMockSchemas(["Patient", "Observation"]); - - const results = await generator.generate({ schemas }); - - // Verify files were written to filesystem - await assertFileSystemState(testFs, [ - { - path: "Patient.test", - contains: ["Test content for Patient"], - size: { min: 10 }, - }, - { - path: "Observation.test", - contains: ["Test content for Observation"], - size: { min: 10 }, - }, - ]); - - expect(results).toHaveLength(2); - }); - - test("handles file conflicts gracefully", async () => { - const schemas = createMockSchemas(["Patient"]); - - // Generate files twice to test overwrite behavior - await generator.generate({ schemas }); - const results = await generator.generate({ schemas }); - - expect(results).toHaveLength(1); - expect(await testFs.fileExists("Patient.test")).toBe(true); - }); - - test("creates proper directory structure", async () => { - const schemas = createMockSchemas(["Patient"]); - - await generator.generate({ schemas }); - - const fileList = await testFs.listFiles(); - expect(fileList).toContain("Patient.test"); - }); - }); - - describe("Progress Reporting", () => { - test("reports progress during generation", async () => { - const progressUpdates: Array<{ - phase: string; - current: number; - total: number; - message?: string; - }> = []; - - generator.onProgress((phase, current, total, message) => { - progressUpdates.push({ phase, current, total, message }); - }); - - const schemas = createMockSchemas(["Patient", "Observation", "Practitioner"]); - await generator.build(schemas); - - // Should have progress updates for different phases - expect(progressUpdates.length).toBeGreaterThan(0); - expect(progressUpdates.some((u) => u.phase === "validation")).toBe(true); - expect(progressUpdates.some((u) => u.phase === "complete")).toBe(true); - - // Progress should increment correctly - const validationUpdates = progressUpdates.filter((u) => u.phase === "validation"); - if (validationUpdates.length > 1) { - expect(validationUpdates[0]?.current).toBeLessThan( - validationUpdates[validationUpdates.length - 1]?.current, - ); - } - }); - - test("reports accurate completion statistics", async () => { - const schemas = createMockSchemas(["A", "B", "C"]); - await generator.build(schemas); - - const stats = generator.getGenerationStats(); - - expect(stats.filesGenerated).toBe(3); - expect(stats.totalSize).toBeGreaterThan(0); - expect(stats.averageFileSize).toBeGreaterThan(0); - expect(stats.generationTime).toBeGreaterThan(0); - expect(stats.averageTimePerFile).toBe(stats.generationTime / stats.filesGenerated); - }); - }); - - describe("Error Recovery and Resilience", () => { - test("continues generation after non-critical errors", async () => { - const validSchemas = createMockSchemas(["Patient", "Observation"]); - - // Create a generator that validates content strictly - const strictGenerator = new (class extends TestGenerator { - protected override async generateSchemaContent(schema: any, context: any): Promise { - if (schema.identifier.name === "Patient") { - return "INVALID content"; // This will fail validation - } - return super.generateSchemaContent(schema, context); - } - })({ - outputDir: testFs.getBasePath(), - logger: logger as any, - validate: true, - }); - - // Should handle validation failure gracefully - await expect(strictGenerator.build(validSchemas)).rejects.toThrow(); - - // Logger should have captured the error - expect(logger.hasLevel("error")).toBe(true); - }); - - test("provides helpful error context for debugging", async () => { - const invalidSchema = { - identifier: { name: null }, // Invalid identifier - } as any; - - try { - await generator.build([invalidSchema]); - } catch (error) { - expect(error instanceof Error).toBe(true); - expect(error.message).toBeTruthy(); - } - - // Should log helpful debugging information - expect(logger.messages.length).toBeGreaterThan(0); - }); - }); - - describe("Performance Under Load", () => { - test("handles reasonable number of schemas efficiently", async () => { - const schemaNames = Array.from({ length: 10 }, (_, i) => `Schema${i}`); - const schemas = createMockSchemas(schemaNames); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(10); - expect(duration).toBeLessThan(1000); // Should complete within 1 second - - // All files should be valid - assertGenerationQuality(results); - }); - - test("memory usage remains reasonable", async () => { - const schemaNames = Array.from({ length: 20 }, (_, i) => `MemoryTest${i}`); - const schemas = createMockSchemas(schemaNames); - - const memoryBefore = process.memoryUsage().heapUsed; - await generator.build(schemas); - const memoryAfter = process.memoryUsage().heapUsed; - - const memoryIncrease = (memoryAfter - memoryBefore) / 1024 / 1024; // MB - - // Memory increase should be reasonable (less than 50MB for 20 schemas) - expect(memoryIncrease).toBeLessThan(50); - }); - }); - - describe("Configuration Integration", () => { - test("respects validation settings", async () => { - const nonValidatingGenerator = new TestGenerator({ - outputDir: testFs.getBasePath(), - logger: logger as any, - validate: false, // Disable validation - }); - - const schemas = createMockSchemas(["Test"]); - const results = await nonValidatingGenerator.build(schemas); - - expect(results).toHaveLength(1); - // Should have validation disabled message in logs - const debugMessages = logger.getMessages("debug"); - expect(debugMessages.some((m) => m.message.includes("validation disabled"))).toBe(true); - }); - - test("respects verbose logging settings", async () => { - const verboseGenerator = new TestGenerator({ - outputDir: testFs.getBasePath(), - logger: logger as any, - verbose: true, - }); - - const schemas = createMockSchemas(["Test"]); - await verboseGenerator.build(schemas); - - // Should have more detailed logging - const infoMessages = logger.getMessages("info"); - expect(infoMessages.length).toBeGreaterThan(0); - }); - }); - - describe("API Compatibility", () => { - test("fluent API methods throw appropriate errors", async () => { - // File and index builders should throw when no template engine is available - expect(() => generator.file("test-file")).toThrow("Template engine is required"); - expect(() => generator.index(".")).toThrow("Template engine is required"); - - // Directory builder should work without template engine - const dirBuilder = generator.directory("test-dir"); - expect(dirBuilder).toBeDefined(); - }); - - test("progress callbacks integrate properly", async () => { - let callbackCalled = false; - - const result = generator.onProgress(() => { - callbackCalled = true; - }); - - expect(result).toBe(generator); // Should return self for chaining - - const schemas = createMockSchemas(["Test"]); - await generator.build(schemas); - - expect(callbackCalled).toBe(true); - }); - - test("capabilities reporting works correctly", () => { - const capabilities = generator.getCapabilities(); - - expect(capabilities.language).toBe("TestLanguage"); - expect(capabilities.fileExtensions).toContain(".test"); - expect(capabilities.supportsTemplates).toBe(true); - expect(capabilities.supportsValidation).toBe(true); - expect(capabilities.version).toBeTruthy(); - }); - }); -}); diff --git a/test/integration/value-set-workflow.test.ts b/test/integration/value-set-workflow.test.ts deleted file mode 100644 index 4057a2a90..000000000 --- a/test/integration/value-set-workflow.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Integration tests for value set workflow - */ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import * as fs from "node:fs"; -import type { BindingTypeSchema, TypeSchema } from "@typeschema/types"; -import { TypeScriptGenerator } from "../../src/api/generators/typescript"; -import { createLogger } from "../../src/utils/codegen-logger"; - -describe("Value Set Integration Workflow", () => { - const testOutputDir = "/tmp/test-value-sets"; - let generator: TypeScriptGenerator; - - beforeEach(async () => { - // Clean up test directory - if (fs.existsSync(testOutputDir)) { - fs.rmSync(testOutputDir, { recursive: true, force: true }); - } - fs.mkdirSync(testOutputDir, { recursive: true }); - - generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - valueSetMode: "custom", - valueSetStrengths: ["required", "preferred"], - valueSetDirectory: "valuesets", - outputDir: testOutputDir, - logger: createLogger({ prefix: "Test", verbose: false }), - }); - }); - - afterEach(() => { - // Clean up test directory - if (fs.existsSync(testOutputDir)) { - fs.rmSync(testOutputDir, { recursive: true, force: true }); - } - }); - - test("should generate complete value set workflow", async () => { - const schemas: TypeSchema[] = [ - // Required binding schema - { - identifier: { - kind: "binding", - name: "AdministrativeGender", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/administrative-gender", - }, - strength: "required", - enum: ["male", "female", "other", "unknown"], - valueset: { url: "http://hl7.org/fhir/ValueSet/administrative-gender" }, - } as BindingTypeSchema, - - // Interface schema using the bindings - { - identifier: { - kind: "complex-type", - name: "Patient", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/Patient", - }, - fields: { - gender: { - type: { kind: "primitive-type", name: "code" }, - binding: { name: "AdministrativeGender" }, - required: false, - }, - }, - }, - ]; - - const results = await generator.generate({ schemas } as any); - - // The main files (Patient.ts) should be generated - expect(results.length).toBeGreaterThan(0); - - // Check that we have a Patient interface file - const patientFile = results.find((r) => r.filename === "Patient.ts"); - expect(patientFile).toBeDefined(); - - // Value set files are generated in post-generation hooks and written to disk - // but not currently included in the return array due to architectural limitations. - // The generation is successful as evidenced by the log messages. - - // Check that value sets are collected by the generator - const collectedValueSets = (generator as any).collectedValueSets; - expect(collectedValueSets.size).toBeGreaterThan(0); - expect(collectedValueSets.has("AdministrativeGender")).toBe(true); - }); - - test("should generate valid TypeScript content", async () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "TestBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2", "value3"], - valueset: { url: "http://test.com/valueset" }, - }; - - const _results = await generator.generate({ schemas: [schema] } as any); - - // Value sets are processed but not returned in the results array - // Check that the value set was collected and can generate correct content - const collectedValueSets = (generator as any).collectedValueSets; - expect(collectedValueSets.has("TestBinding")).toBe(true); - - if (collectedValueSets.has("TestBinding")) { - const binding = collectedValueSets.get("TestBinding"); - const generatedContent = (generator as any).generateValueSetFile(binding); - - // Check that the generated content has correct structure - expect(generatedContent).toContain("export const TestBindingValues"); - expect(generatedContent).toContain("export type TestBinding"); - expect(generatedContent).toContain("] as const;"); - expect(generatedContent).toContain("export const isValidTestBinding"); - } - }); - - test("should handle empty schema list gracefully", async () => { - const results = await generator.generate({ schemas: [] }); - - // Should still generate some files but no value sets - const valueSetFiles = results.filter((r) => r.filename.startsWith("valuesets/")); - expect(valueSetFiles.length).toBe(0); - }); -}); diff --git a/test/performance/generation-speed.test.ts b/test/performance/generation-speed.test.ts deleted file mode 100644 index d4134d393..000000000 --- a/test/performance/generation-speed.test.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Performance benchmarks for code generation speed - */ - -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { assertMemoryUsage, assertPerformanceBenchmark } from "../helpers/assertions"; -import { TestFileSystem } from "../helpers/file-helpers"; -import { MockLogger, TestGenerator } from "../helpers/mock-generators"; -import { createComplexNestedSchema, createMockSchemas } from "../helpers/schema-helpers"; - -describe.skip("Generation Speed Performance", () => { - let generator: TestGenerator; - let logger: MockLogger; - let testFs: TestFileSystem; - - beforeEach(async () => { - logger = new MockLogger(); - testFs = await TestFileSystem.createTempTestDir(); - - generator = new TestGenerator({ - outputDir: testFs.getBasePath(), - logger: logger as any, - verbose: false, // Reduce logging overhead for performance tests - }); - }); - - afterEach(async () => { - await testFs.cleanup(); - }); - - describe("Small Schema Sets (1-10 schemas)", () => { - test("generates single schema within performance target", async () => { - const schema = createMockSchemas(["Patient"])[0]!; - - const startTime = performance.now(); - const results = await generator.build([schema]); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(1); - - // Single schema should generate in under 50ms - assertPerformanceBenchmark(duration, 50, 1.0); // 100% tolerance for CI variance - }); - - test("generates 5 schemas within performance target", async () => { - const schemas = createMockSchemas(["Patient", "Observation", "Practitioner", "Organization", "Location"]); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(5); - - // 5 schemas should generate in under 200ms - assertPerformanceBenchmark(duration, 200, 0.5); - - // Check average time per schema - const averageTime = duration / schemas.length; - expect(averageTime).toBeLessThan(50); // Under 50ms per schema on average - }); - - test("generates 10 schemas within performance target", async () => { - const schemaNames = Array.from({ length: 10 }, (_, i) => `Schema${i}`); - const schemas = createMockSchemas(schemaNames); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(10); - - // 10 schemas should generate in under 400ms - assertPerformanceBenchmark(duration, 400, 0.5); - }); - }); - - describe("Medium Schema Sets (11-50 schemas)", () => { - test("generates 25 schemas within performance target", async () => { - const schemaNames = Array.from({ length: 25 }, (_, i) => `MediumSchema${i}`); - const schemas = createMockSchemas(schemaNames); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(25); - - // 25 schemas should generate in under 1 second - assertPerformanceBenchmark(duration, 1000, 0.5); - - // Check performance scales reasonably - const averageTime = duration / schemas.length; - expect(averageTime).toBeLessThan(50); // Should maintain good per-schema performance - }); - - test("generates 50 schemas within performance target", async () => { - const schemaNames = Array.from({ length: 50 }, (_, i) => `LargeSchema${i}`); - const schemas = createMockSchemas(schemaNames); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(50); - - // 50 schemas should generate in under 2 seconds - assertPerformanceBenchmark(duration, 2000, 0.5); - }); - }); - - describe("Large Schema Sets (51+ schemas)", () => { - test("generates 100 schemas within performance target", async () => { - const schemaNames = Array.from({ length: 100 }, (_, i) => `VeryLargeSchema${i}`); - const schemas = createMockSchemas(schemaNames); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(100); - - // 100 schemas should generate in under 4 seconds - assertPerformanceBenchmark(duration, 4000, 0.5); - - // Performance should scale sub-linearly (better than O(n)) - const averageTime = duration / schemas.length; - expect(averageTime).toBeLessThan(50); // Should maintain efficiency - }); - - test("handles scaling to 200 schemas", async () => { - const schemaNames = Array.from({ length: 200 }, (_, i) => `MassiveSchema${i}`); - const schemas = createMockSchemas(schemaNames); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(200); - - // 200 schemas should generate in under 8 seconds - assertPerformanceBenchmark(duration, 8000, 0.5); - }); - }); - - describe("Complex Schema Performance", () => { - test("handles complex nested schemas efficiently", async () => { - const complexSchemas = [ - createComplexNestedSchema("DiagnosticReport"), - createComplexNestedSchema("Bundle"), - createComplexNestedSchema("Composition"), - createComplexNestedSchema("QuestionnaireResponse"), - createComplexNestedSchema("MessageDefinition"), - ]; - - const startTime = performance.now(); - const results = await generator.build(complexSchemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(5); - - // Complex schemas should still complete in reasonable time - assertPerformanceBenchmark(duration, 500, 0.5); - }); - - test("performance with mixed complexity schemas", async () => { - const mixedSchemas = [ - ...createMockSchemas(["Simple1", "Simple2"]), - createComplexNestedSchema("Complex1"), - ...createMockSchemas(["Simple3", "Simple4"]), - createComplexNestedSchema("Complex2"), - ]; - - const startTime = performance.now(); - const results = await generator.build(mixedSchemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(6); - - // Mixed complexity should handle well - assertPerformanceBenchmark(duration, 400, 0.5); - }); - }); - - describe("Comparative Performance", () => { - test("build() vs generate() performance comparison", async () => { - const schemas = createMockSchemas(["Perf1", "Perf2", "Perf3", "Perf4", "Perf5"]); - - // Test build() performance (no file I/O) - const buildStart = performance.now(); - const buildResults = await generator.build(schemas); - const buildDuration = performance.now() - buildStart; - - // Test generate() performance (with file I/O) - const generateStart = performance.now(); - const generateResults = await generator.generate(schemas); - const generateDuration = performance.now() - generateStart; - - expect(buildResults).toHaveLength(5); - expect(generateResults).toHaveLength(5); - - // build() should be faster than generate() - expect(buildDuration).toBeLessThan(generateDuration); - - // File I/O overhead should be reasonable - const ioOverhead = generateDuration - buildDuration; - expect(ioOverhead).toBeLessThan(200); // Less than 200ms overhead for 5 files - }); - - test("validation enabled vs disabled performance", async () => { - const schemas = createMockSchemas(["Val1", "Val2", "Val3", "Val4", "Val5"]); - - // Test with validation enabled - const validatingGenerator = new TestGenerator({ - outputDir: testFs.getBasePath(), - logger: logger as any, - validate: true, - }); - - const validationStart = performance.now(); - await validatingGenerator.build(schemas); - const validationDuration = performance.now() - validationStart; - - // Test with validation disabled - const nonValidatingGenerator = new TestGenerator({ - outputDir: testFs.getBasePath(), - logger: logger as any, - validate: false, - }); - - const noValidationStart = performance.now(); - await nonValidatingGenerator.build(schemas); - const noValidationDuration = performance.now() - noValidationStart; - - // Validation should add some overhead but not too much - expect(validationDuration).toBeGreaterThan(noValidationDuration); - - const validationOverhead = validationDuration - noValidationDuration; - expect(validationOverhead).toBeLessThan(100); // Less than 100ms validation overhead - }); - }); - - describe("Regression Testing", () => { - test("performance regression detection", async () => { - const schemas = createMockSchemas(["Regression1", "Regression2", "Regression3"]); - - // Run multiple times to get stable baseline - const runs: number[] = []; - for (let i = 0; i < 5; i++) { - const startTime = performance.now(); - await generator.build(schemas); - runs.push(performance.now() - startTime); - } - - const averageTime = runs.reduce((a, b) => a + b, 0) / runs.length; - const maxTime = Math.max(...runs); - const minTime = Math.min(...runs); - - // Results should be consistent (low variance) - const variance = maxTime - minTime; - expect(variance).toBeLessThan(averageTime * 0.5); // Variance should be < 50% of average - - // Average should be within expected range - expect(averageTime).toBeLessThan(200); // 3 schemas in under 200ms - }); - - test("performance with realistic FHIR workload", async () => { - // Simulate a realistic FHIR package with various resource types - const realisticSchemas = createMockSchemas([ - "Patient", - "Observation", - "Practitioner", - "Organization", - "Location", - "Encounter", - "Procedure", - "Medication", - "Device", - "Specimen", - "DiagnosticReport", - "Condition", - "AllergyIntolerance", - "Immunization", - "CarePlan", - ]); - - const startTime = performance.now(); - const results = await generator.build(realisticSchemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(15); - - // Realistic FHIR workload should complete in under 1 second - assertPerformanceBenchmark(duration, 1000, 0.3); - }); - }); - - describe("Resource Efficiency", () => { - test("memory usage stays reasonable during generation", async () => { - const schemas = createMockSchemas(Array.from({ length: 50 }, (_, i) => `MemoryTest${i}`)); - - const memoryBefore = process.memoryUsage(); - await generator.build(schemas); - const memoryAfter = process.memoryUsage(); - - const heapIncrease = (memoryAfter.heapUsed - memoryBefore.heapUsed) / 1024 / 1024; - - // Should not use excessive memory (less than 20MB for 50 schemas) - assertMemoryUsage(memoryBefore.heapUsed / 1024 / 1024 + 20); - expect(heapIncrease).toBeLessThan(20); - }); - - test("garbage collection behavior", async () => { - // Force garbage collection if available - if (global.gc) { - global.gc(); - } - - const initialMemory = process.memoryUsage().heapUsed; - - // Generate many schemas - for (let batch = 0; batch < 5; batch++) { - const schemas = createMockSchemas([`Batch${batch}_A`, `Batch${batch}_B`]); - await generator.build(schemas); - } - - // Force garbage collection again - if (global.gc) { - global.gc(); - } - - const finalMemory = process.memoryUsage().heapUsed; - const memoryGrowth = (finalMemory - initialMemory) / 1024 / 1024; - - // Memory growth should be minimal after GC - expect(memoryGrowth).toBeLessThan(10); // Less than 10MB growth - }); - }); -}); diff --git a/test/unit/api/generators/base/BaseGenerator.test.ts b/test/unit/api/generators/base/BaseGenerator.test.ts deleted file mode 100644 index d08fd3155..000000000 --- a/test/unit/api/generators/base/BaseGenerator.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Unit tests for BaseGenerator - */ - -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { assertGenerationQuality, assertPerformanceBenchmark } from "../../../../helpers/assertions"; -import { TestFileSystem } from "../../../../helpers/file-helpers"; -import { MockLogger, TestGenerator } from "../../../../helpers/mock-generators"; -import { createMockSchema, createMockSchemas, generateMalformedSchemas } from "../../../../helpers/schema-helpers"; - -describe("BaseGenerator", () => { - let generator: TestGenerator; - let logger: MockLogger; - let testFs: TestFileSystem; - - beforeEach(async () => { - logger = new MockLogger(); - testFs = await TestFileSystem.createTempTestDir(); - - generator = new TestGenerator({ - outputDir: testFs.getBasePath(), - logger: logger as any, - verbose: true, - }); - }); - - afterEach(async () => { - await testFs.cleanup(); - }); - - describe("Initialization", () => { - test("initializes with valid configuration", () => { - expect(generator).toBeDefined(); - expect(logger.hasLevel("debug")).toBe(true); - }); - - test("throws error with invalid configuration", () => { - expect(() => { - new TestGenerator({} as any); - }).toThrow(); - }); - - test("validates output directory requirement", () => { - expect(() => { - new TestGenerator({ outputDir: "" }); - }).toThrow("outputDir is required"); - }); - }); - - describe("Schema Processing", () => { - test("processes single schema successfully", async () => { - const schema = createMockSchema(); - const results = await generator.build([schema]); - - assertGenerationQuality(results); - expect(results).toHaveLength(1); - expect(results[0]?.filename).toMatch(/\.test$/); - }); - - test("processes multiple schemas in correct order", async () => { - const schemas = createMockSchemas(["ZebraSchema", "AlphaSchema", "BetaSchema"]); - const results = await generator.build(schemas); - - expect(results).toHaveLength(3); - - // Results should be sorted alphabetically by schema name - const filenames = results.map((r) => r.filename); - expect(filenames[0]).toContain("AlphaSchema"); - expect(filenames[1]).toContain("BetaSchema"); - expect(filenames[2]).toContain("ZebraSchema"); - }); - - test("handles empty schema array", async () => { - const results = await generator.build([]); - expect(results).toHaveLength(0); - }); - - test("validates schemas before processing", async () => { - const malformedSchemas = generateMalformedSchemas(1); - - await expect(generator.build(malformedSchemas as any)).rejects.toThrow(); - }); - }); - - describe("Content Generation", () => { - test("generates expected content structure", async () => { - const schema = createMockSchema({ - identifier: { - name: "TestResource", - kind: "resource", - package: "test.package", - version: "1.0.0", - url: "http://test.com/StructureDefinition/TestResource", - }, - }); - - const results = await generator.build([schema]); - const content = results[0]?.content; - - expect(content).toContain("// Test content for TestResource"); - expect(content).toBeTruthy(); - }); - - test("validates generated content", async () => { - const generator = new (class extends TestGenerator { - protected override async generateSchemaContent(_schema: any, _context: any): Promise { - return "INVALID content that should fail validation"; - } - })({ - outputDir: testFs.getBasePath(), - logger: logger as any, - }); - - const schema = createMockSchema(); - - await expect(generator.build([schema])).rejects.toThrow("operations failed"); - }); - }); - - describe("Error Handling", () => { - test("handles individual schema errors gracefully", async () => { - const validSchema = createMockSchema(); - const invalidSchema = { identifier: null } as any; - - await expect(generator.build([validSchema, invalidSchema])).rejects.toThrow(); - - // Should have logged the error - expect(logger.hasLevel("error")).toBe(true); - }); - - test("provides helpful error context", async () => { - const malformedSchema = generateMalformedSchemas(1)[0]; - - try { - await generator.build([malformedSchema] as any); - } catch (error) { - expect(error instanceof Error).toBe(true); - expect(error.message).toContain("operations failed"); - } - }); - }); - - describe("Performance", () => { - test("generates files within reasonable time", async () => { - const schemas = createMockSchemas(["A", "B", "C", "D", "E"]); - - const startTime = performance.now(); - await generator.build(schemas); - const duration = performance.now() - startTime; - - // Should complete within 1 second for 5 simple schemas - expect(duration).toBeLessThan(1000); - }); - - test("handles large schema sets efficiently", async () => { - const schemaNames = Array.from({ length: 20 }, (_, i) => `Schema${i}`); - const schemas = createMockSchemas(schemaNames); - - const startTime = performance.now(); - const results = await generator.build(schemas); - const duration = performance.now() - startTime; - - expect(results).toHaveLength(20); - - // Should complete within 2 seconds for 20 schemas - assertPerformanceBenchmark(duration, 2000, 0.5); - }); - - test("reports generation statistics", async () => { - const schemas = createMockSchemas(["Test1", "Test2"]); - await generator.build(schemas); - - const stats = generator.getGenerationStats(); - - expect(stats.filesGenerated).toBe(2); - expect(stats.totalSize).toBeGreaterThan(0); - expect(stats.averageFileSize).toBeGreaterThan(0); - expect(stats.generationTime).toBeGreaterThan(0); - }); - }); - - describe("Fluent API", () => { - test("file builder throws error when template engine not available", () => { - expect(() => generator.file("test-file")).toThrow("Template engine is required for fluent file generation"); - }); - - test("directory builder creates directory structure", () => { - const dirBuilder = generator.directory("test-dir"); - expect(dirBuilder).toBeDefined(); - }); - - test("index builder throws error when template engine not available", () => { - expect(() => generator.index(".")).toThrow("Template engine is required for index file generation"); - }); - - test("progress callback receives updates", async () => { - const progressUpdates: Array<{ phase: string; current: number; total: number }> = []; - - generator.onProgress((phase, current, total) => { - progressUpdates.push({ phase, current, total }); - }); - - const schemas = createMockSchemas(["A", "B"]); - await generator.build(schemas); - - expect(progressUpdates.length).toBeGreaterThan(0); - expect(progressUpdates.some((u) => u.phase === "validation")).toBe(true); - expect(progressUpdates.some((u) => u.phase === "complete")).toBe(true); - }); - }); - - describe("Capabilities", () => { - test("reports generator capabilities", () => { - const capabilities = generator.getCapabilities(); - - expect(capabilities.language).toBe("TestLanguage"); - expect(capabilities.fileExtensions).toContain(".test"); - expect(capabilities.supportsTemplates).toBe(true); - expect(capabilities.supportsValidation).toBe(true); - }); - }); - - describe("Logging Integration", () => { - test("logs generation progress", async () => { - const schema = createMockSchema(); - await generator.build([schema]); - - const infoMessages = logger.getMessages("info"); - expect(infoMessages.length).toBeGreaterThan(0); - expect(infoMessages.some((m) => m.message.includes("generation"))).toBe(true); - }); - - test("logs errors with context", async () => { - const invalidSchema = { identifier: null } as any; - - try { - await generator.build([invalidSchema]); - } catch { - // Expected to throw - } - - expect(logger.hasLevel("error")).toBe(true); - }); - }); -}); diff --git a/test/unit/api/generators/base/FileManager.test.ts b/test/unit/api/generators/base/FileManager.test.ts deleted file mode 100644 index c582c1e3a..000000000 --- a/test/unit/api/generators/base/FileManager.test.ts +++ /dev/null @@ -1,319 +0,0 @@ -/** - * Unit tests for FileManager - */ - -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { access, readFile, rm } from "node:fs/promises"; -import { join } from "node:path"; -import { FileOperationError } from "../../../../../src/api/generators/base/errors"; -import { FileManager } from "../../../../../src/api/generators/base/FileManager"; -import { createLogger } from "../../../../../src/utils/codegen-logger"; - -function createMockLogger() { - return createLogger({ prefix: "Test", verbose: false }); -} - -describe("FileManager", () => { - let fileManager: FileManager; - const testOutputDir = "./test-output-filemanager"; - - beforeEach(() => { - fileManager = new FileManager({ - outputDir: testOutputDir, - logger: createMockLogger(), - }); - }); - - afterEach(async () => { - try { - await rm(testOutputDir, { recursive: true, force: true }); - } catch { - // Directory might not exist - } - }); - - describe("File Writing", () => { - test("writes file with automatic directory creation", async () => { - const content = "export interface Test { id: string; }"; - const result = await fileManager.writeFile("nested/deep/test.ts", content); - - expect(result.path).toMatch(/test\.ts$/); - expect(result.size).toBeGreaterThan(0); - expect(result.writeTime).toBeGreaterThan(0); - - // Verify file was actually written - const writtenContent = await readFile(result.path, "utf-8"); - expect(writtenContent).toBe(content); - }); - - test("respects overwrite setting", async () => { - const initialContent = "initial content"; - const newContent = "new content"; - - // Write initial file - await fileManager.writeFile("test.ts", initialContent); - - // Create non-overwrite manager - const noOverwriteManager = new FileManager({ - outputDir: testOutputDir, - logger: createMockLogger(), - overwrite: false, - }); - - // Try to write again - should skip - const result = await noOverwriteManager.writeFile("test.ts", newContent); - - // Should return existing file stats - expect(result.size).toBe(Buffer.byteLength(initialContent, "utf-8")); - expect(result.writeTime).toBe(0); - - // File content should remain unchanged - const content = await readFile(result.path, "utf-8"); - expect(content).toBe(initialContent); - }); - - test("handles file encoding correctly", async () => { - const content = "Hello 世界! 🌍"; - - const result = await fileManager.writeFile("unicode.ts", content, { - encoding: "utf-8", - }); - - const readContent = await readFile(result.path, "utf-8"); - expect(readContent).toBe(content); - expect(result.size).toBe(Buffer.byteLength(content, "utf-8")); - }); - }); - - describe("Batch Operations", () => { - test("writes multiple files efficiently", async () => { - const files = new Map(); - - // Create 50 test files - for (let i = 0; i < 50; i++) { - files.set(`file-${i}.ts`, `export const value${i} = ${i};`); - } - - const startTime = performance.now(); - const results = await fileManager.writeBatch(files); - const batchTime = performance.now() - startTime; - - expect(results).toHaveLength(50); - expect(batchTime).toBeLessThan(2000); // Should complete within 2 seconds - - // Verify all files were written - for (let i = 0; i < 50; i++) { - const filePath = join(testOutputDir, `file-${i}.ts`); - await expect(access(filePath)).resolves.toBeDefined(); - } - }); - - test("handles large batch operations", async () => { - const files = new Map(); - - // Create 100 files in subdirectories - for (let i = 0; i < 100; i++) { - const dir = `group-${Math.floor(i / 10)}`; - files.set(`${dir}/file-${i}.ts`, `export const value${i} = ${i};`); - } - - const results = await fileManager.writeBatch(files); - - expect(results).toHaveLength(100); - - // Verify directory structure was created - for (let i = 0; i < 10; i++) { - const dirPath = join(testOutputDir, `group-${i}`); - await expect(access(dirPath)).resolves.toBeDefined(); - } - }); - - test("uses configurable batch size", async () => { - fileManager.setBatchSize(5); - expect(fileManager.getBatchSize()).toBe(5); - - // Batch size should be clamped to valid range - fileManager.setBatchSize(0); - expect(fileManager.getBatchSize()).toBe(1); - - fileManager.setBatchSize(100); - expect(fileManager.getBatchSize()).toBe(50); - }); - }); - - describe("Directory Operations", () => { - test("ensures directory creation", async () => { - const deepPath = join(testOutputDir, "very/deep/nested/path"); - - await fileManager.ensureDirectory(deepPath); - - await expect(access(deepPath)).resolves.toBeDefined(); - }); - - test("cleans directory recursively", async () => { - // Create some files first - await fileManager.writeFile("dir1/file1.ts", "content1"); - await fileManager.writeFile("dir1/subdir/file2.ts", "content2"); - await fileManager.writeFile("dir2/file3.ts", "content3"); - - // Clean dir1 - await fileManager.cleanDirectory("dir1"); - - // dir1 should be gone, dir2 should remain - await expect(access(join(testOutputDir, "dir1"))).rejects.toThrow(); - await expect(access(join(testOutputDir, "dir2/file3.ts"))).resolves.toBeDefined(); - }); - - test("handles cleaning non-existent directory gracefully", async () => { - // Should not throw error - await expect(fileManager.cleanDirectory("nonexistent")).resolves.toBeUndefined(); - }); - }); - - describe("Import Path Resolution", () => { - test("generates correct relative import paths", async () => { - const fromFile = "components/Patient.ts"; - const toFile = "types/Reference.ts"; - - const relativePath = fileManager.getRelativeImportPath(fromFile, toFile); - - expect(relativePath).toBe("../types/Reference"); - }); - - test("handles same directory imports", async () => { - const fromFile = "types/Patient.ts"; - const toFile = "types/Reference.ts"; - - const relativePath = fileManager.getRelativeImportPath(fromFile, toFile); - - expect(relativePath).toBe("./Reference"); - }); - - test("handles nested to parent directory imports", async () => { - const fromFile = "types/nested/Patient.ts"; - const toFile = "types/Reference.ts"; - - const relativePath = fileManager.getRelativeImportPath(fromFile, toFile); - - expect(relativePath).toBe("../Reference"); - }); - - test("removes file extensions from import paths", async () => { - const fromFile = "components/Patient.tsx"; - const toFile = "types/Reference.d.ts"; - - const relativePath = fileManager.getRelativeImportPath(fromFile, toFile); - - expect(relativePath).toBe("../types/Reference"); - }); - }); - - describe("File Utilities", () => { - test("checks file overwrite correctly", async () => { - const filePath = "test-overwrite.ts"; - - // File doesn't exist yet - expect(await fileManager.wouldOverwrite(filePath)).toBe(false); - - // Create file - await fileManager.writeFile(filePath, "content"); - - // Now it would overwrite - expect(await fileManager.wouldOverwrite(filePath)).toBe(true); - }); - - test("gets file statistics", async () => { - const content = 'export const test = "value";'; - const filePath = "stats-test.ts"; - - // No stats for non-existent file - expect(await fileManager.getFileStats(filePath)).toBeNull(); - - // Create file and get stats - await fileManager.writeFile(filePath, content); - const stats = await fileManager.getFileStats(filePath); - - expect(stats).not.toBeNull(); - expect(stats?.size).toBe(Buffer.byteLength(content, "utf-8")); - expect(stats?.generationTime).toBe(0); // Set by caller - expect(stats?.writeTime).toBe(0); // Set by caller - }); - - test("provides output directory access", () => { - expect(fileManager.getOutputDirectory()).toBe(testOutputDir); - }); - }); - - describe("Error Handling", () => { - test("throws FileOperationError on write failure", async () => { - // Try to write to an invalid path (contains null byte) - await expect(fileManager.writeFile("invalid\0path.ts", "content")).rejects.toThrow(FileOperationError); - }); - - test("provides recovery suggestions in errors", async () => { - try { - // Try to write to an invalid path - await fileManager.writeFile("invalid\0path.ts", "content"); - } catch (error) { - expect(error).toBeInstanceOf(FileOperationError); - const suggestions = (error as FileOperationError).getSuggestions(); - expect(suggestions.length).toBeGreaterThan(0); - expect(suggestions.some((s) => s.includes("backup-output"))).toBe(true); - } - }); - - test("handles directory creation errors", async () => { - // Mock a permission error scenario (hard to test directly) - const originalError = new Error("EACCES: permission denied") as NodeJS.ErrnoException; - originalError.code = "EACCES"; - - // This would normally be tested with a mocked filesystem - // For now, just verify the error structure is correct - expect(() => { - throw new FileOperationError("Failed to create directory", "create", "/invalid/path", originalError, { - canRetry: true, - permissionFix: "chmod 755 /path", - }); - }).toThrow(FileOperationError); - }); - }); - - describe("Performance", () => { - test("batch operations complete successfully", async () => { - const fileCount = 20; - const files = new Map(); - - for (let i = 0; i < fileCount; i++) { - files.set(`perf-test-${i}.ts`, `export const value${i} = ${i};`); - } - - // Test batch write performance - const batchStart = performance.now(); - const results = await fileManager.writeBatch(files); - const batchTime = performance.now() - batchStart; - - expect(results).toHaveLength(fileCount); - expect(batchTime).toBeLessThan(1000); // Should complete within 1 second - - // Verify all files were created - for (let i = 0; i < fileCount; i++) { - const filePath = join(testOutputDir, `perf-test-${i}.ts`); - await expect(access(filePath)).resolves.toBeDefined(); - } - }); - - test("handles memory efficiently with large files", async () => { - const largeContent = "x".repeat(1024 * 1024); // 1MB file - - const startMemory = process.memoryUsage().heapUsed; - - await fileManager.writeFile("large-file.ts", largeContent); - - const endMemory = process.memoryUsage().heapUsed; - const memoryIncrease = endMemory - startMemory; - - // Memory increase should be reasonable (less than 10MB) - expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024); - }); - }); -}); diff --git a/test/unit/api/generators/base/TypeMapper.test.ts b/test/unit/api/generators/base/TypeMapper.test.ts deleted file mode 100644 index e33dd3944..000000000 --- a/test/unit/api/generators/base/TypeMapper.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { PythonTypeMapper } from "../../../../../src/api/generators/base/PythonTypeMapper"; -import { TypeScriptTypeMapper } from "../../../../../src/api/generators/base/TypeScriptTypeMapper"; - -describe("TypeMapper System", () => { - describe("TypeScript Type Mapper", () => { - const mapper = new TypeScriptTypeMapper(); - - test("maps FHIR primitives correctly", () => { - expect(mapper.mapPrimitive("string")).toEqual({ - name: "string", - isPrimitive: true, - nullable: false, - }); - - expect(mapper.mapPrimitive("integer")).toEqual({ - name: "number", - isPrimitive: true, - nullable: false, - }); - - expect(mapper.mapPrimitive("boolean")).toEqual({ - name: "boolean", - isPrimitive: true, - nullable: false, - }); - - expect(mapper.mapPrimitive("dateTime")).toEqual({ - name: "string", - isPrimitive: true, - nullable: false, - }); - }); - - test("handles unknown primitives", () => { - const result = mapper.mapPrimitive("unknownType"); - expect(result.name).toBe("unknown"); - expect(result.isPrimitive).toBe(true); - expect(result.metadata?.warning).toBe("unmapped_primitive"); - expect(result.metadata?.originalType).toBe("unknownType"); - }); - - test("maps references with type parameters", () => { - const result = mapper.mapReference([{ name: "Patient", kind: "resource" }]); - - expect(result.name).toBe("Reference"); - expect(result.generics).toEqual(["'Patient'"]); - expect(result.importPath).toBe("./Reference"); - expect(result.isPrimitive).toBe(false); - }); - - test("maps multiple target references as union", () => { - const result = mapper.mapReference([ - { name: "Patient", kind: "resource" }, - { name: "Practitioner", kind: "resource" }, - ]); - - expect(result.name).toBe("Reference"); - expect(result.generics).toEqual(["'Patient' | 'Practitioner'"]); - }); - - test("maps generic reference when no targets", () => { - const result = mapper.mapReference([]); - - expect(result.name).toBe("Reference"); - expect(result.generics).toEqual(["unknown"]); - }); - - test("maps arrays correctly with suffix syntax", () => { - const elementType = mapper.mapPrimitive("string"); - const arrayType = mapper.mapArray(elementType); - - expect(arrayType.name).toBe("string[]"); - expect(arrayType.isArray).toBe(true); - expect(arrayType.metadata?.arrayStyle).toBe("suffix"); - }); - - test("maps arrays with generic syntax when configured", () => { - const mapperWithGeneric = new TypeScriptTypeMapper({ - preferArraySyntax: false, - }); - - const elementType = mapperWithGeneric.mapPrimitive("string"); - const arrayType = mapperWithGeneric.mapArray(elementType); - - expect(arrayType.name).toBe("Array"); - expect(arrayType.generics).toEqual(["string"]); - expect(arrayType.metadata?.arrayStyle).toBe("generic"); - }); - - test("handles optional types correctly", () => { - const baseType = mapper.mapPrimitive("string"); - const optionalType = mapper.mapOptional(baseType, false); - - expect(optionalType.name).toBe("string | undefined"); - expect(optionalType.nullable).toBe(true); - expect(optionalType.metadata?.wasOptional).toBe(true); - }); - - test("keeps required types unchanged", () => { - const baseType = mapper.mapPrimitive("string"); - const requiredType = mapper.mapOptional(baseType, true); - - expect(requiredType).toEqual(baseType); - }); - - test("maps enums as union types", () => { - const result = mapper.mapEnum(["active", "inactive", "suspended"], "UserStatus"); - - expect(result.name).toBe("'active' | 'inactive' | 'suspended'"); - expect(result.isPrimitive).toBe(false); - expect(result.metadata?.enumName).toBe("UserStatus"); - expect(result.metadata?.values).toEqual(["active", "inactive", "suspended"]); - expect(result.metadata?.isUnionType).toBe(true); - }); - - test("formats type names using PascalCase", () => { - expect(mapper.formatTypeName("patient")).toBe("Patient"); - expect(mapper.formatTypeName("patient_resource")).toBe("PatientResource"); - expect(mapper.formatTypeName("patient-resource")).toBe("PatientResource"); - }); - - test("formats field names using camelCase", () => { - expect(mapper.formatFieldName("first_name")).toBe("firstName"); - expect(mapper.formatFieldName("last-name")).toBe("lastName"); - expect(mapper.formatFieldName("date_of_birth")).toBe("dateOfBirth"); - }); - - test("generates interface fields correctly", () => { - const type = mapper.mapPrimitive("string"); - - const requiredField = mapper.generateInterfaceField("firstName", type, true); - expect(requiredField).toBe("firstName: string;"); - - const optionalField = mapper.generateInterfaceField("lastName", type, false); - expect(optionalField).toBe("lastName?: string;"); - }); - - test("generates import statements for non-primitive types", () => { - const type = { - name: "Reference", - isPrimitive: false, - importPath: "./Reference", - }; - - const importStatement = mapper.generateImportStatement(type); - expect(importStatement).toBe("import type { Reference } from './Reference';"); - }); - - test("skips import statements for primitive types", () => { - const type = { - name: "string", - isPrimitive: true, - }; - - const importStatement = mapper.generateImportStatement(type); - expect(importStatement).toBeUndefined(); - }); - - test("uses custom mappings when provided", () => { - const customMapper = new TypeScriptTypeMapper({ - customMappings: { - customType: "MyCustomType", - }, - }); - - const result = customMapper.mapPrimitive("customType"); - expect(result.name).toBe("MyCustomType"); - }); - - test("respects null vs undefined preference", () => { - const nullMapper = new TypeScriptTypeMapper({ - preferUndefined: false, - }); - - const baseType = nullMapper.mapPrimitive("string"); - const optionalType = nullMapper.mapOptional(baseType, false); - - expect(optionalType.name).toBe("string | null"); - expect(optionalType.metadata?.nullabilityType).toBe("null"); - }); - }); - - describe("Multi-language Consistency", () => { - test("different languages handle same types consistently", () => { - const tsMapper = new TypeScriptTypeMapper(); - const pyMapper = new PythonTypeMapper(); - - // Both should handle strings (even if mapped differently) - const tsString = tsMapper.mapPrimitive("string"); - const pyString = pyMapper.mapPrimitive("string"); - - expect(tsString.isPrimitive).toBe(true); - expect(pyString.isPrimitive).toBe(true); - expect(tsString.nullable).toBe(pyString.nullable); - - // TypeScript maps to 'string', Python to 'str' - expect(tsString.name).toBe("string"); - expect(pyString.name).toBe("str"); - }); - - test("both handle arrays correctly", () => { - const tsMapper = new TypeScriptTypeMapper(); - const pyMapper = new PythonTypeMapper(); - - const tsStringType = tsMapper.mapPrimitive("string"); - const pyStringType = pyMapper.mapPrimitive("string"); - - const tsArrayType = tsMapper.mapArray(tsStringType); - const pyArrayType = pyMapper.mapArray(pyStringType); - - expect(tsArrayType.isArray).toBe(true); - expect(pyArrayType.isArray).toBe(true); - - // Different syntax but same concept - expect(tsArrayType.name).toBe("string[]"); - expect(pyArrayType.name).toBe("List[str]"); - }); - - test("both handle optional types", () => { - const tsMapper = new TypeScriptTypeMapper(); - const pyMapper = new PythonTypeMapper(); - - const tsType = tsMapper.mapPrimitive("string"); - const pyType = pyMapper.mapPrimitive("string"); - - const tsOptional = tsMapper.mapOptional(tsType, false); - const pyOptional = pyMapper.mapOptional(pyType, false); - - expect(tsOptional.nullable).toBe(true); - expect(pyOptional.nullable).toBe(true); - - // Different syntax but same concept - expect(tsOptional.name).toBe("string | undefined"); - expect(pyOptional.name).toBe("Optional[str]"); - }); - - test("both respect naming conventions", () => { - const tsMapper = new TypeScriptTypeMapper(); - const pyMapper = new PythonTypeMapper(); - - // TypeScript uses PascalCase for types - expect(tsMapper.formatTypeName("patient_resource")).toBe("PatientResource"); - expect(tsMapper.formatFieldName("first_name")).toBe("firstName"); - - // Python uses snake_case - expect(pyMapper.formatTypeName("PatientResource")).toBe("PatientResource"); // Uses PascalCase by default - expect(pyMapper.formatFieldName("firstName")).toBe("first_name"); - }); - }); - - describe("Complex Type Mapping", () => { - test("maps complex schema types", () => { - const mapper = new TypeScriptTypeMapper(); - - const complexSchema = { - kind: "complex-type", - name: "Address", - required: true, - }; - - const result = mapper.mapType(complexSchema); - - expect(result.name).toBe("Address"); - expect(result.isPrimitive).toBe(false); - expect(result.importPath).toBe("./Address"); - expect(result.metadata?.kind).toBe("complex-type"); - }); - - test("maps reference schemas", () => { - const mapper = new TypeScriptTypeMapper(); - - const refSchema = { - kind: "reference", - targets: [{ name: "Patient", kind: "resource" }], - }; - - const result = mapper.mapType(refSchema); - - expect(result.name).toBe("Reference"); - expect(result.generics).toEqual(["'Patient'"]); - }); - - test("maps array schemas", () => { - const mapper = new TypeScriptTypeMapper(); - - const arraySchema = { - kind: "array", - element: { - kind: "primitive-type", - name: "string", - }, - }; - - const result = mapper.mapType(arraySchema); - - expect(result.name).toBe("string[]"); - expect(result.isArray).toBe(true); - }); - - test("maps enum schemas", () => { - const mapper = new TypeScriptTypeMapper(); - - const enumSchema = { - kind: "enum", - name: "ContactPointStatus", - values: ["active", "inactive", "entered-in-error"], - }; - - const result = mapper.mapType(enumSchema); - - expect(result.name).toBe("'active' | 'inactive' | 'entered-in-error'"); - expect(result.metadata?.enumName).toBe("ContactPointStatus"); - }); - - test("handles unknown schema types gracefully", () => { - const mapper = new TypeScriptTypeMapper(); - - const unknownSchema = { - kind: "mysterious-type", - name: "Strange", - }; - - const result = mapper.mapType(unknownSchema); - - expect(result.name).toBe("unknown"); - expect(result.metadata?.warning).toBe("unmapped_type"); - expect(result.metadata?.originalType).toEqual(unknownSchema); - }); - }); - - describe("Configuration Options", () => { - test("uses branded types when enabled", () => { - const mapper = new TypeScriptTypeMapper({ - useBrandedTypes: true, - }); - - const result = mapper.mapPrimitive("id"); - - expect(result.name).toBe("string & { readonly __brand: 'id' }"); - expect(result.isPrimitive).toBe(false); - expect(result.importPath).toBe("./brands"); - expect(result.metadata?.isBranded).toBe(true); - }); - - test("respects strict type settings", () => { - const strictMapper = new TypeScriptTypeMapper({ - strictTypes: true, - }); - - const looseMapper = new TypeScriptTypeMapper({ - strictTypes: false, - }); - - // Both should behave consistently for this test - expect(strictMapper.getLanguageName()).toBe("TypeScript"); - expect(looseMapper.getLanguageName()).toBe("TypeScript"); - }); - - test("uses different naming conventions", () => { - const camelCaseMapper = new TypeScriptTypeMapper({ - namingConvention: "camelCase", - }); - - const snakeCaseMapper = new TypeScriptTypeMapper({ - namingConvention: "snake_case", - }); - - expect(camelCaseMapper.formatTypeName("patient_resource")).toBe("patientResource"); - expect(snakeCaseMapper.formatTypeName("PatientResource")).toBe("patient_resource"); - }); - }); -}); diff --git a/test/unit/api/generators/base/builders/FileBuilder.test.ts b/test/unit/api/generators/base/builders/FileBuilder.test.ts deleted file mode 100644 index fe3f0c8c9..000000000 --- a/test/unit/api/generators/base/builders/FileBuilder.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -/** - * Unit tests for FileBuilder - */ - -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { assertValidTypeScript } from "../../../../../helpers/assertions"; -import { TestFileSystem } from "../../../../../helpers/file-helpers"; -import { MockLogger } from "../../../../../helpers/mock-generators"; - -describe("FileBuilder", () => { - let testFs: TestFileSystem; - let _logger: MockLogger; - - beforeEach(async () => { - testFs = await TestFileSystem.createTempTestDir(); - _logger = new MockLogger(); - }); - - afterEach(async () => { - await testFs.cleanup(); - }); - - describe("Mock FileBuilder Tests", () => { - // Note: These tests use mock implementations since we don't have access to the actual FileBuilder - // In a real implementation, these would test the actual FileBuilder class - - test("creates file with correct content", () => { - const mockFileBuilder = { - content: "", - imports: new Map(), - exports: new Set(), - - withContent(content: string) { - this.content = content; - return this; - }, - - addImport(name: string, from: string) { - this.imports.set(name, from); - return this; - }, - - addExport(name: string) { - this.exports.add(name); - return this; - }, - - build() { - return { - filename: "test.ts", - content: this.content, - imports: this.imports, - exports: this.exports, - }; - }, - }; - - const content = "interface Patient { id: string; }"; - mockFileBuilder.withContent(content); - - const context = mockFileBuilder.build(); - expect(context.content).toBe(content); - }); - - test("manages imports correctly", () => { - const mockFileBuilder = { - content: "", - imports: new Map(), - exports: new Set(), - - withContent(content: string) { - this.content = content; - return this; - }, - - addImport(name: string, from: string) { - this.imports.set(name, from); - return this; - }, - - addExport(name: string) { - this.exports.add(name); - return this; - }, - - build() { - return { - filename: "test.ts", - content: this.content, - imports: this.imports, - exports: this.exports, - }; - }, - }; - - mockFileBuilder.addImport("TestType", "./test-type").addImport("AnotherType", "./another-type"); - - const context = mockFileBuilder.build(); - expect(context.imports.get("TestType")).toBe("./test-type"); - expect(context.imports.get("AnotherType")).toBe("./another-type"); - }); - - test("manages exports correctly", () => { - const mockFileBuilder = { - content: "", - imports: new Map(), - exports: new Set(), - - withContent(content: string) { - this.content = content; - return this; - }, - - addImport(name: string, from: string) { - this.imports.set(name, from); - return this; - }, - - addExport(name: string) { - this.exports.add(name); - return this; - }, - - build() { - return { - filename: "test.ts", - content: this.content, - imports: this.imports, - exports: this.exports, - }; - }, - }; - - mockFileBuilder.addExport("TestInterface").addExport("TestType"); - - const context = mockFileBuilder.build(); - expect(context.exports.has("TestInterface")).toBe(true); - expect(context.exports.has("TestType")).toBe(true); - }); - - test("validates TypeScript content", () => { - const validTypeScript = ` -interface Patient { - id: string; - name: string; - active: boolean; -} - -export { Patient }; - `.trim(); - - // Should not throw - expect(() => assertValidTypeScript(validTypeScript)).not.toThrow(); - }); - - test("detects invalid TypeScript syntax", () => { - const invalidTypeScript = ` -interface Patient { - id: string - name: string // Missing semicolon - active: boolean; -} - `.trim(); - - expect(() => assertValidTypeScript(invalidTypeScript)).toThrow(); - }); - - test("supports fluent API pattern", () => { - const mockFileBuilder = { - content: "", - imports: new Map(), - exports: new Set(), - - withContent(content: string) { - this.content = content; - return this; - }, - - addImport(name: string, from: string) { - this.imports.set(name, from); - return this; - }, - - addExport(name: string) { - this.exports.add(name); - return this; - }, - - appendContent(content: string) { - this.content += content; - return this; - }, - - build() { - return { - filename: "test.ts", - content: this.content, - imports: this.imports, - exports: this.exports, - }; - }, - }; - - // Test method chaining - const result = mockFileBuilder - .withContent("interface Test {") - .appendContent("\n id: string;") - .appendContent("\n}") - .addImport("BaseType", "./base") - .addExport("Test") - .build(); - - expect(result.content).toBe("interface Test {\n id: string;\n}"); - expect(result.imports.get("BaseType")).toBe("./base"); - expect(result.exports.has("Test")).toBe(true); - }); - }); - - describe("File System Integration", () => { - test("writes file to test filesystem", async () => { - const content = "interface Test { id: string; }"; - const filename = "test.ts"; - - await testFs.writeTestFile(filename, content); - - const exists = await testFs.fileExists(filename); - expect(exists).toBe(true); - - const writtenContent = await testFs.readTestFile(filename); - expect(writtenContent).toBe(content); - }); - - test("handles file write errors gracefully", async () => { - // Test that we can catch and handle file write errors - const mockWrite = async (_filename: string, _content: string) => { - throw new Error("Permission denied"); - }; - - await expect(mockWrite("test.ts", "content")).rejects.toThrow("Permission denied"); - }); - - test("manages temporary files for testing", async () => { - const files = { - "Patient.ts": "interface Patient { id: string; }", - "Observation.ts": "interface Observation { id: string; }", - "index.ts": 'export * from "./Patient";\nexport * from "./Observation";', - }; - - for (const [filename, content] of Object.entries(files)) { - await testFs.writeTestFile(filename, content); - } - - const fileList = await testFs.listFiles(); - expect(fileList).toContain("Patient.ts"); - expect(fileList).toContain("Observation.ts"); - expect(fileList).toContain("index.ts"); - }); - }); - - describe("Performance", () => { - test("handles large content efficiently", () => { - const largeContent = - "interface Test {\n" + - Array.from({ length: 1000 }, (_, i) => ` field${i}: string;`).join("\n") + - "\n}"; - - const startTime = performance.now(); - - const mockBuilder = { - content: "", - withContent(content: string) { - this.content = content; - return this; - }, - build() { - return { content: this.content }; - }, - }; - - mockBuilder.withContent(largeContent); - const result = mockBuilder.build(); - - const duration = performance.now() - startTime; - - expect(result.content).toBe(largeContent); - expect(duration).toBeLessThan(100); // Should complete quickly - }); - - test("handles many operations efficiently", () => { - const startTime = performance.now(); - - const mockBuilder = { - imports: new Map(), - exports: new Set(), - - addImport(name: string, from: string) { - this.imports.set(name, from); - return this; - }, - - addExport(name: string) { - this.exports.add(name); - return this; - }, - - build() { - return { - imports: this.imports, - exports: this.exports, - }; - }, - }; - - // Add many imports and exports - for (let i = 0; i < 100; i++) { - mockBuilder.addImport(`Type${i}`, `./type${i}`).addExport(`Export${i}`); - } - - const result = mockBuilder.build(); - const duration = performance.now() - startTime; - - expect(result.imports.size).toBe(100); - expect(result.exports.size).toBe(100); - expect(duration).toBeLessThan(50); // Should be fast - }); - }); -}); diff --git a/test/unit/api/generators/base/error-handling.test.ts b/test/unit/api/generators/base/error-handling.test.ts deleted file mode 100644 index 23e7f9ff0..000000000 --- a/test/unit/api/generators/base/error-handling.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -/** - * Comprehensive tests for enhanced error handling system - */ - -import { beforeEach, describe, expect, test } from "bun:test"; -import { - EnhancedFileOperationError, - EnhancedSchemaValidationError, - EnhancedTemplateError, -} from "@root/api/generators/base/enhanced-errors"; -import { ErrorHandler, GeneratorErrorBoundary } from "@root/api/generators/base/error-handler"; -import { MockLogger } from "../../../../helpers/mock-generators"; -import { createMockSchema } from "../../../../helpers/schema-helpers"; - -describe("Enhanced Error Handling", () => { - describe("EnhancedSchemaValidationError", () => { - test("provides helpful suggestions for beginners", () => { - const schema = createMockSchema(); - const error = new EnhancedSchemaValidationError( - "Schema validation failed", - schema, - ["identifier.name is missing"], - { isBeginnerMode: true }, - ); - - const suggestions = error.getSuggestions(); - - expect(suggestions).toContain("Add missing identifier.name field to the schema"); - expect(suggestions.some((s) => s.includes("📚 Review the TypeSchema documentation"))).toBe(true); - }); - - test("provides context-aware suggestions", () => { - const schema = createMockSchema(); - const error = new EnhancedSchemaValidationError("Invalid kind", schema, [ - "identifier.kind must be one of: resource, complex-type, profile", - ]); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("identifier.kind"))).toBe(true); - expect(suggestions.some((s) => s.includes("resource, complex-type, profile"))).toBe(true); - }); - - test("formats error message nicely", () => { - const schema = createMockSchema({ - identifier: { name: "TestSchema", kind: "resource", package: "test.package" }, - }); - const error = new EnhancedSchemaValidationError("Test validation error", schema, [ - "Field missing", - "Type invalid", - ]); - - const formatted = error.getFormattedMessage(); - - expect(formatted).toContain("❌ Schema Validation Failed"); - expect(formatted).toContain("TestSchema"); - expect(formatted).toContain("Field missing"); - expect(formatted).toContain("💡 Suggestions"); - }); - - test("includes beginner-specific guidance", () => { - const schema = createMockSchema(); - const error = new EnhancedSchemaValidationError( - "Schema validation failed", - schema, - ["identifier.name is missing"], - { - isBeginnerMode: true, - previousSuccessfulSchemas: ["Patient", "Observation"], - }, - ); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("Patient"))).toBe(true); - expect(suggestions.some((s) => s.includes("--verbose flag"))).toBe(true); - }); - }); - - describe("EnhancedFileOperationError", () => { - test("provides operation-specific suggestions for write errors", () => { - const error = new EnhancedFileOperationError( - "Failed to write file", - "write", - "/test/path/file.ts", - new Error("EACCES: permission denied"), - { - canRetry: true, - alternativePaths: ["/tmp/output", "/home/user/output"], - }, - ); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("directory exists and is writable"))).toBe(true); - expect(suggestions.some((s) => s.includes("/tmp/output"))).toBe(true); - expect(suggestions.some((s) => s.includes("permission"))).toBe(true); - }); - - test("provides recovery actions", () => { - const error = new EnhancedFileOperationError( - "Permission denied", - "write", - "/test/path/file.ts", - new Error("EACCES: permission denied"), - ); - - const actions = error.getRecoveryActions(); - - expect(actions.length).toBeGreaterThan(0); - expect(actions[0].action).toContain("permissions"); - expect(actions[0].command).toContain("chmod"); - }); - - test("detects missing directory errors", () => { - const error = new EnhancedFileOperationError( - "Directory not found", - "write", - "/nonexistent/path/file.ts", - new Error("ENOENT: no such file or directory"), - ); - - const actions = error.getRecoveryActions(); - - expect(actions.some((a) => a.action.includes("directory"))).toBe(true); - expect(actions.some((a) => a.command?.includes("mkdir"))).toBe(true); - }); - }); - - describe("EnhancedTemplateError", () => { - test("suggests similar template names", () => { - const error = new EnhancedTemplateError( - "Template not found", - "interfac", // typo - { schema: createMockSchema() }, - { - availableTemplates: ["interface", "enum", "type-alias"], - missingVariables: [], - }, - ); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("Did you mean"))).toBe(true); - expect(suggestions.some((s) => s.includes("interface"))).toBe(true); - }); - - test("identifies missing variables", () => { - const error = new EnhancedTemplateError( - "Missing variables", - "interface", - { schema: createMockSchema(), typeMaper: "typo" }, // typo in typeMapper - { - availableTemplates: ["interface"], - missingVariables: ["typeMapper", "imports"], - }, - ); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("Missing template variables"))).toBe(true); - expect(suggestions.some((s) => s.includes("typeMapper"))).toBe(true); - expect(suggestions.some((s) => s.includes("imports"))).toBe(true); - }); - }); - - describe("ErrorHandler", () => { - let logger: MockLogger; - let handler: ErrorHandler; - - beforeEach(() => { - logger = new MockLogger(); - handler = new ErrorHandler({ - logger: logger as any, - outputFormat: "this.options.logger", - verbose: true, - }); - }); - - test("handles single error appropriately", () => { - const schema = createMockSchema(); - const error = new EnhancedSchemaValidationError("Test error", schema, ["test validation error"]); - - // Should not throw, just handle and log - expect(() => handler.handleError(error)).not.toThrow(); - }); - - test("handles unknown errors gracefully", () => { - const unknownError = new Error("Unexpected error"); - - expect(() => handler.handleError(unknownError)).not.toThrow(); - }); - - test("batches similar errors efficiently", () => { - const errors = [ - new EnhancedSchemaValidationError("Error 1", createMockSchema(), ["validation 1"]), - new EnhancedSchemaValidationError("Error 2", createMockSchema(), ["validation 2"]), - new Error("Unknown error"), - ]; - - expect(() => handler.handleBatchErrors(errors)).not.toThrow(); - }); - - test("formats errors as JSON when requested", () => { - const jsonHandler = new ErrorHandler({ - logger: logger as any, - outputFormat: "json", - }); - - const error = new EnhancedSchemaValidationError("Test error", createMockSchema(), [ - "test validation error", - ]); - - // Capture console output - const originalError = logger.error; - let jsonOutput = ""; - logger.error = (output: string) => { - jsonOutput = output; - }; - - jsonHandler.handleError(error); - - logger.error = originalError; - - expect(() => JSON.parse(jsonOutput)).not.toThrow(); - const parsed = JSON.parse(jsonOutput); - expect(parsed.type).toBe("EnhancedSchemaValidationError"); - expect(parsed.suggestions).toBeDefined(); - }); - }); - - describe("GeneratorErrorBoundary", () => { - let logger: MockLogger; - let handler: ErrorHandler; - let boundary: GeneratorErrorBoundary; - - beforeEach(() => { - logger = new MockLogger(); - handler = new ErrorHandler({ logger: logger as any }); - boundary = new GeneratorErrorBoundary(handler); - }); - - test("catches and re-throws errors from async operations", async () => { - const operation = async () => { - throw new Error("Test error"); - }; - - await expect(boundary.withErrorBoundary(operation)).rejects.toThrow("Test error"); - }); - - test("returns result when operation succeeds", async () => { - const operation = async () => { - return "success"; - }; - - const result = await boundary.withErrorBoundary(operation); - expect(result).toBe("success"); - }); - - test("handles batch operations with mixed results", async () => { - const operations = [ - async () => "success1", - async () => { - throw new Error("batch error"); - }, - async () => "success2", - ]; - - await expect(boundary.withBatchErrorBoundary(operations)).rejects.toThrow(); - }); - - test("returns all results when batch operations succeed", async () => { - const operations = [async () => "result1", async () => "result2", async () => "result3"]; - - const results = await boundary.withBatchErrorBoundary(operations); - expect(results).toEqual(["result1", "result2", "result3"]); - }); - }); - - describe("Error Context and Suggestions", () => { - test("provides relevant suggestions based on error type", () => { - const schemaError = new EnhancedSchemaValidationError("Missing identifier", createMockSchema(), [ - "identifier.name is missing", - ]); - - const suggestions = schemaError.getSuggestions(); - expect(suggestions.length).toBeGreaterThan(0); - expect(suggestions.some((s) => s.includes("identifier.name"))).toBe(true); - }); - - test("includes error context for debugging", () => { - const schema = createMockSchema({ - identifier: { name: "TestSchema", kind: "resource" }, - }); - const error = new EnhancedSchemaValidationError("Test error", schema, ["test error"]); - - expect(error.context).toBeDefined(); - expect(error.context?.schemaName).toBe("TestSchema"); - expect(error.context?.schemaKind).toBe("resource"); - }); - - test("provides recovery actions where applicable", () => { - const fileError = new EnhancedFileOperationError( - "Permission denied", - "write", - "/test/file.ts", - new Error("EACCES"), - ); - - const actions = fileError.getRecoveryActions(); - expect(actions.length).toBeGreaterThan(0); - expect(actions[0].command).toBeDefined(); - }); - }); -}); diff --git a/test/unit/api/generators/base/errors.test.ts b/test/unit/api/generators/base/errors.test.ts deleted file mode 100644 index e35983ccb..000000000 --- a/test/unit/api/generators/base/errors.test.ts +++ /dev/null @@ -1,475 +0,0 @@ -/** - * Unit tests for generator error classes - */ - -import { describe, expect, test } from "bun:test"; -import { - BatchOperationError, - ConfigurationError, - createErrorWithContext, - FileOperationError, - GeneratorError, - SchemaValidationError, - TemplateError, - TypeMappingError, -} from "@root/api/generators/base/errors"; -import type { FileContext } from "@root/api/generators/base/types"; -import type { TypeSchema } from "@typeschema/index"; - -// Helper function to create mock schemas -function createMockSchema(overrides: Partial = {}): TypeSchema { - return { - identifier: { - name: "TestSchema", - kind: "resource", - package: "test.package", - url: "http://test.com/StructureDefinition/TestSchema", - ...overrides.identifier, - }, - description: "Test schema for unit tests", - fields: { - id: { - type: { name: "string", kind: "primitive-type" }, - required: false, - array: false, - }, - }, - ...overrides, - } as TypeSchema; -} - -// Concrete implementation of GeneratorError for testing -class TestGeneratorError extends GeneratorError { - constructor( - message: string, - phase: "validation" | "generation" | "writing" | "initialization" = "generation", - context?: Record, - ) { - super(message, phase, context || { testContext: "test-value" }); - } - - getSuggestions(): string[] { - return ["This is a test suggestion", "Try this fix"]; - } -} - -describe("Generator Error Classes", () => { - describe("GeneratorError Base Class", () => { - test("creates error with proper metadata", () => { - const error = new TestGeneratorError("Test error message", "validation"); - - expect(error.message).toBe("Test error message"); - expect(error.phase).toBe("validation"); - expect(error.context?.testContext).toBe("test-value"); - expect(error.timestamp).toBeInstanceOf(Date); - expect(error.errorId).toBeDefined(); - expect(error.errorId).toMatch(/^TestGeneratorError-\d+-\w+$/); - }); - - test("generates detailed error message", () => { - const error = new TestGeneratorError("Test error", "generation"); - const detailed = error.getDetailedMessage(); - - expect(detailed).toContain("❌ TestGeneratorError: Test error"); - expect(detailed).toContain(`Error ID: ${error.errorId}`); - expect(detailed).toContain("Phase: generation"); - expect(detailed).toContain("Time: "); - expect(detailed).toContain("📍 Context:"); - expect(detailed).toContain('testContext: "test-value"'); - }); - - test("formats different context value types correctly", () => { - const error = new TestGeneratorError("Test", "generation"); - - // Use reflection to access private method for testing - const formatValue = (error as any).formatContextValue.bind(error); - - expect(formatValue(null)).toBe("null"); - expect(formatValue(undefined)).toBe("undefined"); - expect(formatValue("string")).toBe('"string"'); - expect(formatValue(123)).toBe("123"); - expect(formatValue({ key: "value" })).toContain('"key": "value"'); - }); - - test("provides default properties", () => { - const error = new TestGeneratorError("Test error"); - - expect(error.getSeverity()).toBe("error"); - expect(error.isRecoverable()).toBe(false); - expect(error.getDocumentationLinks()).toContain( - "https://github.com/atomic-ehr/codegen/docs/troubleshooting.md", - ); - }); - }); - - describe("SchemaValidationError", () => { - test("creates schema validation error with context", () => { - const schema = createMockSchema({ - identifier: { name: "Patient", kind: "resource" }, - }); - const validationErrors = ["identifier.name is missing", "fields are invalid"]; - - const error = new SchemaValidationError("Schema validation failed", schema, validationErrors); - - expect(error.schema).toBe(schema); - expect(error.validationErrors).toEqual(validationErrors); - expect(error.phase).toBe("validation"); - expect(error.context?.schemaName).toBe("Patient"); - expect(error.context?.schemaKind).toBe("resource"); - }); - - test("provides context-aware suggestions", () => { - const schema = createMockSchema(); - const error = new SchemaValidationError("Validation failed", schema, [ - "identifier.name is missing", - "identifier.kind is invalid", - ]); - - const suggestions = error.getSuggestions(); - - expect(suggestions).toContain("Verify the schema follows the TypeSchema specification"); - expect(suggestions.some((s) => s.includes("identifier.name"))).toBe(true); - expect(suggestions.some((s) => s.includes("identifier.kind"))).toBe(true); - }); - - test("provides beginner-friendly suggestions", () => { - const schema = createMockSchema(); - const error = new SchemaValidationError("Validation failed", schema, ["identifier.name is missing"], { - isBeginnerMode: true, - previousSuccessfulSchemas: ["Patient"], - }); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("🎓 Beginner Tips"))).toBe(true); - expect(suggestions.some((s) => s.includes("Quick Start guide"))).toBe(true); - expect(suggestions.some((s) => s.includes("Compare with your working schema: Patient"))).toBe(true); - }); - - test("determines recoverability correctly", () => { - const schema = createMockSchema(); - - const recoverableError = new SchemaValidationError("Simple validation error", schema, [ - "identifier.name is missing", - ]); - - const nonRecoverableError = new SchemaValidationError("Circular reference error", schema, [ - "circular reference detected", - ]); - - expect(recoverableError.isRecoverable()).toBe(true); - expect(nonRecoverableError.isRecoverable()).toBe(false); - }); - }); - - describe("TemplateError", () => { - test("creates template error with debug info", () => { - const context = { schema: { name: "Patient" }, typeMapper: {} }; - const debugInfo = { - availableTemplates: ["interface", "class"], - missingVariables: ["typeName"], - lineNumber: 42, - }; - - const error = new TemplateError("Template rendering failed", "nonexistent-template", context, debugInfo); - - expect(error.templateName).toBe("nonexistent-template"); - expect(error.templateContext).toBe(context); - expect(error.debugInfo).toBe(debugInfo); - expect(error.phase).toBe("generation"); - }); - - test("suggests similar template names", () => { - const error = new TemplateError( - "Template not found", - "interfac", // Typo in 'interface' - {}, - { availableTemplates: ["interface", "class", "enum"] }, - ); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("Did you mean"))).toBe(true); - expect(suggestions.some((s) => s.includes("interface"))).toBe(true); - }); - - test("handles missing variables", () => { - const error = new TemplateError( - "Missing variables", - "interface", - { existingVar: "value" }, - { missingVariables: ["typeName", "fields"] }, - ); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("Missing template variables"))).toBe(true); - expect(suggestions.some((s) => s.includes("typeName"))).toBe(true); - expect(suggestions.some((s) => s.includes("fields"))).toBe(true); - }); - - test("calculates Levenshtein distance correctly", () => { - const error = new TemplateError("Test", "test", {}); - const distance = (error as any).levenshteinDistance("kitten", "sitting"); - expect(distance).toBe(3); // Known Levenshtein distance - }); - - test("is always recoverable", () => { - const error = new TemplateError("Template error", "test", {}); - expect(error.isRecoverable()).toBe(true); - }); - }); - - describe("FileOperationError", () => { - test("creates file operation error with recovery options", () => { - const originalError = new Error("EACCES: permission denied") as NodeJS.ErrnoException; - originalError.code = "EACCES"; - - const recoveryOptions = { - canRetry: true, - alternativePaths: ["/tmp/alt-path"], - permissionFix: "chmod 755 /test/path", - }; - - const error = new FileOperationError( - "Failed to write file", - "write", - "/test/path/file.ts", - originalError, - recoveryOptions, - ); - - expect(error.operation).toBe("write"); - expect(error.filePath).toBe("/test/path/file.ts"); - expect(error.originalError).toBe(originalError); - expect(error.recoveryOptions).toBe(recoveryOptions); - expect(error.phase).toBe("writing"); - }); - - test("provides operation-specific suggestions", () => { - const error = new FileOperationError("Write failed", "write", "/test/file.ts"); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("File writing troubleshooting"))).toBe(true); - expect(suggestions.some((s) => s.includes("output directory exists"))).toBe(true); - expect(suggestions.some((s) => s.includes("write permissions"))).toBe(true); - }); - - test("provides error-code specific suggestions", () => { - const enoentError = new Error("ENOENT: no such file or directory") as NodeJS.ErrnoException; - enoentError.code = "ENOENT"; - - const error = new FileOperationError("File not found", "read", "/nonexistent/file.ts", enoentError); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("📂 File Not Found"))).toBe(true); - expect(suggestions.some((s) => s.includes("mkdir -p"))).toBe(true); - }); - - test("provides recovery actions", () => { - const eaccesError = new Error("EACCES: permission denied") as NodeJS.ErrnoException; - eaccesError.code = "EACCES"; - - const error = new FileOperationError("Permission denied", "write", "/test/file.ts", eaccesError); - - const actions = error.getRecoveryActions(); - - expect(actions).toHaveLength(1); - expect(actions[0].action).toBe("Fix file permissions"); - expect(actions[0].command).toContain("chmod 755"); - expect(actions[0].automatic).toBe(false); - expect(actions[0].riskLevel).toBe("medium"); - }); - - test("determines recoverability from options", () => { - const recoverableError = new FileOperationError("Recoverable error", "write", "/test/file.ts", undefined, { - canRetry: true, - }); - - const nonRecoverableError = new FileOperationError("Non-recoverable error", "write", "/test/file.ts"); - - expect(recoverableError.isRecoverable()).toBe(true); - expect(nonRecoverableError.isRecoverable()).toBe(false); - }); - }); - - describe("TypeMappingError", () => { - test("creates type mapping error with context", () => { - const mappingContext = { - availableMappings: ["string", "integer", "boolean"], - suggestedMappings: { unknownType: "string" }, - }; - - const error = new TypeMappingError("Cannot map type", "unknownType", "TypeScript", mappingContext); - - expect(error.fhirType).toBe("unknownType"); - expect(error.targetLanguage).toBe("TypeScript"); - expect(error.mappingContext).toBe(mappingContext); - }); - - test("suggests available and similar mappings", () => { - const error = new TypeMappingError("Unknown type", "str", "TypeScript", { - availableMappings: ["string", "integer", "boolean"], - suggestedMappings: { str: "string" }, - }); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("Available type mappings"))).toBe(true); - expect(suggestions.some((s) => s.includes("Suggested mappings"))).toBe(true); - expect(suggestions.some((s) => s.includes("string"))).toBe(true); - }); - - test("provides language-specific suggestions", () => { - const tsError = new TypeMappingError("Type error", "unknown", "TypeScript"); - const tsSuggestions = tsError.getSuggestions(); - expect(tsSuggestions.some((s) => s.includes("TYPESCRIPT_PRIMITIVES"))).toBe(true); - - const pyError = new TypeMappingError("Type error", "unknown", "Python"); - const pySuggestions = pyError.getSuggestions(); - expect(pySuggestions.some((s) => s.includes("PYTHON_TYPE_MAP"))).toBe(true); - - const rustError = new TypeMappingError("Type error", "unknown", "Rust"); - const rustSuggestions = rustError.getSuggestions(); - expect(rustSuggestions.some((s) => s.includes("Option"))).toBe(true); - }); - - test("is always recoverable", () => { - const error = new TypeMappingError("Type error", "unknown", "TypeScript"); - expect(error.isRecoverable()).toBe(true); - }); - }); - - describe("ConfigurationError", () => { - test("creates configuration error with validation details", () => { - const error = new ConfigurationError("Invalid config value", "outputDir", 123, "string", [ - "/valid/path", - "./another/path", - ]); - - expect(error.configKey).toBe("outputDir"); - expect(error.providedValue).toBe(123); - expect(error.expectedValue).toBe("string"); - expect(error.validOptions).toEqual(["/valid/path", "./another/path"]); - }); - - test("provides configuration-specific suggestions", () => { - const error = new ConfigurationError("Invalid outputDir", "outputDir", null); - - const suggestions = error.getSuggestions(); - - expect(suggestions.some((s) => s.includes("outputDir"))).toBe(true); - expect(suggestions.some((s) => s.includes("absolute paths"))).toBe(true); - }); - - test("is always recoverable", () => { - const error = new ConfigurationError("Config error", "test", null); - expect(error.isRecoverable()).toBe(true); - }); - }); - - describe("BatchOperationError", () => { - test("creates batch error from multiple individual errors", () => { - const errors = [ - new TestGeneratorError("Error 1", "validation"), - new TestGeneratorError("Error 2", "generation"), - new TestGeneratorError("Error 3", "validation"), - ]; - - const batchError = new BatchOperationError("Multiple operations failed", errors); - - expect(batchError.errors).toBe(errors); - expect(batchError.context?.errorCount).toBe(3); - expect(batchError.context?.errorTypes).toEqual(["TestGeneratorError"]); - expect(batchError.context?.phases).toEqual(["validation", "generation"]); - }); - - test("aggregates unique suggestions", () => { - const errors = [ - new TestGeneratorError("Error 1"), - new TestGeneratorError("Error 2"), - new TestGeneratorError("Error 3"), - ]; - - const batchError = new BatchOperationError("Batch failed", errors); - const suggestions = batchError.getSuggestions(); - - // Should contain unique suggestions from TestGeneratorError (with batch prefix) - expect(suggestions.some((s) => s.includes("This is a test suggestion"))).toBe(true); - expect(suggestions.some((s) => s.includes("Try this fix"))).toBe(true); - - // Should not duplicate suggestions - const suggestionCounts = suggestions.reduce( - (acc, suggestion) => { - acc[suggestion] = (acc[suggestion] || 0) + 1; - return acc; - }, - {} as Record, - ); - - Object.values(suggestionCounts).forEach((count) => { - expect(count).toBe(1); // No duplicates - }); - }); - - test("provides detailed error breakdown", () => { - const schema1 = createMockSchema({ - identifier: { name: "Patient", kind: "resource" }, - }); - const schema2 = createMockSchema({ - identifier: { name: "Observation", kind: "resource" }, - }); - - const errors = [ - new SchemaValidationError("Patient validation failed", schema1, ["missing field"]), - new SchemaValidationError("Observation validation failed", schema2, ["invalid type"]), - ]; - - const batchError = new BatchOperationError("Validation failed", errors); - const breakdown = batchError.getErrorBreakdown(); - - expect(breakdown).toContain("Patient validation failed"); - expect(breakdown).toContain("Observation validation failed"); - expect(breakdown).toContain("Schema: Patient"); - expect(breakdown).toContain("Schema: Observation"); - }); - - test("categorizes recoverable and non-recoverable errors", () => { - const schema = createMockSchema(); - const recoverableError = new SchemaValidationError("Recoverable", schema, ["simple error"]); - const nonRecoverableError = new SchemaValidationError("Non-recoverable", schema, ["circular reference"]); - - const batchError = new BatchOperationError("Mixed errors", [recoverableError, nonRecoverableError]); - - expect(batchError.getRecoverableErrors()).toEqual([recoverableError]); - expect(batchError.getNonRecoverableErrors()).toEqual([nonRecoverableError]); - expect(batchError.isRecoverable()).toBe(true); // At least one is recoverable - }); - }); - - describe("Error Utilities", () => { - test("createErrorWithContext enhances error with file context", () => { - const fileContext: FileContext = { - filename: "Patient.ts", - content: "interface Patient {}", - imports: new Map([["Reference", "./Reference"]]), - exports: new Set(["Patient"]), - metadata: { templateName: "interface" }, - templateName: "interface", - }; - - const error = createErrorWithContext(TestGeneratorError, "Context error", fileContext, { - customContext: "value", - }); - - expect(error.message).toBe("Context error"); - expect(error.context?.filename).toBe("Patient.ts"); - expect(error.context?.importsCount).toBe(1); - expect(error.context?.exportsCount).toBe(1); - expect(error.context?.templateName).toBe("interface"); - expect(error.context?.customContext).toBe("value"); - }); - }); -}); diff --git a/test/unit/api/generators/base/types.test.ts b/test/unit/api/generators/base/types.test.ts deleted file mode 100644 index a6e02b9d4..000000000 --- a/test/unit/api/generators/base/types.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Unit tests for base generator types and interfaces - */ - -import { describe, expect, test } from "bun:test"; -import { - DEFAULT_FILE_BUILDER_OPTIONS, - DEFAULT_GENERATOR_OPTIONS, - isGeneratedFile, - isGeneratorError, - VERSION, - validateGeneratorOptions, -} from "../../../../../src/api/generators/base"; -import { GeneratorError } from "../../../../../src/api/generators/base/errors"; -import type { BaseGeneratorOptions, GeneratedFile, LanguageType } from "../../../../../src/api/generators/base/types"; - -describe("Base Generator Types", () => { - describe("Default Options", () => { - test("DEFAULT_GENERATOR_OPTIONS contains all required fields", () => { - expect(DEFAULT_GENERATOR_OPTIONS).toEqual({ - outputDir: "./generated", - overwrite: true, - validate: true, - verbose: false, - beginnerMode: false, - errorFormat: "console", - }); - }); - - test("DEFAULT_FILE_BUILDER_OPTIONS contains all formatting options", () => { - expect(DEFAULT_FILE_BUILDER_OPTIONS.formatting).toEqual({ - indentSize: 2, - useTabs: false, - maxLineLength: 100, - }); - - expect(DEFAULT_FILE_BUILDER_OPTIONS.importStrategy).toBe("auto"); - expect(DEFAULT_FILE_BUILDER_OPTIONS.validation).toBe("strict"); - expect(DEFAULT_FILE_BUILDER_OPTIONS.prettify).toBe(true); - expect(DEFAULT_FILE_BUILDER_OPTIONS.encoding).toBe("utf-8"); - }); - }); - - describe("Option Validation", () => { - test("validateGeneratorOptions accepts valid options", () => { - const validOptions: BaseGeneratorOptions = { - outputDir: "/tmp/test-output", - overwrite: true, - validate: true, - }; - - const result = validateGeneratorOptions(validOptions); - - expect(result.isValid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - test("validateGeneratorOptions rejects missing outputDir", () => { - const invalidOptions = {} as BaseGeneratorOptions; - - const result = validateGeneratorOptions(invalidOptions); - - expect(result.isValid).toBe(false); - expect(result.errors).toContain("outputDir is required"); - expect(result.suggestions).toContain("Provide a valid output directory path"); - }); - - test("validateGeneratorOptions warns about relative paths", () => { - const options: BaseGeneratorOptions = { - outputDir: "./relative-path", - }; - - const result = validateGeneratorOptions(options); - - expect(result.isValid).toBe(true); - expect(result.warnings).toContain("Using relative path for outputDir - consider using absolute path"); - }); - - test("validateGeneratorOptions rejects invalid types", () => { - const invalidOptions = { - outputDir: 123, // Should be string - overwrite: "yes", // Should be boolean - validate: "true", // Should be boolean - } as any; - - const result = validateGeneratorOptions(invalidOptions); - - expect(result.isValid).toBe(false); - expect(result.errors).toContain("outputDir must be a string"); - expect(result.errors).toContain("overwrite must be a boolean"); - expect(result.errors).toContain("validate must be a boolean"); - }); - }); - - describe("Type Guards", () => { - test("isGeneratedFile identifies valid GeneratedFile objects", () => { - const validFile: GeneratedFile = { - path: "/test/path/file.ts", - filename: "file.ts", - content: "export interface Test {}", - exports: ["Test"], - size: 100, - timestamp: new Date(), - }; - - expect(isGeneratedFile(validFile)).toBe(true); - }); - - test("isGeneratedFile rejects invalid objects", () => { - expect(isGeneratedFile(null)).toBe(false); - expect(isGeneratedFile(undefined)).toBe(false); - expect(isGeneratedFile({})).toBe(false); - expect(isGeneratedFile("string")).toBe(false); - expect(isGeneratedFile(123)).toBe(false); - - // Missing required fields - expect( - isGeneratedFile({ - path: "/test/path", - filename: "test.ts", - // Missing other required fields - }), - ).toBe(false); - }); - - test("isGeneratorError identifies GeneratorError instances", () => { - class TestError extends GeneratorError { - getSuggestions(): string[] { - return ["Test suggestion"]; - } - } - - const error = new TestError("Test error", "generation"); - const normalError = new Error("Normal error"); - - expect(isGeneratorError(error)).toBe(true); - expect(isGeneratorError(normalError)).toBe(false); - expect(isGeneratorError(null)).toBe(false); - expect(isGeneratorError("string")).toBe(false); - }); - }); - - describe("LanguageType Interface", () => { - test("LanguageType can represent primitive types", () => { - const primitiveType: LanguageType = { - name: "string", - isPrimitive: true, - nullable: false, - }; - - expect(primitiveType.isPrimitive).toBe(true); - expect(primitiveType.name).toBe("string"); - expect(primitiveType.importPath).toBeUndefined(); - }); - - test("LanguageType can represent complex types with imports", () => { - const complexType: LanguageType = { - name: "Patient", - isPrimitive: false, - importPath: "./Patient", - nullable: true, - generics: ["T"], - metadata: { - isResource: true, - package: "fhir-types", - }, - }; - - expect(complexType.isPrimitive).toBe(false); - expect(complexType.importPath).toBe("./Patient"); - expect(complexType.generics).toEqual(["T"]); - expect(complexType.metadata?.isResource).toBe(true); - }); - }); - - describe("Version Information", () => { - test("VERSION is properly defined", () => { - expect(VERSION).toBeDefined(); - expect(typeof VERSION).toBe("string"); - expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/); // Semantic versioning pattern - }); - }); - - describe("Generated File Metadata", () => { - test("GeneratedFile supports optional metadata", () => { - const fileWithMetadata: GeneratedFile = { - path: "/test/Patient.ts", - filename: "Patient.ts", - content: "interface Patient {}", - exports: ["Patient"], - size: 20, - timestamp: new Date(), - metadata: { - generationTime: 150, - schemaCount: 1, - templateName: "interface", - warnings: ["Missing description"], - }, - }; - - expect(fileWithMetadata.metadata?.generationTime).toBe(150); - expect(fileWithMetadata.metadata?.templateName).toBe("interface"); - expect(fileWithMetadata.metadata?.warnings).toContain("Missing description"); - }); - }); - - describe("Configuration Merging", () => { - test("partial options merge correctly with defaults", () => { - const partialOptions: BaseGeneratorOptions = { - outputDir: "/custom/output", - }; - - // Simulate merging with defaults (this would happen in BaseGenerator constructor) - const merged = { - ...DEFAULT_GENERATOR_OPTIONS, - ...partialOptions, - }; - - expect(merged.outputDir).toBe("/custom/output"); - expect(merged.overwrite).toBe(true); // From defaults - expect(merged.validate).toBe(true); // From defaults - expect(merged.verbose).toBe(false); // From defaults - }); - }); -}); diff --git a/test/unit/api/generators/runtime-validation.test.ts b/test/unit/api/generators/runtime-validation.test.ts deleted file mode 100644 index 67f6bf7fb..000000000 --- a/test/unit/api/generators/runtime-validation.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Runtime validation tests for generated value set helper functions - */ -import { describe, expect, test } from "bun:test"; -import type { BindingTypeSchema } from "@typeschema/types"; -import { TypeScriptGenerator } from "../../../../src/api/generators/typescript"; -import { createLogger } from "../../../../src/utils/codegen-logger"; - -describe("Generated Code Structure Tests", () => { - test("should generate TypeScript arrays and types correctly", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "TestBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2", "value3"], - valueset: { url: "http://test.com/valueset" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - // Check that the generated code contains expected structures - expect(generatedCode).toContain("export const TestBindingValues = ["); - expect(generatedCode).toContain("'value1',"); - expect(generatedCode).toContain("'value2',"); - expect(generatedCode).toContain("'value3'"); - expect(generatedCode).toContain("] as const;"); - expect(generatedCode).toContain("export type TestBinding = typeof TestBindingValues[number];"); - expect(generatedCode).toContain("export const isValidTestBinding = (value: string): value is TestBinding =>"); - expect(generatedCode).toContain("TestBindingValues.includes(value as TestBinding);"); - }); - - test("should handle special character values in arrays", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "SpecialBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: [ - "1.2.840.10065.1.12.1.1", - "http://example.com/value", - "value-with-hyphens", - "value_with_underscores", - ], - valueset: { url: "http://test.com/valueset" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - // Check that special characters are properly quoted and included - expect(generatedCode).toContain("'1.2.840.10065.1.12.1.1'"); - expect(generatedCode).toContain("'http://example.com/value'"); - expect(generatedCode).toContain("'value-with-hyphens'"); - expect(generatedCode).toContain("'value_with_underscores'"); - expect(generatedCode).toContain("export const SpecialBindingValues = ["); - expect(generatedCode).toContain("export const isValidSpecialBinding"); - }); - - test("should generate proper const assertions and types", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "ConstAssertionBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["readonly1", "readonly2"], - valueset: { url: "http://test.com/valueset" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - // Check that const assertion is properly generated - expect(generatedCode).toContain("] as const;"); - expect(generatedCode).toContain("typeof ConstAssertionBindingValues[number]"); - expect(generatedCode).toContain("'readonly1'"); - expect(generatedCode).toContain("'readonly2'"); - }); - - test("should generate proper type guards when helpers enabled", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "TypeGuardBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["guard1", "guard2"], - valueset: { url: "http://test.com/valueset" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - // Check type guard function signature and implementation - expect(generatedCode).toContain("(value: string): value is TypeGuardBinding"); - expect(generatedCode).toContain("TypeGuardBindingValues.includes(value as TypeGuardBinding)"); - expect(generatedCode).toContain("export const isValidTypeGuardBinding"); - }); - - test("should generate complete syntactically correct TypeScript", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeDocuments: true, - includeValueSetHelpers: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "SyntaxBinding", - kind: "binding", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/syntax-binding", - }, - strength: "required", - enum: ["syntax1", "syntax2", "syntax3"], - valueset: { url: "http://hl7.org/fhir/ValueSet/syntax-binding" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - // Check for complete structure with docs, types, and helpers - expect(generatedCode).toContain("/**"); - expect(generatedCode).toContain("* SyntaxBinding value set"); - expect(generatedCode).toContain("* @see http://hl7.org/fhir/ValueSet/syntax-binding"); - expect(generatedCode).toContain("* @package hl7.fhir.r4.core"); - expect(generatedCode).toContain("* @generated This file is auto-generated. Do not edit manually."); - expect(generatedCode).toContain("*/"); - expect(generatedCode).toContain("export const SyntaxBindingValues = ["); - expect(generatedCode).toContain("] as const;"); - expect(generatedCode).toContain("export type SyntaxBinding = typeof SyntaxBindingValues[number];"); - expect(generatedCode).toContain( - "export const isValidSyntaxBinding = (value: string): value is SyntaxBinding =>", - ); - expect(generatedCode).toContain("SyntaxBindingValues.includes(value as SyntaxBinding);"); - - // Verify it doesn't contain any obvious syntax errors - expect(generatedCode).not.toContain("undefined"); - expect(generatedCode).not.toContain("[object Object]"); - expect(generatedCode).not.toContain("export export"); - }); - - test("should generate arrays with exact values and length", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "ArrayTestBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["first", "second", "third"], - valueset: { url: "http://test.com/valueset" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - // Verify exact array structure - expect(generatedCode).toMatch( - /export const ArrayTestBindingValues = \[\s*'first',\s*'second',\s*'third'\s*\] as const;/, - ); - - // Count occurrences to ensure proper structure - const firstCount = (generatedCode.match(/'first'/g) || []).length; - const secondCount = (generatedCode.match(/'second'/g) || []).length; - const thirdCount = (generatedCode.match(/'third'/g) || []).length; - - // Each value should appear exactly once in the array - expect(firstCount).toBe(1); - expect(secondCount).toBe(1); - expect(thirdCount).toBe(1); - }); -}); diff --git a/test/unit/api/generators/typescript.test.ts b/test/unit/api/generators/typescript.test.ts deleted file mode 100644 index 2d51b0b6b..000000000 --- a/test/unit/api/generators/typescript.test.ts +++ /dev/null @@ -1,892 +0,0 @@ -import { beforeEach, describe, expect, it } from "bun:test"; -import type { TypeSchema } from "@typeschema/types"; -import { TypeScriptGenerator } from "../../../../src/api/generators/typescript"; - -describe("TypeScript Generator Core Logic", () => { - let generator: TypeScriptGenerator; - - beforeEach(() => { - generator = new TypeScriptGenerator({ - outputDir: "/test/output", - moduleFormat: "esm", - generateIndex: true, - namingConvention: "PascalCase", - }); - }); - - describe("TypeScriptAPIGenerator.transformSchema", () => { - it("should transform basic resource schema to TypeScript", async () => { - const schema: TypeSchema = { - identifier: { - kind: "resource", - name: "Patient", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Patient", - }, - fields: { - id: { - type: { - kind: "primitive-type", - name: "id", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - name: { - type: { - kind: "complex-type", - name: "HumanName", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: true, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("export interface Patient"); - expect(result?.content).toContain("resourceType: 'Patient'"); - expect(result?.exports).toContain("Patient"); - }); - - it("should skip value-set schemas", async () => { - const schema: TypeSchema = { - identifier: { - kind: "value-set", - name: "TestValueSet", - package: "test", - version: "1.0.0", - url: "http://example.org/ValueSet/test", - }, - description: "Test value set", - }; - - const result = await generator.transformSchema(schema); - expect(result).toBeUndefined(); - }); - - it("should skip primitive-type schemas", async () => { - const schema: TypeSchema = { - identifier: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/string", - }, - }; - - const result = await generator.transformSchema(schema); - expect(result).toBeUndefined(); - }); - - it("should handle complex type with base interface", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "Address", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Address", - }, - base: { - kind: "complex-type", - name: "Element", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Element", - }, - fields: { - use: { - type: { - kind: "primitive-type", - name: "code", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - line: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: true, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("export interface Address"); - expect(result?.content).toContain("use?: string"); - expect(result?.content).toContain("line?: string[]"); - }); - - it("should handle Reference type with generics", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "Reference", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Reference", - }, - fields: { - type: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - reference: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("Reference"); - expect(result?.content).toContain("import type { ResourceType }"); - }); - - it("should handle optional fields correctly", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "TestType", - package: "test", - version: "1.0.0", - url: "http://example.org/TestType", - }, - fields: { - requiredField: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - required: true, - }, - optionalField: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - required: false, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("requiredField: string"); - expect(result?.content).toContain("optionalField?: string"); - }); - - it("should handle array fields", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "ArrayType", - package: "test", - version: "1.0.0", - url: "http://example.org/ArrayType", - }, - fields: { - items: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: true, - }, - singleItem: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: false, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("items?: string[]"); - expect(result?.content).toContain("singleItem?: string"); - }); - - it("should handle enum fields with small sets", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "EnumType", - package: "test", - version: "1.0.0", - url: "http://example.org/EnumType", - }, - fields: { - status: { - type: { - name: "code", - kind: "primitive-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/code", - }, - enum: ["active", "inactive", "pending"], - required: true, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("status: string"); - expect(result?.content).toContain("export interface EnumType"); - }); - - it("should handle enum fields with large sets", async () => { - const largeEnum = Array.from({ length: 20 }, (_, i) => `value${i}`); - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "LargeEnumType", - package: "test", - version: "1.0.0", - url: "http://example.org/LargeEnumType", - }, - fields: { - code: { - type: { - name: "code", - kind: "primitive-type", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/code", - }, - enum: largeEnum, - required: true, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("code: string"); - expect(result?.content).toContain("export interface LargeEnumType"); - }); - - it("should handle nested types", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "ParentType", - package: "test", - version: "1.0.0", - url: "http://example.org/ParentType", - }, - fields: { - nested: { - type: { - kind: "nested", - name: "NestedType", - package: "test", - version: "1.0.0", - url: "", - }, - }, - }, - nested: [ - { - identifier: { - kind: "nested", - name: "NestedType", - package: "test", - version: "1.0.0", - url: "http://example.org/ParentType#NestedType", - }, - fields: { - value: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - }, - }, - ], - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("export interface ParentType"); - }); - - it("should handle reference fields with specific targets", async () => { - const schema: TypeSchema = { - identifier: { - kind: "resource", - name: "Observation", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Observation", - }, - fields: { - subject: { - reference: [ - { - kind: "resource", - name: "Patient", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - { - kind: "resource", - name: "Group", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - ], - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("resourceType: 'Observation'"); - expect(result?.content).toContain("subject?: any"); - }); - - it("should handle polymorphic fields", async () => { - const schema: TypeSchema = { - identifier: { - kind: "resource", - name: "Observation", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Observation", - }, - fields: { - value: { - polymorphic: true, - types: [ - { - kind: "complex-type", - name: "Quantity", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - { - kind: "primitive-type", - name: "boolean", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - ], - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("value?: any"); - }); - - it("should track resource types for ResourceType union", async () => { - const schemas: TypeSchema[] = [ - { - identifier: { - kind: "resource", - name: "Patient", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Patient", - }, - fields: {}, - dependencies: [], - }, - { - identifier: { - kind: "resource", - name: "Observation", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Observation", - }, - fields: {}, - dependencies: [], - }, - ]; - - // Set a writable output directory for test - generator.setOutputDir("/tmp/test-output"); - - // Transform schemas to populate resourceTypes - const results = await generator.transformSchemas(schemas); - - expect(results).toHaveLength(2); - expect(results.some((r) => r.filename === "Patient.ts")).toBe(true); - expect(results.some((r) => r.filename === "Observation.ts")).toBe(true); - }); - - it("should handle primitive type mapping", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "PrimitiveTest", - package: "test", - version: "1.0.0", - url: "http://example.org/PrimitiveTest", - }, - fields: { - stringField: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - integerField: { - type: { - kind: "primitive-type", - name: "integer", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - booleanField: { - type: { - kind: "primitive-type", - name: "boolean", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - dateField: { - type: { - kind: "primitive-type", - name: "date", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - decimalField: { - type: { - kind: "primitive-type", - name: "decimal", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("stringField?: string"); - expect(result?.content).toContain("integerField?: number"); - expect(result?.content).toContain("booleanField?: boolean"); - expect(result?.content).toContain("dateField?: string"); - expect(result?.content).toContain("decimalField?: number"); - }); - - it("should generate correct imports for complex types", async () => { - const schema: TypeSchema = { - identifier: { - kind: "resource", - name: "Patient", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Patient", - }, - fields: { - name: { - type: { - kind: "complex-type", - name: "HumanName", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: true, - }, - address: { - type: { - kind: "complex-type", - name: "Address", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: true, - }, - identifier: { - type: { - kind: "complex-type", - name: "Identifier", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: true, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.imports.has("HumanName")).toBe(true); - expect(result?.imports.has("Address")).toBe(true); - expect(result?.imports.has("Identifier")).toBe(true); - }); - - it("should handle schemas without fields", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "EmptyType", - package: "test", - version: "1.0.0", - url: "http://example.org/EmptyType", - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("export interface EmptyType {"); - expect(result?.content).toContain("}"); - }); - - it("should respect naming convention option", async () => { - generator.setOptions({ namingConvention: "camelCase" }); - - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "test-type", - package: "test", - version: "1.0.0", - url: "http://example.org/test-type", - }, - fields: {}, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.filename).toBe("TestType.ts"); - }); - - it("should handle DomainResource and Resource base types", async () => { - const schema: TypeSchema = { - identifier: { - kind: "resource", - name: "DomainResource", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/DomainResource", - }, - base: { - kind: "resource", - name: "Resource", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/StructureDefinition/Resource", - }, - fields: {}, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("export interface DomainResource"); - expect(result?.content).toContain("resourceType: 'DomainResource'"); - }); - - it("should handle binding fields", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "BoundType", - package: "test", - version: "1.0.0", - url: "http://example.org/BoundType", - }, - fields: { - status: { - type: { - kind: "primitive-type", - name: "code", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - binding: { - kind: "value-set", - name: "StatusValueSet", - package: "test", - version: "1.0.0", - url: "http://example.org/ValueSet/status", - }, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("status?: string"); - }); - - it("should generate correct filename for types", async () => { - const testCases = [ - { name: "Patient", expected: "Patient.ts" }, - { name: "HumanName", expected: "HumanName.ts" }, - { name: "us-core-patient", expected: "UsCorePatient.ts" }, - { name: "some_snake_case", expected: "SomeSnakeCase.ts" }, - ]; - - for (const testCase of testCases) { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: testCase.name, - package: "test", - version: "1.0.0", - url: `http://example.org/${testCase.name}`, - }, - fields: {}, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - expect(result?.filename).toBe(testCase.expected); - } - }); - - it("should handle circular dependencies gracefully", async () => { - const schema: TypeSchema = { - identifier: { - kind: "complex-type", - name: "CircularType", - package: "test", - version: "1.0.0", - url: "http://example.org/CircularType", - }, - fields: { - parent: { - type: { - kind: "complex-type", - name: "CircularType", - package: "test", - version: "1.0.0", - url: "http://example.org/CircularType", - }, - }, - children: { - type: { - kind: "complex-type", - name: "CircularType", - package: "test", - version: "1.0.0", - url: "http://example.org/CircularType", - }, - array: true, - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("parent?: CircularType"); - expect(result?.content).toContain("children?: CircularType[]"); - }); - - it("should handle mixed field types in one schema", async () => { - const schema: TypeSchema = { - identifier: { - kind: "resource", - name: "MixedResource", - package: "test", - version: "1.0.0", - url: "http://example.org/MixedResource", - }, - fields: { - simpleString: { - type: { - kind: "primitive-type", - name: "string", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - enumField: { - type: { - kind: "primitive-type", - name: "code", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - enum: ["a", "b", "c"], - }, - complexField: { - type: { - kind: "complex-type", - name: "Address", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - }, - arrayField: { - type: { - kind: "primitive-type", - name: "integer", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - array: true, - }, - requiredField: { - type: { - kind: "primitive-type", - name: "boolean", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - required: true, - }, - referenceField: { - reference: [ - { - kind: "resource", - name: "Patient", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "", - }, - ], - }, - }, - dependencies: [], - }; - - const result = await generator.transformSchema(schema); - - expect(result).toBeDefined(); - expect(result?.content).toContain("resourceType: 'MixedResource'"); - expect(result?.content).toContain("simpleString?: string"); - expect(result?.content).toContain("enumField?: string"); - expect(result?.content).toContain("complexField?: Address"); - expect(result?.content).toContain("arrayField?: number[]"); - expect(result?.content).toContain("requiredField: boolean"); - expect(result?.content).toContain("referenceField?: any"); - }); - }); - - describe("TypeScriptAPIGenerator options", () => { - it("should update options correctly", () => { - generator.setOptions({ - moduleFormat: "cjs", - generateIndex: false, - }); - - const options = generator.getOptions(); - expect(options.moduleFormat).toBe("cjs"); - expect(options.generateIndex).toBe(false); - expect(options.namingConvention).toBe("PascalCase"); - }); - - it("should update output directory", () => { - generator.setOutputDir("/new/output/dir"); - const options = generator.getOptions(); - expect(options.outputDir).toBe("/new/output/dir"); - }); - }); -}); diff --git a/test/unit/api/generators/value-set-functionality.test.ts b/test/unit/api/generators/value-set-functionality.test.ts deleted file mode 100644 index d422c5f61..000000000 --- a/test/unit/api/generators/value-set-functionality.test.ts +++ /dev/null @@ -1,451 +0,0 @@ -/** - * Focused unit tests for value set functionality - */ -import { beforeEach, describe, expect, test } from "bun:test"; -import type { BindingTypeSchema, TypeSchema } from "@typeschema/types"; -import { TypeScriptGenerator } from "../../../../src/api/generators/typescript"; -import { createLogger } from "../../../../src/utils/codegen-logger"; - -describe("Value Set Core Functionality", () => { - let generator: TypeScriptGenerator; - - beforeEach(() => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - }); - - describe("Value Set Detection Logic", () => { - test("should detect required bindings with enum values", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "AdministrativeGender", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/administrative-gender", - }, - strength: "required", - enum: ["male", "female", "other", "unknown"], - valueset: { url: "http://hl7.org/fhir/ValueSet/administrative-gender" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(true); - }); - - test("should not detect preferred bindings in required-only mode", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "PreferredBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/preferred", - }, - strength: "preferred", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(false); - }); - - test("should detect preferred bindings in custom mode", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - valueSetMode: "custom", - valueSetStrengths: ["required", "preferred"], - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "PreferredBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/preferred", - }, - strength: "preferred", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(true); - }); - - test("should detect all bindings in all mode", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - valueSetMode: "all", - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "ExampleBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/example", - }, - strength: "example", - enum: ["ex1", "ex2"], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(true); - }); - - test("should not detect bindings without enum", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "NoEnumBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(false); - }); - - test("should not detect bindings with empty enum", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "EmptyEnumBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: [], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(false); - }); - }); - - describe("Value Set File Generation", () => { - test("should generate TypeScript const array and type", () => { - const binding: BindingTypeSchema = { - identifier: { - name: "AdministrativeGender", - kind: "binding", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/administrative-gender", - }, - strength: "required", - enum: ["male", "female", "other", "unknown"], - valueset: { url: "http://hl7.org/fhir/ValueSet/administrative-gender" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).toContain("export const AdministrativeGenderValues = ["); - expect(result).toContain("'male',"); - expect(result).toContain("'female',"); - expect(result).toContain("'other',"); - expect(result).toContain("'unknown'"); - expect(result).toContain("] as const;"); - expect(result).toContain("export type AdministrativeGender = typeof AdministrativeGenderValues[number];"); - }); - - test("should include documentation when enabled", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - includeDocuments: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "TestBinding", - kind: "binding", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/test-binding", - }, - strength: "required", - enum: ["val1", "val2"], - valueset: { url: "http://hl7.org/fhir/ValueSet/test-binding" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).toContain("/**"); - expect(result).toContain("* TestBinding value set"); - expect(result).toContain("* @see http://hl7.org/fhir/ValueSet/test-binding"); - expect(result).toContain("* @package hl7.fhir.r4.core"); - expect(result).toContain("* @generated This file is auto-generated. Do not edit manually."); - expect(result).toContain("*/"); - }); - - test("should include helper functions when enabled", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "TestBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).toContain("export const isValidTestBinding = (value: string): value is TestBinding =>"); - expect(result).toContain("TestBindingValues.includes(value as TestBinding);"); - }); - - test("should not include helpers when disabled", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: false, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "TestBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).not.toContain("export const isValidTestBinding"); - }); - - test("should handle special characters and URIs in enum values", () => { - const binding: BindingTypeSchema = { - identifier: { - name: "SpecialCharsBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: [ - "1.2.840.10065.1.12.1.1", - "http://example.com/value", - "value-with-hyphens", - "value_with_underscores", - ], - valueset: { url: "http://test.com/valueset" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).toContain("'1.2.840.10065.1.12.1.1'"); - expect(result).toContain("'http://example.com/value'"); - expect(result).toContain("'value-with-hyphens'"); - expect(result).toContain("'value_with_underscores'"); - }); - }); - - describe("Configuration Handling", () => { - test("should respect custom configuration", () => { - const customGenerator = new TypeScriptGenerator({ - generateValueSets: true, - valueSetMode: "custom", - valueSetStrengths: ["required", "preferred", "extensible"], - includeValueSetHelpers: true, - valueSetDirectory: "custom-valuesets", - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const options = customGenerator.getOptions(); - expect(options.generateValueSets).toBe(true); - expect(options.valueSetMode).toBe("custom"); - expect(options.valueSetStrengths).toEqual(["required", "preferred", "extensible"]); - expect(options.includeValueSetHelpers).toBe(true); - expect(options.valueSetDirectory).toBe("custom-valuesets"); - }); - - test("should use fallback directory when valueSetDirectory is undefined", () => { - // This tests the || 'valuesets' fallback in the code - const binding: BindingTypeSchema = { - identifier: { - name: "TestBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - // The fallback should work even if valueSetDirectory is undefined - expect((generator as any).generateValueSetFile(binding)).toContain("export const TestBindingValues"); - }); - }); - - describe("Collection and Filtering", () => { - test("should collect qualifying value sets from mixed schemas", () => { - const schemas: TypeSchema[] = [ - // Required binding - should be included - { - identifier: { - kind: "binding", - name: "RequiredBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/required", - }, - strength: "required", - enum: ["val1", "val2"], - valueset: { url: "http://test.com/required-valueset" }, - } as BindingTypeSchema, - - // Preferred binding - should not be included (required-only mode) - { - identifier: { - kind: "binding", - name: "PreferredBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/preferred", - }, - strength: "preferred", - enum: ["pref1", "pref2"], - valueset: { url: "http://test.com/preferred-valueset" }, - } as BindingTypeSchema, - - // Non-binding schema - should be ignored - { - identifier: { - kind: "resource", - name: "Patient", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/Patient", - }, - fields: {}, - }, - - // Binding without enum - should be ignored - { - identifier: { - kind: "binding", - name: "NoEnumBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/no-enum", - }, - strength: "required", - valueset: { url: "http://test.com/no-enum-valueset" }, - } as BindingTypeSchema, - ]; - - const result = (generator as any).collectValueSets(schemas); - - expect(result.size).toBe(1); - expect(result.has("RequiredBinding")).toBe(true); - expect(result.has("PreferredBinding")).toBe(false); - expect(result.has("Patient")).toBe(false); - expect(result.has("NoEnumBinding")).toBe(false); - }); - - test("should collect multiple bindings in custom mode", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - valueSetMode: "custom", - valueSetStrengths: ["required", "preferred", "extensible"], - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const schemas: TypeSchema[] = [ - { - identifier: { - kind: "binding", - name: "RequiredBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/required", - }, - strength: "required", - enum: ["r1", "r2"], - valueset: { url: "http://test.com/required-valueset" }, - } as BindingTypeSchema, - { - identifier: { - kind: "binding", - name: "PreferredBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/preferred", - }, - strength: "preferred", - enum: ["p1", "p2"], - valueset: { url: "http://test.com/preferred-valueset" }, - } as BindingTypeSchema, - { - identifier: { - kind: "binding", - name: "ExtensibleBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/extensible", - }, - strength: "extensible", - enum: ["e1", "e2"], - valueset: { url: "http://test.com/extensible-valueset" }, - } as BindingTypeSchema, - { - identifier: { - kind: "binding", - name: "ExampleBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/example", - }, - strength: "example", - enum: ["ex1", "ex2"], - valueset: { url: "http://test.com/example-valueset" }, - } as BindingTypeSchema, - ]; - - const result = (generator as any).collectValueSets(schemas); - - expect(result.size).toBe(3); - expect(result.has("RequiredBinding")).toBe(true); - expect(result.has("PreferredBinding")).toBe(true); - expect(result.has("ExtensibleBinding")).toBe(true); - expect(result.has("ExampleBinding")).toBe(false); - }); - }); -}); diff --git a/test/unit/api/generators/value-set-generation.test.ts b/test/unit/api/generators/value-set-generation.test.ts deleted file mode 100644 index 4ee3e6f64..000000000 --- a/test/unit/api/generators/value-set-generation.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * Unit tests for value set generation functionality - */ -import { beforeEach, describe, expect, test } from "bun:test"; -import type { BindingTypeSchema } from "@typeschema/types"; -import { TypeScriptGenerator } from "../../../../src/api/generators/typescript"; -import { createLogger } from "../../../../src/utils/codegen-logger"; - -describe("Value Set Detection", () => { - let generator: TypeScriptGenerator; - - beforeEach(() => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - }); - - test("should detect required binding with enum", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "TestBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2", "value3"], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(true); - }); - - test("should not detect preferred binding by default (required-only mode)", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "TestBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "preferred", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(false); - }); - - test("should detect preferred binding when configured for custom mode", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - valueSetMode: "custom", - valueSetStrengths: ["required", "preferred"], - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "TestBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "preferred", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(true); - }); - - test("should detect all bindings when configured for all mode", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - valueSetMode: "all", - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "TestBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "example", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(true); - }); - - test("should not detect binding without enum", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "TestBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(false); - }); - - test("should not detect binding with empty enum", () => { - const schema: BindingTypeSchema = { - identifier: { - kind: "binding", - name: "TestBinding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: [], - valueset: { url: "http://test.com/valueset" }, - }; - - expect((generator as any).shouldGenerateValueSet(schema)).toBe(false); - }); -}); - -describe("Value Set File Generation", () => { - let generator: TypeScriptGenerator; - - beforeEach(() => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - includeDocuments: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - }); - - test("should generate correct TypeScript file content", () => { - const binding: BindingTypeSchema = { - identifier: { - name: "AdministrativeGender", - kind: "binding", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/administrative-gender", - }, - strength: "required", - enum: ["male", "female", "other", "unknown"], - valueset: { url: "http://hl7.org/fhir/ValueSet/administrative-gender" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).toContain("export const AdministrativeGenderValues"); - expect(result).toContain("export type AdministrativeGender"); - expect(result).toContain("'male'"); - expect(result).toContain("'female'"); - expect(result).toContain("'other'"); - expect(result).toContain("'unknown'"); - expect(result).toContain("@see http://hl7.org/fhir/ValueSet/administrative-gender"); - expect(result).toContain("hl7.fhir.r4.core"); - }); - - test("should include helper functions when enabled", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "TestBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).toContain("export const isValidTestBinding"); - expect(result).toContain("TestBindingValues.includes(value as TestBinding)"); - }); - - test("should not include helpers when disabled", () => { - generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: false, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "TestBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["value1", "value2"], - valueset: { url: "http://test.com/valueset" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).not.toContain("export const isValidTestBinding"); - }); - - test("should handle special characters in enum values", () => { - const binding: BindingTypeSchema = { - identifier: { - name: "SpecialBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/binding", - }, - strength: "required", - enum: ["1.2.840.10065.1.12.1.1", "http://example.com/value", "value-with-hyphens"], - valueset: { url: "http://test.com/valueset" }, - }; - - const result = (generator as any).generateValueSetFile(binding); - - expect(result).toContain("'1.2.840.10065.1.12.1.1'"); - expect(result).toContain("'http://example.com/value'"); - expect(result).toContain("'value-with-hyphens'"); - }); -}); - -describe("Configuration and Validation", () => { - test("should accept valid value set strengths when setting options", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - // Should not throw for valid strengths - expect(() => { - generator.setOptions({ - valueSetStrengths: ["required", "preferred"], - }); - }).not.toThrow(); - - expect(generator.getOptions().valueSetStrengths).toEqual(["required", "preferred"]); - }); - - test("should accept valid directory paths when setting options", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - // Should not throw for valid directory paths - expect(() => { - generator.setOptions({ - valueSetDirectory: "custom-valuesets", - }); - }).not.toThrow(); - - expect(generator.getOptions().valueSetDirectory).toBe("custom-valuesets"); - }); - - test("should have correct default options", () => { - const generator = new TypeScriptGenerator({ - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - const options = generator.getOptions(); - - // Check that basic options are set - expect(options.outputDir).toBe("/tmp/test"); - expect(options).toHaveProperty("outputDir"); - expect(options).toHaveProperty("logger"); - }); - - test("should handle custom configuration correctly", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - valueSetMode: "custom", - valueSetStrengths: ["required", "preferred", "extensible"], - includeValueSetHelpers: true, - valueSetDirectory: "custom-valuesets", - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const options = generator.getOptions(); - expect(options.generateValueSets).toBe(true); - expect(options.valueSetMode).toBe("custom"); - expect(options.valueSetStrengths).toEqual(["required", "preferred", "extensible"]); - expect(options.includeValueSetHelpers).toBe(true); - expect(options.valueSetDirectory).toBe("custom-valuesets"); - }); - - test("should validate all supported binding strengths", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - expect(() => { - generator.setOptions({ - valueSetStrengths: ["required", "preferred", "extensible", "example"], - }); - }).not.toThrow(); - }); -}); diff --git a/test/unit/api/generators/value-set-snapshots.test.ts b/test/unit/api/generators/value-set-snapshots.test.ts deleted file mode 100644 index c2f4ba401..000000000 --- a/test/unit/api/generators/value-set-snapshots.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -/** - * Snapshot tests for value set generation output - */ -import { describe, expect, test } from "bun:test"; -import type { BindingTypeSchema } from "@typeschema/types"; -import { TypeScriptGenerator } from "../../../../src/api/generators/typescript"; -import { createLogger } from "../../../../src/utils/codegen-logger"; - -describe("Value Set Generation Snapshots", () => { - test("should generate consistent output for basic binding", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - includeDocuments: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "AdministrativeGender", - kind: "binding", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/administrative-gender", - }, - strength: "required", - enum: ["male", "female", "other", "unknown"], - valueset: { url: "http://hl7.org/fhir/ValueSet/administrative-gender" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - // This snapshot test ensures the output remains consistent - expect(generatedCode).toBe(`/** - * AdministrativeGender value set - * @see http://hl7.org/fhir/ValueSet/administrative-gender - * @package hl7.fhir.r4.core - * @generated This file is auto-generated. Do not edit manually. - */ - -export const AdministrativeGenderValues = [ - 'male', - 'female', - 'other', - 'unknown' -] as const; - -export type AdministrativeGender = typeof AdministrativeGenderValues[number]; - -export const isValidAdministrativeGender = (value: string): value is AdministrativeGender => - AdministrativeGenderValues.includes(value as AdministrativeGender);`); - }); - - test("should generate consistent output for binding without helpers", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: false, - includeDocuments: true, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "ContactPointSystem", - kind: "binding", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/contact-point-system", - }, - strength: "required", - enum: ["phone", "fax", "email", "pager", "url", "sms", "other"], - valueset: { url: "http://hl7.org/fhir/ValueSet/contact-point-system" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - expect(generatedCode).toBe(`/** - * ContactPointSystem value set - * @see http://hl7.org/fhir/ValueSet/contact-point-system - * @package hl7.fhir.r4.core - * @generated This file is auto-generated. Do not edit manually. - */ - -export const ContactPointSystemValues = [ - 'phone', - 'fax', - 'email', - 'pager', - 'url', - 'sms', - 'other' -] as const; - -export type ContactPointSystem = typeof ContactPointSystemValues[number];`); - }); - - test("should generate consistent output without documentation", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - includeDocuments: false, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "PublicationStatus", - kind: "binding", - package: "hl7.fhir.r4.core", - version: "4.0.1", - url: "http://hl7.org/fhir/publication-status", - }, - strength: "required", - enum: ["draft", "active", "retired", "unknown"], - valueset: { url: "http://hl7.org/fhir/ValueSet/publication-status" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - expect(generatedCode).toBe(`export const PublicationStatusValues = [ - 'draft', - 'active', - 'retired', - 'unknown' -] as const; - -export type PublicationStatus = typeof PublicationStatusValues[number]; - -export const isValidPublicationStatus = (value: string): value is PublicationStatus => - PublicationStatusValues.includes(value as PublicationStatus);`); - }); - - test("should generate consistent output for special characters", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: true, - includeDocuments: false, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "SpecialCharsBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/special-chars", - }, - strength: "required", - enum: [ - "1.2.840.10065.1.12.1.1", - "http://terminology.hl7.org/CodeSystem/v3-ActCode", - "value-with-hyphens", - "value_with_underscores", - ], - valueset: { url: "http://test.com/valueset/special-chars" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - expect(generatedCode).toBe(`export const SpecialCharsBindingValues = [ - '1.2.840.10065.1.12.1.1', - 'http://terminology.hl7.org/CodeSystem/v3-ActCode', - 'value-with-hyphens', - 'value_with_underscores' -] as const; - -export type SpecialCharsBinding = typeof SpecialCharsBindingValues[number]; - -export const isValidSpecialCharsBinding = (value: string): value is SpecialCharsBinding => - SpecialCharsBindingValues.includes(value as SpecialCharsBinding);`); - }); - - test("should generate consistent output for minimal binding", () => { - const generator = new TypeScriptGenerator({ - generateValueSets: true, - includeValueSetHelpers: false, - includeDocuments: false, - outputDir: "/tmp/test", - logger: createLogger({ prefix: "Test", verbose: false }), - }); - - const binding: BindingTypeSchema = { - identifier: { - name: "MinimalBinding", - kind: "binding", - package: "test.package", - version: "1.0.0", - url: "http://test.com/minimal", - }, - strength: "required", - enum: ["one", "two"], - valueset: { url: "http://test.com/valueset/minimal" }, - }; - - const generatedCode = (generator as any).generateValueSetFile(binding); - - expect(generatedCode).toBe(`export const MinimalBindingValues = [ - 'one', - 'two' -] as const; - -export type MinimalBinding = typeof MinimalBindingValues[number];`); - }); -}); diff --git a/test/unit/typeschema/r4.test.ts b/test/unit/typeschema/r4.test.ts index 6f77e3d9a..ccedd05bd 100644 --- a/test/unit/typeschema/r4.test.ts +++ b/test/unit/typeschema/r4.test.ts @@ -7,7 +7,10 @@ describe("TypeSchema R4 generation", async () => { const r4 = await mkR4Register(); it("Bundle and elementReference", async () => { - const profile = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/Bundle" as CanonicalUrl)!; + const profile = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/Bundle" as CanonicalUrl); + if (!profile) { + throw new Error("Bundle profile not found"); + } const ts = (await registerFsAndMkTs(r4, profile))[0] as RegularTypeSchema; expect(ts?.nested).toHaveLength(5); expect(ts).toMatchObject({ @@ -47,7 +50,10 @@ describe("TypeSchema R4 generation", async () => { }); it("markdown", async () => { - const md = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/markdown" as CanonicalUrl)!; + const md = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/markdown" as CanonicalUrl); + if (!md) { + throw new Error("markdown type not found"); + } const ts = (await registerFsAndMkTs(r4, md))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { @@ -67,9 +73,13 @@ describe("TypeSchema R4 generation", async () => { const parameters = r4.resolveFs( r4Package, "http://hl7.org/fhir/StructureDefinition/Parameters" as CanonicalUrl, - )!; + ); + if (!parameters) { + throw new Error("Parameters resource not found"); + } const ts = (await registerFsAndMkTs(r4, parameters))[0] as RegularTypeSchema; - expect(ts.dependencies!).toContainEqual({ + expect(ts.dependencies).toBeDefined(); + expect(ts.dependencies).toContainEqual({ kind: "primitive-type", package: "hl7.fhir.r4.core", version: "4.0.1", diff --git a/test/unit/typeschema/register.test.ts b/test/unit/typeschema/register.test.ts index 50cfe4779..ff1ab3004 100644 --- a/test/unit/typeschema/register.test.ts +++ b/test/unit/typeschema/register.test.ts @@ -27,7 +27,10 @@ describe("Register tests", async () => { const patientSD = r4.resolveSd( r4Package, "http://hl7.org/fhir/StructureDefinition/Patient" as CanonicalUrl, - )!; + ); + if (!patientSD) { + throw new Error("Patient StructureDefinition not found"); + } expect(patientSD).toBeDefined(); }); }); @@ -37,17 +40,20 @@ describe("Register tests", async () => { const patientFS = r4.resolveFs( r4Package, "http://hl7.org/fhir/StructureDefinition/Patient" as CanonicalUrl, - )!; + ); + if (!patientFS) { + throw new Error("Patient FHIRSchema not found"); + } expect(patientFS).toBeDefined(); expect(patientFS.package_meta).toMatchObject(r4Package); }); }); describe("Genealogy", () => { - const pat = r4.resolveFsGenealogy( - r4Package, - "http://hl7.org/fhir/StructureDefinition/Patient" as CanonicalUrl, - )!; + const pat = r4.resolveFsGenealogy(r4Package, "http://hl7.org/fhir/StructureDefinition/Patient" as CanonicalUrl); + if (!pat) { + throw new Error("Patient genealogy not found"); + } expect(pat.map((fs) => fs.url)).toMatchObject([ "http://hl7.org/fhir/StructureDefinition/Patient", diff --git a/test/unit/typeschema/transformer/ccda.test.ts b/test/unit/typeschema/transformer/ccda.test.ts index 765515b03..5df622666 100644 --- a/test/unit/typeschema/transformer/ccda.test.ts +++ b/test/unit/typeschema/transformer/ccda.test.ts @@ -11,7 +11,10 @@ describe("TypeSchema CCDA generation", async () => { const resource = ccda.resolveFs( ccdaPackage, "http://hl7.org/fhir/StructureDefinition/workflow-protectiveFactor" as CanonicalUrl, - )!; + ); + if (!resource) { + throw new Error("workflow-protectiveFactor not found"); + } const ts = (await registerFsAndMkTs(ccda, resource))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { @@ -67,7 +70,10 @@ describe("TypeSchema CCDA generation", async () => { const resource = ccda.resolveFs( ccdaPackage, "http://hl7.org/cda/stds/core/StructureDefinition/ON" as CanonicalUrl, - )!; + ); + if (!resource) { + throw new Error("ON StructureDefinition not found"); + } const ts = (await registerFsAndMkTs(ccda, resource))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { @@ -181,7 +187,10 @@ describe("TypeSchema CCDA generation", async () => { const resource = ccda.resolveFs( ccdaPackage, "http://hl7.org/fhir/StructureDefinition/ehrsrle-auditevent" as CanonicalUrl, - )!; + ); + if (!resource) { + throw new Error("ehrsrle-auditevent not found"); + } const ts = (await registerFsAndMkTs(ccda, resource))[0] as RegularTypeSchema; // console.log(JSON.stringify(ts, null, 2)); // NOTE: problem: canonical manager recomend us to use R5, but we failing on R4 AuditEvent. diff --git a/test/unit/typeschema/transformer/constraint.test.ts b/test/unit/typeschema/transformer/constraint.test.ts index 68fe93501..dd4f791c1 100644 --- a/test/unit/typeschema/transformer/constraint.test.ts +++ b/test/unit/typeschema/transformer/constraint.test.ts @@ -92,7 +92,10 @@ describe("TypeSchema Processing constraint generation", async () => { const profile = r4.resolveFs( r4Package, "http://hl7.org/fhir/StructureDefinition/shareablecodesystem" as CanonicalUrl, - )!; + ); + if (!profile) { + throw new Error("shareablecodesystem profile not found"); + } expect(await registerFsAndMkTs(r4, profile)).toMatchObject([ { base: { kind: "resource", url: "http://hl7.org/fhir/StructureDefinition/CodeSystem" }, diff --git a/test/unit/typeschema/transformer/r4.test.ts b/test/unit/typeschema/transformer/r4.test.ts index b9d294bb6..cff2c5051 100644 --- a/test/unit/typeschema/transformer/r4.test.ts +++ b/test/unit/typeschema/transformer/r4.test.ts @@ -7,7 +7,10 @@ describe("TypeSchema R4 generation", async () => { const r4 = await mkR4Register(); it("Bundle and elementReference", async () => { - const profile = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/Bundle" as CanonicalUrl)!; + const profile = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/Bundle" as CanonicalUrl); + if (!profile) { + throw new Error("Bundle profile not found"); + } const ts = (await registerFsAndMkTs(r4, profile))[0] as RegularTypeSchema; expect(ts?.nested).toHaveLength(5); expect(ts).toMatchObject({ @@ -47,7 +50,10 @@ describe("TypeSchema R4 generation", async () => { }); it("markdown", async () => { - const md = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/markdown" as CanonicalUrl)!; + const md = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/markdown" as CanonicalUrl); + if (!md) { + throw new Error("markdown type not found"); + } const ts = (await registerFsAndMkTs(r4, md))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { @@ -67,9 +73,13 @@ describe("TypeSchema R4 generation", async () => { const parameters = r4.resolveFs( r4Package, "http://hl7.org/fhir/StructureDefinition/Parameters" as CanonicalUrl, - )!; + ); + if (!parameters) { + throw new Error("Parameters resource not found"); + } const ts = (await registerFsAndMkTs(r4, parameters))[0] as RegularTypeSchema; - expect(ts.dependencies!).toContainEqual({ + expect(ts.dependencies).toBeDefined(); + expect(ts.dependencies).toContainEqual({ kind: "primitive-type", package: "hl7.fhir.r4.core", version: "4.0.1",