@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,261 @@
1
+ /**
2
+ * Test CSS extraction and injection into HTML
3
+ */
4
+
5
+ import { test, describe } from "node:test";
6
+ import { strict as assert } from "node:assert";
7
+ import path from "node:path";
8
+ import { promises as fs } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const testDir = path.join(__dirname, "..", "..", "..", "test-css-extraction");
13
+
14
+ describe("CSS Extraction", () => {
15
+ test("should extract CSS imports from entry point file", async () => {
16
+ // Create test directory
17
+ try {
18
+ await fs.mkdir(testDir, { recursive: true });
19
+ } catch (e) {
20
+ // Directory might already exist
21
+ }
22
+
23
+ try {
24
+ // Create test files
25
+ const srcDir = path.join(testDir, "src");
26
+ await fs.mkdir(srcDir, { recursive: true });
27
+ await fs.mkdir(path.join(srcDir, "styles"), { recursive: true });
28
+
29
+ // Create index.ui that imports App.uix
30
+ const indexUi = `import { SwissApp } from '@swissjs/core'
31
+ import { App } from './App.uix'
32
+
33
+ SwissApp.mount(App, '#root')`;
34
+
35
+ // Create App.uix with CSS imports
36
+ const appUix = `import { SwissComponent } from '@swissjs/core'
37
+ import './styles/globals.css'
38
+ import './styles/cyber-theme.css'
39
+
40
+ export class App extends SwissComponent {
41
+ render() {
42
+ return <div>Test</div>
43
+ }
44
+ }`;
45
+
46
+ // Create CSS files
47
+ const globalsCss = `body { margin: 0; }`;
48
+ const cyberCss = `:root { --color: #00ff00; }`;
49
+
50
+ await fs.writeFile(path.join(srcDir, "index.ui"), indexUi);
51
+ await fs.writeFile(path.join(srcDir, "App.uix"), appUix);
52
+ await fs.writeFile(path.join(srcDir, "styles", "globals.css"), globalsCss);
53
+ await fs.writeFile(path.join(srcDir, "styles", "cyber-theme.css"), cyberCss);
54
+
55
+ // Test CSS extraction logic
56
+ const entryPointPath = path.join(srcDir, "index.ui");
57
+ const entryPointContent = await fs.readFile(entryPointPath, "utf-8");
58
+
59
+ // Extract CSS imports using regex
60
+ const cssImportPattern = /import\s+['"](.*?\.css)['"];?/g;
61
+ const cssImports = new Set<string>();
62
+ let match;
63
+
64
+ // Check entry point
65
+ while ((match = cssImportPattern.exec(entryPointContent)) !== null) {
66
+ cssImports.add(match[1]);
67
+ }
68
+
69
+ // Also check imported files (like App.uix) for CSS imports
70
+ const importPattern = /import\s+.*?from\s+['"](.*?)['"];?/g;
71
+ const importedFiles: string[] = [];
72
+ let importMatch;
73
+ cssImportPattern.lastIndex = 0; // Reset regex
74
+ while ((importMatch = importPattern.exec(entryPointContent)) !== null) {
75
+ const importPath = importMatch[1];
76
+ // Skip node_modules and absolute imports
77
+ if (!importPath.startsWith("@") && !importPath.startsWith("/") && !importPath.startsWith(".")) {
78
+ continue;
79
+ }
80
+ // Resolve relative imports
81
+ if (importPath.startsWith(".")) {
82
+ importedFiles.push(importPath);
83
+ }
84
+ }
85
+
86
+ // Check imported files for CSS
87
+ for (const importedFile of importedFiles) {
88
+ try {
89
+ const importedFilePath = path.resolve(path.dirname(entryPointPath), importedFile);
90
+ // Try different extensions
91
+ const extensions = [".uix", ".ui", ".ts", ".js"];
92
+ let found = false;
93
+ for (const ext of extensions) {
94
+ const testPath = importedFilePath.endsWith(ext) ? importedFilePath : importedFilePath + ext;
95
+ try {
96
+ const importedContent = await fs.readFile(testPath, "utf-8");
97
+ found = true;
98
+ // Extract CSS imports from this file
99
+ cssImportPattern.lastIndex = 0; // Reset regex
100
+ let cssMatch2;
101
+ while ((cssMatch2 = cssImportPattern.exec(importedContent)) !== null) {
102
+ // Resolve relative CSS path
103
+ const cssPath = cssMatch2[1];
104
+ if (cssPath.startsWith(".")) {
105
+ const resolvedCssPath = path.resolve(path.dirname(testPath), cssPath);
106
+ const relativeCssPath = path.relative(srcDir, resolvedCssPath);
107
+ const normalizedPath = relativeCssPath.replace(/\\/g, "/");
108
+ cssImports.add(normalizedPath);
109
+ } else {
110
+ cssImports.add(cssPath);
111
+ }
112
+ }
113
+ break;
114
+ } catch (err) {
115
+ // File doesn't exist with this extension, try next
116
+ }
117
+ }
118
+ } catch (error) {
119
+ // Could not read imported file, skip
120
+ }
121
+ }
122
+
123
+ // Verify CSS imports were found
124
+ assert.strictEqual(cssImports.size, 2);
125
+ assert.strictEqual(cssImports.has("styles/globals.css"), true);
126
+ assert.strictEqual(cssImports.has("styles/cyber-theme.css"), true);
127
+ } finally {
128
+ // Clean up test directory
129
+ try {
130
+ await fs.rm(testDir, { recursive: true, force: true });
131
+ } catch (e) {
132
+ // Ignore cleanup errors
133
+ }
134
+ }
135
+ });
136
+
137
+ test("should inject CSS links into HTML", async () => {
138
+ const html = `<!DOCTYPE html>
139
+ <html lang="en">
140
+ <head>
141
+ <meta charset="UTF-8">
142
+ <title>Test</title>
143
+ </head>
144
+ <body>
145
+ <div id="root"></div>
146
+ </body>
147
+ </html>`;
148
+
149
+ const cssImports = ["styles/globals.css", "styles/cyber-theme.css"];
150
+ const cssLinks = cssImports.map(cssPath => {
151
+ const url = cssPath.startsWith("/") ? cssPath : `/src/${cssPath}`;
152
+ return ` <link rel="stylesheet" href="${url}">`;
153
+ }).join("\n");
154
+
155
+ const beforeReplace = html;
156
+ const modifiedHtml = html.replace(/\s*<\/head>/i, `${cssLinks}\n </head>`);
157
+
158
+ assert.notStrictEqual(modifiedHtml, beforeReplace);
159
+ assert.ok(modifiedHtml.includes('<link rel="stylesheet" href="/src/styles/globals.css">'));
160
+ assert.ok(modifiedHtml.includes('<link rel="stylesheet" href="/src/styles/cyber-theme.css">'));
161
+ });
162
+
163
+ test("should handle CSS imports in nested files", async () => {
164
+ // Create test directory
165
+ try {
166
+ await fs.mkdir(testDir, { recursive: true });
167
+ } catch (e) {
168
+ // Directory might already exist
169
+ }
170
+
171
+ try {
172
+ // Create test files with nested imports
173
+ const srcDir = path.join(testDir, "src");
174
+ await fs.mkdir(srcDir, { recursive: true });
175
+ await fs.mkdir(path.join(srcDir, "styles"), { recursive: true });
176
+ await fs.mkdir(path.join(srcDir, "components"), { recursive: true });
177
+
178
+ const indexUi = `import { SwissApp } from '@swissjs/core'
179
+ import { App } from './App.uix'
180
+
181
+ SwissApp.mount(App, '#root')`;
182
+
183
+ const appUix = `import { SwissComponent } from '@swissjs/core'
184
+ import { Header } from './components/Header.uix'
185
+
186
+ export class App extends SwissComponent {
187
+ render() {
188
+ return <div><Header /></div>
189
+ }
190
+ }`;
191
+
192
+ const headerUix = `import { SwissComponent } from '@swissjs/core'
193
+ import '../styles/globals.css'
194
+ import '../styles/cyber-theme.css'
195
+
196
+ export class Header extends SwissComponent {
197
+ render() {
198
+ return <header>Header</header>
199
+ }
200
+ }`;
201
+
202
+ const globalsCss = `body { margin: 0; }`;
203
+ const cyberCss = `:root { --color: #00ff00; }`;
204
+
205
+ await fs.writeFile(path.join(srcDir, "index.ui"), indexUi);
206
+ await fs.writeFile(path.join(srcDir, "App.uix"), appUix);
207
+ await fs.writeFile(path.join(srcDir, "components", "Header.uix"), headerUix);
208
+ await fs.writeFile(path.join(srcDir, "styles", "globals.css"), globalsCss);
209
+ await fs.writeFile(path.join(srcDir, "styles", "cyber-theme.css"), cyberCss);
210
+
211
+ // Test extraction (simplified - would need recursive traversal in real implementation)
212
+ const entryPointPath = path.join(srcDir, "index.ui");
213
+ const entryPointContent = await fs.readFile(entryPointPath, "utf-8");
214
+
215
+ const cssImportPattern = /import\s+['"](.*?\.css)['"];?/g;
216
+ const cssImports = new Set<string>();
217
+
218
+ // Check entry point
219
+ let match;
220
+ while ((match = cssImportPattern.exec(entryPointContent)) !== null) {
221
+ cssImports.add(match[1]);
222
+ }
223
+
224
+ // Check App.uix
225
+ const appPath = path.join(srcDir, "App.uix");
226
+ const appContent = await fs.readFile(appPath, "utf-8");
227
+ cssImportPattern.lastIndex = 0;
228
+ while ((match = cssImportPattern.exec(appContent)) !== null) {
229
+ cssImports.add(match[1]);
230
+ }
231
+
232
+ // Check Header.uix
233
+ const headerPath = path.join(srcDir, "components", "Header.uix");
234
+ const headerContent = await fs.readFile(headerPath, "utf-8");
235
+ cssImportPattern.lastIndex = 0;
236
+ while ((match = cssImportPattern.exec(headerContent)) !== null) {
237
+ const cssPath = match[1];
238
+ if (cssPath.startsWith("..")) {
239
+ // Resolve relative path from Header.uix location
240
+ const resolvedCssPath = path.resolve(path.dirname(headerPath), cssPath);
241
+ const relativeCssPath = path.relative(srcDir, resolvedCssPath);
242
+ const normalizedPath = relativeCssPath.replace(/\\/g, "/");
243
+ cssImports.add(normalizedPath);
244
+ } else {
245
+ cssImports.add(cssPath);
246
+ }
247
+ }
248
+
249
+ assert.strictEqual(cssImports.size, 2);
250
+ assert.strictEqual(cssImports.has("styles/globals.css"), true);
251
+ assert.strictEqual(cssImports.has("styles/cyber-theme.css"), true);
252
+ } finally {
253
+ // Clean up test directory
254
+ try {
255
+ await fs.rm(testDir, { recursive: true, force: true });
256
+ } catch (e) {
257
+ // Ignore cleanup errors
258
+ }
259
+ }
260
+ });
261
+ });
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Integration test for CSS injection in HTML middleware
3
+ */
4
+
5
+ import { test, describe } from "node:test";
6
+ import { strict as assert } from "node:assert";
7
+ import path from "node:path";
8
+ import { promises as fs } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const testDir = path.join(__dirname, "..", "..", "..", "test-css-injection");
13
+
14
+ // Simulate the CSS extraction logic from static-files.ts
15
+ async function extractAndInjectCSS(
16
+ configRoot: string,
17
+ html: string
18
+ ): Promise<string> {
19
+ const entryPointPath = path.join(configRoot, "src", "index.ui");
20
+
21
+ try {
22
+ const entryPointContent = await fs.readFile(entryPointPath, "utf-8");
23
+
24
+ // Extract CSS imports using regex
25
+ const cssImportPattern = /import\s+['"](.*?\.css)['"];?/g;
26
+ const cssImports = new Set<string>();
27
+ let match;
28
+
29
+ // Check entry point
30
+ while ((match = cssImportPattern.exec(entryPointContent)) !== null) {
31
+ cssImports.add(match[1]);
32
+ }
33
+
34
+ // Also check imported files (like App.uix) for CSS imports
35
+ const importPattern = /import\s+.*?from\s+['"](.*?)['"];?/g;
36
+ const importedFiles: string[] = [];
37
+ let importMatch;
38
+ cssImportPattern.lastIndex = 0; // Reset regex
39
+ while ((importMatch = importPattern.exec(entryPointContent)) !== null) {
40
+ const importPath = importMatch[1];
41
+ // Skip node_modules and absolute imports
42
+ if (!importPath.startsWith("@") && !importPath.startsWith("/") && !importPath.startsWith(".")) {
43
+ continue;
44
+ }
45
+ // Resolve relative imports
46
+ if (importPath.startsWith(".")) {
47
+ importedFiles.push(importPath);
48
+ }
49
+ }
50
+
51
+ // Check imported files for CSS
52
+ for (const importedFile of importedFiles) {
53
+ try {
54
+ const importedFilePath = path.resolve(path.dirname(entryPointPath), importedFile);
55
+ // Try different extensions
56
+ const extensions = [".uix", ".ui", ".ts", ".js"];
57
+ let found = false;
58
+ for (const ext of extensions) {
59
+ const testPath = importedFilePath.endsWith(ext) ? importedFilePath : importedFilePath + ext;
60
+ try {
61
+ const importedContent = await fs.readFile(testPath, "utf-8");
62
+ found = true;
63
+ // Extract CSS imports from this file
64
+ cssImportPattern.lastIndex = 0; // Reset regex
65
+ let cssMatch2;
66
+ while ((cssMatch2 = cssImportPattern.exec(importedContent)) !== null) {
67
+ // Resolve relative CSS path
68
+ const cssPath = cssMatch2[1];
69
+ if (cssPath.startsWith(".")) {
70
+ const resolvedCssPath = path.resolve(path.dirname(testPath), cssPath);
71
+ const relativeCssPath = path.relative(path.join(configRoot, "src"), resolvedCssPath);
72
+ const normalizedPath = relativeCssPath.replace(/\\/g, "/");
73
+ cssImports.add(normalizedPath);
74
+ } else {
75
+ cssImports.add(cssPath);
76
+ }
77
+ }
78
+ break;
79
+ } catch (err) {
80
+ // File doesn't exist with this extension, try next
81
+ }
82
+ }
83
+ } catch (error) {
84
+ // Could not read imported file, skip
85
+ }
86
+ }
87
+
88
+ if (cssImports.size > 0) {
89
+ const cssArray = Array.from(cssImports);
90
+
91
+ // Convert relative paths to absolute URLs
92
+ const cssLinks = cssArray.map(cssPath => {
93
+ // If it's already absolute (starts with /), use as-is
94
+ // Otherwise, make it relative to /src
95
+ const url = cssPath.startsWith("/") ? cssPath : `/src/${cssPath}`;
96
+ return ` <link rel="stylesheet" href="${url}">`;
97
+ }).join("\n");
98
+
99
+ // Inject CSS links before </head>
100
+ const beforeReplace = html;
101
+ html = html.replace(/\s*<\/head>/i, `${cssLinks}\n </head>`);
102
+ if (html === beforeReplace) {
103
+ throw new Error("Failed to inject CSS links - </head> not found");
104
+ }
105
+ }
106
+
107
+ return html;
108
+ } catch (error) {
109
+ // If entry point doesn't exist or can't be read, return HTML as-is
110
+ return html;
111
+ }
112
+ }
113
+
114
+ describe("CSS Injection Integration", () => {
115
+ test("should extract and inject CSS from real project structure", async () => {
116
+ // Create test directory structure matching pos-site
117
+ const srcDir = path.join(testDir, "src");
118
+ const stylesDir = path.join(srcDir, "styles");
119
+ const publicDir = path.join(testDir, "public");
120
+
121
+ await fs.mkdir(srcDir, { recursive: true });
122
+ await fs.mkdir(stylesDir, { recursive: true });
123
+ await fs.mkdir(publicDir, { recursive: true });
124
+
125
+ try {
126
+ // Create index.ui (entry point)
127
+ const indexUi = `import { SwissApp } from '@swissjs/core'
128
+ import { App } from './App.uix'
129
+
130
+ SwissApp.mount(App, '#root')`;
131
+
132
+ // Create App.uix with CSS imports (matching pos-site structure)
133
+ const appUix = `import { SwissComponent } from '@swissjs/core'
134
+ import './styles/globals.css'
135
+ import './styles/cyber-theme.css'
136
+
137
+ export class App extends SwissComponent {
138
+ render() {
139
+ return <div>Test App</div>
140
+ }
141
+ }`;
142
+
143
+ // Create CSS files
144
+ const globalsCss = `body { margin: 0; padding: 0; }`;
145
+ const cyberCss = `:root { --cyber-color: #00ff00; }`;
146
+
147
+ // Create HTML file
148
+ const html = `<!DOCTYPE html>
149
+ <html lang="en">
150
+ <head>
151
+ <meta charset="UTF-8">
152
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
153
+ <title>Test</title>
154
+ </head>
155
+ <body>
156
+ <div id="root"></div>
157
+ </body>
158
+ </html>`;
159
+
160
+ await fs.writeFile(path.join(srcDir, "index.ui"), indexUi);
161
+ await fs.writeFile(path.join(srcDir, "App.uix"), appUix);
162
+ await fs.writeFile(path.join(stylesDir, "globals.css"), globalsCss);
163
+ await fs.writeFile(path.join(stylesDir, "cyber-theme.css"), cyberCss);
164
+ await fs.writeFile(path.join(publicDir, "index.html"), html);
165
+
166
+ // Test the extraction and injection
167
+ const resultHtml = await extractAndInjectCSS(testDir, html);
168
+
169
+ // Verify CSS links were injected
170
+ assert.ok(resultHtml.includes('<link rel="stylesheet" href="/src/styles/globals.css">'));
171
+ assert.ok(resultHtml.includes('<link rel="stylesheet" href="/src/styles/cyber-theme.css">'));
172
+ assert.ok(resultHtml.includes('</head>'));
173
+
174
+ // Verify HTML structure is intact
175
+ assert.ok(resultHtml.includes('<div id="root"></div>'));
176
+ assert.ok(resultHtml.includes('<!DOCTYPE html>'));
177
+
178
+ console.log("\n=== HTML WITH CSS INJECTED ===");
179
+ console.log(resultHtml);
180
+ } finally {
181
+ // Clean up
182
+ try {
183
+ await fs.rm(testDir, { recursive: true, force: true });
184
+ } catch (e) {
185
+ // Ignore cleanup errors
186
+ }
187
+ }
188
+ });
189
+
190
+ test("should handle missing entry point gracefully", async () => {
191
+ const html = `<!DOCTYPE html>
192
+ <html>
193
+ <head>
194
+ <title>Test</title>
195
+ </head>
196
+ <body></body>
197
+ </html>`;
198
+
199
+ // Should not throw even if entry point doesn't exist
200
+ const resultHtml = await extractAndInjectCSS("/nonexistent/path", html);
201
+
202
+ // Should return HTML unchanged
203
+ assert.strictEqual(resultHtml, html);
204
+ });
205
+
206
+ test("should handle entry point without CSS imports", async () => {
207
+ const srcDir = path.join(testDir, "src");
208
+ await fs.mkdir(srcDir, { recursive: true });
209
+
210
+ try {
211
+ const indexUi = `import { SwissApp } from '@swissjs/core'
212
+ import { App } from './App.uix'
213
+
214
+ SwissApp.mount(App, '#root')`;
215
+
216
+ const appUix = `import { SwissComponent } from '@swissjs/core'
217
+
218
+ export class App extends SwissComponent {
219
+ render() {
220
+ return <div>No CSS</div>
221
+ }
222
+ }`;
223
+
224
+ const html = `<!DOCTYPE html>
225
+ <html>
226
+ <head>
227
+ <title>Test</title>
228
+ </head>
229
+ <body></body>
230
+ </html>`;
231
+
232
+ await fs.writeFile(path.join(srcDir, "index.ui"), indexUi);
233
+ await fs.writeFile(path.join(srcDir, "App.uix"), appUix);
234
+
235
+ const resultHtml = await extractAndInjectCSS(testDir, html);
236
+
237
+ // Should return HTML unchanged (no CSS to inject)
238
+ assert.strictEqual(resultHtml, html);
239
+ } finally {
240
+ try {
241
+ await fs.rm(testDir, { recursive: true, force: true });
242
+ } catch (e) {
243
+ // Ignore cleanup errors
244
+ }
245
+ }
246
+ });
247
+ });
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Test CSS extraction middleware function directly
3
+ */
4
+
5
+ import { test, describe } from "node:test";
6
+ import { strict as assert } from "node:assert";
7
+ import path from "node:path";
8
+ import { promises as fs } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import type { Request, Response } from "express";
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const testDir = path.join(__dirname, "..", "..", "..", "test-css-middleware");
14
+
15
+ // Extract the CSS extraction logic as a testable function
16
+ async function extractCSSImports(configRoot: string): Promise<string[]> {
17
+ const entryPointPath = path.join(configRoot, "src", "index.ui");
18
+
19
+ try {
20
+ const entryPointContent = await fs.readFile(entryPointPath, "utf-8");
21
+
22
+ const cssImportPattern = /import\s+['"](.*?\.css)['"];?/g;
23
+ const cssImports = new Set<string>();
24
+ let match;
25
+
26
+ // Check entry point
27
+ while ((match = cssImportPattern.exec(entryPointContent)) !== null) {
28
+ cssImports.add(match[1]);
29
+ }
30
+
31
+ // Check imported files
32
+ const importPattern = /import\s+.*?from\s+['"](.*?)['"];?/g;
33
+ const importedFiles: string[] = [];
34
+ let importMatch;
35
+ cssImportPattern.lastIndex = 0;
36
+ while ((importMatch = importPattern.exec(entryPointContent)) !== null) {
37
+ const importPath = importMatch[1];
38
+ if (!importPath.startsWith("@") && !importPath.startsWith("/") && !importPath.startsWith(".")) {
39
+ continue;
40
+ }
41
+ if (importPath.startsWith(".")) {
42
+ importedFiles.push(importPath);
43
+ }
44
+ }
45
+
46
+ // Check imported files for CSS
47
+ for (const importedFile of importedFiles) {
48
+ try {
49
+ const importedFilePath = path.resolve(path.dirname(entryPointPath), importedFile);
50
+ const extensions = [".uix", ".ui", ".ts", ".js"];
51
+ let found = false;
52
+ for (const ext of extensions) {
53
+ const testPath = importedFilePath.endsWith(ext) ? importedFilePath : importedFilePath + ext;
54
+ try {
55
+ const importedContent = await fs.readFile(testPath, "utf-8");
56
+ found = true;
57
+ cssImportPattern.lastIndex = 0;
58
+ let cssMatch2;
59
+ while ((cssMatch2 = cssImportPattern.exec(importedContent)) !== null) {
60
+ const cssPath = cssMatch2[1];
61
+ if (cssPath.startsWith(".")) {
62
+ const resolvedCssPath = path.resolve(path.dirname(testPath), cssPath);
63
+ const relativeCssPath = path.relative(path.join(configRoot, "src"), resolvedCssPath);
64
+ const normalizedPath = relativeCssPath.replace(/\\/g, "/");
65
+ cssImports.add(normalizedPath);
66
+ } else {
67
+ cssImports.add(cssPath);
68
+ }
69
+ }
70
+ break;
71
+ } catch (err) {
72
+ // File doesn't exist with this extension, try next
73
+ }
74
+ }
75
+ } catch (error) {
76
+ // Could not read imported file, skip
77
+ }
78
+ }
79
+
80
+ return Array.from(cssImports);
81
+ } catch (error) {
82
+ // If entry point doesn't exist or can't be read, return empty array
83
+ return [];
84
+ }
85
+ }
86
+
87
+ function injectCSSLinks(html: string, cssImports: string[]): string {
88
+ if (cssImports.length === 0) {
89
+ return html;
90
+ }
91
+
92
+ const cssLinks = cssImports.map(cssPath => {
93
+ const url = cssPath.startsWith("/") ? cssPath : `/src/${cssPath}`;
94
+ return ` <link rel="stylesheet" href="${url}">`;
95
+ }).join("\n");
96
+
97
+ const beforeReplace = html;
98
+ html = html.replace(/\s*<\/head>/i, `${cssLinks}\n </head>`);
99
+ if (html === beforeReplace) {
100
+ throw new Error("Failed to inject CSS links - </head> not found");
101
+ }
102
+
103
+ return html;
104
+ }
105
+
106
+ describe("CSS Middleware Function", () => {
107
+ test("should extract CSS from pos-site structure", async () => {
108
+ const srcDir = path.join(testDir, "src");
109
+ const stylesDir = path.join(srcDir, "styles");
110
+
111
+ await fs.mkdir(srcDir, { recursive: true });
112
+ await fs.mkdir(stylesDir, { recursive: true });
113
+
114
+ try {
115
+ // Create files matching pos-site structure
116
+ const indexUi = `import { SwissApp } from '@swissjs/core'
117
+ import { App } from './App.uix'
118
+
119
+ SwissApp.mount(App, '#root')`;
120
+
121
+ const appUix = `import { SwissComponent } from '@swissjs/core'
122
+ import './styles/globals.css'
123
+ import './styles/cyber-theme.css'
124
+
125
+ export class App extends SwissComponent {
126
+ render() {
127
+ return <div>App</div>
128
+ }
129
+ }`;
130
+
131
+ await fs.writeFile(path.join(srcDir, "index.ui"), indexUi);
132
+ await fs.writeFile(path.join(srcDir, "App.uix"), appUix);
133
+ await fs.writeFile(path.join(stylesDir, "globals.css"), "body { margin: 0; }");
134
+ await fs.writeFile(path.join(stylesDir, "cyber-theme.css"), ":root { --color: #00ff00; }");
135
+
136
+ // Test extraction
137
+ const cssImports = await extractCSSImports(testDir);
138
+
139
+ assert.strictEqual(cssImports.length, 2);
140
+ assert.ok(cssImports.includes("styles/globals.css"));
141
+ assert.ok(cssImports.includes("styles/cyber-theme.css"));
142
+
143
+ // Test injection
144
+ const html = `<!DOCTYPE html>
145
+ <html>
146
+ <head>
147
+ <title>Test</title>
148
+ </head>
149
+ <body></body>
150
+ </html>`;
151
+
152
+ const resultHtml = injectCSSLinks(html, cssImports);
153
+
154
+ assert.ok(resultHtml.includes('<link rel="stylesheet" href="/src/styles/globals.css">'));
155
+ assert.ok(resultHtml.includes('<link rel="stylesheet" href="/src/styles/cyber-theme.css">'));
156
+
157
+ console.log("\n=== EXTRACTED CSS IMPORTS ===");
158
+ console.log(cssImports);
159
+ console.log("\n=== HTML WITH CSS ===");
160
+ console.log(resultHtml);
161
+ } finally {
162
+ try {
163
+ await fs.rm(testDir, { recursive: true, force: true });
164
+ } catch (e) {
165
+ // Ignore cleanup errors
166
+ }
167
+ }
168
+ });
169
+
170
+ test("should match actual pos-site file structure", async () => {
171
+ // Test with actual pos-site structure
172
+ const posSiteRoot = path.join(__dirname, "..", "..", "..", "websites", "pos-site");
173
+
174
+ try {
175
+ const cssImports = await extractCSSImports(posSiteRoot);
176
+
177
+ console.log("\n=== POS SITE CSS IMPORTS ===");
178
+ console.log(`Found ${cssImports.length} CSS import(s):`, cssImports);
179
+
180
+ // Should find the CSS imports from App.uix
181
+ assert.ok(cssImports.length >= 2, "Should find at least 2 CSS imports");
182
+ assert.ok(
183
+ cssImports.some(css => css.includes("globals.css") || css.includes("cyber-theme.css")),
184
+ "Should find globals.css or cyber-theme.css"
185
+ );
186
+ } catch (error) {
187
+ // If pos-site doesn't exist, skip this test
188
+ console.log("Skipping pos-site test - directory not found");
189
+ }
190
+ });
191
+ });