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