@swissjs/swite 0.3.5 → 0.4.1

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 (49) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/DIRECTIVE.md +57 -2
  3. package/__tests__/import-rewriter-bug.test.ts +122 -135
  4. package/__tests__/security-r001-r002.test.ts +190 -0
  5. package/dist/build-engine/builder.js +9 -9
  6. package/dist/config/config.d.ts +0 -5
  7. package/dist/config/config.d.ts.map +1 -1
  8. package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
  9. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  10. package/dist/dev-engine/handlers/base-handler.js +91 -0
  11. package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
  12. package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
  13. package/dist/dev-engine/handlers/ui-handler.js +2 -64
  14. package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
  15. package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
  16. package/dist/dev-engine/handlers/uix-handler.js +2 -58
  17. package/dist/dev-engine/hmr/hmr.d.ts +10 -1
  18. package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
  19. package/dist/dev-engine/hmr/hmr.js +40 -2
  20. package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
  21. package/dist/dev-engine/middleware/static-files.js +145 -62
  22. package/dist/dev-engine/pythonDevManager.js +1 -1
  23. package/dist/dev-engine/router/file-router.d.ts.map +1 -1
  24. package/dist/dev-engine/router/file-router.js +2 -29
  25. package/dist/dev-engine/server.d.ts +7 -0
  26. package/dist/dev-engine/server.d.ts.map +1 -1
  27. package/dist/dev-engine/server.js +31 -3
  28. package/dist/kernel/package-finder.d.ts +0 -8
  29. package/dist/kernel/package-finder.d.ts.map +1 -1
  30. package/dist/kernel/package-finder.js +2 -2
  31. package/dist/kernel/package-registry.d.ts +6 -0
  32. package/dist/kernel/package-registry.d.ts.map +1 -1
  33. package/dist/kernel/package-registry.js +8 -0
  34. package/dist/kernel/workspace.d.ts.map +1 -1
  35. package/dist/kernel/workspace.js +12 -9
  36. package/package.json +26 -14
  37. package/src/build-engine/builder.ts +9 -9
  38. package/src/config/config.ts +0 -5
  39. package/src/dev-engine/handlers/base-handler.ts +109 -0
  40. package/src/dev-engine/handlers/ui-handler.ts +2 -82
  41. package/src/dev-engine/handlers/uix-handler.ts +2 -76
  42. package/src/dev-engine/hmr/hmr.ts +46 -1
  43. package/src/dev-engine/middleware/static-files.ts +813 -731
  44. package/src/dev-engine/pythonDevManager.ts +1 -1
  45. package/src/dev-engine/router/file-router.ts +2 -45
  46. package/src/dev-engine/server.ts +33 -3
  47. package/src/kernel/package-finder.ts +2 -2
  48. package/src/kernel/package-registry.ts +9 -0
  49. package/src/kernel/workspace.ts +8 -10
