@swissjs/swite 0.3.0

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 (163) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/publish.yml +50 -0
  4. package/.github/workflows/release.yml +53 -0
  5. package/BUILD_ANALYSIS.md +89 -0
  6. package/BUILD_STRATEGY.md +75 -0
  7. package/CHANGELOG.md +53 -0
  8. package/DIRECTIVE.md +488 -0
  9. package/__tests__/css-extraction.test.ts +261 -0
  10. package/__tests__/css-injection-integration.test.ts +247 -0
  11. package/__tests__/css-middleware.test.ts +191 -0
  12. package/__tests__/import-rewriter-bug.test.ts +135 -0
  13. package/dist/builder.d.ts +36 -0
  14. package/dist/builder.d.ts.map +1 -0
  15. package/dist/builder.js +772 -0
  16. package/dist/cache/compilation-cache.d.ts +33 -0
  17. package/dist/cache/compilation-cache.d.ts.map +1 -0
  18. package/dist/cache/compilation-cache.js +130 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +85 -0
  22. package/dist/config-loader.d.ts +8 -0
  23. package/dist/config-loader.d.ts.map +1 -0
  24. package/dist/config-loader.js +40 -0
  25. package/dist/config.d.ts +29 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +7 -0
  28. package/dist/dev/pythonDevManager.d.ts +12 -0
  29. package/dist/dev/pythonDevManager.d.ts.map +1 -0
  30. package/dist/dev/pythonDevManager.js +85 -0
  31. package/dist/env.d.ts +19 -0
  32. package/dist/env.d.ts.map +1 -0
  33. package/dist/env.js +112 -0
  34. package/dist/handlers/base-handler.d.ts +21 -0
  35. package/dist/handlers/base-handler.d.ts.map +1 -0
  36. package/dist/handlers/base-handler.js +38 -0
  37. package/dist/handlers/js-handler.d.ts +10 -0
  38. package/dist/handlers/js-handler.d.ts.map +1 -0
  39. package/dist/handlers/js-handler.js +87 -0
  40. package/dist/handlers/mjs-handler.d.ts +8 -0
  41. package/dist/handlers/mjs-handler.d.ts.map +1 -0
  42. package/dist/handlers/mjs-handler.js +44 -0
  43. package/dist/handlers/node-module-handler.d.ts +16 -0
  44. package/dist/handlers/node-module-handler.d.ts.map +1 -0
  45. package/dist/handlers/node-module-handler.js +267 -0
  46. package/dist/handlers/ts-handler.d.ts +11 -0
  47. package/dist/handlers/ts-handler.d.ts.map +1 -0
  48. package/dist/handlers/ts-handler.js +120 -0
  49. package/dist/handlers/ui-handler.d.ts +12 -0
  50. package/dist/handlers/ui-handler.d.ts.map +1 -0
  51. package/dist/handlers/ui-handler.js +182 -0
  52. package/dist/handlers/uix-handler.d.ts +12 -0
  53. package/dist/handlers/uix-handler.d.ts.map +1 -0
  54. package/dist/handlers/uix-handler.js +135 -0
  55. package/dist/hmr.d.ts +20 -0
  56. package/dist/hmr.d.ts.map +1 -0
  57. package/dist/hmr.js +265 -0
  58. package/dist/import-rewriter.d.ts +3 -0
  59. package/dist/import-rewriter.d.ts.map +1 -0
  60. package/dist/import-rewriter.js +351 -0
  61. package/dist/index.d.ts +14 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +13 -0
  64. package/dist/middleware/hmr-routes.d.ts +12 -0
  65. package/dist/middleware/hmr-routes.d.ts.map +1 -0
  66. package/dist/middleware/hmr-routes.js +97 -0
  67. package/dist/middleware/middleware-setup.d.ts +23 -0
  68. package/dist/middleware/middleware-setup.d.ts.map +1 -0
  69. package/dist/middleware/middleware-setup.js +596 -0
  70. package/dist/middleware/static-files.d.ts +15 -0
  71. package/dist/middleware/static-files.d.ts.map +1 -0
  72. package/dist/middleware/static-files.js +585 -0
  73. package/dist/proxy/SwiteProxyError.d.ts +6 -0
  74. package/dist/proxy/SwiteProxyError.d.ts.map +1 -0
  75. package/dist/proxy/SwiteProxyError.js +9 -0
  76. package/dist/proxy/proxyToPython.d.ts +28 -0
  77. package/dist/proxy/proxyToPython.d.ts.map +1 -0
  78. package/dist/proxy/proxyToPython.js +66 -0
  79. package/dist/resolver/bare-import-resolver.d.ts +9 -0
  80. package/dist/resolver/bare-import-resolver.d.ts.map +1 -0
  81. package/dist/resolver/bare-import-resolver.js +363 -0
  82. package/dist/resolver/symlink-registry.d.ts +13 -0
  83. package/dist/resolver/symlink-registry.d.ts.map +1 -0
  84. package/dist/resolver/symlink-registry.js +98 -0
  85. package/dist/resolver/url-resolver.d.ts +11 -0
  86. package/dist/resolver/url-resolver.d.ts.map +1 -0
  87. package/dist/resolver/url-resolver.js +268 -0
  88. package/dist/resolver/workspace-package-resolver.d.ts +10 -0
  89. package/dist/resolver/workspace-package-resolver.d.ts.map +1 -0
  90. package/dist/resolver/workspace-package-resolver.js +185 -0
  91. package/dist/resolver.d.ts +17 -0
  92. package/dist/resolver.d.ts.map +1 -0
  93. package/dist/resolver.js +191 -0
  94. package/dist/router/file-router.d.ts +19 -0
  95. package/dist/router/file-router.d.ts.map +1 -0
  96. package/dist/router/file-router.js +114 -0
  97. package/dist/server.d.ts +22 -0
  98. package/dist/server.d.ts.map +1 -0
  99. package/dist/server.js +122 -0
  100. package/dist/utils/cdn-fallback.d.ts +14 -0
  101. package/dist/utils/cdn-fallback.d.ts.map +1 -0
  102. package/dist/utils/cdn-fallback.js +36 -0
  103. package/dist/utils/file-path-resolver.d.ts +9 -0
  104. package/dist/utils/file-path-resolver.d.ts.map +1 -0
  105. package/dist/utils/file-path-resolver.js +187 -0
  106. package/dist/utils/generate-import-map-cli.d.ts +3 -0
  107. package/dist/utils/generate-import-map-cli.d.ts.map +1 -0
  108. package/dist/utils/generate-import-map-cli.js +32 -0
  109. package/dist/utils/generate-import-map.d.ts +21 -0
  110. package/dist/utils/generate-import-map.d.ts.map +1 -0
  111. package/dist/utils/generate-import-map.js +119 -0
  112. package/dist/utils/package-finder.d.ts +24 -0
  113. package/dist/utils/package-finder.d.ts.map +1 -0
  114. package/dist/utils/package-finder.js +161 -0
  115. package/dist/utils/package-registry.d.ts +36 -0
  116. package/dist/utils/package-registry.d.ts.map +1 -0
  117. package/dist/utils/package-registry.js +159 -0
  118. package/dist/utils/workspace.d.ts +6 -0
  119. package/dist/utils/workspace.d.ts.map +1 -0
  120. package/dist/utils/workspace.js +65 -0
  121. package/docs/IMPORT_REWRITING.md +164 -0
  122. package/docs/IMPORT_REWRITING_TROUBLESHOOTING.md +139 -0
  123. package/docs/PATH_RESOLUTION_GUIDE.md +221 -0
  124. package/package.json +49 -0
  125. package/src/adapters/proxy/SwiteProxyError.ts +12 -0
  126. package/src/adapters/proxy/proxyToPython.ts +88 -0
  127. package/src/build-engine/builder.ts +960 -0
  128. package/src/cli.ts +109 -0
  129. package/src/config/config-loader.ts +46 -0
  130. package/src/config/config.ts +34 -0
  131. package/src/config/env.ts +98 -0
  132. package/src/dev-engine/handlers/base-handler.ts +68 -0
  133. package/src/dev-engine/handlers/js-handler.ts +134 -0
  134. package/src/dev-engine/handlers/mjs-handler.ts +65 -0
  135. package/src/dev-engine/handlers/node-module-handler.ts +339 -0
  136. package/src/dev-engine/handlers/ts-handler.ts +143 -0
  137. package/src/dev-engine/handlers/ui-handler.ts +105 -0
  138. package/src/dev-engine/handlers/uix-handler.ts +90 -0
  139. package/src/dev-engine/hmr/hmr-client-template.ts +122 -0
  140. package/src/dev-engine/hmr/hmr.ts +173 -0
  141. package/src/dev-engine/middleware/hmr-routes.ts +120 -0
  142. package/src/dev-engine/middleware/middleware-setup.ts +351 -0
  143. package/src/dev-engine/middleware/static-files.ts +728 -0
  144. package/src/dev-engine/pythonDevManager.ts +116 -0
  145. package/src/dev-engine/router/file-router.ts +164 -0
  146. package/src/dev-engine/server.ts +152 -0
  147. package/src/index.ts +26 -0
  148. package/src/internal/cache/compilation-cache.ts +182 -0
  149. package/src/internal/generate-import-map-cli.ts +40 -0
  150. package/src/internal/generate-import-map.ts +154 -0
  151. package/src/kernel/package-finder.ts +164 -0
  152. package/src/kernel/package-registry.ts +198 -0
  153. package/src/kernel/workspace.ts +62 -0
  154. package/src/resolution/bare-import-resolver.ts +400 -0
  155. package/src/resolution/cdn/cdn-fallback.ts +37 -0
  156. package/src/resolution/path/file-path-resolver.ts +190 -0
  157. package/src/resolution/path/path-fixup.ts +19 -0
  158. package/src/resolution/resolver.ts +198 -0
  159. package/src/resolution/rewriting/import-rewriter.ts +237 -0
  160. package/src/resolution/symlink-registry.ts +114 -0
  161. package/src/resolution/url-resolver.ts +231 -0
  162. package/src/resolution/workspace-package-resolver.ts +94 -0
  163. package/tsconfig.json +37 -0
