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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -3
  3. package/bin/commands/add-esbuild.js +7 -3
  4. package/bin/commands/add-tailwind.js +9 -5
  5. package/bin/commands/build.js +20 -4
  6. package/bin/commands/dev.js +57 -7
  7. package/bin/commands/doctor.js +42 -1
  8. package/bin/commands/favicon-generate.js +10 -3
  9. package/bin/commands/new.js +8 -3
  10. package/bin/commands/start.js +3 -1
  11. package/bin/utils/add-command.js +16 -0
  12. package/core/app.js +45 -3
  13. package/core/auth/authenticate.js +3 -3
  14. package/core/auth/hash.js +167 -43
  15. package/core/auth/index.js +13 -1
  16. package/core/auth/jwt.js +21 -2
  17. package/core/auth/manager.js +63 -14
  18. package/core/auth/middleware.js +4 -2
  19. package/core/auth/oauth.js +5 -1
  20. package/core/auth/tokens.js +7 -2
  21. package/core/auth/totp.js +42 -6
  22. package/core/auth/user-agent.js +2 -0
  23. package/core/client/adapter/datastar.js +1254 -0
  24. package/core/client/api.js +1033 -0
  25. package/core/client/controller.js +1081 -0
  26. package/core/client/debug.js +367 -0
  27. package/core/client/forms.js +1021 -0
  28. package/core/client/fragments.js +738 -0
  29. package/core/client/index.d.ts +40 -0
  30. package/core/client/index.js +347 -0
  31. package/core/client/lite.js +164 -0
  32. package/core/client/server-fragments.js +859 -0
  33. package/core/client/utils.js +144 -0
  34. package/core/compression/middleware.js +277 -26
  35. package/core/context/app-context.js +25 -3
  36. package/core/context/request.js +32 -4
  37. package/core/errors/middleware.js +14 -0
  38. package/core/events/dispatcher.js +33 -0
  39. package/core/forms/flash.js +136 -102
  40. package/core/health/middleware.js +36 -6
  41. package/core/http/pipeline.js +45 -6
  42. package/core/index.js +21 -7
  43. package/core/kernel/view.js +41 -10
  44. package/core/metrics/middleware.js +30 -6
  45. package/core/metrics/registry.js +13 -5
  46. package/core/orm/cache/fingerprint.js +32 -11
  47. package/core/orm/cache/layer.js +19 -2
  48. package/core/orm/hidden.js +145 -0
  49. package/core/orm/model.js +48 -2
  50. package/core/orm/query-builder.js +343 -6
  51. package/core/orm/query-filters.js +14 -2
  52. package/core/orm/repository.js +64 -13
  53. package/core/orm/schema-helpers.js +9 -6
  54. package/core/orm/transaction.js +50 -6
  55. package/core/orm/types.js +4 -0
  56. package/core/queue/adapters/database.js +184 -2
  57. package/core/queue/adapters/memory.js +153 -5
  58. package/core/queue/adapters/redis.js +59 -4
  59. package/core/queue/job.js +14 -1
  60. package/core/queue/manager.js +115 -0
  61. package/core/resilience/circuit-breaker.js +27 -8
  62. package/core/services/builtins/exchange.js +174 -7
  63. package/core/services/builtins/file-manager.js +36 -0
  64. package/core/services/builtins/mail.js +27 -0
  65. package/core/services/builtins/queue.js +144 -0
  66. package/core/services/builtins/security.js +118 -0
  67. package/core/services/builtins/webhook.js +110 -0
  68. package/core/services/executor.js +38 -16
  69. package/core/services/index.js +4 -1
  70. package/core/sse/stream.js +28 -5
  71. package/core/utils/timeout.js +8 -1
  72. package/core/validation/compiler.js +10 -7
  73. package/index.d.ts +1127 -71
  74. package/index.js +124 -8
  75. package/package.json +75 -26
  76. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  77. package/plugins/admin-panel/index.js +19 -8
  78. package/plugins/csrf/index.js +3 -3
  79. package/plugins/data-exchange/export.js +214 -0
  80. package/plugins/data-exchange/import.js +48 -5
  81. package/plugins/data-exchange/index.js +70 -28
  82. package/plugins/data-exchange/preview.js +266 -0
  83. package/plugins/data-exchange/template.js +242 -0
  84. package/plugins/email/admin-component.js +783 -141
  85. package/plugins/email/api-handlers.js +51 -1
  86. package/plugins/email/index.js +5 -0
  87. package/plugins/email/service.js +48 -0
  88. package/plugins/file-manager/admin-component.js +1546 -70
  89. package/plugins/file-manager/api-handlers.js +166 -1
  90. package/plugins/file-manager/core.js +598 -8
  91. package/plugins/file-manager/index.js +8 -0
  92. package/plugins/index.js +7 -0
  93. package/plugins/mcp/auth.js +3 -3
  94. package/plugins/oauth/index.js +36 -3
  95. package/plugins/queue/admin-component.js +1092 -0
  96. package/plugins/queue/api-handlers.js +248 -0
  97. package/plugins/queue/index.js +69 -0
  98. package/plugins/rest-resources/index.js +7 -2
  99. package/plugins/security/admin-component.js +689 -0
  100. package/plugins/security/api-handlers.js +169 -0
  101. package/plugins/security/audit.js +153 -0
  102. package/plugins/security/bruteforce.js +244 -0
  103. package/plugins/security/firewall.js +459 -0
  104. package/plugins/security/headers.js +214 -0
  105. package/plugins/security/honeypot.js +234 -0
  106. package/plugins/security/incident-recorder.js +137 -0
  107. package/plugins/security/index.js +211 -0
  108. package/plugins/seo-checker/analyzer.js +20 -7
  109. package/plugins/upload/index.js +36 -0
  110. package/plugins/webhooks/admin-component.js +702 -175
  111. package/plugins/webhooks/api-handlers.js +75 -1
  112. package/plugins/webhooks/dispatcher.js +290 -11
  113. package/plugins/webhooks/index.js +37 -2
  114. package/plugins/webhooks/sender.js +56 -0
  115. package/plugins/xlsx/engine.js +19 -4
  116. package/src/api/api-loader.js +19 -1
  117. package/src/bundle/index.js +443 -17
  118. package/src/client-runtime/mount.js +9 -2
  119. package/src/dev/api-typegen.js +1300 -0
  120. package/src/dev/banner.js +10 -1
  121. package/src/dev/colors.js +1 -1
  122. package/src/dev/index.js +7 -1
  123. package/src/dev/typegen.js +173 -17
  124. package/src/dev-reload/index.js +1 -1
  125. package/src/discovery/file-route-parser.js +17 -3
  126. package/src/discovery/scan-routes.js +4 -1
  127. package/src/file-router.js +249 -69
  128. package/src/helpers.js +46 -26
  129. package/src/njk-frontmatter.js +30 -4
  130. package/src/routing/cascading.js +26 -8
  131. package/src/routing/mount-discovered-routes.js +69 -0
  132. package/src/server.js +237 -66
  133. package/.agents/ADMIN.md +0 -426
  134. package/.agents/AGENTS.md +0 -96
  135. package/.agents/ARCHITECTURE.md +0 -163
  136. package/.agents/AUTH.md +0 -581
  137. package/.agents/CACHE.md +0 -148
  138. package/.agents/CLI.md +0 -649
  139. package/.agents/COMPRESSION.md +0 -59
  140. package/.agents/CONTEXT.md +0 -152
  141. package/.agents/CONVENTIONS.md +0 -50
  142. package/.agents/EARLY_HINTS.md +0 -120
  143. package/.agents/EMAIL.md +0 -223
  144. package/.agents/ERRORS.md +0 -260
  145. package/.agents/EVENTS.md +0 -179
  146. package/.agents/FLAGS.md +0 -118
  147. package/.agents/HEALTH.md +0 -118
  148. package/.agents/LIMITS.md +0 -109
  149. package/.agents/LOGGING.md +0 -93
  150. package/.agents/MCP.md +0 -103
  151. package/.agents/METRICS.md +0 -127
  152. package/.agents/MIGRATION_JOBS.md +0 -95
  153. package/.agents/ORM.md +0 -700
  154. package/.agents/PLUGINS.md +0 -729
  155. package/.agents/QUEUE.md +0 -290
  156. package/.agents/RATE_LIMIT.md +0 -147
  157. package/.agents/REALTIME.md +0 -213
  158. package/.agents/RESILIENCE.md +0 -235
  159. package/.agents/ROUTING.md +0 -605
  160. package/.agents/SCALE_OUT.md +0 -109
  161. package/.agents/SECRETS.md +0 -117
  162. package/.agents/SERVICES.md +0 -341
  163. package/.agents/SHUTDOWN.md +0 -74
  164. package/.agents/STREAMING.md +0 -108
  165. package/.agents/WORKFLOW.md +0 -27
  166. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  167. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  168. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  169. package/.agents/skills/webspresso-framework/SKILL.md +0 -89
  170. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
  171. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
  172. package/.agents/skills/webspresso-services/SKILL.md +0 -134
  173. package/.cursorrules +0 -14
  174. package/AGENTS.md +0 -203
  175. package/CLAUDE.md +0 -54
  176. package/core/kernel/run-demo.js +0 -58
  177. package/plugins/admin-panel/styles.js +0 -9
  178. 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * Webspresso Dev CLI Colors
5
- * Lightweight, zero-dependency ANSI color utilities with NO_COLOR & TTY detection
5
+ * Lightweight ANSI color utilities with NO_COLOR & TTY detection
6
6
  * @module src/dev/colors
7
7
  */
8
8
 
package/src/dev/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * Webspresso Development & DX Tooling
5
- * Zero-dependency banner, route table introspection, request profiler, and N+1 query detection
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
  };
@@ -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
- return zodToTsType(zType._def.schema, indent);
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
- return (zType._def.options || []).map((opt) => zodToTsType(opt, indent)).join(' | ') || 'unknown';
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 = 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
- });
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 envContent = `/// <reference path="./${relToOut}" />\n`;
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
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Webspresso Development Live & Hot Reload Hub
3
- * Zero-dependency SSE-based CSS Hot Reload & Page Reload
3
+ * Built-in SSE-based CSS Hot Reload & Page Reload
4
4
  * @module dev-reload
5
5
  */
6
6
 
@@ -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(`.${methodMatch[1]}.js`, '');
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
- if (items.length > 1) {
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';