@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,339 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
* SWITE - SWISS Development Server
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Response } from "express";
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
|
|
12
|
+
import { BaseHandler, type HandlerContext } from "./base-handler.js";
|
|
13
|
+
import { UIHandler } from "./ui-handler.js";
|
|
14
|
+
import { UIXHandler } from "./uix-handler.js";
|
|
15
|
+
import { TSHandler } from "./ts-handler.js";
|
|
16
|
+
import { findWorkspaceRoot } from "../../kernel/workspace.js";
|
|
17
|
+
import { shouldUseCdnFallback } from "../../resolution/cdn/cdn-fallback.js";
|
|
18
|
+
|
|
19
|
+
export class NodeModuleHandler extends BaseHandler {
|
|
20
|
+
private uiHandler: UIHandler;
|
|
21
|
+
private uixHandler: UIXHandler;
|
|
22
|
+
private tsHandler: TSHandler;
|
|
23
|
+
|
|
24
|
+
constructor(context: HandlerContext) {
|
|
25
|
+
super(context);
|
|
26
|
+
this.uiHandler = new UIHandler(context);
|
|
27
|
+
this.uixHandler = new UIXHandler(context);
|
|
28
|
+
this.tsHandler = new TSHandler(context);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handle(url: string, res: Response): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
// Special case: reflect-metadata/reflect.js -> Reflect.js (case fix)
|
|
34
|
+
if (url.includes("/reflect-metadata/reflect.js")) {
|
|
35
|
+
url = url.replace("/reflect.js", "/Reflect.js");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Handle node_modules paths - try multiple locations
|
|
39
|
+
// URL is like /node_modules/reflect-metadata/Reflect.js
|
|
40
|
+
// We need to remove the leading / and join with the appropriate root
|
|
41
|
+
const urlPath = url.startsWith("/") ? url.slice(1) : url;
|
|
42
|
+
let filePath: string | null = null;
|
|
43
|
+
const workspaceRoot =
|
|
44
|
+
this.context.workspaceRoot ||
|
|
45
|
+
(await findWorkspaceRoot(this.context.root));
|
|
46
|
+
|
|
47
|
+
console.log(chalk.blue(`[node_modules] Processing: ${url}`));
|
|
48
|
+
console.log(chalk.blue(`[node_modules] App root: ${this.context.root}`));
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.blue(`[node_modules] Workspace root: ${workspaceRoot || "none"}`),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Walk up directory tree from app root to find node_modules at any level
|
|
54
|
+
// (handles pnpm isolated AND hoisted workspace layouts)
|
|
55
|
+
{
|
|
56
|
+
let current = path.resolve(this.context.root);
|
|
57
|
+
const visited = new Set<string>();
|
|
58
|
+
for (let i = 0; i < 8; i++) {
|
|
59
|
+
const candidate = path.join(current, urlPath);
|
|
60
|
+
if (!visited.has(candidate)) {
|
|
61
|
+
visited.add(candidate);
|
|
62
|
+
console.log(chalk.blue(`[node_modules] Trying path: ${candidate}`));
|
|
63
|
+
try {
|
|
64
|
+
const resolvedPath = await fs.realpath(candidate);
|
|
65
|
+
console.log(chalk.blue(`[node_modules] Resolved to: ${resolvedPath}`));
|
|
66
|
+
await fs.access(resolvedPath);
|
|
67
|
+
filePath = resolvedPath;
|
|
68
|
+
console.log(chalk.green(`[node_modules] ✓ Found: ${urlPath}`));
|
|
69
|
+
break;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.log(chalk.yellow(`[node_modules] Path failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const parent = path.dirname(current);
|
|
75
|
+
if (parent === current) break;
|
|
76
|
+
current = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!filePath) {
|
|
80
|
+
// Try workspace root node_modules
|
|
81
|
+
if (workspaceRoot) {
|
|
82
|
+
const workspaceNodeModulesPath = path.join(workspaceRoot, urlPath);
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.blue(
|
|
85
|
+
`[node_modules] Trying workspace path: ${workspaceNodeModulesPath}`,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
try {
|
|
89
|
+
// Try to resolve symlinks first (realpath works even if path is a symlink)
|
|
90
|
+
const resolvedPath = await fs.realpath(workspaceNodeModulesPath);
|
|
91
|
+
console.log(
|
|
92
|
+
chalk.blue(`[node_modules] Resolved to: ${resolvedPath}`),
|
|
93
|
+
);
|
|
94
|
+
// Verify the resolved path exists
|
|
95
|
+
await fs.access(resolvedPath);
|
|
96
|
+
filePath = resolvedPath;
|
|
97
|
+
console.log(
|
|
98
|
+
chalk.green(`[node_modules] ✓ Found in workspace: ${urlPath}`),
|
|
99
|
+
);
|
|
100
|
+
} catch (err2) {
|
|
101
|
+
console.log(
|
|
102
|
+
chalk.yellow(
|
|
103
|
+
`[node_modules] Workspace path failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
// Try swiss-lib monorepo node_modules (dynamically found)
|
|
107
|
+
const { findSwissLibMonorepo } = await import("../../kernel/package-finder.js");
|
|
108
|
+
const swissLib = await findSwissLibMonorepo(this.context.root);
|
|
109
|
+
if (swissLib) {
|
|
110
|
+
const swissNodeModulesPath = path.join(swissLib, urlPath);
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.blue(
|
|
113
|
+
`[node_modules] Trying swiss-lib path: ${swissNodeModulesPath}`,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
try {
|
|
117
|
+
// Try to resolve symlinks first (realpath works even if path is a symlink)
|
|
118
|
+
const resolvedPath = await fs.realpath(swissNodeModulesPath);
|
|
119
|
+
console.log(
|
|
120
|
+
chalk.blue(`[node_modules] Resolved to: ${resolvedPath}`),
|
|
121
|
+
);
|
|
122
|
+
// Verify the resolved path exists
|
|
123
|
+
await fs.access(resolvedPath);
|
|
124
|
+
filePath = resolvedPath;
|
|
125
|
+
console.log(
|
|
126
|
+
chalk.green(
|
|
127
|
+
`[node_modules] ✓ Found in swiss-lib monorepo: ${urlPath}`,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
} catch (err3) {
|
|
131
|
+
console.log(
|
|
132
|
+
chalk.yellow(
|
|
133
|
+
`[node_modules] swiss-lib path failed: ${err3 instanceof Error ? err3.message : String(err3)}`,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
// File not found in any location, will trigger case-insensitive search below
|
|
137
|
+
filePath = path.join(this.context.root, urlPath);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// File not found in any location, will trigger case-insensitive search below
|
|
141
|
+
filePath = path.join(this.context.root, urlPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
filePath = path.join(this.context.root, urlPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(
|
|
150
|
+
chalk.gray(`[node_modules] Resolving: ${url} -> ${filePath}`),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// File path is already resolved from above, no need to resolve again
|
|
154
|
+
|
|
155
|
+
// Check if file exists, if .js doesn't exist try case-insensitive match and alternatives
|
|
156
|
+
try {
|
|
157
|
+
await fs.access(filePath);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.log(
|
|
160
|
+
chalk.yellow(
|
|
161
|
+
`[node_modules] File not found at ${filePath}, trying case-insensitive match...`,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
// File doesn't exist with exact case, try case-insensitive match (for Reflect.js vs reflect.js)
|
|
165
|
+
if (url.endsWith(".js")) {
|
|
166
|
+
const dir = path.dirname(filePath);
|
|
167
|
+
const requestedName = path.basename(filePath);
|
|
168
|
+
try {
|
|
169
|
+
// Resolve directory symlink (for pnpm)
|
|
170
|
+
const resolvedDir = await fs.realpath(dir).catch(() => dir);
|
|
171
|
+
// Check if directory exists first
|
|
172
|
+
await fs.access(resolvedDir);
|
|
173
|
+
const files = await fs.readdir(resolvedDir);
|
|
174
|
+
const caseInsensitiveMatch = files.find(
|
|
175
|
+
(f) => f.toLowerCase() === requestedName.toLowerCase(),
|
|
176
|
+
);
|
|
177
|
+
if (caseInsensitiveMatch) {
|
|
178
|
+
filePath = path.join(resolvedDir, caseInsensitiveMatch);
|
|
179
|
+
console.log(
|
|
180
|
+
chalk.yellow(
|
|
181
|
+
`[node_modules] Case-insensitive match: ${requestedName} -> ${caseInsensitiveMatch}`,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
// Verify the file exists with the correct case
|
|
185
|
+
await fs.access(filePath);
|
|
186
|
+
// File found, continue to serve it below
|
|
187
|
+
} else {
|
|
188
|
+
throw new Error("No case-insensitive match found");
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// Directory doesn't exist or no case-insensitive match, try alternatives
|
|
192
|
+
console.log(
|
|
193
|
+
chalk.gray(
|
|
194
|
+
`[node_modules] Case-insensitive match failed for ${url}, trying alternatives...`,
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
const basePath = filePath.slice(0, -3); // Remove .js
|
|
198
|
+
const alternatives = [
|
|
199
|
+
{
|
|
200
|
+
ext: ".ts",
|
|
201
|
+
handler: () =>
|
|
202
|
+
this.tsHandler.handle(url.replace(/\.js$/, ".ts"), res),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
ext: ".ui",
|
|
206
|
+
handler: () =>
|
|
207
|
+
this.uiHandler.handle(url.replace(/\.js$/, ".ui"), res),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
ext: ".uix",
|
|
211
|
+
handler: () =>
|
|
212
|
+
this.uixHandler.handle(url.replace(/\.js$/, ".uix"), res),
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const alt of alternatives) {
|
|
217
|
+
try {
|
|
218
|
+
await fs.access(basePath + alt.ext);
|
|
219
|
+
console.log(
|
|
220
|
+
chalk.yellow(
|
|
221
|
+
`[.js→${alt.ext}] ${url} → ${url.replace(/\.js$/, alt.ext)}`,
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
return await alt.handler();
|
|
225
|
+
} catch {
|
|
226
|
+
// Try next alternative
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// No alternatives found - redirect to CDN instead of 500
|
|
231
|
+
const cdnRedirect = this.getNodeModuleCdnRedirect(url);
|
|
232
|
+
if (cdnRedirect) {
|
|
233
|
+
console.log(chalk.yellow(`[node_modules] Not found locally, redirecting to CDN: ${cdnRedirect}`));
|
|
234
|
+
res.redirect(302, cdnRedirect);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
res.status(404).send(`Module not found: ${url}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// Not a .js file and doesn't exist - try CDN redirect or 404
|
|
242
|
+
const cdnRedirect = this.getNodeModuleCdnRedirect(url);
|
|
243
|
+
if (cdnRedirect) {
|
|
244
|
+
res.redirect(302, cdnRedirect);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
res.status(404).send(`Module not found: ${url}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// File exists, process it normally
|
|
253
|
+
// For node_modules files, skip import rewriting - they should work as-is
|
|
254
|
+
// and rewriting can cause issues with package internals
|
|
255
|
+
try {
|
|
256
|
+
console.log(chalk.blue(`[node_modules] Reading file: ${filePath}`));
|
|
257
|
+
const source = await fs.readFile(filePath, "utf-8");
|
|
258
|
+
console.log(
|
|
259
|
+
chalk.green(
|
|
260
|
+
`[node_modules] ✓ File read successfully, length: ${source.length}`,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Skip import rewriting for node_modules - serve as-is
|
|
265
|
+
// This is safer and faster for third-party packages
|
|
266
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
267
|
+
res.send(source);
|
|
268
|
+
console.log(chalk.green(`[node_modules] ✓ Served ${url} successfully`));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(
|
|
271
|
+
chalk.red(`[node_modules] Error processing ${url} at ${filePath}:`),
|
|
272
|
+
);
|
|
273
|
+
console.error(chalk.red(`[node_modules] Error details:`), error);
|
|
274
|
+
if (error instanceof Error) {
|
|
275
|
+
console.error(chalk.red(`[node_modules] Error stack:`), error.stack);
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
} catch (outerError) {
|
|
280
|
+
console.error(chalk.red(`[node_modules] FATAL ERROR handling ${url}:`));
|
|
281
|
+
console.error(
|
|
282
|
+
chalk.red(
|
|
283
|
+
`[node_modules] Error type: ${outerError instanceof Error ? outerError.constructor.name : typeof outerError}`,
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
console.error(
|
|
287
|
+
chalk.red(
|
|
288
|
+
`[node_modules] Error message: ${outerError instanceof Error ? outerError.message : String(outerError)}`,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
if (outerError instanceof Error && outerError.stack) {
|
|
292
|
+
console.error(chalk.red(`[node_modules] Stack trace:`));
|
|
293
|
+
console.error(outerError.stack);
|
|
294
|
+
}
|
|
295
|
+
// Try CDN redirect before giving up with 500
|
|
296
|
+
const cdnRedirect = this.getNodeModuleCdnRedirect(url);
|
|
297
|
+
if (cdnRedirect) {
|
|
298
|
+
console.log(chalk.yellow(`[node_modules] Error handling locally, redirecting to CDN: ${cdnRedirect}`));
|
|
299
|
+
res.redirect(302, cdnRedirect);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
res.status(404).setHeader("Content-Type", "text/plain").send(
|
|
303
|
+
`Module not found: ${url}. ${outerError instanceof Error ? outerError.message : String(outerError)}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get CDN URL for a /node_modules/... request when the file is not found locally.
|
|
310
|
+
* Uses jsDelivr (+esm) for reliable ESM delivery; esm.sh can return 500 for some packages.
|
|
311
|
+
* e.g. /node_modules/reflect-metadata/Reflect.js -> https://cdn.jsdelivr.net/npm/reflect-metadata/+esm
|
|
312
|
+
*/
|
|
313
|
+
private getNodeModuleCdnRedirect(url: string): string | null {
|
|
314
|
+
const prefix = "/node_modules/";
|
|
315
|
+
if (!url.startsWith(prefix)) return null;
|
|
316
|
+
|
|
317
|
+
// For nested paths like /node_modules/@scope/pkg/node_modules/dep/file.js,
|
|
318
|
+
// find the LAST node_modules segment and extract the package name from there.
|
|
319
|
+
const lastNodeModulesIdx = url.lastIndexOf("/node_modules/");
|
|
320
|
+
const after = url.slice(lastNodeModulesIdx + prefix.length);
|
|
321
|
+
if (!after) return null;
|
|
322
|
+
|
|
323
|
+
// Extract package name: handle @scope/name and plain-name
|
|
324
|
+
let pkgName: string;
|
|
325
|
+
if (after.startsWith("@")) {
|
|
326
|
+
// Scoped package: need TWO path segments — @scope/name
|
|
327
|
+
const secondSlash = after.indexOf("/", after.indexOf("/") + 1);
|
|
328
|
+
pkgName = secondSlash === -1 ? after : after.slice(0, secondSlash);
|
|
329
|
+
} else {
|
|
330
|
+
const firstSlash = after.indexOf("/");
|
|
331
|
+
pkgName = firstSlash === -1 ? after : after.slice(0, firstSlash);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!pkgName || pkgName === "." || pkgName === "..") return null;
|
|
335
|
+
if (!shouldUseCdnFallback(pkgName)) return null;
|
|
336
|
+
// jsDelivr +esm serves ESM build; works for reflect-metadata and most npm packages
|
|
337
|
+
return `https://cdn.jsdelivr.net/npm/${pkgName}/+esm`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
* SWITE - SWISS Development Server
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Response } from "express";
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
|
|
11
|
+
import { inlineEnvReferences } from "../../config/env.js";
|
|
12
|
+
import { compilationCache } from "../../internal/cache/compilation-cache.js";
|
|
13
|
+
import {
|
|
14
|
+
BaseHandler,
|
|
15
|
+
setDevHeaders,
|
|
16
|
+
type HandlerContext,
|
|
17
|
+
} from "./base-handler.js";
|
|
18
|
+
|
|
19
|
+
export class TSHandler extends BaseHandler {
|
|
20
|
+
constructor(context: HandlerContext) {
|
|
21
|
+
super(context);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async handle(url: string, res: Response): Promise<void> {
|
|
25
|
+
const filePath = await this.resolveFilePath(url);
|
|
26
|
+
console.log(chalk.gray(`[.ts] ${url}`));
|
|
27
|
+
|
|
28
|
+
// Check if .ts file exists, if not try .ui, .uix
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(filePath);
|
|
31
|
+
} catch {
|
|
32
|
+
// .ts doesn't exist, try alternatives
|
|
33
|
+
const basePath = filePath.slice(0, -3); // Remove .ts
|
|
34
|
+
const alternatives = [
|
|
35
|
+
{
|
|
36
|
+
ext: ".ui",
|
|
37
|
+
url: url.replace(/\.ts$/, ".ui"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
ext: ".uix",
|
|
41
|
+
url: url.replace(/\.ts$/, ".uix"),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const alt of alternatives) {
|
|
46
|
+
try {
|
|
47
|
+
const altPath = basePath + alt.ext;
|
|
48
|
+
await fs.access(altPath);
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.yellow(
|
|
51
|
+
`[.ts→${alt.ext}] ${url} → ${alt.url} (file: ${altPath})`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
// Import and use the appropriate handler
|
|
55
|
+
if (alt.ext === ".ui") {
|
|
56
|
+
const { UIHandler } = await import("./ui-handler.js");
|
|
57
|
+
const uiHandler = new UIHandler(this.context);
|
|
58
|
+
return await uiHandler.handle(alt.url, res);
|
|
59
|
+
} else if (alt.ext === ".uix") {
|
|
60
|
+
const { UIXHandler } = await import("./uix-handler.js");
|
|
61
|
+
const uixHandler = new UIXHandler(this.context);
|
|
62
|
+
return await uixHandler.handle(alt.url, res);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Try next alternative
|
|
66
|
+
console.log(
|
|
67
|
+
chalk.gray(
|
|
68
|
+
`[.ts→${alt.ext}] ${basePath + alt.ext} not found, trying next...`,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// No alternatives found, throw error
|
|
75
|
+
console.error(
|
|
76
|
+
chalk.red(`[.ts] File not found: ${filePath} (and no alternatives found)`),
|
|
77
|
+
);
|
|
78
|
+
res.status(404).send(`File not found: ${url}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check cache first
|
|
83
|
+
const cached = await compilationCache.get(
|
|
84
|
+
filePath,
|
|
85
|
+
(compiled) => this.getDependencies(compiled),
|
|
86
|
+
);
|
|
87
|
+
if (cached) {
|
|
88
|
+
setDevHeaders(res);
|
|
89
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
90
|
+
res.send(cached);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Cache miss - compile
|
|
95
|
+
const source = await fs.readFile(filePath, "utf-8");
|
|
96
|
+
|
|
97
|
+
// Use esbuild for fast TS transformation
|
|
98
|
+
const esbuild = await import("esbuild");
|
|
99
|
+
const result = await esbuild.transform(source, {
|
|
100
|
+
loader: "ts",
|
|
101
|
+
format: "esm",
|
|
102
|
+
target: "esnext",
|
|
103
|
+
sourcefile: filePath,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const inlined = inlineEnvReferences(result.code, this.context.env);
|
|
107
|
+
const rewritten = await rewriteImports(
|
|
108
|
+
inlined,
|
|
109
|
+
filePath,
|
|
110
|
+
this.context.resolver,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await compilationCache.set(
|
|
114
|
+
filePath,
|
|
115
|
+
result.code,
|
|
116
|
+
rewritten,
|
|
117
|
+
(compiled) => this.getDependencies(compiled),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Debug: Check for bare imports after rewriting
|
|
121
|
+
const bareImportPattern =
|
|
122
|
+
/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
|
|
123
|
+
if (bareImportPattern.test(rewritten)) {
|
|
124
|
+
console.log(
|
|
125
|
+
chalk.red(
|
|
126
|
+
`[.ts] ERROR: Bare imports still present after rewriting: ${url}`,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
const matches = Array.from(
|
|
130
|
+
rewritten.matchAll(
|
|
131
|
+
/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
for (const match of matches.slice(0, 3)) {
|
|
135
|
+
console.log(chalk.red(`[.ts] Unresolved import: ${match[1]}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setDevHeaders(res);
|
|
140
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
141
|
+
res.send(rewritten);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
* SWITE - SWISS Development Server
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Response } from "express";
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import { UiCompiler } from "@swissjs/compiler";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
|
|
12
|
+
import { inlineEnvReferences } from "../../config/env.js";
|
|
13
|
+
import { compilationCache } from "../../internal/cache/compilation-cache.js";
|
|
14
|
+
import { fixSwissLibPaths } from "../../resolution/path/path-fixup.js";
|
|
15
|
+
import {
|
|
16
|
+
BaseHandler,
|
|
17
|
+
setDevHeaders,
|
|
18
|
+
type HandlerContext,
|
|
19
|
+
} from "./base-handler.js";
|
|
20
|
+
|
|
21
|
+
export class UIHandler extends BaseHandler {
|
|
22
|
+
private compiler = new UiCompiler();
|
|
23
|
+
|
|
24
|
+
constructor(context: HandlerContext) {
|
|
25
|
+
super(context);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handle(url: string, res: Response): Promise<void> {
|
|
29
|
+
const filePath = await this.resolveFilePath(url);
|
|
30
|
+
console.log(chalk.blue(`[.ui] ${url} → ${filePath}`));
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await fs.access(filePath);
|
|
34
|
+
} catch {
|
|
35
|
+
console.error(chalk.red(`[.ui] File not found: ${filePath}`));
|
|
36
|
+
throw new Error(`File not found: ${url} (resolved to: ${filePath})`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cache hit
|
|
40
|
+
const cached = await compilationCache.get(filePath, (compiled) => this.getDependencies(compiled));
|
|
41
|
+
if (cached) {
|
|
42
|
+
const fixed = fixSwissLibPaths(cached);
|
|
43
|
+
setDevHeaders(res);
|
|
44
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
45
|
+
res.setHeader("Content-Length", Buffer.byteLength(fixed, "utf-8"));
|
|
46
|
+
res.end(fixed, "utf-8");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Cache miss — compile
|
|
51
|
+
const source = await fs.readFile(filePath, "utf-8");
|
|
52
|
+
let compiled = await this.compiler.compileAsync(source, filePath);
|
|
53
|
+
|
|
54
|
+
const esbuild = await import("esbuild");
|
|
55
|
+
const tsResult = await esbuild.transform(compiled, {
|
|
56
|
+
loader: "ts",
|
|
57
|
+
format: "esm",
|
|
58
|
+
target: "esnext",
|
|
59
|
+
sourcefile: filePath,
|
|
60
|
+
});
|
|
61
|
+
compiled = tsResult.code;
|
|
62
|
+
|
|
63
|
+
// Fix compiler-emitted wrong paths before import rewriting
|
|
64
|
+
compiled = fixSwissLibPaths(compiled);
|
|
65
|
+
|
|
66
|
+
// Inline import.meta.env references before import rewriting
|
|
67
|
+
compiled = inlineEnvReferences(compiled, this.context.env);
|
|
68
|
+
|
|
69
|
+
// Strip CSS static-asset imports — they are not ES modules
|
|
70
|
+
compiled = stripCssImports(compiled, url);
|
|
71
|
+
|
|
72
|
+
const bareImportPattern = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
|
|
73
|
+
if (bareImportPattern.test(compiled)) {
|
|
74
|
+
console.warn(`[.ui] Compiled output contains bare imports: ${url}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
|
|
78
|
+
const finalCode = fixSwissLibPaths(rewritten);
|
|
79
|
+
|
|
80
|
+
await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
|
|
81
|
+
|
|
82
|
+
if (bareImportPattern.test(finalCode)) {
|
|
83
|
+
console.error(`[.ui] Bare imports still present after rewriting: ${url}`);
|
|
84
|
+
for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
|
|
85
|
+
console.error(`[.ui] Unresolved import: ${m[1]}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setDevHeaders(res);
|
|
90
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
91
|
+
res.setHeader("Content-Length", Buffer.byteLength(finalCode, "utf-8"));
|
|
92
|
+
res.end(finalCode, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stripCssImports(code: string, url: string): string {
|
|
97
|
+
// Single well-ordered pass: static imports first, then dynamic imports
|
|
98
|
+
const before = code;
|
|
99
|
+
code = code.replace(/^[^\S\r\n]*import\s[^'"]*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
|
|
100
|
+
code = code.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "undefined");
|
|
101
|
+
if (before !== code) {
|
|
102
|
+
console.log(chalk.blue(`[.ui] Stripped CSS imports from ${url}`));
|
|
103
|
+
}
|
|
104
|
+
return code;
|
|
105
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
* SWITE - SWISS Development Server
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Response } from "express";
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import { UiCompiler } from "@swissjs/compiler";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
|
|
12
|
+
import { inlineEnvReferences } from "../../config/env.js";
|
|
13
|
+
import { compilationCache } from "../../internal/cache/compilation-cache.js";
|
|
14
|
+
import { fixSwissLibPaths } from "../../resolution/path/path-fixup.js";
|
|
15
|
+
import {
|
|
16
|
+
BaseHandler,
|
|
17
|
+
setDevHeaders,
|
|
18
|
+
type HandlerContext,
|
|
19
|
+
} from "./base-handler.js";
|
|
20
|
+
|
|
21
|
+
export class UIXHandler extends BaseHandler {
|
|
22
|
+
private compiler = new UiCompiler();
|
|
23
|
+
|
|
24
|
+
constructor(context: HandlerContext) {
|
|
25
|
+
super(context);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handle(url: string, res: Response): Promise<void> {
|
|
29
|
+
const filePath = await this.resolveFilePath(url);
|
|
30
|
+
console.log(chalk.blue(`[.uix] ${url}`));
|
|
31
|
+
|
|
32
|
+
// Cache hit
|
|
33
|
+
const cached = await compilationCache.get(filePath, (compiled) => this.getDependencies(compiled));
|
|
34
|
+
if (cached) {
|
|
35
|
+
const fixed = fixSwissLibPaths(cached);
|
|
36
|
+
setDevHeaders(res);
|
|
37
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
38
|
+
res.send(fixed);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Cache miss — compile
|
|
43
|
+
const source = await fs.readFile(filePath, "utf-8");
|
|
44
|
+
let compiled = await this.compiler.compileAsync(source, filePath);
|
|
45
|
+
|
|
46
|
+
const esbuild = await import("esbuild");
|
|
47
|
+
const tsResult = await esbuild.transform(compiled, {
|
|
48
|
+
loader: "ts",
|
|
49
|
+
format: "esm",
|
|
50
|
+
target: "esnext",
|
|
51
|
+
sourcefile: filePath,
|
|
52
|
+
});
|
|
53
|
+
compiled = tsResult.code;
|
|
54
|
+
|
|
55
|
+
// Fix compiler-emitted wrong paths before import rewriting
|
|
56
|
+
compiled = fixSwissLibPaths(compiled);
|
|
57
|
+
|
|
58
|
+
// Inline import.meta.env references before import rewriting
|
|
59
|
+
compiled = inlineEnvReferences(compiled, this.context.env);
|
|
60
|
+
|
|
61
|
+
// Strip CSS static-asset imports — they are not ES modules
|
|
62
|
+
const beforeCss = compiled;
|
|
63
|
+
compiled = compiled.replace(/^[^\S\r\n]*import\s[^'"]*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
|
|
64
|
+
compiled = compiled.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "undefined");
|
|
65
|
+
if (beforeCss !== compiled) {
|
|
66
|
+
console.log(chalk.blue(`[.uix] Stripped CSS imports from ${url}`));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const bareImportPattern = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
|
|
70
|
+
if (bareImportPattern.test(compiled)) {
|
|
71
|
+
console.warn(`[.uix] Compiled output contains bare imports: ${url}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
|
|
75
|
+
const finalCode = fixSwissLibPaths(rewritten);
|
|
76
|
+
|
|
77
|
+
await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
|
|
78
|
+
|
|
79
|
+
if (bareImportPattern.test(finalCode)) {
|
|
80
|
+
console.error(`[.uix] Bare imports still present after rewriting: ${url}`);
|
|
81
|
+
for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
|
|
82
|
+
console.error(`[.uix] Unresolved import: ${m[1]}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setDevHeaders(res);
|
|
87
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
88
|
+
res.send(finalCode);
|
|
89
|
+
}
|
|
90
|
+
}
|