@@ -0,0 +1,351 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import type { Express, Request, Response, NextFunction } from "express";
8
+ import type { RouteDefinition } from "@swissjs/core";
9
+ import { RouteScanner } from "@swissjs/plugin-file-router/core";
10
+ import { createFileWatcher } from "@swissjs/plugin-file-router/dev";
11
+ import chalk from "chalk";
12
+ import path from "path";
13
+ import fs from "fs/promises";
14
+ import { ModuleResolver } from "../../resolution/resolver.js";
15
+ import { UIHandler } from "../handlers/ui-handler.js";
16
+ import { UIXHandler } from "../handlers/uix-handler.js";
17
+ import { TSHandler } from "../handlers/ts-handler.js";
18
+ import { JSHandler } from "../handlers/js-handler.js";
19
+ import { MJSHandler } from "../handlers/mjs-handler.js";
20
+ import { NodeModuleHandler } from "../handlers/node-module-handler.js";
21
+ import { setupStaticFiles, setupSPAFallback } from "./static-files.js";
22
+ import { setupHMRRoutes } from "./hmr-routes.js";
23
+ import {
24
+ setupFileRouter,
25
+ type FileRouterResult,
26
+ } from "../router/file-router.js";
27
+ import { HMREngine } from "../hmr/hmr.js";
28
+ import { findWorkspaceRoot } from "../../kernel/workspace.js";
29
+ import { loadImportMap } from "../../internal/generate-import-map.js";
30
+ import { loadEnv } from "../../config/env.js";
31
+
32
+ export interface MiddlewareConfig {
33
+ root: string;
34
+ workspaceRoot?: string | null;
35
+ publicDir: string;
36
+ resolver: ModuleResolver;
37
+ hmr: HMREngine;
38
+ }
39
+
40
+ export interface MiddlewareResult {
41
+ routes: RouteDefinition[];
42
+ routeScanner: RouteScanner | null;
43
+ routeWatcher: Awaited<ReturnType<typeof createFileWatcher>> | null;
44
+ }
45
+
46
+ const SOURCE_EXTS = new Set([".ui", ".uix", ".ts", ".mjs"]);
47
+
48
+ function isFileNotFoundError(error: unknown): boolean {
49
+ return (
50
+ error instanceof Error &&
51
+ (("code" in error && (error as any).code === "ENOENT") ||
52
+ ("errno" in error && (error as any).errno === -4058))
53
+ );
54
+ }
55
+
56
+ function sendSourceError(res: Response, error: unknown, fullPath: string): void {
57
+ if (res.headersSent) return;
58
+ const status = isFileNotFoundError(error) ? 404 : 500;
59
+ res.status(status).setHeader("Content-Type", "text/plain");
60
+ res.send(
61
+ isFileNotFoundError(error)
62
+ ? `File not found: ${fullPath}`
63
+ : `Error loading module: ${error instanceof Error ? error.message : String(error)}`
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Setup all middleware for the SWITE server.
69
+ *
70
+ * Middleware registration order (matters for Express):
71
+ * 1. File router + HMR routes
72
+ * 2. /packages source files
73
+ * 3. /src source files (highest priority for that prefix)
74
+ * 4. /lib source files (pre-static guard)
75
+ * 5. .ui/.uix MIME-type guard (belt-and-suspenders for slipped-through requests)
76
+ * 6. /.skltn/modules.css → 204
77
+ * 7. Static file serving (public/, node_modules/, lib/)
78
+ * 8. General source-file transformation (all other paths)
79
+ * 9. SPA fallback
80
+ */
81
+ export async function setupMiddleware(
82
+ app: Express,
83
+ config: MiddlewareConfig
84
+ ): Promise<MiddlewareResult> {
85
+ // ── 1. File router + HMR ───────────────────────────────────────────────────
86
+ const fileRouterResult: FileRouterResult = await setupFileRouter({
87
+ root: config.root,
88
+ hmr: config.hmr,
89
+ });
90
+
91
+ setupHMRRoutes(app, {
92
+ hmr: config.hmr,
93
+ routes: fileRouterResult.routes,
94
+ });
95
+
96
+ // ── Workspace + import-map setup ───────────────────────────────────────────
97
+ const workspaceRoot = await findWorkspaceRoot(config.root);
98
+ console.log(chalk.blue(`[SWITE] App root: ${config.root}`));
99
+ console.log(chalk.blue(`[SWITE] Workspace root: ${workspaceRoot}`));
100
+
101
+ const { join } = await import("node:path");
102
+ const importMapPath = join(config.root, ".swite", "import-map.json");
103
+ const importMap = await loadImportMap(importMapPath);
104
+ if (importMap) {
105
+ config.resolver.setImportMap(importMap);
106
+ } else {
107
+ console.log(chalk.yellow(`[SWITE] No import map at ${importMapPath}, using runtime resolution`));
108
+ }
109
+
110
+ // ── Load .env files for import.meta.env inlining ──────────────────────────
111
+ const mode = process.env.NODE_ENV === "production" ? "production" : "development";
112
+ const env = loadEnv(config.root, mode);
113
+
114
+ // ── Create handlers ────────────────────────────────────────────────────────
115
+ const handlerContext = {
116
+ resolver: config.resolver,
117
+ root: config.root,
118
+ workspaceRoot,
119
+ env,
120
+ };
121
+
122
+ const uiHandler = new UIHandler(handlerContext);
123
+ const uixHandler = new UIXHandler(handlerContext);
124
+ const tsHandler = new TSHandler(handlerContext);
125
+ const jsHandler = new JSHandler(handlerContext);
126
+ const mjsHandler = new MJSHandler(handlerContext);
127
+ const nodeModuleHandler = new NodeModuleHandler(handlerContext);
128
+
129
+ // ── 2. /packages workspace source files ────────────────────────────────────
130
+ app.use("/packages", async (req: Request, res: Response, next: NextFunction) => {
131
+ const rawUrl = req.url?.split("?")[0] || "";
132
+ const fullUrl = "/packages" + (rawUrl.startsWith("/") ? rawUrl : "/" + rawUrl);
133
+
134
+ try {
135
+ if (fullUrl.endsWith(".ts") && !fullUrl.endsWith(".d.ts")) {
136
+ await tsHandler.handle(fullUrl, res);
137
+ if (res.headersSent) return;
138
+ } else if (fullUrl.endsWith(".js") || fullUrl.endsWith(".mjs")) {
139
+ await (fullUrl.endsWith(".js") ? jsHandler : mjsHandler).handle(fullUrl, res);
140
+ if (res.headersSent) return;
141
+ }
142
+ } catch (error) {
143
+ console.error(chalk.red(`[/packages] Error ${fullUrl}:`), error);
144
+ if (!res.headersSent) res.status(500).setHeader("Content-Type", "text/plain").send(String(error));
145
+ return;
146
+ }
147
+ next();
148
+ });
149
+
150
+ // ── 3. /src source files ───────────────────────────────────────────────────
151
+ // When Express mounts at "/src", req.url is relative (e.g. "/index.ui")
152
+ app.use("/src", async (req: Request, res: Response, next: NextFunction) => {
153
+ const relativeUrl = req.url.split("?")[0];
154
+ const fullPath = "/src" + relativeUrl;
155
+
156
+ if (relativeUrl.endsWith(".ui")) {
157
+ if (res.headersSent) return;
158
+ if (req.method === "HEAD") {
159
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
160
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
161
+ res.setHeader("Pragma", "no-cache");
162
+ res.setHeader("Expires", "0");
163
+ return res.status(200).end();
164
+ }
165
+ try {
166
+ await uiHandler.handle(fullPath, res);
167
+ if (!res.headersSent) res.status(500).send("Internal server error: handler did not send response");
168
+ } catch (error) {
169
+ console.error(chalk.red(`[/src] .ui error ${fullPath}:`), error);
170
+ sendSourceError(res, error, fullPath);
171
+ }
172
+ return;
173
+ }
174
+
175
+ if (relativeUrl.endsWith(".uix")) {
176
+ try {
177
+ await uixHandler.handle(fullPath, res);
178
+ if (!res.headersSent) res.status(500).setHeader("Content-Type", "text/plain").send("Error loading module");
179
+ } catch (error) {
180
+ console.error(chalk.red(`[/src] .uix error ${fullPath}:`), error);
181
+ sendSourceError(res, error, fullPath);
182
+ }
183
+ return;
184
+ }
185
+
186
+ if (relativeUrl.endsWith(".ts") && !relativeUrl.endsWith(".d.ts")) {
187
+ try {
188
+ await tsHandler.handle(fullPath, res);
189
+ if (!res.headersSent) res.status(500).setHeader("Content-Type", "text/plain").send("Error loading module");
190
+ } catch (error) {
191
+ console.error(chalk.red(`[/src] .ts error ${fullPath}:`), error);
192
+ sendSourceError(res, error, fullPath);
193
+ }
194
+ return;
195
+ }
196
+
197
+ if (relativeUrl.endsWith(".js") || relativeUrl.endsWith(".mjs")) {
198
+ try {
199
+ await (relativeUrl.endsWith(".js") ? jsHandler : mjsHandler).handle(fullPath, res);
200
+ if (res.headersSent) return;
201
+ } catch {
202
+ // fall through to static file serving below
203
+ }
204
+ if (!res.headersSent) return next();
205
+ return;
206
+ }
207
+
208
+ // Other files under /src (CSS, images, etc.) — serve as static
209
+ const filePath = path.join(config.root, "src", relativeUrl);
210
+ try {
211
+ const stats = await fs.stat(filePath);
212
+ if (!stats.isFile()) return next();
213
+
214
+ const ext = path.extname(relativeUrl).toLowerCase();
215
+ // Guard: source files should never reach here
216
+ if (SOURCE_EXTS.has(ext) || ext === ".js") {
217
+ res.status(404).setHeader("Content-Type", "text/plain");
218
+ return res.send(`Source file not found: ${fullPath}`);
219
+ }
220
+
221
+ const contentTypeMap: Record<string, string> = {
222
+ ".css": "text/css",
223
+ ".json": "application/json",
224
+ ".png": "image/png",
225
+ ".jpg": "image/jpeg",
226
+ ".jpeg": "image/jpeg",
227
+ ".gif": "image/gif",
228
+ ".svg": "image/svg+xml",
229
+ ".webp": "image/webp",
230
+ };
231
+
232
+ const content = await fs.readFile(filePath);
233
+ res.setHeader("Content-Type", contentTypeMap[ext] || "application/octet-stream");
234
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
235
+ res.setHeader("Pragma", "no-cache");
236
+ res.setHeader("Expires", "0");
237
+ res.send(content);
238
+ } catch {
239
+ res.status(404).setHeader("Content-Type", "text/plain");
240
+ res.send(`File not found: ${fullPath}`);
241
+ }
242
+ });
243
+
244
+ // ── 4. /lib source files (pre-static guard) ────────────────────────────────
245
+ app.use(async (req: Request, res: Response, next: NextFunction) => {
246
+ const url = req.url.split("?")[0];
247
+ if (!url.startsWith("/lib/")) return next();
248
+
249
+ const isSource =
250
+ url.endsWith(".ui") ||
251
+ url.endsWith(".uix") ||
252
+ url.endsWith(".ts") ||
253
+ (url.endsWith(".js") && !url.includes("node_modules"));
254
+
255
+ if (!isSource) return next();
256
+
257
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
258
+
259
+ try {
260
+ if (url.endsWith(".ui")) { await uiHandler.handle(url, res); return; }
261
+ if (url.endsWith(".uix")) { await uixHandler.handle(url, res); return; }
262
+ // .ts/.js — fall through to general middleware
263
+ return next();
264
+ } catch (error) {
265
+ console.error(chalk.red(`[/lib] Error ${url}:`), error);
266
+ if (!res.headersSent) res.status(500).send("Internal server error");
267
+ }
268
+ });
269
+
270
+ // ── 5. .ui/.uix MIME guard (belt-and-suspenders) ──────────────────────────
271
+ app.use((req: Request, res: Response, next: NextFunction) => {
272
+ const url = req.url.split("?")[0];
273
+ if (url.endsWith(".ui") || url.endsWith(".uix")) {
274
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
275
+ }
276
+ next();
277
+ });
278
+
279
+ // ── 6. /.skltn/modules.css → 204 (dev mode — CSS not bundled) ─────────────
280
+ app.use("/.skltn/modules.css", (_req: Request, res: Response) => {
281
+ res.status(204).end();
282
+ });
283
+
284
+ // ── 7. Static file serving ─────────────────────────────────────────────────
285
+ await setupStaticFiles(app, {
286
+ root: config.root,
287
+ publicDir: config.publicDir,
288
+ workspaceRoot: config.workspaceRoot ?? null,
289
+ });
290
+
291
+ // ── 8. General source-file transformation ──────────────────────────────────
292
+ app.use(async (req: Request, res: Response, next: NextFunction) => {
293
+ const url = req.url.split("?")[0];
294
+
295
+ // /src is already handled above
296
+ if (url.startsWith("/src")) return next();
297
+
298
+ // /lib static files are handled by static middleware; source files handled above
299
+ if (url.startsWith("/lib/")) {
300
+ const isSource =
301
+ url.endsWith(".ui") ||
302
+ url.endsWith(".uix") ||
303
+ url.endsWith(".ts") ||
304
+ (url.endsWith(".js") && !url.includes("node_modules"));
305
+ if (!isSource) return next();
306
+ }
307
+
308
+ try {
309
+ if (url.endsWith(".ui")) {
310
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
311
+ if (res.headersSent) return;
312
+ await uiHandler.handle(url, res);
313
+ if (!res.headersSent) res.status(500).send("Internal server error: handler did not send response");
314
+ return;
315
+ }
316
+
317
+ if (url.endsWith(".uix")) { await uixHandler.handle(url, res); return; }
318
+
319
+ if (url.endsWith(".ts") && !url.endsWith(".d.ts")) { await tsHandler.handle(url, res); return; }
320
+
321
+ if ((url.endsWith(".js") || url.endsWith(".mjs")) && url.includes("node_modules")) {
322
+ await nodeModuleHandler.handle(url, res);
323
+ return;
324
+ }
325
+
326
+ if (url.endsWith(".js")) { await jsHandler.handle(url, res); return; }
327
+
328
+ if (url.endsWith(".mjs")) { await mjsHandler.handle(url, res); return; }
329
+
330
+ // Static assets — pass to static middleware
331
+ next();
332
+ } catch (error) {
333
+ console.error(chalk.red(`[middleware] Error ${url}:`), error);
334
+ if (!res.headersSent) {
335
+ res.status(500).send(`Error: ${error instanceof Error ? error.message : String(error)}`);
336
+ }
337
+ }
338
+ });
339
+
340
+ // ── 9. SPA fallback ────────────────────────────────────────────────────────
341
+ await setupSPAFallback(app, {
342
+ root: config.root,
343
+ publicDir: config.publicDir,
344
+ });
345
+
346
+ return {
347
+ routes: fileRouterResult.routes,
348
+ routeScanner: fileRouterResult.routeScanner,
349
+ routeWatcher: fileRouterResult.routeWatcher,
350
+ };
351
+ }