webspresso 0.0.96 → 0.0.98

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.
Files changed (121) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  7. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  8. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  9. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  10. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/admin-password.js +2 -1
  14. package/bin/commands/admin-setup.js +1 -1
  15. package/bin/commands/agents-init.js +51 -11
  16. package/bin/commands/analytics-prune.js +57 -0
  17. package/bin/commands/api.js +207 -15
  18. package/bin/commands/audit-prune.js +1 -1
  19. package/bin/commands/build.js +99 -0
  20. package/bin/commands/dev.js +64 -35
  21. package/bin/commands/doctor.js +26 -0
  22. package/bin/commands/email-prune.js +47 -0
  23. package/bin/commands/favicon-generate.js +1 -1
  24. package/bin/commands/make-job.js +86 -0
  25. package/bin/commands/make-model.js +250 -0
  26. package/bin/commands/new.js +45 -28
  27. package/bin/commands/openapi.js +102 -0
  28. package/bin/commands/orm-map.js +5 -4
  29. package/bin/commands/page.js +6 -3
  30. package/bin/commands/polar-migrate.js +1 -1
  31. package/bin/commands/queue-prune.js +59 -0
  32. package/bin/commands/routes.js +52 -0
  33. package/bin/commands/seed.js +1 -1
  34. package/bin/commands/service.js +6 -3
  35. package/bin/commands/typegen.js +106 -0
  36. package/bin/commands/upload-prune.js +79 -0
  37. package/bin/webspresso.js +22 -0
  38. package/core/applySchema.js +13 -6
  39. package/core/auth/hash.js +31 -17
  40. package/core/compression/middleware.js +23 -17
  41. package/core/openapi/build-from-api-routes.js +160 -29
  42. package/core/orm/cache/fingerprint.js +15 -1
  43. package/core/orm/cache/memory-provider.js +27 -2
  44. package/core/orm/index.js +12 -7
  45. package/core/orm/model.js +49 -7
  46. package/core/queue/adapters/database.js +94 -0
  47. package/core/queue/adapters/memory.js +62 -0
  48. package/core/queue/adapters/redis.js +80 -0
  49. package/core/queue/manager.js +123 -0
  50. package/core/ssr/stream.js +11 -0
  51. package/index.d.ts +660 -10
  52. package/index.js +44 -96
  53. package/package.json +32 -12
  54. package/plugins/admin-panel/api.js +279 -17
  55. package/plugins/admin-panel/app.js +124 -31
  56. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  57. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  58. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  59. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  60. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  61. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  62. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  63. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  64. package/plugins/admin-panel/core/admin-module.js +13 -7
  65. package/plugins/admin-panel/core/api-extensions.js +34 -2
  66. package/plugins/admin-panel/core/registry.js +96 -16
  67. package/plugins/admin-panel/index.js +96 -0
  68. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  69. package/plugins/admin-panel/modules/admin-users.js +16 -1
  70. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  71. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  72. package/plugins/admin-panel/modules/dashboard.js +283 -0
  73. package/plugins/admin-panel/modules/menu.js +56 -18
  74. package/plugins/admin-panel/modules/user-management.js +24 -6
  75. package/plugins/audit-log/admin-component.js +11 -0
  76. package/plugins/audit-log/api-handlers.js +34 -0
  77. package/plugins/audit-log/index.js +129 -3
  78. package/plugins/audit-log/purge.js +60 -10
  79. package/plugins/basic-auth/index.js +3 -1
  80. package/plugins/content/client/inline-edit.js +12 -12
  81. package/plugins/cors.js +6 -7
  82. package/plugins/dashboard/app.js +152 -27
  83. package/plugins/dashboard/index.js +143 -39
  84. package/plugins/dashboard/styles.js +78 -0
  85. package/plugins/data-exchange/export-xlsx.js +9 -2
  86. package/plugins/data-exchange/import.js +9 -2
  87. package/plugins/data-exchange/index.js +6 -3
  88. package/plugins/email/index.js +132 -2
  89. package/plugins/email/purge.js +60 -10
  90. package/plugins/file-manager/core.js +1 -1
  91. package/plugins/file-manager/security.js +33 -23
  92. package/plugins/index.js +49 -87
  93. package/plugins/mcp/transports/sse.js +4 -2
  94. package/plugins/orm-cache-admin/index.js +4 -2
  95. package/plugins/queue/index.js +11 -0
  96. package/plugins/schema-explorer/admin-component.js +714 -0
  97. package/plugins/schema-explorer/relations-graph.js +205 -0
  98. package/plugins/schema-explorer.js +248 -69
  99. package/plugins/site-analytics/admin-component.js +30 -8
  100. package/plugins/site-analytics/api-handlers.js +42 -0
  101. package/plugins/site-analytics/index.js +147 -2
  102. package/plugins/site-analytics/purge.js +168 -0
  103. package/plugins/swagger.js +261 -13
  104. package/plugins/upload/index.js +26 -0
  105. package/plugins/upload/orphan-pruner.js +292 -0
  106. package/src/api/api-loader.js +3 -1
  107. package/src/bundle/index.js +517 -0
  108. package/src/bundle/manifest.js +165 -0
  109. package/src/dev/banner.js +296 -0
  110. package/src/dev/colors.js +70 -0
  111. package/src/dev/index.js +62 -0
  112. package/src/dev/profiler.js +354 -0
  113. package/src/dev/typegen.js +497 -0
  114. package/src/dev-reload/index.js +611 -0
  115. package/src/file-router.js +213 -63
  116. package/src/helpers.js +958 -98
  117. package/src/njk-frontmatter.js +12 -0
  118. package/src/pages/page-loader.js +61 -11
  119. package/src/server.js +629 -63
  120. package/src/services/discovery.js +6 -1
  121. package/src/services/index.js +6 -13
