webspresso 0.0.108 → 0.0.109
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/LICENSE +21 -0
- package/README.md +3 -3
- package/bin/commands/add-esbuild.js +7 -3
- package/bin/commands/add-tailwind.js +9 -5
- package/bin/commands/build.js +20 -4
- package/bin/commands/dev.js +57 -7
- package/bin/commands/doctor.js +42 -1
- package/bin/commands/favicon-generate.js +10 -3
- package/bin/commands/new.js +8 -3
- package/bin/commands/start.js +3 -1
- package/bin/utils/add-command.js +16 -0
- package/core/app.js +45 -3
- package/core/auth/authenticate.js +3 -3
- package/core/auth/hash.js +167 -43
- package/core/auth/index.js +13 -1
- package/core/auth/jwt.js +21 -2
- package/core/auth/manager.js +63 -14
- package/core/auth/middleware.js +4 -2
- package/core/auth/oauth.js +5 -1
- package/core/auth/tokens.js +7 -2
- package/core/auth/totp.js +42 -6
- package/core/auth/user-agent.js +2 -0
- package/core/client/adapter/datastar.js +1254 -0
- package/core/client/api.js +1033 -0
- package/core/client/controller.js +1081 -0
- package/core/client/debug.js +367 -0
- package/core/client/forms.js +1021 -0
- package/core/client/fragments.js +738 -0
- package/core/client/index.d.ts +40 -0
- package/core/client/index.js +347 -0
- package/core/client/lite.js +164 -0
- package/core/client/server-fragments.js +859 -0
- package/core/client/utils.js +144 -0
- package/core/compression/middleware.js +277 -26
- package/core/context/app-context.js +25 -3
- package/core/context/request.js +32 -4
- package/core/errors/middleware.js +14 -0
- package/core/events/dispatcher.js +33 -0
- package/core/forms/flash.js +136 -102
- package/core/health/middleware.js +36 -6
- package/core/http/pipeline.js +45 -6
- package/core/index.js +21 -7
- package/core/kernel/view.js +41 -10
- package/core/metrics/middleware.js +30 -6
- package/core/metrics/registry.js +13 -5
- package/core/orm/cache/fingerprint.js +32 -11
- package/core/orm/cache/layer.js +19 -2
- package/core/orm/hidden.js +145 -0
- package/core/orm/model.js +48 -2
- package/core/orm/query-builder.js +343 -6
- package/core/orm/query-filters.js +14 -2
- package/core/orm/repository.js +64 -13
- package/core/orm/schema-helpers.js +9 -6
- package/core/orm/transaction.js +50 -6
- package/core/orm/types.js +4 -0
- package/core/queue/adapters/database.js +184 -2
- package/core/queue/adapters/memory.js +153 -5
- package/core/queue/adapters/redis.js +59 -4
- package/core/queue/job.js +14 -1
- package/core/queue/manager.js +115 -0
- package/core/resilience/circuit-breaker.js +27 -8
- package/core/services/builtins/exchange.js +174 -7
- package/core/services/builtins/file-manager.js +36 -0
- package/core/services/builtins/mail.js +27 -0
- package/core/services/builtins/queue.js +144 -0
- package/core/services/builtins/security.js +118 -0
- package/core/services/builtins/webhook.js +110 -0
- package/core/services/executor.js +38 -16
- package/core/services/index.js +4 -1
- package/core/sse/stream.js +28 -5
- package/core/utils/timeout.js +8 -1
- package/core/validation/compiler.js +10 -7
- package/index.d.ts +1127 -71
- package/index.js +124 -8
- package/package.json +75 -26
- package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
- package/plugins/admin-panel/index.js +19 -8
- package/plugins/csrf/index.js +3 -3
- package/plugins/data-exchange/export.js +214 -0
- package/plugins/data-exchange/import.js +48 -5
- package/plugins/data-exchange/index.js +70 -28
- package/plugins/data-exchange/preview.js +266 -0
- package/plugins/data-exchange/template.js +242 -0
- package/plugins/email/admin-component.js +783 -141
- package/plugins/email/api-handlers.js +51 -1
- package/plugins/email/index.js +5 -0
- package/plugins/email/service.js +48 -0
- package/plugins/file-manager/admin-component.js +1546 -70
- package/plugins/file-manager/api-handlers.js +166 -1
- package/plugins/file-manager/core.js +598 -8
- package/plugins/file-manager/index.js +8 -0
- package/plugins/index.js +7 -0
- package/plugins/mcp/auth.js +3 -3
- package/plugins/oauth/index.js +36 -3
- package/plugins/queue/admin-component.js +1092 -0
- package/plugins/queue/api-handlers.js +248 -0
- package/plugins/queue/index.js +69 -0
- package/plugins/rest-resources/index.js +7 -2
- package/plugins/security/admin-component.js +689 -0
- package/plugins/security/api-handlers.js +169 -0
- package/plugins/security/audit.js +153 -0
- package/plugins/security/bruteforce.js +244 -0
- package/plugins/security/firewall.js +459 -0
- package/plugins/security/headers.js +214 -0
- package/plugins/security/honeypot.js +234 -0
- package/plugins/security/incident-recorder.js +137 -0
- package/plugins/security/index.js +211 -0
- package/plugins/seo-checker/analyzer.js +20 -7
- package/plugins/upload/index.js +36 -0
- package/plugins/webhooks/admin-component.js +702 -175
- package/plugins/webhooks/api-handlers.js +75 -1
- package/plugins/webhooks/dispatcher.js +290 -11
- package/plugins/webhooks/index.js +37 -2
- package/plugins/webhooks/sender.js +56 -0
- package/plugins/xlsx/engine.js +19 -4
- package/src/api/api-loader.js +19 -1
- package/src/bundle/index.js +443 -17
- package/src/client-runtime/mount.js +9 -2
- package/src/dev/api-typegen.js +1300 -0
- package/src/dev/banner.js +10 -1
- package/src/dev/colors.js +1 -1
- package/src/dev/index.js +7 -1
- package/src/dev/typegen.js +173 -17
- package/src/dev-reload/index.js +1 -1
- package/src/discovery/file-route-parser.js +17 -3
- package/src/discovery/scan-routes.js +4 -1
- package/src/file-router.js +249 -69
- package/src/helpers.js +46 -26
- package/src/njk-frontmatter.js +30 -4
- package/src/routing/cascading.js +26 -8
- package/src/routing/mount-discovered-routes.js +69 -0
- package/src/server.js +237 -66
- package/.agents/ADMIN.md +0 -426
- package/.agents/AGENTS.md +0 -96
- package/.agents/ARCHITECTURE.md +0 -163
- package/.agents/AUTH.md +0 -581
- package/.agents/CACHE.md +0 -148
- package/.agents/CLI.md +0 -649
- package/.agents/COMPRESSION.md +0 -59
- package/.agents/CONTEXT.md +0 -152
- package/.agents/CONVENTIONS.md +0 -50
- package/.agents/EARLY_HINTS.md +0 -120
- package/.agents/EMAIL.md +0 -223
- package/.agents/ERRORS.md +0 -260
- package/.agents/EVENTS.md +0 -179
- package/.agents/FLAGS.md +0 -118
- package/.agents/HEALTH.md +0 -118
- package/.agents/LIMITS.md +0 -109
- package/.agents/LOGGING.md +0 -93
- package/.agents/MCP.md +0 -103
- package/.agents/METRICS.md +0 -127
- package/.agents/MIGRATION_JOBS.md +0 -95
- package/.agents/ORM.md +0 -700
- package/.agents/PLUGINS.md +0 -729
- package/.agents/QUEUE.md +0 -290
- package/.agents/RATE_LIMIT.md +0 -147
- package/.agents/REALTIME.md +0 -213
- package/.agents/RESILIENCE.md +0 -235
- package/.agents/ROUTING.md +0 -605
- package/.agents/SCALE_OUT.md +0 -109
- package/.agents/SECRETS.md +0 -117
- package/.agents/SERVICES.md +0 -341
- package/.agents/SHUTDOWN.md +0 -74
- package/.agents/STREAMING.md +0 -108
- package/.agents/WORKFLOW.md +0 -27
- package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
- package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
- package/.agents/skills/webspresso-framework/SKILL.md +0 -89
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
- package/.agents/skills/webspresso-services/SKILL.md +0 -134
- package/.cursorrules +0 -14
- package/AGENTS.md +0 -203
- package/CLAUDE.md +0 -54
- package/core/kernel/run-demo.js +0 -58
- package/plugins/admin-panel/styles.js +0 -9
- package/plugins/data-exchange/export-xlsx.js +0 -119
package/src/dev/banner.js
CHANGED
|
@@ -94,10 +94,19 @@ function collectBannerSummary(context = {}) {
|
|
|
94
94
|
db,
|
|
95
95
|
serviceRegistry,
|
|
96
96
|
pluginManager,
|
|
97
|
-
port = 7331,
|
|
98
97
|
host = 'localhost',
|
|
99
98
|
} = context;
|
|
100
99
|
|
|
100
|
+
// Prefer the port the server actually bound to over configured/default values
|
|
101
|
+
let boundPort;
|
|
102
|
+
try {
|
|
103
|
+
const addr = context.server && typeof context.server.address === 'function' ? context.server.address() : null;
|
|
104
|
+
boundPort = addr && typeof addr === 'object' ? addr.port : undefined;
|
|
105
|
+
} catch {
|
|
106
|
+
boundPort = undefined;
|
|
107
|
+
}
|
|
108
|
+
const port = context.port || boundPort || context.options?.server?.port || process.env.PORT || 7331;
|
|
109
|
+
|
|
101
110
|
// 1. Routes summary
|
|
102
111
|
const routes = Array.isArray(routeTable)
|
|
103
112
|
? routeTable
|
package/src/dev/colors.js
CHANGED
package/src/dev/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Webspresso Development & DX Tooling
|
|
5
|
-
*
|
|
5
|
+
* Built-in banner, route table introspection, request profiler, and N+1 query detection
|
|
6
6
|
* @module src/dev
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -27,7 +27,10 @@ const {
|
|
|
27
27
|
collectModelMetadata,
|
|
28
28
|
collectServiceMetadata,
|
|
29
29
|
collectRouteMetadata,
|
|
30
|
+
collectApiRouteContracts,
|
|
30
31
|
generateDeclarationCode,
|
|
32
|
+
generateApiDeclarationCode,
|
|
33
|
+
checkTemplateFormContracts,
|
|
31
34
|
generateTypes,
|
|
32
35
|
} = require('./typegen');
|
|
33
36
|
|
|
@@ -57,6 +60,9 @@ module.exports = {
|
|
|
57
60
|
collectModelMetadata,
|
|
58
61
|
collectServiceMetadata,
|
|
59
62
|
collectRouteMetadata,
|
|
63
|
+
collectApiRouteContracts,
|
|
60
64
|
generateDeclarationCode,
|
|
65
|
+
generateApiDeclarationCode,
|
|
66
|
+
checkTemplateFormContracts,
|
|
61
67
|
generateTypes,
|
|
62
68
|
};
|
package/src/dev/typegen.js
CHANGED
|
@@ -13,30 +13,136 @@ const { hasServiceRegistry, getServiceRegistry } = require('../../core/context')
|
|
|
13
13
|
const { scanRoutes } = require('../../src/discovery/scan-routes');
|
|
14
14
|
const { compileRouteTable } = require('../../src/routing/route-table');
|
|
15
15
|
const { z } = require('../../core/validation');
|
|
16
|
+
const {
|
|
17
|
+
collectApiRouteContracts: collectApiRouteContractsRaw,
|
|
18
|
+
generateApiDeclarationCode,
|
|
19
|
+
checkTemplateFormContracts: checkTemplateFormContractsRaw,
|
|
20
|
+
} = require('./api-typegen');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Infer the output TypeScript type of a Zod `.transform(fn)` effect when possible.
|
|
24
|
+
*
|
|
25
|
+
* @param {Function} transformFn
|
|
26
|
+
* @param {import('zod').ZodTypeAny} innerSchema
|
|
27
|
+
* @param {number} indent
|
|
28
|
+
* @param {'output'|'input'|'wire'} mode
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function inferZodTransformOutputType(transformFn, innerSchema, indent, mode) {
|
|
32
|
+
if (typeof transformFn !== 'function') {
|
|
33
|
+
return zodToTsType(innerSchema, indent, mode);
|
|
34
|
+
}
|
|
35
|
+
const fnSrc = Function.prototype.toString.call(transformFn).trim();
|
|
36
|
+
if (/^(?:function\s*\([^)]*\)\s*\{\s*return\s+)?Number\s*\(/.test(fnSrc) || /=>\s*Number\s*\(/.test(fnSrc) || /=>\s*parseInt\s*\(/.test(fnSrc) || /=>\s*parseFloat\s*\(/.test(fnSrc) || /=>\s*\+\s*[a-zA-Z_$]/.test(fnSrc)) {
|
|
37
|
+
return 'number';
|
|
38
|
+
}
|
|
39
|
+
if (/=>\s*String\s*\(/.test(fnSrc) || /\.trim\(\)\s*$/.test(fnSrc) || /\.toLowerCase\(\)\s*$/.test(fnSrc) || /\.toUpperCase\(\)\s*$/.test(fnSrc)) {
|
|
40
|
+
return 'string';
|
|
41
|
+
}
|
|
42
|
+
if (/=>\s*Boolean\s*\(/.test(fnSrc) || /=>\s*!!/.test(fnSrc)) {
|
|
43
|
+
return 'boolean';
|
|
44
|
+
}
|
|
45
|
+
if (/\.split\s*\(/.test(fnSrc)) {
|
|
46
|
+
return 'Array<string>';
|
|
47
|
+
}
|
|
48
|
+
if (/=>\s*new\s+Date\s*\(/.test(fnSrc)) {
|
|
49
|
+
return mode === 'wire' ? 'string' : 'Date';
|
|
50
|
+
}
|
|
51
|
+
// Try executing transform on a safe dummy sample if innerSchema is primitive
|
|
52
|
+
const innerType = innerSchema && innerSchema._def && innerSchema._def.typeName;
|
|
53
|
+
try {
|
|
54
|
+
let sample;
|
|
55
|
+
if (innerType === 'ZodString') sample = '1';
|
|
56
|
+
else if (innerType === 'ZodNumber') sample = 1;
|
|
57
|
+
else if (innerType === 'ZodBoolean') sample = true;
|
|
58
|
+
if (sample !== undefined) {
|
|
59
|
+
const out = transformFn(sample, { addIssue() {}, path: [] });
|
|
60
|
+
if (typeof out === 'number') return 'number';
|
|
61
|
+
if (typeof out === 'string') return 'string';
|
|
62
|
+
if (typeof out === 'boolean') return 'boolean';
|
|
63
|
+
if (out instanceof Date) return mode === 'wire' ? 'string' : 'Date';
|
|
64
|
+
if (Array.isArray(out)) return 'Array<unknown>';
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Ignore sample execution errors
|
|
68
|
+
}
|
|
69
|
+
return zodToTsType(innerSchema, indent, mode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checks whether a Zod field is optional in the given mode ('input' vs 'output' vs 'wire').
|
|
74
|
+
* Notice: `ZodDefault` is optional in 'input' mode (client may omit it),
|
|
75
|
+
* but required in 'output' mode (server receives the defaulted value).
|
|
76
|
+
*
|
|
77
|
+
* @param {import('zod').ZodTypeAny} zField
|
|
78
|
+
* @param {'output'|'input'|'wire'} mode
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
function isZodFieldOptional(zField, mode = 'output') {
|
|
82
|
+
if (!zField || !zField._def) return false;
|
|
83
|
+
const t = zField._def.typeName;
|
|
84
|
+
if (t === 'ZodOptional') return true;
|
|
85
|
+
if (t === 'ZodDefault') {
|
|
86
|
+
return mode === 'input';
|
|
87
|
+
}
|
|
88
|
+
if (t === 'ZodEffects' && zField._def.schema) {
|
|
89
|
+
return isZodFieldOptional(zField._def.schema, mode);
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Unwraps outer `ZodOptional` / `ZodDefault` for property type emission inside `ZodObject`.
|
|
96
|
+
*/
|
|
97
|
+
function unwrapOptionalOrDefault(zField, mode = 'output') {
|
|
98
|
+
if (!zField || !zField._def) return zField;
|
|
99
|
+
const t = zField._def.typeName;
|
|
100
|
+
if (t === 'ZodOptional') {
|
|
101
|
+
return zField._def.innerType;
|
|
102
|
+
}
|
|
103
|
+
if (t === 'ZodDefault' && mode === 'input') {
|
|
104
|
+
return zField._def.innerType;
|
|
105
|
+
}
|
|
106
|
+
return zField;
|
|
107
|
+
}
|
|
16
108
|
|
|
17
109
|
/**
|
|
18
110
|
* Converts a Zod type schema to a TypeScript type string
|
|
19
111
|
* @param {import('zod').ZodTypeAny} zType
|
|
20
112
|
* @param {number} [indent=0]
|
|
113
|
+
* @param {'output'|'input'|'wire'} [mode='output'] - 'input' (client payload), 'output' (server parsed), or 'wire' (JSON wire format)
|
|
21
114
|
* @returns {string}
|
|
22
115
|
*/
|
|
23
|
-
function zodToTsType(zType, indent = 0) {
|
|
116
|
+
function zodToTsType(zType, indent = 0, mode = 'output') {
|
|
24
117
|
if (!zType || !zType._def) return 'unknown';
|
|
25
118
|
const typeName = zType._def.typeName;
|
|
26
119
|
const spaces = ' '.repeat(indent);
|
|
27
120
|
const nextSpaces = ' '.repeat(indent + 1);
|
|
28
121
|
|
|
122
|
+
// Support custom zdb.file() metadata
|
|
123
|
+
if (zType._zdbMeta && zType._zdbMeta.columnType === 'file') {
|
|
124
|
+
return mode === 'input' ? 'File | Blob | string' : 'string';
|
|
125
|
+
}
|
|
126
|
+
|
|
29
127
|
switch (typeName) {
|
|
30
128
|
case 'ZodString':
|
|
31
129
|
return 'string';
|
|
32
130
|
case 'ZodNumber':
|
|
131
|
+
if (mode === 'input' && zType._def.coerce) {
|
|
132
|
+
return 'number | string';
|
|
133
|
+
}
|
|
33
134
|
return 'number';
|
|
34
135
|
case 'ZodBoolean':
|
|
136
|
+
if (mode === 'input' && zType._def.coerce) {
|
|
137
|
+
return 'boolean | string';
|
|
138
|
+
}
|
|
35
139
|
return 'boolean';
|
|
36
140
|
case 'ZodDate':
|
|
141
|
+
if (mode === 'wire') return 'string';
|
|
142
|
+
if (mode === 'input') return 'Date | string';
|
|
37
143
|
return 'Date';
|
|
38
144
|
case 'ZodBigInt':
|
|
39
|
-
return 'bigint';
|
|
145
|
+
return mode === 'wire' ? 'string' : 'bigint';
|
|
40
146
|
case 'ZodNull':
|
|
41
147
|
return 'null';
|
|
42
148
|
case 'ZodUndefined':
|
|
@@ -58,29 +164,52 @@ function zodToTsType(zType, indent = 0) {
|
|
|
58
164
|
case 'ZodNativeEnum':
|
|
59
165
|
return 'number | string';
|
|
60
166
|
case 'ZodArray':
|
|
61
|
-
return `Array<${zodToTsType(zType._def.type, indent)}>`;
|
|
167
|
+
return `Array<${zodToTsType(zType._def.type, indent, mode)}>`;
|
|
62
168
|
case 'ZodOptional':
|
|
63
|
-
return `${zodToTsType(zType._def.innerType, indent)} | undefined`;
|
|
169
|
+
return `${zodToTsType(zType._def.innerType, indent, mode)} | undefined`;
|
|
64
170
|
case 'ZodNullable':
|
|
65
|
-
return `${zodToTsType(zType._def.innerType, indent)} | null`;
|
|
171
|
+
return `${zodToTsType(zType._def.innerType, indent, mode)} | null`;
|
|
66
172
|
case 'ZodDefault':
|
|
67
|
-
return zodToTsType(zType._def.innerType, indent);
|
|
68
|
-
case 'ZodEffects':
|
|
69
|
-
|
|
173
|
+
return zodToTsType(zType._def.innerType, indent, mode);
|
|
174
|
+
case 'ZodEffects': {
|
|
175
|
+
if (mode === 'input') {
|
|
176
|
+
if (zType._def.effect && zType._def.effect.type === 'preprocess') {
|
|
177
|
+
return `${zodToTsType(zType._def.schema, indent, mode)} | string`;
|
|
178
|
+
}
|
|
179
|
+
return zodToTsType(zType._def.schema, indent, mode);
|
|
180
|
+
}
|
|
181
|
+
if (zType._def.effect && zType._def.effect.type === 'transform') {
|
|
182
|
+
return inferZodTransformOutputType(
|
|
183
|
+
zType._def.effect.transform,
|
|
184
|
+
zType._def.schema,
|
|
185
|
+
indent,
|
|
186
|
+
mode
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return zodToTsType(zType._def.schema, indent, mode);
|
|
190
|
+
}
|
|
70
191
|
case 'ZodUnion':
|
|
71
|
-
|
|
192
|
+
case 'ZodDiscriminatedUnion':
|
|
193
|
+
return (zType._def.options || []).map((opt) => zodToTsType(opt, indent, mode)).join(' | ') || 'unknown';
|
|
72
194
|
case 'ZodObject': {
|
|
73
|
-
const shape = zType.shape;
|
|
195
|
+
const shape = typeof zType.shape === 'function' ? zType.shape() : zType.shape;
|
|
74
196
|
if (!shape || Object.keys(shape).length === 0) return 'Record<string, unknown>';
|
|
75
|
-
const entries =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
197
|
+
const entries = [];
|
|
198
|
+
for (const [k, v] of Object.entries(shape)) {
|
|
199
|
+
if (mode === 'wire' && v && v._def && (v._def.typeName === 'ZodUndefined' || v._def.typeName === 'ZodVoid')) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const isOpt = isZodFieldOptional(v, mode);
|
|
203
|
+
const unwrapped = unwrapOptionalOrDefault(v, mode);
|
|
204
|
+
const innerType = zodToTsType(unwrapped, indent + 1, mode);
|
|
205
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `'${k}'`;
|
|
206
|
+
entries.push(`${nextSpaces}${safeKey}${isOpt ? '?' : ''}: ${innerType};`);
|
|
207
|
+
}
|
|
208
|
+
if (entries.length === 0) return 'Record<string, never>';
|
|
80
209
|
return `{\n${entries.join('\n')}\n${spaces}}`;
|
|
81
210
|
}
|
|
82
211
|
case 'ZodRecord':
|
|
83
|
-
return `Record<string, ${zodToTsType(zType._def.valueType, indent)}>`;
|
|
212
|
+
return `Record<string, ${zodToTsType(zType._def.valueType, indent, mode)}>`;
|
|
84
213
|
default:
|
|
85
214
|
return 'unknown';
|
|
86
215
|
}
|
|
@@ -449,11 +578,14 @@ function generateTypes(options = {}) {
|
|
|
449
578
|
const models = collectModelMetadata(modelsDir);
|
|
450
579
|
const services = collectServiceMetadata(servicesDir);
|
|
451
580
|
const routes = collectRouteMetadata(rootDir, pagesRel);
|
|
581
|
+
const apiContracts = collectApiRouteContracts(rootDir, { pagesDir: pagesRel });
|
|
452
582
|
|
|
453
583
|
const code = generateDeclarationCode({ models, services, routes });
|
|
584
|
+
const apiCode = generateApiDeclarationCode(apiContracts);
|
|
454
585
|
|
|
455
586
|
const outDir = path.resolve(rootDir, options.outDir || '.webspresso');
|
|
456
587
|
const outFile = options.outFile ? path.resolve(rootDir, options.outFile) : path.join(outDir, 'types.d.ts');
|
|
588
|
+
const apiOutFile = options.apiOutFile ? path.resolve(rootDir, options.apiOutFile) : path.join(path.dirname(outFile), 'api.d.ts');
|
|
457
589
|
const envFile = options.createEnvFile !== false ? path.join(rootDir, 'webspresso-env.d.ts') : null;
|
|
458
590
|
|
|
459
591
|
if (options.write !== false) {
|
|
@@ -463,35 +595,59 @@ function generateTypes(options = {}) {
|
|
|
463
595
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
464
596
|
}
|
|
465
597
|
fs.writeFileSync(outFile, code, 'utf8');
|
|
598
|
+
fs.writeFileSync(apiOutFile, apiCode, 'utf8');
|
|
466
599
|
|
|
467
600
|
// Create root environment declaration helper if enabled
|
|
468
601
|
if (envFile) {
|
|
469
602
|
const relToOut = path.relative(rootDir, outFile).replace(/\\/g, '/');
|
|
470
|
-
const
|
|
603
|
+
const relToApi = path.relative(rootDir, apiOutFile).replace(/\\/g, '/');
|
|
604
|
+
const envContent = `/// <reference path="./${relToOut}" />\n/// <reference path="./${relToApi}" />\n`;
|
|
471
605
|
fs.writeFileSync(envFile, envContent, 'utf8');
|
|
472
606
|
}
|
|
473
607
|
|
|
474
608
|
if (!options.silent) {
|
|
475
609
|
console.log(`[typegen] ✓ Generated types for ${models.length} models, ${services.length} services, and ${routes.length} routes.`);
|
|
476
610
|
console.log(`[typegen] → ${path.relative(rootDir, outFile)}`);
|
|
611
|
+
console.log(`[typegen] → ${path.relative(rootDir, apiOutFile)}`);
|
|
477
612
|
}
|
|
478
613
|
}
|
|
479
614
|
|
|
480
615
|
return {
|
|
481
616
|
code,
|
|
617
|
+
apiCode,
|
|
482
618
|
modelsCount: models.length,
|
|
483
619
|
servicesCount: services.length,
|
|
484
620
|
routesCount: routes.length,
|
|
621
|
+
apiContractsCount: apiContracts.length,
|
|
622
|
+
apiContracts,
|
|
485
623
|
outFile,
|
|
624
|
+
apiOutFile,
|
|
486
625
|
envFile,
|
|
487
626
|
};
|
|
488
627
|
}
|
|
489
628
|
|
|
629
|
+
function collectApiRouteContracts(rootDir, options = {}) {
|
|
630
|
+
return collectApiRouteContractsRaw(rootDir, {
|
|
631
|
+
...options,
|
|
632
|
+
zodToTsType,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function checkTemplateFormContracts(rootDir, options = {}) {
|
|
637
|
+
return checkTemplateFormContractsRaw(rootDir, {
|
|
638
|
+
...options,
|
|
639
|
+
zodToTsType,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
490
643
|
module.exports = {
|
|
491
644
|
zodToTsType,
|
|
492
645
|
collectModelMetadata,
|
|
493
646
|
collectServiceMetadata,
|
|
494
647
|
collectRouteMetadata,
|
|
648
|
+
collectApiRouteContracts,
|
|
495
649
|
generateDeclarationCode,
|
|
650
|
+
generateApiDeclarationCode,
|
|
651
|
+
checkTemplateFormContracts,
|
|
496
652
|
generateTypes,
|
|
497
653
|
};
|
package/src/dev-reload/index.js
CHANGED
|
@@ -11,8 +11,19 @@ const path = require('path');
|
|
|
11
11
|
|
|
12
12
|
const SUPPORTED_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head']);
|
|
13
13
|
const SUPPORTED_EXTENSIONS = new Set(['.js', '.njk']);
|
|
14
|
-
const METHOD_SUFFIX_REGEX = /\.([a-zA-Z]+)\.js$/;
|
|
14
|
+
const METHOD_SUFFIX_REGEX = /\.([a-zA-Z]+)\.(?:js|ts)$/;
|
|
15
15
|
const TEMPORARY_FILE_REGEX = /(?:~|\.swp|\.tmp|\.bak|\.DS_Store)$/;
|
|
16
|
+
const CLIENT_BROWSER_FILE_REGEX = /\.(?:client\.(?:js|ts|mjs|cjs|jsx|tsx)|d\.ts)$/i;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a filename is a browser `.client.*` or `.d.ts` file that must be excluded from server routing
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function isClientBrowserFile(name) {
|
|
24
|
+
if (!name || typeof name !== 'string') return false;
|
|
25
|
+
return CLIENT_BROWSER_FILE_REGEX.test(name);
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* Safely scans a directory recursively for route files
|
|
@@ -66,6 +77,8 @@ function isPrivateOrIgnored(name) {
|
|
|
66
77
|
if (name.startsWith('_')) return true;
|
|
67
78
|
// Temporary editor files
|
|
68
79
|
if (TEMPORARY_FILE_REGEX.test(name)) return true;
|
|
80
|
+
// Browser client files (.client.js, .client.ts) and TypeScript declaration files (.d.ts)
|
|
81
|
+
if (CLIENT_BROWSER_FILE_REGEX.test(name)) return true;
|
|
69
82
|
return false;
|
|
70
83
|
}
|
|
71
84
|
|
|
@@ -144,12 +157,12 @@ function parseFileRoute(relativePath, options = {}) {
|
|
|
144
157
|
originalMethodPart = rawMethod;
|
|
145
158
|
if (SUPPORTED_METHODS.has(rawMethod)) {
|
|
146
159
|
method = rawMethod;
|
|
147
|
-
cleanFilename = filename.replace(
|
|
160
|
+
cleanFilename = filename.replace(new RegExp(`\\.${methodMatch[1]}\\.(?:js|ts)$`, 'i'), '');
|
|
148
161
|
} else {
|
|
149
162
|
// Invalid HTTP method in filename
|
|
150
163
|
return { path: '', method: rawMethod, isValid: false, isPrivate: false, invalidMethod: true };
|
|
151
164
|
}
|
|
152
|
-
} else if (filename.endsWith('.js')) {
|
|
165
|
+
} else if (filename.endsWith('.js') || filename.endsWith('.ts')) {
|
|
153
166
|
cleanFilename = filename.slice(0, -3);
|
|
154
167
|
} else if (filename.endsWith('.njk')) {
|
|
155
168
|
cleanFilename = filename.slice(0, -4);
|
|
@@ -236,6 +249,7 @@ module.exports = {
|
|
|
236
249
|
isRouteGroupSegment,
|
|
237
250
|
isApiRouteFile,
|
|
238
251
|
isPrivateOrIgnored,
|
|
252
|
+
isClientBrowserFile,
|
|
239
253
|
normalizePrefix,
|
|
240
254
|
transformDynamicSegment,
|
|
241
255
|
scanDirSafely,
|
|
@@ -79,7 +79,10 @@ function scanDirSafely(dirPath, baseDir = dirPath, checkWarnings = false) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
for (const [norm, items] of groups.entries()) {
|
|
82
|
-
|
|
82
|
+
// A page template and its companion loader (`index.njk` + `index.js`,
|
|
83
|
+
// `[slug].njk` + `[slug].js`) share one route — only distinct names collide.
|
|
84
|
+
const distinctNames = new Set(items.map((item) => item.name));
|
|
85
|
+
if (distinctNames.size > 1) {
|
|
83
86
|
const namesList = items.map((item) => `"${item.original}"`).join(' and ');
|
|
84
87
|
const relDir = path.relative(baseDir, dirPath);
|
|
85
88
|
const dirLabel = relDir ? `pages/${relDir.split(path.sep).join('/')}` : 'pages';
|