vitek-plugin 0.1.0-beta → 0.1.2-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Shared request handler for API routes
3
+ * Used by both dev server and preview/production server
4
+ */
5
+ import { matchRoute } from '../routing/route-matcher.js';
6
+ import { createContext, isVitekResponse } from '../context/create-context.js';
7
+ import { getApplicableMiddlewares } from '../middleware/get-applicable-middlewares.js';
8
+ import { compose } from '../middleware/compose.js';
9
+ import { API_BASE_PATH } from '../../shared/constants.js';
10
+ import { HttpError } from '../../shared/errors.js';
11
+ const noop = () => { };
12
+ /**
13
+ * Creates a Connect-style middleware that handles /api/* requests using the given routes and middlewares.
14
+ */
15
+ export function createRequestHandler(options) {
16
+ const { routes, middlewares, logger } = options;
17
+ const logRouteMatched = logger?.routeMatched ?? noop;
18
+ const logRequest = logger?.request ?? noop;
19
+ const logWarn = logger?.warn ?? noop;
20
+ const logError = logger?.error ?? noop;
21
+ return async (req, res, next) => {
22
+ if (!req.url?.startsWith(API_BASE_PATH)) {
23
+ return next();
24
+ }
25
+ const startTime = Date.now();
26
+ const requestMethod = req.method?.toLowerCase() || 'get';
27
+ const requestPath = req.url.split('?')[0];
28
+ try {
29
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
30
+ const routePath = url.pathname.replace(API_BASE_PATH, '') || '/';
31
+ const method = requestMethod;
32
+ const match = matchRoute(routes, routePath, method);
33
+ if (!match) {
34
+ const duration = Date.now() - startTime;
35
+ res.statusCode = 404;
36
+ res.setHeader('Content-Type', 'application/json');
37
+ res.end(JSON.stringify({ error: 'Route not found' }));
38
+ logRequest(requestMethod, requestPath, 404, duration);
39
+ return;
40
+ }
41
+ logRouteMatched(match.route.pattern, method);
42
+ const query = {};
43
+ url.searchParams.forEach((value, key) => {
44
+ if (query[key]) {
45
+ const existing = query[key];
46
+ query[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
47
+ }
48
+ else {
49
+ query[key] = value;
50
+ }
51
+ });
52
+ let body;
53
+ if (['post', 'put', 'patch'].includes(method)) {
54
+ body = await new Promise((resolve) => {
55
+ const chunks = [];
56
+ req.on('data', (chunk) => chunks.push(chunk));
57
+ req.on('end', () => {
58
+ const rawBody = Buffer.concat(chunks).toString();
59
+ if (!rawBody) {
60
+ resolve(undefined);
61
+ return;
62
+ }
63
+ try {
64
+ resolve(JSON.parse(rawBody));
65
+ }
66
+ catch {
67
+ resolve(rawBody);
68
+ }
69
+ });
70
+ });
71
+ }
72
+ const context = createContext({
73
+ url: req.url,
74
+ method,
75
+ headers: (req.headers || {}),
76
+ body,
77
+ }, match.params, query);
78
+ const applicableMiddlewares = getApplicableMiddlewares(middlewares, match.route.pattern);
79
+ const composed = compose(applicableMiddlewares);
80
+ const handler = async () => {
81
+ const result = await match.route.handler(context);
82
+ if (isVitekResponse(result)) {
83
+ const response = result;
84
+ const statusCode = response.status || 200;
85
+ if (response.headers) {
86
+ for (const [key, value] of Object.entries(response.headers)) {
87
+ res.setHeader(key, value);
88
+ }
89
+ }
90
+ if (!response.headers || !response.headers['Content-Type']) {
91
+ if (response.body !== undefined) {
92
+ res.setHeader('Content-Type', 'application/json');
93
+ }
94
+ }
95
+ res.statusCode = statusCode;
96
+ if (response.body === undefined) {
97
+ res.end();
98
+ }
99
+ else if (typeof response.body === 'string') {
100
+ res.end(response.body);
101
+ }
102
+ else {
103
+ res.end(JSON.stringify(response.body));
104
+ }
105
+ logRequest(requestMethod, requestPath, statusCode, Date.now() - startTime);
106
+ }
107
+ else {
108
+ res.setHeader('Content-Type', 'application/json');
109
+ res.statusCode = 200;
110
+ res.end(JSON.stringify(result));
111
+ logRequest(requestMethod, requestPath, 200, Date.now() - startTime);
112
+ }
113
+ };
114
+ await composed(context, handler);
115
+ }
116
+ catch (error) {
117
+ const duration = Date.now() - startTime;
118
+ if (error instanceof HttpError) {
119
+ const httpError = error;
120
+ logWarn(`HTTP Error ${httpError.statusCode}: ${httpError.message}`);
121
+ res.statusCode = httpError.statusCode;
122
+ res.setHeader('Content-Type', 'application/json');
123
+ res.end(JSON.stringify({
124
+ error: httpError.name,
125
+ message: httpError.message,
126
+ code: httpError.code,
127
+ }));
128
+ logRequest(requestMethod, requestPath, httpError.statusCode, duration);
129
+ }
130
+ else {
131
+ const errorMessage = error instanceof Error ? error.message : String(error);
132
+ logError(`Error handling request: ${errorMessage}`);
133
+ res.statusCode = 500;
134
+ res.setHeader('Content-Type', 'application/json');
135
+ res.end(JSON.stringify({
136
+ error: 'Internal server error',
137
+ message: errorMessage,
138
+ }));
139
+ logRequest(requestMethod, requestPath, 500, duration);
140
+ }
141
+ }
142
+ };
143
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/core/types/generate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CA8FvF;AA6LD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,EAAE,EACrB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAE,OAAc,GAAG,MAAM,CAuKxH;AA6KD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,EAAE,EACrB,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,OAAc,GAC3B,OAAO,CAAC,IAAI,CAAC,CAUf"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/core/types/generate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CA8FvF;AA4LD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,EAAE,EACrB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAE,OAAc,GAAG,MAAM,CAuKxH;AA6KD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,EAAE,EACrB,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,OAAc,GAC3B,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -248,7 +248,7 @@ function generateUniqueTypeName(route, baseName, conflictingRoutes, allUsedNames
248
248
  */
249
249
  function generateParamsType(params) {
250
250
  if (params.length === 0) {
251
- return ' // No dynamic params';
251
+ return '';
252
252
  }
253
253
  return params.map(param => ` ${param}: string;`).join('\n');
254
254
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Export público do pacote Vitek
2
+ * Public exports for the Vitek package
3
3
  */
4
4
  export { vitek } from './plugin.js';
5
5
  export type { VitekOptions } from './plugin.js';
6
- export type { VitekContext, VitekRequest, } from './core/context/create-context.js';
6
+ export type { VitekContext, VitekRequest, VitekResponse, } from './core/context/create-context.js';
7
7
  export type { Route, RouteHandler, Middleware, RouteMatch, } from './core/routing/route-types.js';
8
+ export { json, ok, created, noContent, badRequest, unauthorized, forbidden, notFound, conflict, unprocessableEntity, tooManyRequests, internalServerError, redirect, } from './shared/response-helpers.js';
9
+ export { VitekError, HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, ValidationError, TooManyRequestsError, InternalServerError, } from './shared/errors.js';
10
+ export { validate, validateOrThrow, validateBody, validateQuery, } from './core/validation/validator.js';
11
+ export type { ValidationSchema, ValidationRule, ValidationResult } from './core/validation/types.js';
8
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,YAAY,EACV,YAAY,EACZ,YAAY,GACb,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,KAAK,EACL,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,KAAK,EACL,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,IAAI,EACJ,EAAE,EACF,OAAO,EACP,SAAS,EACT,UAAU,EACV,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EACnB,QAAQ,GACT,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,UAAU,EACV,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,QAAQ,EACR,eAAe,EACf,YAAY,EACZ,aAAa,GACd,MAAM,gCAAgC,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  /**
2
- * Export público do pacote Vitek
2
+ * Public exports for the Vitek package
3
3
  */
4
4
  export { vitek } from './plugin.js';
5
+ export { json, ok, created, noContent, badRequest, unauthorized, forbidden, notFound, conflict, unprocessableEntity, tooManyRequests, internalServerError, redirect, } from './shared/response-helpers.js';
6
+ export { VitekError, HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, ValidationError, TooManyRequestsError, InternalServerError, } from './shared/errors.js';
7
+ export { validate, validateOrThrow, validateBody, validateQuery, } from './core/validation/validator.js';
package/dist/plugin.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface VitekOptions {
8
8
  apiDir?: string;
9
9
  /** API base path (default: /api) */
10
10
  apiBasePath?: string;
11
+ /** Build API bundle for preview/production (default: true). Set to false to skip. */
12
+ buildApi?: boolean;
11
13
  /** Enable request validation (default: false) */
12
14
  enableValidation?: boolean;
13
15
  /** Logging configuration */
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAOnC,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4BAA4B;IAC5B,OAAO,CAAC,EAAE;QACR,uEAAuE;QACvE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;QAC5C,uDAAuD;QACvD,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAC/B,iDAAiD;QACjD,kBAAkB,CAAC,EAAE,OAAO,CAAC;KAC9B,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,MAAM,CAoDxD"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAWnC,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4BAA4B;IAC5B,OAAO,CAAC,EAAE;QACR,uEAAuE;QACvE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;QAC5C,uDAAuD;QACvD,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAC/B,iDAAiD;QACjD,kBAAkB,CAAC,EAAE,OAAO,CAAC;KAC9B,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,MAAM,CAgHxD"}
package/dist/plugin.js CHANGED
@@ -4,29 +4,33 @@
4
4
  */
5
5
  import * as path from 'path';
6
6
  import * as fs from 'fs';
7
+ import { pathToFileURL } from 'url';
7
8
  import { createViteDevServerMiddleware } from './adapters/vite/dev-server.js';
8
9
  import { createViteLogger } from './adapters/vite/logger.js';
9
- import { API_DIR_NAME } from './shared/constants.js';
10
+ import { createRequestHandler } from './core/server/request-handler.js';
11
+ import { buildApiBundle, getApiBundleFilename } from './build/build-api-bundle.js';
12
+ import { API_BASE_PATH, API_DIR_NAME } from './shared/constants.js';
10
13
  /**
11
14
  * Vite plugin for Vitek
12
15
  */
13
16
  export function vitek(options = {}) {
14
- const apiDir = options.apiDir || `src/${API_DIR_NAME}`;
17
+ const apiDirOption = options.apiDir || `src/${API_DIR_NAME}`;
18
+ const buildApi = options.buildApi !== false;
15
19
  let root;
20
+ let buildOutDir;
16
21
  let cleanupFn = null;
17
22
  return {
18
23
  name: 'vitek',
19
24
  configResolved(config) {
20
25
  root = config.root;
26
+ buildOutDir = path.resolve(root, config.build?.outDir ?? 'dist');
21
27
  },
22
28
  configureServer(server) {
23
- const fullApiDir = path.resolve(root, apiDir);
24
- // Check if directory exists
29
+ const fullApiDir = path.resolve(root, apiDirOption);
25
30
  if (!fs.existsSync(fullApiDir)) {
26
31
  server.config.logger.warn(`[vitek] API directory not found: ${fullApiDir}`);
27
32
  return;
28
33
  }
29
- // Create logger and middleware
30
34
  const logger = createViteLogger(server.config.logger, options.logging);
31
35
  const { middleware, cleanup } = createViteDevServerMiddleware({
32
36
  root,
@@ -36,15 +40,63 @@ export function vitek(options = {}) {
36
40
  enableValidation: options.enableValidation || false,
37
41
  });
38
42
  cleanupFn = cleanup;
39
- // Register middleware in Vite server
40
43
  server.middlewares.use(middleware);
41
44
  logger.info('Vitek plugin initialized');
42
- // Show relative path to root
43
45
  const relativeApiDir = path.relative(root, fullApiDir);
44
46
  logger.info(`API directory: ./${relativeApiDir.replace(/\\/g, '/')}`);
45
47
  },
48
+ configurePreviewServer(server) {
49
+ if (!buildApi) {
50
+ return;
51
+ }
52
+ const bundlePath = path.join(buildOutDir, getApiBundleFilename());
53
+ if (!fs.existsSync(bundlePath)) {
54
+ server.config.logger.warn('[vitek] API bundle not found; preview serving static assets only. Run `vite build` first.');
55
+ return;
56
+ }
57
+ const bundleUrl = pathToFileURL(bundlePath).href;
58
+ const bundleLoadPromise = import(bundleUrl);
59
+ let apiHandler = null;
60
+ const apiMiddleware = (req, res, next) => {
61
+ if (!req.url?.startsWith(API_BASE_PATH)) {
62
+ return next();
63
+ }
64
+ bundleLoadPromise
65
+ .then((mod) => {
66
+ if (!apiHandler) {
67
+ apiHandler = createRequestHandler({
68
+ routes: mod.routes,
69
+ middlewares: mod.middlewares,
70
+ });
71
+ server.config.logger.info('[vitek] API middleware registered for preview');
72
+ }
73
+ apiHandler(req, res, next);
74
+ })
75
+ .catch((err) => {
76
+ server.config.logger.error(`[vitek] Failed to load API bundle: ${err instanceof Error ? err.message : String(err)}`);
77
+ res.statusCode = 500;
78
+ res.setHeader('Content-Type', 'application/json');
79
+ res.end(JSON.stringify({ error: 'Internal server error', message: 'Failed to load API bundle' }));
80
+ });
81
+ };
82
+ server.middlewares.use(apiMiddleware);
83
+ },
84
+ async closeBundle() {
85
+ if (buildApi) {
86
+ const fullApiDir = path.resolve(root, apiDirOption);
87
+ try {
88
+ await buildApiBundle({
89
+ root,
90
+ apiDir: fullApiDir,
91
+ outDir: buildOutDir,
92
+ });
93
+ }
94
+ catch (err) {
95
+ console.error('[vitek] Failed to build API bundle:', err instanceof Error ? err.message : err);
96
+ }
97
+ }
98
+ },
46
99
  buildEnd() {
47
- // Clean up resources when build ends
48
100
  if (cleanupFn) {
49
101
  cleanupFn();
50
102
  cleanupFn = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitek-plugin",
3
- "version": "0.1.0-beta",
3
+ "version": "0.1.2-beta",
4
4
  "description": "Vite plugin for file-based HTTP API generation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,11 +14,6 @@
14
14
  "files": [
15
15
  "dist"
16
16
  ],
17
- "scripts": {
18
- "build": "tsc",
19
- "dev": "tsc --watch",
20
- "prepublishOnly": "npm run build"
21
- },
22
17
  "keywords": [
23
18
  "vite",
24
19
  "plugin",
@@ -39,10 +34,29 @@
39
34
  "peerDependencies": {
40
35
  "vite": "^5.0.0"
41
36
  },
37
+ "dependencies": {
38
+ "esbuild": "^0.24.0",
39
+ "connect": "^3.7.0",
40
+ "serve-static": "^1.16.0"
41
+ },
42
42
  "devDependencies": {
43
43
  "@types/node": "^20.0.0",
44
+ "concurrently": "^9.0.0",
44
45
  "typescript": "^5.0.0",
45
- "vite": "^5.0.0"
46
+ "vite": "^5.0.0",
47
+ "vitepress": "^1.0.0",
48
+ "@types/connect": "^3.4.0",
49
+ "@types/serve-static": "^1.15.0"
50
+ },
51
+ "bin": {
52
+ "vitek-serve": "./dist/cli/serve.js"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc",
56
+ "dev": "concurrently \"pnpm:dev:tsc\" \"pnpm:docs:dev\"",
57
+ "dev:tsc": "tsc --watch",
58
+ "docs:dev": "vitepress dev docs",
59
+ "docs:build": "vitepress build docs",
60
+ "docs:preview": "vitepress preview docs"
46
61
  }
47
- }
48
-
62
+ }