@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.
- package/.changeset/config.json +11 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/publish.yml +50 -0
- package/.github/workflows/release.yml +53 -0
- package/BUILD_ANALYSIS.md +89 -0
- package/BUILD_STRATEGY.md +75 -0
- package/CHANGELOG.md +53 -0
- package/DIRECTIVE.md +488 -0
- package/__tests__/css-extraction.test.ts +261 -0
- package/__tests__/css-injection-integration.test.ts +247 -0
- package/__tests__/css-middleware.test.ts +191 -0
- package/__tests__/import-rewriter-bug.test.ts +135 -0
- package/dist/builder.d.ts +36 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +772 -0
- package/dist/cache/compilation-cache.d.ts +33 -0
- package/dist/cache/compilation-cache.d.ts.map +1 -0
- package/dist/cache/compilation-cache.js +130 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +85 -0
- package/dist/config-loader.d.ts +8 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +40 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +7 -0
- package/dist/dev/pythonDevManager.d.ts +12 -0
- package/dist/dev/pythonDevManager.d.ts.map +1 -0
- package/dist/dev/pythonDevManager.js +85 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +112 -0
- package/dist/handlers/base-handler.d.ts +21 -0
- package/dist/handlers/base-handler.d.ts.map +1 -0
- package/dist/handlers/base-handler.js +38 -0
- package/dist/handlers/js-handler.d.ts +10 -0
- package/dist/handlers/js-handler.d.ts.map +1 -0
- package/dist/handlers/js-handler.js +87 -0
- package/dist/handlers/mjs-handler.d.ts +8 -0
- package/dist/handlers/mjs-handler.d.ts.map +1 -0
- package/dist/handlers/mjs-handler.js +44 -0
- package/dist/handlers/node-module-handler.d.ts +16 -0
- package/dist/handlers/node-module-handler.d.ts.map +1 -0
- package/dist/handlers/node-module-handler.js +267 -0
- package/dist/handlers/ts-handler.d.ts +11 -0
- package/dist/handlers/ts-handler.d.ts.map +1 -0
- package/dist/handlers/ts-handler.js +120 -0
- package/dist/handlers/ui-handler.d.ts +12 -0
- package/dist/handlers/ui-handler.d.ts.map +1 -0
- package/dist/handlers/ui-handler.js +182 -0
- package/dist/handlers/uix-handler.d.ts +12 -0
- package/dist/handlers/uix-handler.d.ts.map +1 -0
- package/dist/handlers/uix-handler.js +135 -0
- package/dist/hmr.d.ts +20 -0
- package/dist/hmr.d.ts.map +1 -0
- package/dist/hmr.js +265 -0
- package/dist/import-rewriter.d.ts +3 -0
- package/dist/import-rewriter.d.ts.map +1 -0
- package/dist/import-rewriter.js +351 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/middleware/hmr-routes.d.ts +12 -0
- package/dist/middleware/hmr-routes.d.ts.map +1 -0
- package/dist/middleware/hmr-routes.js +97 -0
- package/dist/middleware/middleware-setup.d.ts +23 -0
- package/dist/middleware/middleware-setup.d.ts.map +1 -0
- package/dist/middleware/middleware-setup.js +596 -0
- package/dist/middleware/static-files.d.ts +15 -0
- package/dist/middleware/static-files.d.ts.map +1 -0
- package/dist/middleware/static-files.js +585 -0
- package/dist/proxy/SwiteProxyError.d.ts +6 -0
- package/dist/proxy/SwiteProxyError.d.ts.map +1 -0
- package/dist/proxy/SwiteProxyError.js +9 -0
- package/dist/proxy/proxyToPython.d.ts +28 -0
- package/dist/proxy/proxyToPython.d.ts.map +1 -0
- package/dist/proxy/proxyToPython.js +66 -0
- package/dist/resolver/bare-import-resolver.d.ts +9 -0
- package/dist/resolver/bare-import-resolver.d.ts.map +1 -0
- package/dist/resolver/bare-import-resolver.js +363 -0
- package/dist/resolver/symlink-registry.d.ts +13 -0
- package/dist/resolver/symlink-registry.d.ts.map +1 -0
- package/dist/resolver/symlink-registry.js +98 -0
- package/dist/resolver/url-resolver.d.ts +11 -0
- package/dist/resolver/url-resolver.d.ts.map +1 -0
- package/dist/resolver/url-resolver.js +268 -0
- package/dist/resolver/workspace-package-resolver.d.ts +10 -0
- package/dist/resolver/workspace-package-resolver.d.ts.map +1 -0
- package/dist/resolver/workspace-package-resolver.js +185 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +191 -0
- package/dist/router/file-router.d.ts +19 -0
- package/dist/router/file-router.d.ts.map +1 -0
- package/dist/router/file-router.js +114 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +122 -0
- package/dist/utils/cdn-fallback.d.ts +14 -0
- package/dist/utils/cdn-fallback.d.ts.map +1 -0
- package/dist/utils/cdn-fallback.js +36 -0
- package/dist/utils/file-path-resolver.d.ts +9 -0
- package/dist/utils/file-path-resolver.d.ts.map +1 -0
- package/dist/utils/file-path-resolver.js +187 -0
- package/dist/utils/generate-import-map-cli.d.ts +3 -0
- package/dist/utils/generate-import-map-cli.d.ts.map +1 -0
- package/dist/utils/generate-import-map-cli.js +32 -0
- package/dist/utils/generate-import-map.d.ts +21 -0
- package/dist/utils/generate-import-map.d.ts.map +1 -0
- package/dist/utils/generate-import-map.js +119 -0
- package/dist/utils/package-finder.d.ts +24 -0
- package/dist/utils/package-finder.d.ts.map +1 -0
- package/dist/utils/package-finder.js +161 -0
- package/dist/utils/package-registry.d.ts +36 -0
- package/dist/utils/package-registry.d.ts.map +1 -0
- package/dist/utils/package-registry.js +159 -0
- package/dist/utils/workspace.d.ts +6 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +65 -0
- package/docs/IMPORT_REWRITING.md +164 -0
- package/docs/IMPORT_REWRITING_TROUBLESHOOTING.md +139 -0
- package/docs/PATH_RESOLUTION_GUIDE.md +221 -0
- package/package.json +49 -0
- package/src/adapters/proxy/SwiteProxyError.ts +12 -0
- package/src/adapters/proxy/proxyToPython.ts +88 -0
- package/src/build-engine/builder.ts +960 -0
- package/src/cli.ts +109 -0
- package/src/config/config-loader.ts +46 -0
- package/src/config/config.ts +34 -0
- package/src/config/env.ts +98 -0
- package/src/dev-engine/handlers/base-handler.ts +68 -0
- package/src/dev-engine/handlers/js-handler.ts +134 -0
- package/src/dev-engine/handlers/mjs-handler.ts +65 -0
- package/src/dev-engine/handlers/node-module-handler.ts +339 -0
- package/src/dev-engine/handlers/ts-handler.ts +143 -0
- package/src/dev-engine/handlers/ui-handler.ts +105 -0
- package/src/dev-engine/handlers/uix-handler.ts +90 -0
- package/src/dev-engine/hmr/hmr-client-template.ts +122 -0
- package/src/dev-engine/hmr/hmr.ts +173 -0
- package/src/dev-engine/middleware/hmr-routes.ts +120 -0
- package/src/dev-engine/middleware/middleware-setup.ts +351 -0
- package/src/dev-engine/middleware/static-files.ts +728 -0
- package/src/dev-engine/pythonDevManager.ts +116 -0
- package/src/dev-engine/router/file-router.ts +164 -0
- package/src/dev-engine/server.ts +152 -0
- package/src/index.ts +26 -0
- package/src/internal/cache/compilation-cache.ts +182 -0
- package/src/internal/generate-import-map-cli.ts +40 -0
- package/src/internal/generate-import-map.ts +154 -0
- package/src/kernel/package-finder.ts +164 -0
- package/src/kernel/package-registry.ts +198 -0
- package/src/kernel/workspace.ts +62 -0
- package/src/resolution/bare-import-resolver.ts +400 -0
- package/src/resolution/cdn/cdn-fallback.ts +37 -0
- package/src/resolution/path/file-path-resolver.ts +190 -0
- package/src/resolution/path/path-fixup.ts +19 -0
- package/src/resolution/resolver.ts +198 -0
- package/src/resolution/rewriting/import-rewriter.ts +237 -0
- package/src/resolution/symlink-registry.ts +114 -0
- package/src/resolution/url-resolver.ts +231 -0
- package/src/resolution/workspace-package-resolver.ts +94 -0
- 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
|
+
});
|