@@ -0,0 +1,497 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso TypeGen Engine
5
+ * Automatic TypeScript & IDE auto-complete generator for ORM models, business services, and routes
6
+ * @module src/dev/typegen
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { getAllModels } = require('../../core/orm/model');
12
+ const { hasServiceRegistry, getServiceRegistry } = require('../../src/app-context');
13
+ const { scanRoutes } = require('../../src/discovery/scan-routes');
14
+ const { compileRouteTable } = require('../../src/routing/route-table');
15
+ const { z } = require('../../core/validation');
16
+
17
+ /**
18
+ * Converts a Zod type schema to a TypeScript type string
19
+ * @param {import('zod').ZodTypeAny} zType
20
+ * @param {number} [indent=0]
21
+ * @returns {string}
22
+ */
23
+ function zodToTsType(zType, indent = 0) {
24
+ if (!zType || !zType._def) return 'unknown';
25
+ const typeName = zType._def.typeName;
26
+ const spaces = ' '.repeat(indent);
27
+ const nextSpaces = ' '.repeat(indent + 1);
28
+
29
+ switch (typeName) {
30
+ case 'ZodString':
31
+ return 'string';
32
+ case 'ZodNumber':
33
+ return 'number';
34
+ case 'ZodBoolean':
35
+ return 'boolean';
36
+ case 'ZodDate':
37
+ return 'Date';
38
+ case 'ZodBigInt':
39
+ return 'bigint';
40
+ case 'ZodNull':
41
+ return 'null';
42
+ case 'ZodUndefined':
43
+ return 'undefined';
44
+ case 'ZodVoid':
45
+ return 'void';
46
+ case 'ZodAny':
47
+ return 'any';
48
+ case 'ZodUnknown':
49
+ return 'unknown';
50
+ case 'ZodNever':
51
+ return 'never';
52
+ case 'ZodLiteral':
53
+ return typeof zType._def.value === 'string'
54
+ ? `'${zType._def.value}'`
55
+ : String(zType._def.value);
56
+ case 'ZodEnum':
57
+ return (zType._def.values || []).map((v) => `'${v}'`).join(' | ') || 'string';
58
+ case 'ZodNativeEnum':
59
+ return 'number | string';
60
+ case 'ZodArray':
61
+ return `Array<${zodToTsType(zType._def.type, indent)}>`;
62
+ case 'ZodOptional':
63
+ return `${zodToTsType(zType._def.innerType, indent)} | undefined`;
64
+ case 'ZodNullable':
65
+ return `${zodToTsType(zType._def.innerType, indent)} | null`;
66
+ case 'ZodDefault':
67
+ return zodToTsType(zType._def.innerType, indent);
68
+ case 'ZodEffects':
69
+ return zodToTsType(zType._def.schema, indent);
70
+ case 'ZodUnion':
71
+ return (zType._def.options || []).map((opt) => zodToTsType(opt, indent)).join(' | ') || 'unknown';
72
+ case 'ZodObject': {
73
+ const shape = zType.shape;
74
+ if (!shape || Object.keys(shape).length === 0) return 'Record<string, unknown>';
75
+ const entries = Object.entries(shape).map(([k, v]) => {
76
+ const isOpt = v?._def?.typeName === 'ZodOptional';
77
+ const innerType = isOpt ? zodToTsType(v._def.innerType, indent + 1) : zodToTsType(v, indent + 1);
78
+ return `${nextSpaces}${k}${isOpt ? '?' : ''}: ${innerType};`;
79
+ });
80
+ return `{\n${entries.join('\n')}\n${spaces}}`;
81
+ }
82
+ case 'ZodRecord':
83
+ return `Record<string, ${zodToTsType(zType._def.valueType, indent)}>`;
84
+ default:
85
+ return 'unknown';
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Collects ORM model definitions from memory and filesystem
91
+ * @param {string} [modelsDir]
92
+ * @returns {Array<Object>}
93
+ */
94
+ function collectModelMetadata(modelsDir) {
95
+ // If modelsDir exists, load JS files into registry if not already loaded
96
+ if (modelsDir && fs.existsSync(modelsDir)) {
97
+ try {
98
+ const files = fs.readdirSync(modelsDir);
99
+ for (const file of files) {
100
+ if (file.endsWith('.js') && !file.startsWith('_') && !file.startsWith('.')) {
101
+ const filePath = path.join(modelsDir, file);
102
+ try {
103
+ require(path.resolve(filePath));
104
+ } catch {
105
+ // ignore if requires database connection
106
+ }
107
+ }
108
+ }
109
+ } catch {
110
+ // directory read failure
111
+ }
112
+ }
113
+
114
+ const result = [];
115
+ try {
116
+ const registered = getAllModels();
117
+ for (const [name, model] of registered.entries()) {
118
+ const columns = [];
119
+ if (model.schema && model.schema.shape) {
120
+ for (const [colName, colZod] of Object.entries(model.schema.shape)) {
121
+ const isOpt = colZod?._def?.typeName === 'ZodOptional';
122
+ const innerZod = isOpt ? colZod._def.innerType : colZod;
123
+ columns.push({
124
+ name: colName,
125
+ isOptional: isOpt,
126
+ tsType: zodToTsType(innerZod, 1),
127
+ });
128
+ }
129
+ }
130
+
131
+ // Check if primaryKey exists in columns, otherwise add it
132
+ const pkName = model.primaryKey || 'id';
133
+ if (!columns.some((c) => c.name === pkName)) {
134
+ columns.unshift({
135
+ name: pkName,
136
+ isOptional: false,
137
+ tsType: 'number | string',
138
+ });
139
+ }
140
+
141
+ // Timestamps scope
142
+ const hasTimestamps = Boolean(model.scopes?.timestamps || model.timestamps);
143
+ if (hasTimestamps) {
144
+ if (!columns.some((c) => c.name === 'created_at')) {
145
+ columns.push({ name: 'created_at', isOptional: true, tsType: 'Date' });
146
+ }
147
+ if (!columns.some((c) => c.name === 'updated_at')) {
148
+ columns.push({ name: 'updated_at', isOptional: true, tsType: 'Date' });
149
+ }
150
+ }
151
+
152
+ // Soft delete scope
153
+ const hasSoftDelete = Boolean(
154
+ model.scopes?.softDelete || model.scopes?.notDeleted || model.softDelete || model.paranoid
155
+ );
156
+ if (hasSoftDelete) {
157
+ if (!columns.some((c) => c.name === 'deleted_at')) {
158
+ columns.push({ name: 'deleted_at', isOptional: true, tsType: 'Date | null' });
159
+ }
160
+ }
161
+
162
+ result.push({
163
+ name,
164
+ table: model.table || name.toLowerCase(),
165
+ primaryKey: pkName,
166
+ columns,
167
+ relations: model.relations || {},
168
+ });
169
+ }
170
+ } catch {
171
+ // registry read failure
172
+ }
173
+
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Scans services directory recursively
179
+ * @param {string} dir
180
+ * @param {string} [baseDir]
181
+ * @returns {Array<Object>}
182
+ */
183
+ function scanServicesDir(dir, baseDir = dir) {
184
+ if (!dir || !fs.existsSync(dir)) return [];
185
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
186
+ const services = [];
187
+
188
+ for (const entry of entries) {
189
+ const fullPath = path.join(dir, entry.name);
190
+ if (entry.isDirectory()) {
191
+ services.push(...scanServicesDir(fullPath, baseDir));
192
+ } else if (entry.isFile() && entry.name.endsWith('.js') && !entry.name.startsWith('_') && !entry.name.startsWith('.')) {
193
+ const relPath = path.relative(baseDir, fullPath);
194
+ const dotName = relPath.replace(/\.js$/, '').split(path.sep).join('.');
195
+
196
+ let inputTsType = 'unknown';
197
+ let hasSchema = false;
198
+
199
+ try {
200
+ const mod = require(path.resolve(fullPath));
201
+ const def = mod && mod.default ? mod.default : mod;
202
+ let schema = def && def.schema;
203
+ if (typeof schema === 'function') {
204
+ try {
205
+ schema = schema({ z });
206
+ } catch {
207
+ schema = null;
208
+ }
209
+ }
210
+
211
+ if (schema && typeof schema.parse !== 'function' && typeof schema === 'object') {
212
+ if (schema.body && typeof schema.body.parse === 'function') {
213
+ schema = schema.body;
214
+ } else if (Object.values(schema).some((v) => v && typeof v.parse === 'function')) {
215
+ schema = z.object(schema);
216
+ }
217
+ }
218
+
219
+ if (schema && typeof schema.parse === 'function') {
220
+ hasSchema = true;
221
+ inputTsType = zodToTsType(schema, 2);
222
+ }
223
+ } catch {
224
+ // Module failed to load
225
+ }
226
+
227
+ services.push({
228
+ name: dotName,
229
+ filePath: fullPath,
230
+ inputTsType,
231
+ hasSchema,
232
+ });
233
+ }
234
+ }
235
+
236
+ return services;
237
+ }
238
+
239
+ /**
240
+ * Collects services from directory and active ServiceRegistry
241
+ * @param {string} [servicesDir]
242
+ * @returns {Array<Object>}
243
+ */
244
+ function collectServiceMetadata(servicesDir) {
245
+ const map = new Map();
246
+
247
+ // 1. Filesystem scan
248
+ if (servicesDir && fs.existsSync(servicesDir)) {
249
+ const fileServices = scanServicesDir(servicesDir);
250
+ for (const s of fileServices) {
251
+ map.set(s.name, s);
252
+ }
253
+ }
254
+
255
+ // 2. Active runtime registry
256
+ try {
257
+ if (hasServiceRegistry()) {
258
+ const registry = getServiceRegistry();
259
+ if (registry && registry.services) {
260
+ for (const [name, def] of registry.services.entries()) {
261
+ if (!map.has(name)) {
262
+ let inputTsType = 'unknown';
263
+ let schema = def?.schema;
264
+ if (typeof schema === 'function') {
265
+ try {
266
+ schema = schema({ z });
267
+ } catch {
268
+ schema = null;
269
+ }
270
+ }
271
+ if (schema && typeof schema.parse === 'function') {
272
+ inputTsType = zodToTsType(schema, 2);
273
+ }
274
+ map.set(name, {
275
+ name,
276
+ filePath: '',
277
+ inputTsType,
278
+ hasSchema: Boolean(schema),
279
+ });
280
+ }
281
+ }
282
+ }
283
+ }
284
+ } catch {
285
+ // Registry not active
286
+ }
287
+
288
+ return Array.from(map.values());
289
+ }
290
+
291
+ /**
292
+ * Collects discovered routes from pages directory
293
+ * @param {string} rootDir
294
+ * @param {string} pagesRelDir
295
+ * @returns {Array<Object>}
296
+ */
297
+ function collectRouteMetadata(rootDir, pagesRelDir) {
298
+ const pagesDir = path.resolve(rootDir, pagesRelDir);
299
+ if (!fs.existsSync(pagesDir)) return [];
300
+
301
+ try {
302
+ const apiRelDir = path.join(pagesRelDir, 'api');
303
+ const apiDir = path.resolve(rootDir, apiRelDir);
304
+ const descriptors = scanRoutes({
305
+ rootDir,
306
+ pages: { dir: pagesRelDir },
307
+ api: fs.existsSync(apiDir) ? { dir: apiRelDir } : false,
308
+ });
309
+ const table = compileRouteTable(descriptors, { isDev: true });
310
+ return table.list();
311
+ } catch {
312
+ return [];
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Generates the complete TypeScript declaration content
318
+ * @param {Object} data
319
+ * @param {Array<Object>} data.models
320
+ * @param {Array<Object>} data.services
321
+ * @param {Array<Object>} data.routes
322
+ * @returns {string}
323
+ */
324
+ function generateDeclarationCode({ models = [], services = [], routes = [] }) {
325
+ const timestamp = new Date().toISOString();
326
+
327
+ let code = `/**
328
+ * Webspresso Application Type Definitions
329
+ * Auto-generated by "npx webspresso typegen" on ${timestamp}
330
+ * DO NOT EDIT DIRECTLY. Regenerate using: npx webspresso typegen
331
+ */
332
+
333
+ import type { Repository, ScopeContext } from 'webspresso';
334
+
335
+ /* ============================================================================
336
+ * 1. ORM Model Entity Interfaces
337
+ * ========================================================================= */
338
+ `;
339
+
340
+ if (models.length === 0) {
341
+ code += `\n// No ORM models detected in models/ directory.\n`;
342
+ } else {
343
+ for (const m of models) {
344
+ code += `\nexport interface ${m.name}Model {\n`;
345
+ for (const col of m.columns) {
346
+ code += ` ${col.name}${col.isOptional ? '?' : ''}: ${col.tsType};\n`;
347
+ }
348
+ code += `}\n`;
349
+ }
350
+ }
351
+
352
+ code += `\n/* ============================================================================
353
+ * 2. Business Services Mapping
354
+ * ========================================================================= */\n`;
355
+
356
+ code += `\nexport interface WebspressoServicesMap {\n`;
357
+ if (services.length === 0) {
358
+ code += ` [key: string]: { input: any; output: any };\n`;
359
+ } else {
360
+ for (const s of services) {
361
+ code += ` '${s.name}': {\n input: ${s.inputTsType};\n output: unknown;\n };\n`;
362
+ }
363
+ }
364
+ code += `}\n`;
365
+
366
+ code += `\n/* ============================================================================
367
+ * 3. Application Routes
368
+ * ========================================================================= */\n`;
369
+
370
+ if (routes.length === 0) {
371
+ code += `\nexport type AppRoute = string;\nexport type AppRoutePath = string;\n`;
372
+ } else {
373
+ const routeKeys = routes.map((r) => `'${(r.method || 'GET').toUpperCase()} ${r.path || r.pattern}'`);
374
+ const pathKeys = Array.from(new Set(routes.map((r) => `'${r.path || r.pattern}'`)));
375
+ code += `\nexport type AppRoute = \n | ${routeKeys.join('\n | ')};\n`;
376
+ code += `\nexport type AppRoutePath = \n | ${pathKeys.join('\n | ')};\n`;
377
+ }
378
+
379
+ code += `\n/* ============================================================================
380
+ * 4. Webspresso Framework Module Augmentation
381
+ * ========================================================================= */\n\n`;
382
+
383
+ code += `declare module 'webspresso' {\n`;
384
+
385
+ // Augment WebspressoModels
386
+ code += ` interface WebspressoModels {\n`;
387
+ if (models.length === 0) {
388
+ code += ` [key: string]: Record<string, any>;\n`;
389
+ } else {
390
+ for (const m of models) {
391
+ code += ` ${m.name}: ${m.name}Model;\n`;
392
+ }
393
+ }
394
+ code += ` }\n\n`;
395
+
396
+ // Augment WebspressoServices
397
+ code += ` interface WebspressoServices extends WebspressoServicesMap {}\n\n`;
398
+
399
+ // Augment DatabaseInstance getRepository
400
+ code += ` interface DatabaseInstance {\n`;
401
+ code += ` getRepository<K extends keyof WebspressoModels>(modelName: K, scopeContext?: ScopeContext): Repository<WebspressoModels[K]>;\n`;
402
+ code += ` getRepository<T = Record<string, any>>(modelName: string, scopeContext?: ScopeContext): Repository<T>;\n`;
403
+ code += ` }\n\n`;
404
+
405
+ // Augment TransactionContext getRepository
406
+ code += ` interface TransactionContext {\n`;
407
+ code += ` getRepository<K extends keyof WebspressoModels>(modelName: K, scopeContext?: ScopeContext): Repository<WebspressoModels[K]>;\n`;
408
+ code += ` getRepository<T = Record<string, any>>(modelName: string, scopeContext?: ScopeContext): Repository<T>;\n`;
409
+ code += ` }\n\n`;
410
+
411
+ // Augment ServiceRegistry
412
+ code += ` interface ServiceRegistry {\n`;
413
+ code += ` call<K extends keyof WebspressoServicesMap>(name: K, input?: WebspressoServicesMap[K]['input'], ctx?: unknown, options?: unknown): Promise<WebspressoServicesMap[K]['output']>;\n`;
414
+ code += ` call<T = unknown>(name: string, input?: unknown, ctx?: unknown, options?: unknown): Promise<T>;\n`;
415
+ code += ` }\n`;
416
+
417
+ code += `}\n\n`;
418
+
419
+ code += `export type AppModelName = keyof import('webspresso').WebspressoModels;\n`;
420
+ code += `export type AppServiceName = keyof WebspressoServicesMap;\n`;
421
+
422
+ return code;
423
+ }
424
+
425
+ /**
426
+ * Main TypeGen Runner
427
+ * Scans models, services, and routes, and writes TypeScript declarations
428
+ * @param {Object} [options]
429
+ * @param {string} [options.rootDir] - Root directory of the project
430
+ * @param {string} [options.modelsDir] - Models directory (default: 'models')
431
+ * @param {string} [options.servicesDir] - Services directory (default: 'services')
432
+ * @param {string} [options.pagesDir] - Pages directory (default: 'pages')
433
+ * @param {string} [options.outDir] - Output directory (default: '.webspresso')
434
+ * @param {string} [options.outFile] - Custom output file path
435
+ * @param {boolean} [options.createEnvFile=true] - Create webspresso-env.d.ts at project root
436
+ * @param {boolean} [options.write=true] - Write declaration files to disk
437
+ * @param {boolean} [options.silent=false] - Suppress console output
438
+ * @returns {{ code: string, modelsCount: number, servicesCount: number, routesCount: number, outFile: string, envFile: string|null }}
439
+ */
440
+ function generateTypes(options = {}) {
441
+ const rootDir = path.resolve(options.rootDir || options.cwd || process.cwd());
442
+ const modelsRel = options.modelsDir || (fs.existsSync(path.join(rootDir, 'src/models')) ? 'src/models' : 'models');
443
+ const servicesRel = options.servicesDir || (fs.existsSync(path.join(rootDir, 'src/services')) ? 'src/services' : 'services');
444
+ const pagesRel = options.pagesDir || (fs.existsSync(path.join(rootDir, 'src/pages')) ? 'src/pages' : 'pages');
445
+
446
+ const modelsDir = path.resolve(rootDir, modelsRel);
447
+ const servicesDir = path.resolve(rootDir, servicesRel);
448
+
449
+ const models = collectModelMetadata(modelsDir);
450
+ const services = collectServiceMetadata(servicesDir);
451
+ const routes = collectRouteMetadata(rootDir, pagesRel);
452
+
453
+ const code = generateDeclarationCode({ models, services, routes });
454
+
455
+ const outDir = path.resolve(rootDir, options.outDir || '.webspresso');
456
+ const outFile = options.outFile ? path.resolve(rootDir, options.outFile) : path.join(outDir, 'types.d.ts');
457
+ const envFile = options.createEnvFile !== false ? path.join(rootDir, 'webspresso-env.d.ts') : null;
458
+
459
+ if (options.write !== false) {
460
+ // Ensure destination directory exists
461
+ const targetDir = path.dirname(outFile);
462
+ if (!fs.existsSync(targetDir)) {
463
+ fs.mkdirSync(targetDir, { recursive: true });
464
+ }
465
+ fs.writeFileSync(outFile, code, 'utf8');
466
+
467
+ // Create root environment declaration helper if enabled
468
+ if (envFile) {
469
+ const relToOut = path.relative(rootDir, outFile).replace(/\\/g, '/');
470
+ const envContent = `/// <reference path="./${relToOut}" />\n`;
471
+ fs.writeFileSync(envFile, envContent, 'utf8');
472
+ }
473
+
474
+ if (!options.silent) {
475
+ console.log(`[typegen] ✓ Generated types for ${models.length} models, ${services.length} services, and ${routes.length} routes.`);
476
+ console.log(`[typegen] → ${path.relative(rootDir, outFile)}`);
477
+ }
478
+ }
479
+
480
+ return {
481
+ code,
482
+ modelsCount: models.length,
483
+ servicesCount: services.length,
484
+ routesCount: routes.length,
485
+ outFile,
486
+ envFile,
487
+ };
488
+ }
489
+
490
+ module.exports = {
491
+ zodToTsType,
492
+ collectModelMetadata,
493
+ collectServiceMetadata,
494
+ collectRouteMetadata,
495
+ generateDeclarationCode,
496
+ generateTypes,
497
+ };