diff --git a/Dockerfile b/Dockerfile index 24a4a22..e059342 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force # Copy compiled output from builder COPY --from=builder /app/dist ./dist +COPY --from=builder /app/openapi.json ./openapi.json # Run as non-root for security USER node diff --git a/package.json b/package.json index befb1fe..f4116bd 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "commonjs", "scripts": { "dev": "ts-node-dev --respawn --transpile-only -r tsconfig-paths/register src/server.ts", - "build": "tsc && tsc-alias", + "build": "npm run build:docs && tsc && tsc-alias", + "build:docs": "ts-node -r tsconfig-paths/register src/docs/generateDoc.ts", "start": "node dist/server.js", "lint": "eslint . --ext .ts --fix", "lint:check": "eslint . --ext .ts", @@ -33,12 +34,12 @@ "pino-http": "^11.0.0", "swagger-ui-express": "^5.0.1", "tsconfig-paths": "^4.2.0", - "tspec": "^0.1.116", "zeptomail": "^6.2.1", "zod": "^3.23.8", "pino-pretty": "^13.1.3" }, "devDependencies": { + "tspec": "^0.1.116", "@eslint/js": "^9.37.0", "@types/cookie": "^0.6.0", "@types/cookie-signature": "^1.1.2", @@ -54,7 +55,6 @@ "husky": "^8.0.0", "jest": "^30.2.0", "mongodb-memory-server": "^10.2.3", - "pino-pretty": "^13.1.3", "prettier": "^3.7.4", "supertest": "^7.1.4", "ts-jest": "^29.4.5", diff --git a/src/docs/generateDoc.ts b/src/docs/generateDoc.ts new file mode 100644 index 0000000..baef372 --- /dev/null +++ b/src/docs/generateDoc.ts @@ -0,0 +1,31 @@ +import { generateTspec, Tspec } from "tspec"; +import fs from "fs"; +import path from "path"; + +const options: Tspec.GenerateParams = { + specPathGlobs: ["src/**/*.ts"], + tsconfigPath: "./tsconfig.json", + outputPath: "openapi.json", + specVersion: 3, + openapi: { + title: "Timesheets By Exploit", + version: "1.0.0", + description: + "This is the official documentation of the Timesheets By Exploit API", + }, + debug: false, + ignoreErrors: true, +}; + +async function generate() { + console.log("Generating OpenAPI specification..."); + const spec = await generateTspec(options); + const outputPath = path.join(process.cwd(), "openapi.json"); + fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2)); + console.log(`OpenAPI specification generated at ${outputPath}`); +} + +generate().catch((err) => { + console.error("Failed to generate OpenAPI specification:", err); + process.exit(1); +}); diff --git a/src/docs/tspecGenerator.ts b/src/docs/tspecGenerator.ts index 1fa2053..6a28148 100644 --- a/src/docs/tspecGenerator.ts +++ b/src/docs/tspecGenerator.ts @@ -1,6 +1,8 @@ -import { generateTspec, Tspec } from "tspec"; +import fs from "fs"; +import path from "path"; +import { Tspec } from "tspec"; -const options: Tspec.GenerateParams = { +const options = { specPathGlobs: ["src/**/*.ts"], tsconfigPath: "./tsconfig.json", outputPath: "openapi.json", @@ -16,5 +18,21 @@ const options: Tspec.GenerateParams = { }; export async function getTSpec() { - return await generateTspec(options); + if ( + process.env.NODE_ENV === "production" || + process.env.TSPEC_STATIC === "true" + ) { + try { + const specPath = path.join(process.cwd(), "openapi.json"); + const spec = JSON.parse(fs.readFileSync(specPath, "utf8")); + return spec; + } catch (error) { + console.error("Failed to load pre-generated OpenAPI spec:", error); + throw error; + } + } + + // Dynamic import to avoid runtime dependency in production + const { generateTspec } = await import("tspec"); + return await generateTspec(options as Tspec.GenerateParams | undefined); }