@@ -1,731 +1,813 @@
1
- /*
2
- * Copyright (c) 2024 Themba Mzumara
3
- * SWITE - SWISS Development Server
4
- * Licensed under the MIT License.
5
- */
6
-
7
- import express from "express";
8
- import type { Express } from "express";
9
- import { promises as fs, realpathSync, existsSync } from "node:fs";
10
- import path from "node:path";
11
- import chalk from "chalk";
12
- import { findWorkspaceRoot } from "../../kernel/workspace.js";
13
-
14
- export interface StaticFilesConfig {
15
- root: string;
16
- publicDir: string;
17
- workspaceRoot?: string | null;
18
- /** Entry file for CSS extraction, relative to root. Defaults to "src/index.ui". */
19
- entry?: string;
20
- }
21
-
22
- /**
23
- * Setup static file serving for public directory, node_modules, and workspace packages
24
- */
25
- export async function setupStaticFiles(
26
- app: Express,
27
- config: StaticFilesConfig,
28
- ): Promise<void> {
29
- console.log(chalk.magenta(`[static-files] setupStaticFiles called with root: ${config.root}`));
30
-
31
- // Static file serving - ONLY serve public directory
32
- // Do NOT serve dist/ folder - it contains old build artifacts with bare imports
33
- const publicPath = path.join(config.root, config.publicDir);
34
-
35
- // Serve static files from public/ directory
36
- // IMPORTANT: Skip source files (.ui, .uix, .ts, .js) - they should be handled by module transformation middleware
37
- // Wrap express.static to prevent it from serving source files
38
- const publicStaticMiddleware = express.static(publicPath, {
39
- // Exclude dist folder and other build artifacts
40
- dotfiles: "ignore",
41
- index: false, // Don't serve index files from static middleware
42
- setHeaders: (res, filePath) => {
43
- // Add cache-busting headers for all static files in dev mode
44
- res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
45
- res.setHeader("Pragma", "no-cache");
46
- res.setHeader("Expires", "0");
47
- },
48
- });
49
-
50
- app.use((req, res, next) => {
51
- const url = req.url.split("?")[0];
52
- // Skip source files - let module transformation middleware handle them
53
- // DO NOT call express.static for source files - it will serve them with wrong MIME type
54
- if (
55
- url.endsWith(".ui") ||
56
- url.endsWith(".uix") ||
57
- url.endsWith(".ts") ||
58
- (url.endsWith(".js") && !url.includes("node_modules")) ||
59
- url.endsWith(".mjs")
60
- ) {
61
- // Skip express.static for source files - pass to next middleware instead
62
- return next();
63
- }
64
- // For non-source files, use express.static
65
- publicStaticMiddleware(req, res, next);
66
- });
67
-
68
- // NOTE: We do NOT serve /src as static files here anymore
69
- // The module transformation middleware in middleware-setup.ts handles ALL /src requests first
70
- // This ensures .ui, .uix, .ts files are processed correctly before static middleware can interfere
71
- // Only non-source files (CSS, images, etc.) will pass through to be served as static
72
- // But they should be handled by the module transformation middleware's next() call
73
-
74
- // Serve node_modules as static files from multiple locations
75
- // 1. App root node_modules
76
- // Wrap to skip source files
77
- const nodeModulesStatic = express.static(path.join(config.root, "node_modules"));
78
- app.use("/node_modules", (req, res, next) => {
79
- const url = req.url.split("?")[0];
80
- // Skip source files in node_modules - they should be handled by module transformation
81
- if (
82
- url.endsWith(".ui") ||
83
- url.endsWith(".uix") ||
84
- url.endsWith(".ts") ||
85
- (url.endsWith(".js") && !url.includes("/dist/") && !url.includes("/lib/")) ||
86
- url.endsWith(".mjs")
87
- ) {
88
- return next();
89
- }
90
- nodeModulesStatic(req, res, next);
91
- });
92
-
93
- // 2. Workspace root node_modules (if different from app root)
94
- const workspaceRootForNodeModules =
95
- config.workspaceRoot ?? (await findWorkspaceRoot(config.root));
96
- if (
97
- workspaceRootForNodeModules &&
98
- workspaceRootForNodeModules !== config.root
99
- ) {
100
- const workspaceNodeModules = path.join(
101
- workspaceRootForNodeModules,
102
- "node_modules",
103
- );
104
- try {
105
- await fs.access(workspaceNodeModules);
106
- // Serve workspace node_modules with a different path to avoid conflicts
107
- // But also check if package exists in app node_modules first
108
- app.use("/node_modules", (req, res, next) => {
109
- const url = req.url.split("?")[0];
110
- // Skip source files - let module transformation middleware handle them
111
- if (
112
- url.endsWith(".ui") ||
113
- url.endsWith(".uix") ||
114
- url.endsWith(".ts") ||
115
- (url.endsWith(".js") && !url.includes("/dist/") && !url.includes("/lib/")) ||
116
- url.endsWith(".mjs")
117
- ) {
118
- return next();
119
- }
120
-
121
- // Strip leading slash: req.path inside app.use('/node_modules') is already
122
- // relative to that prefix, e.g. '/@swissjs/shell/design-tokens/primitive.css'
123
- const relPath = req.path.replace(/^\/+/, "");
124
- if (!relPath) return next();
125
-
126
- // Resolve pnpm symlinks explicitly (same mechanism UIHandler uses).
127
- // express.static / fs.access can silently fail on pnpm virtual-store symlinks
128
- // in production containers, so we use realpathSync to get the real path first.
129
- const isScoped = relPath.startsWith("@");
130
- const parts = relPath.split("/");
131
- const pkgName = isScoped ? `${parts[0]}/${parts[1]}` : parts[0];
132
- const subPath = isScoped ? parts.slice(2).join("/") : parts.slice(1).join("/");
133
-
134
- const pkgSymLink = path.join(workspaceNodeModules, pkgName);
135
- try {
136
- const pkgReal = realpathSync(pkgSymLink);
137
- const realAbs = path.join(pkgReal, subPath);
138
- if (existsSync(realAbs)) {
139
- res.sendFile(realAbs);
140
- return;
141
- }
142
- } catch {
143
- // symlink missing or broken — fall through to next()
144
- }
145
-
146
- next();
147
- });
148
- console.log(
149
- chalk.gray(
150
- ` 📦 Serving workspace node_modules from ${workspaceNodeModules}`,
151
- ),
152
- );
153
- } catch {
154
- // Workspace node_modules doesn't exist, skip
155
- }
156
- }
157
-
158
- // Serve workspace packages (lib/, libraries/, packages/, modules/, etc.)
159
- // This allows workspace packages to be served via HTTP
160
- // Reuse workspaceRoot from above if it exists, otherwise find it
161
- const workspaceRoot =
162
- workspaceRootForNodeModules || (await findWorkspaceRoot(config.root));
163
-
164
- console.log(
165
- chalk.blue(`[static-files] Workspace root: ${workspaceRoot}`),
166
- );
167
- console.log(
168
- chalk.blue(`[static-files] App root: ${config.root}`),
169
- );
170
-
171
- // Try to serve lib/ directory - check both workspace root and app root parent
172
- let libPath: string | null = null;
173
-
174
- console.log(
175
- chalk.blue(`[static-files] Determining lib/ path... workspaceRoot: ${workspaceRoot}, config.root: ${config.root}`),
176
- );
177
-
178
- // First, try workspace root
179
- if (workspaceRoot && workspaceRoot !== config.root) {
180
- libPath = path.join(workspaceRoot, "lib");
181
- console.log(
182
- chalk.blue(`[static-files] Trying workspace root lib/: ${libPath}`),
183
- );
184
- } else {
185
- console.log(
186
- chalk.yellow(`[static-files] Workspace root equals app root, trying parent directories...`),
187
- );
188
- // If workspace root equals app root, try going up from app root
189
- const parentDir = path.dirname(config.root);
190
- const parentLibPath = path.join(parentDir, "lib");
191
- console.log(
192
- chalk.blue(`[static-files] Trying parent lib/: ${parentLibPath}`),
193
- );
194
- try {
195
- await fs.access(parentLibPath);
196
- libPath = parentLibPath;
197
- console.log(
198
- chalk.blue(`[static-files] Using parent directory lib/: ${libPath}`),
199
- );
200
- } catch (error) {
201
- console.log(
202
- chalk.yellow(`[static-files] Parent lib/ not found: ${error instanceof Error ? error.message : String(error)}`),
203
- );
204
- // Parent lib/ doesn't exist, try grandparent
205
- const grandparentDir = path.dirname(parentDir);
206
- const grandparentLibPath = path.join(grandparentDir, "lib");
207
- console.log(
208
- chalk.blue(`[static-files] Trying grandparent lib/: ${grandparentLibPath}`),
209
- );
210
- try {
211
- await fs.access(grandparentLibPath);
212
- libPath = grandparentLibPath;
213
- console.log(
214
- chalk.blue(`[static-files] Using grandparent directory lib/: ${libPath}`),
215
- );
216
- } catch (error2) {
217
- console.log(
218
- chalk.yellow(`[static-files] Grandparent lib/ not found: ${error2 instanceof Error ? error2.message : String(error2)}`),
219
- );
220
- }
221
- }
222
- }
223
-
224
- // Serve lib/ directory if found
225
- console.log(
226
- chalk.blue(`[static-files] Checking for lib/ directory... libPath: ${libPath}`),
227
- );
228
-
229
- // ALWAYS try to register /lib/ static serving
230
- // Calculate the lib path - prefer workspace root, fallback to parent of app root
231
- let finalLibPath: string;
232
- if (libPath) {
233
- finalLibPath = libPath;
234
- } else if (workspaceRoot && workspaceRoot !== config.root) {
235
- finalLibPath = path.join(workspaceRoot, "lib");
236
- } else {
237
- // Go up from app root to find lib/
238
- const parentDir = path.dirname(config.root);
239
- finalLibPath = path.join(parentDir, "lib");
240
- }
241
-
242
- console.log(
243
- chalk.blue(`[static-files] Final lib path to check: ${finalLibPath}`),
244
- );
245
- console.log(
246
- chalk.blue(`[static-files] workspaceRoot: ${workspaceRoot}, config.root: ${config.root}`),
247
- );
248
-
249
- // Try to access the directory
250
- let libPathExists = false;
251
- try {
252
- await fs.access(finalLibPath);
253
- libPathExists = true;
254
- console.log(
255
- chalk.green(`[static-files] ✅ Found lib/ directory at: ${finalLibPath}`),
256
- );
257
-
258
- // Verify the CSS file exists
259
- const testCssPath = path.join(finalLibPath, "skltn", "src", "css", "index.css");
260
- try {
261
- await fs.access(testCssPath);
262
- console.log(
263
- chalk.green(`[static-files] Test CSS file exists: ${testCssPath}`),
264
- );
265
- } catch (error) {
266
- console.error(
267
- chalk.yellow(`[static-files] ⚠️ Test CSS file NOT found: ${testCssPath}`),
268
- );
269
- }
270
- } catch (error) {
271
- console.error(
272
- chalk.red(`[static-files] ❌ lib/ directory not found at: ${finalLibPath}`),
273
- );
274
- console.error(
275
- chalk.red(`[static-files] Error: ${error instanceof Error ? error.message : String(error)}`),
276
- );
277
- console.error(
278
- chalk.red(`[static-files] ⚠️ /lib middleware will NOT be registered - CSS files will 404!`),
279
- );
280
- }
281
-
282
- // Register static file middleware ONLY if directory exists
283
- if (libPathExists) {
284
- console.log(chalk.green(`[static-files] Registering /lib middleware with finalLibPath: ${finalLibPath}`));
285
-
286
- // CRITICAL: First middleware to block source files BEFORE any express.static can serve them
287
- // This MUST run before express.static to prevent wrong MIME types
288
- app.use("/lib", (req, res, next) => {
289
- const url = req.url.split("?")[0];
290
- const path = req.path.split("?")[0];
291
-
292
- // CRITICAL: Block source files immediately - check both url and path
293
- // When express.static is mounted at /lib, req.path is stripped of /lib prefix
294
- const isSourceFile =
295
- url.endsWith(".ui") || url.endsWith(".uix") || url.endsWith(".ts") ||
296
- (url.endsWith(".js") && !url.includes("node_modules")) || url.endsWith(".mjs") ||
297
- path.endsWith(".ui") || path.endsWith(".uix") || path.endsWith(".ts") ||
298
- (path.endsWith(".js") && !path.includes("node_modules")) || path.endsWith(".mjs");
299
-
300
- if (isSourceFile) {
301
- console.log(
302
- chalk.red(
303
- `[static-files] ⚠️ FIRST BLOCK: Skipping source file: url=${url}, path=${path} - should be handled by module middleware`
304
- )
305
- );
306
- return next(); // Let module transformation middleware handle it
307
- }
308
-
309
- console.log(
310
- chalk.cyan(`[static-files] Request for /lib${path} (static file)`),
311
- );
312
- next();
313
- });
314
-
315
- // REMOVED: Logging middleware - it was just adding noise
316
- // The blocking middleware above already handles source files
317
-
318
- // CRITICAL: Add express.static for /lib/ but wrap it to skip source files
319
- // Source files should be handled by module transformation middleware (registered before this)
320
- const libStatic = express.static(finalLibPath, {
321
- setHeaders: (res, filePath) => {
322
- try {
323
- res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
324
- res.setHeader("Pragma", "no-cache");
325
- res.setHeader("Expires", "0");
326
- } catch (error) {
327
- console.error(chalk.red(`[static-files] Error setting headers for ${filePath}:`), error);
328
- }
329
- },
330
- });
331
-
332
- // CRITICAL: Wrap express.static to prevent serving source files
333
- // Check BOTH req.url (full path) and req.path (stripped path) to catch all cases
334
- app.use("/lib", (req, res, next) => {
335
- const url = req.url.split("?")[0];
336
- const path = req.path.split("?")[0];
337
-
338
- // CRITICAL: Skip source files - they should be handled by module transformation middleware
339
- // Check both url and path because express.static strips the mount path
340
- const isSourceFile =
341
- url.endsWith(".ui") || url.endsWith(".uix") || url.endsWith(".ts") ||
342
- (url.endsWith(".js") && !url.includes("node_modules")) || url.endsWith(".mjs") ||
343
- path.endsWith(".ui") || path.endsWith(".uix") || path.endsWith(".ts") ||
344
- (path.endsWith(".js") && !path.includes("node_modules")) || path.endsWith(".mjs");
345
-
346
- if (isSourceFile) {
347
- console.log(
348
- chalk.red(
349
- `[static-files /lib express.static] ⚠️ BLOCKING source file: url=${url}, path=${path} - should be handled by module transformation middleware`
350
- )
351
- );
352
- // CRITICAL: Don't call libStatic - return next() to skip it
353
- return next(); // Let module transformation middleware handle it
354
- }
355
- // For static files (CSS, images), use express.static
356
- libStatic(req, res, next);
357
- });
358
- // This was serving source files with wrong MIME type (application/octet-stream)
359
- // Source files should be handled by module transformation middleware (registered before this)
360
- // Only static files (CSS, images) should be served, and they're handled by the custom handler above
361
- // If a file isn't found, let it 404 rather than serving with wrong MIME type
362
- console.log(
363
- chalk.gray(` 📦 Serving workspace lib/ from ${finalLibPath}`),
364
- );
365
- }
366
-
367
- // Continue with other workspace directories if workspaceRoot is different from app root
368
- if (workspaceRoot && workspaceRoot !== config.root) {
369
-
370
- // Serve libraries/ directory (legacy support)
371
- const librariesPath = path.join(workspaceRoot, "libraries");
372
- try {
373
- await fs.access(librariesPath);
374
- app.use("/libraries", express.static(librariesPath));
375
- console.log(
376
- chalk.gray(` 📦 Serving workspace libraries/ from ${librariesPath}`),
377
- );
378
- } catch {
379
- // libraries/ doesn't exist, skip
380
- }
381
-
382
- // NOTE: Do NOT serve /packages/ as static files
383
- // Workspace packages contain source files (.ts, .ui, .uix) that need to be processed
384
- // by handlers (TSHandler, UIHandler, etc.) to rewrite imports and compile them
385
- // Static file serving would bypass this processing, causing bare imports to fail
386
- // Only serve /packages/ if they're already compiled assets (handled by handlers)
387
-
388
- // Serve modules/ directory (for CSS, assets, etc.)
389
- // Source files (.ui, .uix, .ts) must NOT be served statically they need
390
- // to fall through to the compiler middleware (general handler). CG-07.
391
- const modulesPath = path.join(workspaceRoot, "modules");
392
- try {
393
- await fs.access(modulesPath);
394
- app.use("/modules", (req: express.Request, res: express.Response, next: express.NextFunction) => {
395
- const url = req.url.split("?")[0];
396
- const isSourceFile =
397
- url.endsWith(".ui") ||
398
- url.endsWith(".uix") ||
399
- url.endsWith(".ts") ||
400
- (url.endsWith(".js") && !url.includes("node_modules")) ||
401
- url.endsWith(".mjs");
402
- if (isSourceFile) {
403
- return next();
404
- }
405
- express.static(modulesPath)(req, res, next);
406
- });
407
- console.log(
408
- chalk.gray(` 📦 Serving workspace modules/ from ${modulesPath}`),
409
- );
410
- } catch {
411
- // modules/ doesn't exist, skip
412
- }
413
- }
414
-
415
- // NOTE: SWISS packages are NOT served as static files
416
- // They are processed by the middleware (TS/JS handlers) to rewrite imports
417
- // This ensures all bare imports in SWISS packages are rewritten correctly
418
- // The /swiss-packages/ URLs are handled by the middleware in middleware-setup.ts
419
- }
420
-
421
- /**
422
- * Setup SPA fallback - serves index.html for all unmatched routes
423
- */
424
- export async function setupSPAFallback(
425
- app: Express,
426
- config: StaticFilesConfig,
427
- ): Promise<void> {
428
- console.log(chalk.magenta(`[SWITE] setupSPAFallback loaded - VERSION 3.0.0 (NO HARDCODED CSS)`));
429
- // Use app.all() to catch ALL HTTP methods, but only for non-source files
430
- app.all("*", async (req, res, next) => {
431
- const url = req.url.split("?")[0];
432
- const fullUrl = req.url;
433
- const accept = String(req.headers?.accept || "");
434
-
435
- // DEBUG: Verify handler is being called
436
- process.stderr.write(`[SPA FALLBACK] Handler called for: ${req.method} ${fullUrl}\n`);
437
- console.error(`[SWITE CSS DEBUG] ========== SPA FALLBACK HANDLER START ==========`);
438
- console.error(`[SWITE CSS DEBUG] URL: ${url}, Full URL: ${fullUrl}`);
439
-
440
- // --- CRITICAL SAFETY CHECK ---
441
- // NEVER serve HTML for /src/* requests - these are source files that must be handled by middleware
442
- // Even if middleware fails, we should return 404, not HTML
443
- if (req.path?.startsWith("/src/") || url.startsWith("/src/")) {
444
- console.error(chalk.red(`[SPA FALLBACK] ⚠️ BLOCKED: Attempt to serve HTML for source path: ${req.method} ${fullUrl}`));
445
- console.error(chalk.red(`[SPA FALLBACK] This should have been handled by /src middleware! Returning 404.`));
446
- res.status(404).setHeader("Content-Type", "text/plain");
447
- res.send(`File not found: ${url}`);
448
- return;
449
- }
450
-
451
- // --- CRITICAL SAFETY CHECK ---
452
- // NEVER serve HTML for /swiss-packages/* requests - these are SWISS framework packages
453
- // They should be handled by TS/JS handlers to rewrite imports
454
- if (req.path?.startsWith("/swiss-packages/") || url.startsWith("/swiss-packages/")) {
455
- console.error(chalk.red(`[SPA FALLBACK] ⚠️ BLOCKED: Attempt to serve HTML for SWISS package: ${req.method} ${fullUrl}`));
456
- console.error(chalk.red(`[SPA FALLBACK] This should have been handled by module transformation middleware! Returning 404.`));
457
- res.status(404).setHeader("Content-Type", "text/plain");
458
- res.send(`File not found: ${url}`);
459
- return;
460
- }
461
-
462
- // --- CRITICAL SAFETY CHECK ---
463
- // NEVER serve HTML for /lib/* requests - these are workspace library files
464
- // They should be handled by static file middleware
465
- if (req.path?.startsWith("/lib/") || url.startsWith("/lib/")) {
466
- console.error(chalk.red(`[SPA FALLBACK] ⚠️ BLOCKED: Attempt to serve HTML for /lib/ path: ${req.method} ${fullUrl}`));
467
- console.error(chalk.red(`[SPA FALLBACK] This should have been handled by static file middleware! Returning 404.`));
468
- res.status(404).setHeader("Content-Type", "text/plain");
469
- res.send(`File not found: ${url}`);
470
- return;
471
- }
472
-
473
- // Log every request that hits the fallback (for diagnostics)
474
- console.log(chalk.gray(`[SPA FALLBACK] Serving HTML for: ${req.method} ${fullUrl}`));
475
- process.stderr.write(`[SPA FALLBACK] About to read HTML file...\n`);
476
-
477
- // Log if SPA fallback is being hit for .ui files (this should NOT happen after /src check)
478
- if (url.endsWith(".ui")) {
479
- console.error(chalk.red(`[SPA FALLBACK] ⚠️ WARNING: SPA fallback intercepted .ui file: ${fullUrl}`));
480
- console.error(chalk.red(`[SPA FALLBACK] This should have been handled by module transformation middleware!`));
481
- }
482
-
483
- // DO NOT serve HTML for source files - they should be handled by handlers
484
- // This prevents the SPA fallback from catching .ui, .uix, .ts, .js, .mjs files
485
- // If we reach here, it means the middleware handlers didn't process it
486
- if (
487
- url.endsWith(".ui") ||
488
- url.endsWith(".uix") ||
489
- url.endsWith(".ts") ||
490
- url.endsWith(".js") ||
491
- url.endsWith(".mjs") ||
492
- url.endsWith(".css") ||
493
- url.endsWith(".json")
494
- ) {
495
- // These should have been handled by middleware handlers
496
- // If we reach here, the file wasn't found, return 404 with proper content type
497
- console.error(chalk.red(`[SPA FALLBACK] Returning 404 for ${url} - should have been handled earlier`));
498
- res.status(404).setHeader("Content-Type", "text/plain");
499
- res.send(`File not found: ${url}`);
500
- return;
501
- }
502
-
503
- // Only serve SPA HTML for real navigation/document requests.
504
- // If a script/style/module fetch hits the fallback, returning HTML causes strict MIME failures.
505
- if (!accept.includes("text/html")) {
506
- res.status(404).setHeader("Content-Type", "text/plain");
507
- res.send(`Not found: ${url}`);
508
- return;
509
- }
510
-
511
- // Add cache-busting headers for HTML files during development
512
- res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
513
- res.setHeader("Pragma", "no-cache");
514
- res.setHeader("Expires", "0");
515
-
516
- // Inject timestamp into script tag to force fresh load
517
- const htmlPath = path.join(config.root, config.publicDir, "index.html");
518
- let html = await fs.readFile(htmlPath, "utf-8");
519
- const timestamp = Date.now();
520
- const random = Math.random().toString(36).substring(7);
521
- // More aggressive cache busting - replace ALL script src attributes
522
- html = html.replace(/src="([^"]*index\.ui[^"]*)"/g, (match, src) => {
523
- // Remove any existing cache-busting params
524
- const cleanSrc = src.split("?")[0].split("&")[0];
525
- return `src="${cleanSrc}?v=dev&t=${timestamp}&r=${random}"`;
526
- });
527
- // Also replace any script tags with type="module" that have src attributes
528
- html = html.replace(
529
- /<script\s+type=["']module["'][^>]*src=["']([^"']*index\.ui[^"']*)["'][^>]*>/g,
530
- (match, src) => {
531
- const cleanSrc = src.split("?")[0].split("&")[0];
532
- return match.replace(
533
- src,
534
- `${cleanSrc}?v=dev&t=${timestamp}&r=${random}`,
535
- );
536
- },
537
- );
538
-
539
- // Add cache-busting meta tags to prevent browser caching
540
- if (!html.includes('<meta http-equiv="Cache-Control"')) {
541
- html = html.replace(
542
- "<head>",
543
- `<head>\n <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">\n <meta http-equiv="Pragma" content="no-cache">\n <meta http-equiv="Expires" content="0">`,
544
- );
545
- }
546
-
547
- // Extract CSS imports from entry point and inject as <link> tags
548
- // This dynamically discovers CSS files from the app's entry point
549
- // CRITICAL: This MUST run before import map injection
550
- // IMPORTANT: Only inject CSS files that actually exist in the app's directory
551
- console.log(chalk.magenta(`[SWITE CSS] ========== CSS EXTRACTION START (VERSION 3.0.0) ==========`));
552
- console.log(chalk.magenta(`[SWITE CSS] App root: ${config.root}`));
553
- try {
554
- const entryFile = config.entry ?? "src/index.ui";
555
- const entryPointPath = path.join(config.root, entryFile);
556
- console.log(chalk.blue(`[SWITE CSS] Checking entry point: ${entryPointPath}`));
557
- const entryPointContent = await fs.readFile(entryPointPath, "utf-8");
558
-
559
- // Extract CSS imports using regex
560
- const cssImportPattern = /import\s+['"](.*?\.css)['"];?/g;
561
- const cssImports = new Set<string>();
562
- let match;
563
-
564
- // Check entry point
565
- while ((match = cssImportPattern.exec(entryPointContent)) !== null) {
566
- cssImports.add(match[1]);
567
- }
568
-
569
- // Also check imported files (like App.uix) for CSS imports
570
- const importPattern = /import\s+.*?from\s+['"](.*?)['"];?/g;
571
- const importedFiles: string[] = [];
572
- let importMatch;
573
- cssImportPattern.lastIndex = 0; // Reset regex
574
- while ((importMatch = importPattern.exec(entryPointContent)) !== null) {
575
- const importPath = importMatch[1];
576
- // Skip node_modules and absolute imports
577
- if (!importPath.startsWith("@") && !importPath.startsWith("/") && !importPath.startsWith(".")) {
578
- continue;
579
- }
580
- // Resolve relative imports
581
- if (importPath.startsWith(".")) {
582
- importedFiles.push(importPath);
583
- }
584
- }
585
-
586
- // Check imported files for CSS
587
- for (const importedFile of importedFiles) {
588
- try {
589
- const importedFilePath = path.resolve(path.dirname(entryPointPath), importedFile);
590
- // Try different extensions
591
- const extensions = [".uix", ".ui", ".ts", ".js"];
592
- let found = false;
593
- for (const ext of extensions) {
594
- const testPath = importedFilePath.endsWith(ext) ? importedFilePath : importedFilePath + ext;
595
- try {
596
- const importedContent = await fs.readFile(testPath, "utf-8");
597
- found = true;
598
- // Extract CSS imports from this file
599
- cssImportPattern.lastIndex = 0; // Reset regex
600
- let cssMatch2;
601
- while ((cssMatch2 = cssImportPattern.exec(importedContent)) !== null) {
602
- // Resolve relative CSS path
603
- const cssPath = cssMatch2[1];
604
- if (cssPath.startsWith(".")) {
605
- const resolvedCssPath = path.resolve(path.dirname(testPath), cssPath);
606
- const relativeCssPath = path.relative(path.join(config.root, "src"), resolvedCssPath);
607
- const normalizedPath = relativeCssPath.replace(/\\/g, "/");
608
- cssImports.add(normalizedPath);
609
- } else {
610
- cssImports.add(cssPath);
611
- }
612
- }
613
- break;
614
- } catch (err) {
615
- // File doesn't exist with this extension, try next
616
- }
617
- }
618
- } catch (error) {
619
- // Could not read imported file, skip
620
- }
621
- }
622
-
623
- console.log(chalk.blue(`[SWITE CSS] Found ${cssImports.size} CSS import(s) in code`));
624
- if (cssImports.size > 0) {
625
- const cssArray = Array.from(cssImports);
626
- console.log(chalk.blue(`[SWITE CSS] CSS imports found: ${cssArray.join(", ")}`));
627
-
628
- // Verify CSS files exist before injecting them
629
- const existingCssFiles: string[] = [];
630
- for (const cssPath of cssArray) {
631
- // Convert to file system path
632
- const url = cssPath.startsWith("/") ? cssPath : `/src/${cssPath}`;
633
- const filePath = url.startsWith("/src/")
634
- ? path.join(config.root, url.substring(1)) // Remove leading /
635
- : path.join(config.root, "src", cssPath);
636
-
637
- console.log(chalk.blue(`[SWITE CSS] Checking if CSS file exists: ${filePath} (url: ${url})`));
638
- try {
639
- await fs.access(filePath);
640
- console.log(chalk.green(`[SWITE CSS] CSS file exists: ${filePath}`));
641
- existingCssFiles.push(url);
642
- } catch {
643
- // CSS file doesn't exist, skip it
644
- // This allows different apps/websites to have different CSS files
645
- console.log(chalk.yellow(`[SWITE CSS] ⚠️ CSS file NOT found: ${filePath}, skipping`));
646
- }
647
- }
648
-
649
- // Only inject CSS files that actually exist
650
- console.log(chalk.blue(`[SWITE CSS] ${existingCssFiles.length} CSS file(s) exist out of ${cssArray.length} found`));
651
- if (existingCssFiles.length === 0) {
652
- console.log(chalk.yellow(`[SWITE CSS] ⚠️ No CSS files exist, skipping injection`));
653
- } else if (existingCssFiles.length > 0) {
654
- const cssLinks = existingCssFiles
655
- .map(url => ` <link rel="stylesheet" href="${url}">`)
656
- .join("\n");
657
-
658
- // Check if CSS links are already in HTML (to avoid duplicates)
659
- const alreadyInjected = existingCssFiles.some(url =>
660
- html.includes(`href="${url}"`) || html.includes(`href='${url}'`)
661
- );
662
-
663
- if (!alreadyInjected) {
664
- // Inject CSS links before </head> - MUST happen before import map injection
665
- const beforeReplace = html;
666
- html = html.replace(/\s*<\/head>/i, `${cssLinks}\n </head>`);
667
- if (html === beforeReplace) {
668
- console.warn(chalk.yellow("[SWITE] Failed to inject CSS links - </head> not found"));
669
- } else {
670
- console.log(chalk.green(`[SWITE] ✅ Injected ${existingCssFiles.length} CSS link(s): ${existingCssFiles.join(", ")}`));
671
- }
672
- } else {
673
- console.log(chalk.blue(`[SWITE CSS] CSS links already in HTML, skipping injection`));
674
- }
675
- }
676
- }
677
- } catch (error) {
678
- // If entry point doesn't exist or can't be read, continue without CSS injection
679
- // Silently continue - CSS injection is optional
680
- console.log(chalk.yellow(`[SWITE CSS] Could not extract CSS imports: ${error instanceof Error ? error.message : String(error)}`));
681
- }
682
-
683
- // Add/merge import map to help browser resolve bare module specifiers.
684
- // If an importmap already exists in HTML, merge .swite/import-map.json entries
685
- // into it — existing HTML entries take priority (never overwrite manual entries).
686
- const cachedMapPath = path.join(config.root, ".swite", "import-map.json");
687
- let switeImports: Record<string, string> = {};
688
- try {
689
- const raw = await fs.readFile(cachedMapPath, "utf-8");
690
- const parsed = JSON.parse(raw);
691
- if (parsed?.imports && typeof parsed.imports === "object") {
692
- switeImports = parsed.imports;
693
- }
694
- } catch {
695
- // no cached map — nothing to merge
696
- }
697
-
698
- if (!html.includes('type="importmap"')) {
699
- // No importmap at all — inject one from .swite/import-map.json
700
- const importMap = `\n <script type="importmap">\n ${JSON.stringify({ imports: switeImports }, null, 2).replace(/\n/g, "\n ")}\n </script>`;
701
- const beforeReplace = html;
702
- html = html.replace(/\s*<\/head>/i, `${importMap}\n </head>`);
703
- if (html === beforeReplace) {
704
- console.warn("[SWITE] Failed to add import map - </head> not found or already replaced");
705
- } else {
706
- console.log(`[SWITE] Added import map with ${Object.keys(switeImports).length} entries`);
707
- }
708
- } else {
709
- // Importmap already in HTML — merge swite entries without overwriting existing ones
710
- console.log("[SWITE] Import map already exists in HTML — merging swite entries");
711
- if (Object.keys(switeImports).length > 0) {
712
- html = html.replace(
713
- /(<script\s+type=["']importmap["'][^>]*>)\s*([\s\S]*?)(\s*<\/script>)/i,
714
- (_match, open, body, close) => {
715
- try {
716
- const existing = JSON.parse(body.trim());
717
- const existingImports: Record<string, string> = existing?.imports ?? {};
718
- // Swite entries fill gaps; existing HTML entries win
719
- const merged = { ...switeImports, ...existingImports };
720
- return `${open}\n ${JSON.stringify({ imports: merged }, null, 2).replace(/\n/g, "\n ")}${close}`;
721
- } catch {
722
- return _match; // parse failed — leave importmap untouched
723
- }
724
- }
725
- );
726
- }
727
- }
728
-
729
- res.send(html);
730
- });
731
- }
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import express from "express";
8
+ import type { Express } from "express";
9
+ import { promises as fs, realpathSync, existsSync } from "node:fs";
10
+ import path from "node:path";
11
+ import chalk from "chalk";
12
+ import { findWorkspaceRoot } from "../../kernel/workspace.js";
13
+
14
+ export interface StaticFilesConfig {
15
+ root: string;
16
+ publicDir: string;
17
+ workspaceRoot?: string | null;
18
+ /** Entry file for CSS extraction, relative to root. Defaults to "src/index.ui". */
19
+ entry?: string;
20
+ }
21
+
22
+ /**
23
+ * Setup static file serving for public directory, node_modules, and workspace packages
24
+ */
25
+ export async function setupStaticFiles(
26
+ app: Express,
27
+ config: StaticFilesConfig,
28
+ ): Promise<void> {
29
+ const _debug = process.env["SWITE_DEBUG"] === "1";
30
+ if (_debug) console.log(chalk.magenta(`[static-files] ⚡ setupStaticFiles called with root: ${config.root}`));
31
+
32
+ // Static file serving - ONLY serve public directory
33
+ // Do NOT serve dist/ folder - it contains old build artifacts with bare imports
34
+ const publicPath = path.join(config.root, config.publicDir);
35
+
36
+ // Serve static files from public/ directory
37
+ // IMPORTANT: Skip source files (.ui, .uix, .ts, .js) - they should be handled by module transformation middleware
38
+ // Wrap express.static to prevent it from serving source files
39
+ const publicStaticMiddleware = express.static(publicPath, {
40
+ // Exclude dist folder and other build artifacts
41
+ dotfiles: "ignore",
42
+ index: false, // Don't serve index files from static middleware
43
+ setHeaders: (res, filePath) => {
44
+ // Add cache-busting headers for all static files in dev mode
45
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
46
+ res.setHeader("Pragma", "no-cache");
47
+ res.setHeader("Expires", "0");
48
+ },
49
+ });
50
+
51
+ app.use((req, res, next) => {
52
+ const url = req.url.split("?")[0];
53
+ // Skip source files - let module transformation middleware handle them
54
+ // DO NOT call express.static for source files - it will serve them with wrong MIME type
55
+ if (
56
+ url.endsWith(".ui") ||
57
+ url.endsWith(".uix") ||
58
+ url.endsWith(".ts") ||
59
+ (url.endsWith(".js") && !url.includes("node_modules")) ||
60
+ url.endsWith(".mjs")
61
+ ) {
62
+ // Skip express.static for source files - pass to next middleware instead
63
+ return next();
64
+ }
65
+ // For non-source files, use express.static
66
+ publicStaticMiddleware(req, res, next);
67
+ });
68
+
69
+ // NOTE: We do NOT serve /src as static files here anymore
70
+ // The module transformation middleware in middleware-setup.ts handles ALL /src requests first
71
+ // This ensures .ui, .uix, .ts files are processed correctly before static middleware can interfere
72
+ // Only non-source files (CSS, images, etc.) will pass through to be served as static
73
+ // But they should be handled by the module transformation middleware's next() call
74
+
75
+ // Serve node_modules as static files from multiple locations
76
+ // 1. App root node_modules
77
+ // Wrap to skip source files
78
+ const nodeModulesStatic = express.static(path.join(config.root, "node_modules"));
79
+ app.use("/node_modules", (req, res, next) => {
80
+ const url = req.url.split("?")[0];
81
+ // Skip source files in node_modules - they should be handled by module transformation
82
+ if (
83
+ url.endsWith(".ui") ||
84
+ url.endsWith(".uix") ||
85
+ url.endsWith(".ts") ||
86
+ (url.endsWith(".js") && !url.includes("/dist/") && !url.includes("/lib/")) ||
87
+ url.endsWith(".mjs")
88
+ ) {
89
+ return next();
90
+ }
91
+ nodeModulesStatic(req, res, next);
92
+ });
93
+
94
+ // 2. Workspace root node_modules (if different from app root)
95
+ const workspaceRootForNodeModules =
96
+ config.workspaceRoot ?? (await findWorkspaceRoot(config.root));
97
+ if (
98
+ workspaceRootForNodeModules &&
99
+ workspaceRootForNodeModules !== config.root
100
+ ) {
101
+ const workspaceNodeModules = path.join(
102
+ workspaceRootForNodeModules,
103
+ "node_modules",
104
+ );
105
+ try {
106
+ await fs.access(workspaceNodeModules);
107
+ // Serve workspace node_modules with a different path to avoid conflicts
108
+ // But also check if package exists in app node_modules first
109
+ app.use("/node_modules", (req, res, next) => {
110
+ const url = req.url.split("?")[0];
111
+ // Skip source files - let module transformation middleware handle them
112
+ if (
113
+ url.endsWith(".ui") ||
114
+ url.endsWith(".uix") ||
115
+ url.endsWith(".ts") ||
116
+ (url.endsWith(".js") && !url.includes("/dist/") && !url.includes("/lib/")) ||
117
+ url.endsWith(".mjs")
118
+ ) {
119
+ return next();
120
+ }
121
+
122
+ // Strip leading slash: req.path inside app.use('/node_modules') is already
123
+ // relative to that prefix, e.g. '/@swissjs/shell/design-tokens/primitive.css'
124
+ const relPath = req.path.replace(/^\/+/, "");
125
+ if (!relPath) return next();
126
+
127
+ // Resolve pnpm symlinks explicitly (same mechanism UIHandler uses).
128
+ // express.static / fs.access can silently fail on pnpm virtual-store symlinks
129
+ // in production containers, so we use realpathSync to get the real path first.
130
+ const isScoped = relPath.startsWith("@");
131
+ const parts = relPath.split("/");
132
+ const pkgName = isScoped ? `${parts[0]}/${parts[1]}` : parts[0];
133
+ const subPath = isScoped ? parts.slice(2).join("/") : parts.slice(1).join("/");
134
+
135
+ const pkgSymLink = path.join(workspaceNodeModules, pkgName);
136
+ try {
137
+ const pkgReal = realpathSync(pkgSymLink);
138
+ const realAbs = path.join(pkgReal, subPath);
139
+ if (existsSync(realAbs)) {
140
+ res.sendFile(realAbs);
141
+ return;
142
+ }
143
+ } catch {
144
+ // symlink missing or broken — fall through to next()
145
+ }
146
+
147
+ next();
148
+ });
149
+ if (_debug) {
150
+ console.log(
151
+ chalk.gray(
152
+ ` 📦 Serving workspace node_modules from ${workspaceNodeModules}`,
153
+ ),
154
+ );
155
+ }
156
+ } catch {
157
+ // Workspace node_modules doesn't exist, skip
158
+ }
159
+ }
160
+
161
+ // Serve workspace packages (lib/, libraries/, packages/, modules/, etc.)
162
+ // This allows workspace packages to be served via HTTP
163
+ // Reuse workspaceRoot from above if it exists, otherwise find it
164
+ const workspaceRoot =
165
+ workspaceRootForNodeModules || (await findWorkspaceRoot(config.root));
166
+
167
+ if (_debug) {
168
+ console.log(
169
+ chalk.blue(`[static-files] Workspace root: ${workspaceRoot}`),
170
+ );
171
+ }
172
+ if (_debug) {
173
+ console.log(
174
+ chalk.blue(`[static-files] App root: ${config.root}`),
175
+ );
176
+ }
177
+
178
+ // Try to serve lib/ directory - check both workspace root and app root parent
179
+ let libPath: string | null = null;
180
+
181
+ if (_debug) {
182
+ console.log(
183
+ chalk.blue(`[static-files] Determining lib/ path... workspaceRoot: ${workspaceRoot}, config.root: ${config.root}`),
184
+ );
185
+ }
186
+
187
+ // First, try workspace root
188
+ if (workspaceRoot && workspaceRoot !== config.root) {
189
+ libPath = path.join(workspaceRoot, "lib");
190
+ if (_debug) {
191
+ console.log(
192
+ chalk.blue(`[static-files] Trying workspace root lib/: ${libPath}`),
193
+ );
194
+ }
195
+ } else {
196
+ if (_debug) {
197
+ console.log(
198
+ chalk.yellow(`[static-files] Workspace root equals app root, trying parent directories...`),
199
+ );
200
+ }
201
+ // If workspace root equals app root, try going up from app root
202
+ const parentDir = path.dirname(config.root);
203
+ const parentLibPath = path.join(parentDir, "lib");
204
+ if (_debug) {
205
+ console.log(
206
+ chalk.blue(`[static-files] Trying parent lib/: ${parentLibPath}`),
207
+ );
208
+ }
209
+ try {
210
+ await fs.access(parentLibPath);
211
+ libPath = parentLibPath;
212
+ if (_debug) {
213
+ console.log(
214
+ chalk.blue(`[static-files] Using parent directory lib/: ${libPath}`),
215
+ );
216
+ }
217
+ } catch (error) {
218
+ if (_debug) {
219
+ console.log(
220
+ chalk.yellow(`[static-files] Parent lib/ not found: ${error instanceof Error ? error.message : String(error)}`),
221
+ );
222
+ }
223
+ // Parent lib/ doesn't exist, try grandparent
224
+ const grandparentDir = path.dirname(parentDir);
225
+ const grandparentLibPath = path.join(grandparentDir, "lib");
226
+ if (_debug) {
227
+ console.log(
228
+ chalk.blue(`[static-files] Trying grandparent lib/: ${grandparentLibPath}`),
229
+ );
230
+ }
231
+ try {
232
+ await fs.access(grandparentLibPath);
233
+ libPath = grandparentLibPath;
234
+ if (_debug) {
235
+ console.log(
236
+ chalk.blue(`[static-files] Using grandparent directory lib/: ${libPath}`),
237
+ );
238
+ }
239
+ } catch (error2) {
240
+ if (_debug) {
241
+ console.log(
242
+ chalk.yellow(`[static-files] Grandparent lib/ not found: ${error2 instanceof Error ? error2.message : String(error2)}`),
243
+ );
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ // Serve lib/ directory if found
250
+ if (_debug) {
251
+ console.log(
252
+ chalk.blue(`[static-files] Checking for lib/ directory... libPath: ${libPath}`),
253
+ );
254
+ }
255
+
256
+ // ALWAYS try to register /lib/ static serving
257
+ // Calculate the lib path - prefer workspace root, fallback to parent of app root
258
+ let finalLibPath: string;
259
+ if (libPath) {
260
+ finalLibPath = libPath;
261
+ } else if (workspaceRoot && workspaceRoot !== config.root) {
262
+ finalLibPath = path.join(workspaceRoot, "lib");
263
+ } else {
264
+ // Go up from app root to find lib/
265
+ const parentDir = path.dirname(config.root);
266
+ finalLibPath = path.join(parentDir, "lib");
267
+ }
268
+
269
+ if (_debug) {
270
+ console.log(
271
+ chalk.blue(`[static-files] Final lib path to check: ${finalLibPath}`),
272
+ );
273
+ }
274
+ if (_debug) {
275
+ console.log(
276
+ chalk.blue(`[static-files] workspaceRoot: ${workspaceRoot}, config.root: ${config.root}`),
277
+ );
278
+ }
279
+
280
+ // Try to access the directory
281
+ let libPathExists = false;
282
+ try {
283
+ await fs.access(finalLibPath);
284
+ libPathExists = true;
285
+ if (_debug) {
286
+ console.log(
287
+ chalk.green(`[static-files] Found lib/ directory at: ${finalLibPath}`),
288
+ );
289
+ }
290
+
291
+ // Verify the CSS file exists
292
+ const testCssPath = path.join(finalLibPath, "skltn", "src", "css", "index.css");
293
+ try {
294
+ await fs.access(testCssPath);
295
+ if (_debug) {
296
+ console.log(
297
+ chalk.green(`[static-files] Test CSS file exists: ${testCssPath}`),
298
+ );
299
+ }
300
+ } catch (error) {
301
+ if (_debug) {
302
+ console.error(
303
+ chalk.yellow(`[static-files] ⚠️ Test CSS file NOT found: ${testCssPath}`),
304
+ );
305
+ }
306
+ }
307
+ } catch (error) {
308
+ if (_debug) {
309
+ console.error(
310
+ chalk.red(`[static-files] lib/ directory not found at: ${finalLibPath}`),
311
+ );
312
+ }
313
+ if (_debug) {
314
+ console.error(
315
+ chalk.red(`[static-files] Error: ${error instanceof Error ? error.message : String(error)}`),
316
+ );
317
+ }
318
+ if (_debug) {
319
+ console.error(
320
+ chalk.red(`[static-files] ⚠️ /lib middleware will NOT be registered - CSS files will 404!`),
321
+ );
322
+ }
323
+ }
324
+
325
+ // Register static file middleware ONLY if directory exists
326
+ if (libPathExists) {
327
+ if (_debug) {
328
+ console.log(chalk.green(`[static-files] ✅ Registering /lib middleware with finalLibPath: ${finalLibPath}`));
329
+ }
330
+
331
+ // CRITICAL: First middleware to block source files BEFORE any express.static can serve them
332
+ // This MUST run before express.static to prevent wrong MIME types
333
+ app.use("/lib", (req, res, next) => {
334
+ const url = req.url.split("?")[0];
335
+ const path = req.path.split("?")[0];
336
+
337
+ // CRITICAL: Block source files immediately - check both url and path
338
+ // When express.static is mounted at /lib, req.path is stripped of /lib prefix
339
+ const isSourceFile =
340
+ url.endsWith(".ui") || url.endsWith(".uix") || url.endsWith(".ts") ||
341
+ (url.endsWith(".js") && !url.includes("node_modules")) || url.endsWith(".mjs") ||
342
+ path.endsWith(".ui") || path.endsWith(".uix") || path.endsWith(".ts") ||
343
+ (path.endsWith(".js") && !path.includes("node_modules")) || path.endsWith(".mjs");
344
+
345
+ if (isSourceFile) {
346
+ if (_debug) {
347
+ console.log(
348
+ chalk.red(
349
+ `[static-files] ⚠️ FIRST BLOCK: Skipping source file: url=${url}, path=${path} - should be handled by module middleware`
350
+ )
351
+ );
352
+ }
353
+ return next(); // Let module transformation middleware handle it
354
+ }
355
+
356
+ if (_debug) {
357
+ console.log(
358
+ chalk.cyan(`[static-files] Request for /lib${path} (static file)`),
359
+ );
360
+ }
361
+ next();
362
+ });
363
+
364
+ // REMOVED: Logging middleware - it was just adding noise
365
+ // The blocking middleware above already handles source files
366
+
367
+ // CRITICAL: Add express.static for /lib/ but wrap it to skip source files
368
+ // Source files should be handled by module transformation middleware (registered before this)
369
+ const libStatic = express.static(finalLibPath, {
370
+ setHeaders: (res, filePath) => {
371
+ try {
372
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
373
+ res.setHeader("Pragma", "no-cache");
374
+ res.setHeader("Expires", "0");
375
+ } catch (error) {
376
+ if (_debug) {
377
+ console.error(chalk.red(`[static-files] Error setting headers for ${filePath}:`), error);
378
+ }
379
+ }
380
+ },
381
+ });
382
+
383
+ // CRITICAL: Wrap express.static to prevent serving source files
384
+ // Check BOTH req.url (full path) and req.path (stripped path) to catch all cases
385
+ app.use("/lib", (req, res, next) => {
386
+ const url = req.url.split("?")[0];
387
+ const path = req.path.split("?")[0];
388
+
389
+ // CRITICAL: Skip source files - they should be handled by module transformation middleware
390
+ // Check both url and path because express.static strips the mount path
391
+ const isSourceFile =
392
+ url.endsWith(".ui") || url.endsWith(".uix") || url.endsWith(".ts") ||
393
+ (url.endsWith(".js") && !url.includes("node_modules")) || url.endsWith(".mjs") ||
394
+ path.endsWith(".ui") || path.endsWith(".uix") || path.endsWith(".ts") ||
395
+ (path.endsWith(".js") && !path.includes("node_modules")) || path.endsWith(".mjs");
396
+
397
+ if (isSourceFile) {
398
+ if (_debug) {
399
+ console.log(
400
+ chalk.red(
401
+ `[static-files /lib express.static] ⚠️ BLOCKING source file: url=${url}, path=${path} - should be handled by module transformation middleware`
402
+ )
403
+ );
404
+ }
405
+ // CRITICAL: Don't call libStatic - return next() to skip it
406
+ return next(); // Let module transformation middleware handle it
407
+ }
408
+ // For static files (CSS, images), use express.static
409
+ libStatic(req, res, next);
410
+ });
411
+ // This was serving source files with wrong MIME type (application/octet-stream)
412
+ // Source files should be handled by module transformation middleware (registered before this)
413
+ // Only static files (CSS, images) should be served, and they're handled by the custom handler above
414
+ // If a file isn't found, let it 404 rather than serving with wrong MIME type
415
+ if (_debug) {
416
+ console.log(
417
+ chalk.gray(` 📦 Serving workspace lib/ from ${finalLibPath}`),
418
+ );
419
+ }
420
+ }
421
+
422
+ // Continue with other workspace directories if workspaceRoot is different from app root
423
+ if (workspaceRoot && workspaceRoot !== config.root) {
424
+
425
+ // Serve libraries/ directory (legacy support)
426
+ const librariesPath = path.join(workspaceRoot, "libraries");
427
+ try {
428
+ await fs.access(librariesPath);
429
+ app.use("/libraries", express.static(librariesPath));
430
+ if (_debug) {
431
+ console.log(
432
+ chalk.gray(` 📦 Serving workspace libraries/ from ${librariesPath}`),
433
+ );
434
+ }
435
+ } catch {
436
+ // libraries/ doesn't exist, skip
437
+ }
438
+
439
+ // NOTE: Do NOT serve /packages/ as static files
440
+ // Workspace packages contain source files (.ts, .ui, .uix) that need to be processed
441
+ // by handlers (TSHandler, UIHandler, etc.) to rewrite imports and compile them
442
+ // Static file serving would bypass this processing, causing bare imports to fail
443
+ // Only serve /packages/ if they're already compiled assets (handled by handlers)
444
+
445
+ // Serve modules/ directory (for CSS, assets, etc.)
446
+ // Source files (.ui, .uix, .ts) must NOT be served statically — they need
447
+ // to fall through to the compiler middleware (general handler). CG-07.
448
+ const modulesPath = path.join(workspaceRoot, "modules");
449
+ try {
450
+ await fs.access(modulesPath);
451
+ app.use("/modules", (req: express.Request, res: express.Response, next: express.NextFunction) => {
452
+ const url = req.url.split("?")[0];
453
+ const isSourceFile =
454
+ url.endsWith(".ui") ||
455
+ url.endsWith(".uix") ||
456
+ url.endsWith(".ts") ||
457
+ (url.endsWith(".js") && !url.includes("node_modules")) ||
458
+ url.endsWith(".mjs");
459
+ if (isSourceFile) {
460
+ return next();
461
+ }
462
+ express.static(modulesPath)(req, res, next);
463
+ });
464
+ if (_debug) {
465
+ console.log(
466
+ chalk.gray(` 📦 Serving workspace modules/ from ${modulesPath}`),
467
+ );
468
+ }
469
+ } catch {
470
+ // modules/ doesn't exist, skip
471
+ }
472
+ }
473
+
474
+ // NOTE: SWISS packages are NOT served as static files
475
+ // They are processed by the middleware (TS/JS handlers) to rewrite imports
476
+ // This ensures all bare imports in SWISS packages are rewritten correctly
477
+ // The /swiss-packages/ URLs are handled by the middleware in middleware-setup.ts
478
+ }
479
+
480
+ /**
481
+ * Setup SPA fallback - serves index.html for all unmatched routes
482
+ */
483
+ export async function setupSPAFallback(
484
+ app: Express,
485
+ config: StaticFilesConfig,
486
+ ): Promise<void> {
487
+ const _debug = process.env["SWITE_DEBUG"] === "1";
488
+ if (_debug) {
489
+ console.log(chalk.magenta(`[SWITE] setupSPAFallback loaded - VERSION 0.3.5 (NO HARDCODED CSS)`));
490
+ }
491
+ // Use app.all() to catch ALL HTTP methods, but only for non-source files
492
+ app.all("*", async (req, res, next) => {
493
+ const url = req.url.split("?")[0];
494
+ const fullUrl = req.url;
495
+ const accept = String(req.headers?.accept || "");
496
+
497
+ // DEBUG: Verify handler is being called
498
+
499
+ // --- CRITICAL SAFETY CHECK ---
500
+ // NEVER serve HTML for /src/* requests - these are source files that must be handled by middleware
501
+ // Even if middleware fails, we should return 404, not HTML
502
+ if (req.path?.startsWith("/src/") || url.startsWith("/src/")) {
503
+ res.status(404).setHeader("Content-Type", "text/plain");
504
+ res.send(`File not found: ${url}`);
505
+ return;
506
+ }
507
+
508
+ // --- CRITICAL SAFETY CHECK ---
509
+ // NEVER serve HTML for /swiss-packages/* requests - these are SWISS framework packages
510
+ // They should be handled by TS/JS handlers to rewrite imports
511
+ if (req.path?.startsWith("/swiss-packages/") || url.startsWith("/swiss-packages/")) {
512
+ res.status(404).setHeader("Content-Type", "text/plain");
513
+ res.send(`File not found: ${url}`);
514
+ return;
515
+ }
516
+
517
+ // --- CRITICAL SAFETY CHECK ---
518
+ // NEVER serve HTML for /lib/* requests - these are workspace library files
519
+ // They should be handled by static file middleware
520
+ if (req.path?.startsWith("/lib/") || url.startsWith("/lib/")) {
521
+ res.status(404).setHeader("Content-Type", "text/plain");
522
+ res.send(`File not found: ${url}`);
523
+ return;
524
+ }
525
+
526
+ // Log every request that hits the fallback (for diagnostics)
527
+
528
+ // Log if SPA fallback is being hit for .ui files (this should NOT happen after /src check)
529
+ if (url.endsWith(".ui")) {
530
+ }
531
+
532
+ // DO NOT serve HTML for source files - they should be handled by handlers
533
+ // This prevents the SPA fallback from catching .ui, .uix, .ts, .js, .mjs files
534
+ // If we reach here, it means the middleware handlers didn't process it
535
+ if (
536
+ url.endsWith(".ui") ||
537
+ url.endsWith(".uix") ||
538
+ url.endsWith(".ts") ||
539
+ url.endsWith(".js") ||
540
+ url.endsWith(".mjs") ||
541
+ url.endsWith(".css") ||
542
+ url.endsWith(".json")
543
+ ) {
544
+ // These should have been handled by middleware handlers
545
+ // If we reach here, the file wasn't found, return 404 with proper content type
546
+ res.status(404).setHeader("Content-Type", "text/plain");
547
+ res.send(`File not found: ${url}`);
548
+ return;
549
+ }
550
+
551
+ // Only serve SPA HTML for real navigation/document requests.
552
+ // If a script/style/module fetch hits the fallback, returning HTML causes strict MIME failures.
553
+ if (!accept.includes("text/html")) {
554
+ res.status(404).setHeader("Content-Type", "text/plain");
555
+ res.send(`Not found: ${url}`);
556
+ return;
557
+ }
558
+
559
+ // Add cache-busting headers for HTML files during development
560
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
561
+ res.setHeader("Pragma", "no-cache");
562
+ res.setHeader("Expires", "0");
563
+
564
+ // Inject timestamp into script tag to force fresh load
565
+ const htmlPath = path.join(config.root, config.publicDir, "index.html");
566
+ let html = await fs.readFile(htmlPath, "utf-8");
567
+ const timestamp = Date.now();
568
+ const random = Math.random().toString(36).substring(7);
569
+ // More aggressive cache busting - replace ALL script src attributes
570
+ html = html.replace(/src="([^"]*index\.ui[^"]*)"/g, (match, src) => {
571
+ // Remove any existing cache-busting params
572
+ const cleanSrc = src.split("?")[0].split("&")[0];
573
+ return `src="${cleanSrc}?v=dev&t=${timestamp}&r=${random}"`;
574
+ });
575
+ // Also replace any script tags with type="module" that have src attributes
576
+ html = html.replace(
577
+ /<script\s+type=["']module["'][^>]*src=["']([^"']*index\.ui[^"']*)["'][^>]*>/g,
578
+ (match, src) => {
579
+ const cleanSrc = src.split("?")[0].split("&")[0];
580
+ return match.replace(
581
+ src,
582
+ `${cleanSrc}?v=dev&t=${timestamp}&r=${random}`,
583
+ );
584
+ },
585
+ );
586
+
587
+ // Add cache-busting meta tags to prevent browser caching
588
+ if (!html.includes('<meta http-equiv="Cache-Control"')) {
589
+ html = html.replace(
590
+ "<head>",
591
+ `<head>\n <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">\n <meta http-equiv="Pragma" content="no-cache">\n <meta http-equiv="Expires" content="0">`,
592
+ );
593
+ }
594
+
595
+ // Extract CSS imports from entry point and inject as <link> tags
596
+ // This dynamically discovers CSS files from the app's entry point
597
+ // CRITICAL: This MUST run before import map injection
598
+ // IMPORTANT: Only inject CSS files that actually exist in the app's directory
599
+ if (_debug) {
600
+ console.log(chalk.magenta(`[SWITE CSS] ========== CSS EXTRACTION START (VERSION 0.3.5) ==========`));
601
+ }
602
+ if (_debug) {
603
+ console.log(chalk.magenta(`[SWITE CSS] App root: ${config.root}`));
604
+ }
605
+ try {
606
+ const entryFile = config.entry ?? "src/index.ui";
607
+ const entryPointPath = path.join(config.root, entryFile);
608
+ if (_debug) {
609
+ console.log(chalk.blue(`[SWITE CSS] Checking entry point: ${entryPointPath}`));
610
+ }
611
+ const entryPointContent = await fs.readFile(entryPointPath, "utf-8");
612
+
613
+ // Extract CSS imports using regex
614
+ const cssImportPattern = /import\s+['"](.*?\.css)['"];?/g;
615
+ const cssImports = new Set<string>();
616
+ let match;
617
+
618
+ // Check entry point
619
+ while ((match = cssImportPattern.exec(entryPointContent)) !== null) {
620
+ cssImports.add(match[1]);
621
+ }
622
+
623
+ // Also check imported files (like App.uix) for CSS imports
624
+ const importPattern = /import\s+.*?from\s+['"](.*?)['"];?/g;
625
+ const importedFiles: string[] = [];
626
+ let importMatch;
627
+ cssImportPattern.lastIndex = 0; // Reset regex
628
+ while ((importMatch = importPattern.exec(entryPointContent)) !== null) {
629
+ const importPath = importMatch[1];
630
+ // Skip node_modules and absolute imports
631
+ if (!importPath.startsWith("@") && !importPath.startsWith("/") && !importPath.startsWith(".")) {
632
+ continue;
633
+ }
634
+ // Resolve relative imports
635
+ if (importPath.startsWith(".")) {
636
+ importedFiles.push(importPath);
637
+ }
638
+ }
639
+
640
+ // Check imported files for CSS
641
+ for (const importedFile of importedFiles) {
642
+ try {
643
+ const importedFilePath = path.resolve(path.dirname(entryPointPath), importedFile);
644
+ // Try different extensions
645
+ const extensions = [".uix", ".ui", ".ts", ".js"];
646
+ let found = false;
647
+ for (const ext of extensions) {
648
+ const testPath = importedFilePath.endsWith(ext) ? importedFilePath : importedFilePath + ext;
649
+ try {
650
+ const importedContent = await fs.readFile(testPath, "utf-8");
651
+ found = true;
652
+ // Extract CSS imports from this file
653
+ cssImportPattern.lastIndex = 0; // Reset regex
654
+ let cssMatch2;
655
+ while ((cssMatch2 = cssImportPattern.exec(importedContent)) !== null) {
656
+ // Resolve relative CSS path
657
+ const cssPath = cssMatch2[1];
658
+ if (cssPath.startsWith(".")) {
659
+ const resolvedCssPath = path.resolve(path.dirname(testPath), cssPath);
660
+ const relativeCssPath = path.relative(path.join(config.root, "src"), resolvedCssPath);
661
+ const normalizedPath = relativeCssPath.replace(/\\/g, "/");
662
+ cssImports.add(normalizedPath);
663
+ } else {
664
+ cssImports.add(cssPath);
665
+ }
666
+ }
667
+ break;
668
+ } catch (err) {
669
+ // File doesn't exist with this extension, try next
670
+ }
671
+ }
672
+ } catch (error) {
673
+ // Could not read imported file, skip
674
+ }
675
+ }
676
+
677
+ if (_debug) {
678
+ console.log(chalk.blue(`[SWITE CSS] Found ${cssImports.size} CSS import(s) in code`));
679
+ }
680
+ if (cssImports.size > 0) {
681
+ const cssArray = Array.from(cssImports);
682
+ if (_debug) {
683
+ console.log(chalk.blue(`[SWITE CSS] CSS imports found: ${cssArray.join(", ")}`));
684
+ }
685
+
686
+ // Verify CSS files exist before injecting them
687
+ const existingCssFiles: string[] = [];
688
+ for (const cssPath of cssArray) {
689
+ // Convert to file system path
690
+ const url = cssPath.startsWith("/") ? cssPath : `/src/${cssPath}`;
691
+ const filePath = url.startsWith("/src/")
692
+ ? path.join(config.root, url.substring(1)) // Remove leading /
693
+ : path.join(config.root, "src", cssPath);
694
+
695
+ if (_debug) {
696
+ console.log(chalk.blue(`[SWITE CSS] Checking if CSS file exists: ${filePath} (url: ${url})`));
697
+ }
698
+ try {
699
+ await fs.access(filePath);
700
+ if (_debug) {
701
+ console.log(chalk.green(`[SWITE CSS] CSS file exists: ${filePath}`));
702
+ }
703
+ existingCssFiles.push(url);
704
+ } catch {
705
+ // CSS file doesn't exist, skip it
706
+ // This allows different apps/websites to have different CSS files
707
+ if (_debug) {
708
+ console.log(chalk.yellow(`[SWITE CSS] ⚠️ CSS file NOT found: ${filePath}, skipping`));
709
+ }
710
+ }
711
+ }
712
+
713
+ // Only inject CSS files that actually exist
714
+ if (_debug) {
715
+ console.log(chalk.blue(`[SWITE CSS] ${existingCssFiles.length} CSS file(s) exist out of ${cssArray.length} found`));
716
+ }
717
+ if (existingCssFiles.length === 0) {
718
+ if (_debug) {
719
+ console.log(chalk.yellow(`[SWITE CSS] ⚠️ No CSS files exist, skipping injection`));
720
+ }
721
+ } else if (existingCssFiles.length > 0) {
722
+ const cssLinks = existingCssFiles
723
+ .map(url => ` <link rel="stylesheet" href="${url}">`)
724
+ .join("\n");
725
+
726
+ // Check if CSS links are already in HTML (to avoid duplicates)
727
+ const alreadyInjected = existingCssFiles.some(url =>
728
+ html.includes(`href="${url}"`) || html.includes(`href='${url}'`)
729
+ );
730
+
731
+ if (!alreadyInjected) {
732
+ // Inject CSS links before </head> - MUST happen before import map injection
733
+ const beforeReplace = html;
734
+ html = html.replace(/\s*<\/head>/i, `${cssLinks}\n </head>`);
735
+ if (html === beforeReplace) {
736
+ if (_debug) {
737
+ console.warn(chalk.yellow("[SWITE] Failed to inject CSS links - </head> not found"));
738
+ }
739
+ } else {
740
+ if (_debug) {
741
+ console.log(chalk.green(`[SWITE] ✅ Injected ${existingCssFiles.length} CSS link(s): ${existingCssFiles.join(", ")}`));
742
+ }
743
+ }
744
+ } else {
745
+ if (_debug) {
746
+ console.log(chalk.blue(`[SWITE CSS] CSS links already in HTML, skipping injection`));
747
+ }
748
+ }
749
+ }
750
+ }
751
+ } catch (error) {
752
+ // If entry point doesn't exist or can't be read, continue without CSS injection
753
+ // Silently continue - CSS injection is optional
754
+ if (_debug) {
755
+ console.log(chalk.yellow(`[SWITE CSS] Could not extract CSS imports: ${error instanceof Error ? error.message : String(error)}`));
756
+ }
757
+ }
758
+
759
+ // Add/merge import map to help browser resolve bare module specifiers.
760
+ // If an importmap already exists in HTML, merge .swite/import-map.json entries
761
+ // into it — existing HTML entries take priority (never overwrite manual entries).
762
+ const cachedMapPath = path.join(config.root, ".swite", "import-map.json");
763
+ let switeImports: Record<string, string> = {};
764
+ try {
765
+ const raw = await fs.readFile(cachedMapPath, "utf-8");
766
+ const parsed = JSON.parse(raw);
767
+ if (parsed?.imports && typeof parsed.imports === "object") {
768
+ switeImports = parsed.imports;
769
+ }
770
+ } catch {
771
+ // no cached map — nothing to merge
772
+ }
773
+
774
+ if (!html.includes('type="importmap"')) {
775
+ // No importmap at all — inject one from .swite/import-map.json
776
+ const importMap = `\n <script type="importmap">\n ${JSON.stringify({ imports: switeImports }, null, 2).replace(/\n/g, "\n ")}\n </script>`;
777
+ const beforeReplace = html;
778
+ html = html.replace(/\s*<\/head>/i, `${importMap}\n </head>`);
779
+ if (html === beforeReplace) {
780
+ if (_debug) {
781
+ console.warn("[SWITE] Failed to add import map - </head> not found or already replaced");
782
+ }
783
+ } else {
784
+ if (_debug) {
785
+ console.log(`[SWITE] Added import map with ${Object.keys(switeImports).length} entries`);
786
+ }
787
+ }
788
+ } else {
789
+ // Importmap already in HTML — merge swite entries without overwriting existing ones
790
+ if (_debug) {
791
+ console.log("[SWITE] Import map already exists in HTML — merging swite entries");
792
+ }
793
+ if (Object.keys(switeImports).length > 0) {
794
+ html = html.replace(
795
+ /(<script\s+type=["']importmap["'][^>]*>)\s*([\s\S]*?)(\s*<\/script>)/i,
796
+ (_match, open, body, close) => {
797
+ try {
798
+ const existing = JSON.parse(body.trim());
799
+ const existingImports: Record<string, string> = existing?.imports ?? {};
800
+ // Swite entries fill gaps; existing HTML entries win
801
+ const merged = { ...switeImports, ...existingImports };
802
+ return `${open}\n ${JSON.stringify({ imports: merged }, null, 2).replace(/\n/g, "\n ")}${close}`;
803
+ } catch {
804
+ return _match; // parse failed — leave importmap untouched
805
+ }
806
+ }
807
+ );
808
+ }
809
+ }
810
+
811
+ res.send(html);
812
+ });
813
+ }