webspresso 0.0.97 → 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.
- package/.agents/ADMIN.md +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/index.js +4 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +210 -62
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
|
@@ -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
|
+
};
|