@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,400 @@
1
+ /*
2
+ * Bare Import Resolver - Resolves bare module specifiers (@swissjs/core, etc.)
3
+ * Extracted from resolver.ts for modularity
4
+ */
5
+
6
+ import path from "node:path";
7
+ import { promises as fs } from "node:fs";
8
+ import chalk from "chalk";
9
+ import { findSwissLibMonorepo } from "../kernel/package-finder.js";
10
+ import { shouldUseCdnFallback } from "./cdn/cdn-fallback.js";
11
+ import type { UrlResolverContext, WorkspacePackageResolverContext } from "./url-resolver.js";
12
+ import { resolveWorkspacePackage } from "./workspace-package-resolver.js";
13
+ import { toUrl } from "./url-resolver.js";
14
+
15
+ export interface BareImportResolverContext extends UrlResolverContext {
16
+ resolveWorkspacePackage: (pkgName: string) => Promise<string | null>;
17
+ }
18
+
19
+ /**
20
+ * Resolve bare import specifier (e.g., @swissjs/core, react, etc.)
21
+ */
22
+ export async function resolveBareImport(
23
+ specifier: string,
24
+ context: BareImportResolverContext
25
+ ): Promise<string> {
26
+
27
+ // Extract package name outside the try/catch so fallback logic can reference it.
28
+ // This must stay project-agnostic: works for both scoped and unscoped packages.
29
+ const parts = specifier.split("/");
30
+ const isScoped = specifier.startsWith("@");
31
+ const pkgName = isScoped ? `${parts[0]}/${parts[1]}` : parts[0];
32
+ const subPath = isScoped ? parts.slice(2).join("/") : parts.slice(1).join("/");
33
+
34
+ try {
35
+ // Find package.json - check multiple node_modules locations
36
+ let pkgDir: string | null = null;
37
+ let pkgJsonPath: string | null = null;
38
+
39
+ const nodeModulesLocations: string[] = [
40
+ path.join(context.root, "node_modules"),
41
+ path.join(path.dirname(context.root), "node_modules"),
42
+ ];
43
+
44
+ // Add workspace root node_modules
45
+ const workspaceRoot = await context.getWorkspaceRoot();
46
+ if (workspaceRoot) {
47
+ nodeModulesLocations.push(path.join(workspaceRoot, "node_modules"));
48
+ }
49
+
50
+ // Add swiss-lib monorepo node_modules
51
+ const swissLib = await findSwissLibMonorepo(context.root);
52
+ if (swissLib) {
53
+ const swissNodeModules = path.join(swissLib, "node_modules");
54
+ if (await context.fileExists(swissNodeModules)) {
55
+ nodeModulesLocations.push(swissNodeModules);
56
+ }
57
+ }
58
+
59
+ // Try each location
60
+ for (const nodeModulesPath of nodeModulesLocations) {
61
+ const testPkgDir = path.join(nodeModulesPath, pkgName);
62
+ const testPkgJsonPath = path.join(testPkgDir, "package.json");
63
+ if (await context.fileExists(testPkgJsonPath)) {
64
+ pkgDir = testPkgDir;
65
+ pkgJsonPath = testPkgJsonPath;
66
+ break;
67
+ }
68
+ }
69
+
70
+ if (!pkgJsonPath || !pkgDir) {
71
+ const workspacePkg = await context.resolveWorkspacePackage(pkgName);
72
+ if (workspacePkg) {
73
+ return await resolveWorkspacePackageEntry(
74
+ workspacePkg,
75
+ pkgName,
76
+ subPath,
77
+ specifier,
78
+ context,
79
+ );
80
+ }
81
+
82
+ if (!shouldUseCdnFallback(pkgName)) {
83
+ console.warn(
84
+ `[SWITE] Package ${pkgName} not found anywhere. Scoped package detected; CDN fallback is disabled by default.`,
85
+ );
86
+ return `/node_modules/${specifier}`;
87
+ }
88
+
89
+ console.warn(`[SWITE] Package ${pkgName} not found anywhere, using CDN fallback`);
90
+ return `https://cdn.jsdelivr.net/npm/${specifier}/+esm`;
91
+ }
92
+
93
+ // Continue with normal resolution if we found it
94
+ if (pkgJsonPath && pkgDir) {
95
+ return await resolveWorkspacePackageEntry(pkgDir, pkgName, subPath, specifier, context);
96
+ }
97
+
98
+ // Check if this is a workspace package (symlinked)
99
+ let realPkgDir: string;
100
+ try {
101
+ realPkgDir = await fs.realpath(pkgDir);
102
+ } catch {
103
+ realPkgDir = pkgDir;
104
+ }
105
+
106
+ const workspacePkg = await context.resolveWorkspacePackage(pkgName);
107
+ if (workspacePkg) {
108
+ const normalizedWorkspacePkg = path
109
+ .resolve(workspacePkg)
110
+ .replace(/\\/g, "/")
111
+ .toLowerCase();
112
+ const normalizedRealPkgDir = path
113
+ .resolve(realPkgDir)
114
+ .replace(/\\/g, "/")
115
+ .toLowerCase();
116
+
117
+ // If the real path is the workspace package, use workspace resolution
118
+ if (
119
+ normalizedRealPkgDir === normalizedWorkspacePkg ||
120
+ normalizedRealPkgDir.includes(normalizedWorkspacePkg)
121
+ ) {
122
+ return await resolveWorkspacePackageEntry(
123
+ workspacePkg,
124
+ pkgName,
125
+ subPath,
126
+ specifier,
127
+ context,
128
+ );
129
+ }
130
+ }
131
+
132
+ // Read package.json for exports/main resolution
133
+ const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath!, "utf-8"));
134
+
135
+ // Handle exports field if present
136
+ if (pkgJson.exports) {
137
+ const exportKey = subPath ? `./${subPath}` : ".";
138
+ const exportEntry = pkgJson.exports[exportKey];
139
+
140
+ if (exportEntry) {
141
+ let entryPoint: string;
142
+ if (typeof exportEntry === "string") {
143
+ entryPoint = exportEntry;
144
+ } else if (exportEntry.import) {
145
+ entryPoint = exportEntry.import;
146
+ } else if (exportEntry.default) {
147
+ entryPoint = exportEntry.default;
148
+ } else {
149
+ entryPoint = exportEntry as unknown as string;
150
+ }
151
+
152
+ const fullPath = path.join(pkgDir, entryPoint);
153
+
154
+ // Prefer source over dist
155
+ const normalizedFullPath = fullPath.replace(/\\/g, "/");
156
+ if (normalizedFullPath.includes("/dist/")) {
157
+ const srcFullPath = normalizedFullPath
158
+ .replace("/dist/", "/src/")
159
+ .replace(/\.js$/, ".ts");
160
+ if (await context.fileExists(srcFullPath)) {
161
+ return await toUrl(srcFullPath, context);
162
+ }
163
+ }
164
+
165
+ if (await context.fileExists(fullPath)) {
166
+ return await toUrl(fullPath, context);
167
+ }
168
+
169
+ // Try extensions
170
+ for (const ext of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
171
+ const withExt = fullPath.replace(/\.(js|mjs|ts|ui|uix)$/, ext);
172
+ if (await context.fileExists(withExt)) {
173
+ return await toUrl(withExt, context);
174
+ }
175
+ }
176
+
177
+ // Try case-insensitive match
178
+ const dir = path.dirname(fullPath);
179
+ const fileName = path.basename(fullPath);
180
+ try {
181
+ const files = await fs.readdir(dir);
182
+ const caseInsensitiveMatch = files.find(
183
+ (f) => f.toLowerCase() === fileName.toLowerCase(),
184
+ );
185
+ if (caseInsensitiveMatch) {
186
+ const correctedPath = path.join(dir, caseInsensitiveMatch);
187
+ if (await context.fileExists(correctedPath)) {
188
+ console.log(
189
+ chalk.yellow(
190
+ `[SWITE] Case-insensitive match for ${pkgName}: ${fileName} -> ${caseInsensitiveMatch}`,
191
+ ),
192
+ );
193
+ return await toUrl(correctedPath, context);
194
+ }
195
+ }
196
+ } catch {
197
+ // Directory doesn't exist, continue to fallback
198
+ }
199
+ }
200
+ }
201
+
202
+ // Determine entry point
203
+ let entryPoint: string;
204
+ if (subPath) {
205
+ entryPoint = subPath;
206
+ } else {
207
+ entryPoint = pkgJson.module || pkgJson.main || "index.js";
208
+ }
209
+
210
+ const fullPath = path.join(pkgDir, entryPoint);
211
+
212
+ // Try the exact path first
213
+ if (await context.fileExists(fullPath)) {
214
+ return await toUrl(fullPath, context);
215
+ }
216
+
217
+ // Try with extensions
218
+ for (const ext of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
219
+ if (await context.fileExists(fullPath + ext)) {
220
+ return await toUrl(fullPath + ext, context);
221
+ }
222
+ }
223
+
224
+ // Fallback to CDN (jsDelivr; esm.sh returns 500 for some packages) when allowed.
225
+ console.warn(`[SWITE] Could not resolve ${specifier}, using fallback`);
226
+ return shouldUseCdnFallback(pkgName)
227
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
228
+ : `/node_modules/${specifier}`;
229
+ } catch (error) {
230
+ console.warn(`[SWITE] Error resolving ${specifier}:`, error);
231
+ return shouldUseCdnFallback(pkgName)
232
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
233
+ : `/node_modules/${specifier}`;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Resolve entry point for workspace package
239
+ */
240
+ async function resolveWorkspacePackageEntry(
241
+ workspacePkg: string,
242
+ pkgName: string,
243
+ subPath: string,
244
+ specifier: string,
245
+ context: BareImportResolverContext,
246
+ ): Promise<string> {
247
+ const workspacePkgJson = JSON.parse(
248
+ await fs.readFile(path.join(workspacePkg, "package.json"), "utf-8"),
249
+ );
250
+
251
+ // Handle exports field if present
252
+ if (workspacePkgJson.exports) {
253
+ const subPathWithoutExt = subPath
254
+ ? subPath.replace(/\.(js|ts|ui|uix)$/, "")
255
+ : "";
256
+ let exportKey = subPathWithoutExt ? `./${subPathWithoutExt}` : ".";
257
+
258
+ // Try to find matching export
259
+ if (subPath && !workspacePkgJson.exports[exportKey]) {
260
+ if (subPathWithoutExt) {
261
+ const withoutExtKey = `./${subPathWithoutExt}`;
262
+ if (workspacePkgJson.exports[withoutExtKey]) {
263
+ exportKey = withoutExtKey;
264
+ }
265
+ }
266
+
267
+ if (!workspacePkgJson.exports[exportKey]) {
268
+ const subPathParts = subPathWithoutExt.split("/");
269
+ if (subPathParts.length > 1) {
270
+ const dirPath = subPathParts.slice(0, -1).join("/");
271
+ const dirExportKey = `./${dirPath}`;
272
+ if (workspacePkgJson.exports[dirExportKey]) {
273
+ exportKey = dirExportKey;
274
+ } else {
275
+ const firstDir = subPathParts[0];
276
+ const firstDirExportKey = `./${firstDir}`;
277
+ if (workspacePkgJson.exports[firstDirExportKey]) {
278
+ exportKey = firstDirExportKey;
279
+ } else {
280
+ const lastDir = subPathParts[subPathParts.length - 2];
281
+ const lastDirExportKey = `./${lastDir}`;
282
+ if (workspacePkgJson.exports[lastDirExportKey]) {
283
+ exportKey = lastDirExportKey;
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ const exportEntry = workspacePkgJson.exports[exportKey];
292
+
293
+ if (exportEntry) {
294
+ let entryPoint: string;
295
+ if (typeof exportEntry === "string") {
296
+ entryPoint = exportEntry;
297
+ } else if (exportEntry.import) {
298
+ entryPoint = exportEntry.import;
299
+ } else if (exportEntry.default) {
300
+ entryPoint = exportEntry.default;
301
+ } else {
302
+ entryPoint = exportEntry;
303
+ }
304
+
305
+ const normalizedEntryPoint = entryPoint.startsWith("./")
306
+ ? entryPoint.slice(2)
307
+ : entryPoint;
308
+ const fullPath = path.join(workspacePkg, normalizedEntryPoint);
309
+
310
+ // Dev: prefer src over dist for workspace packages (unbuilt or dev mode)
311
+ const normalizedFull = fullPath.replace(/\\/g, "/");
312
+ if (normalizedFull.includes("/dist/")) {
313
+ const srcPath = fullPath.replace(/[/\\]dist[/\\]/, path.sep + "src" + path.sep).replace(/\.js$/i, ".ts");
314
+ if (await context.fileExists(srcPath)) {
315
+ return await toUrl(srcPath, context);
316
+ }
317
+ }
318
+
319
+ if (await context.fileExists(fullPath)) {
320
+ return await toUrl(fullPath, context);
321
+ }
322
+
323
+ // Try extensions
324
+ for (const ext of [".ui", ".uix", ".ts", ".js"]) {
325
+ const withExt = fullPath.replace(/\.(js|ts|ui|uix)$/, ext);
326
+ if (await context.fileExists(withExt)) {
327
+ return await toUrl(withExt, context);
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ // Fallback to old logic
334
+ let entryPoint: string;
335
+ if (subPath) {
336
+ entryPoint = subPath;
337
+ } else {
338
+ entryPoint =
339
+ workspacePkgJson.module || workspacePkgJson.main || "index.js";
340
+ }
341
+
342
+ const fullPath = path.join(workspacePkg, entryPoint);
343
+
344
+ // Dev: prefer src over dist for workspace packages
345
+ const normalizedFull = fullPath.replace(/\\/g, "/");
346
+ if (normalizedFull.includes("/dist/")) {
347
+ const srcPath = fullPath.replace(/[/\\]dist[/\\]/, path.sep + "src" + path.sep).replace(/\.js$/i, ".ts");
348
+ if (await context.fileExists(srcPath)) {
349
+ return await toUrl(srcPath, context);
350
+ }
351
+ }
352
+
353
+ if (await context.fileExists(fullPath)) {
354
+ return await toUrl(fullPath, context);
355
+ }
356
+
357
+ // Try extensions
358
+ const ext = path.extname(entryPoint);
359
+ if (ext) {
360
+ const basePath = fullPath.slice(0, -ext.length);
361
+ for (const tryExt of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
362
+ if (await context.fileExists(basePath + tryExt)) {
363
+ return await toUrl(basePath + tryExt, context);
364
+ }
365
+ }
366
+ } else {
367
+ for (const tryExt of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
368
+ if (await context.fileExists(fullPath + tryExt)) {
369
+ return await toUrl(fullPath + tryExt, context);
370
+ }
371
+ }
372
+ }
373
+
374
+ // Try index files
375
+ for (const tryExt of [".js", ".ts", ".ui", ".uix"]) {
376
+ const indexFile = path.join(fullPath, `index${tryExt}`);
377
+ if (await context.fileExists(indexFile)) {
378
+ return await toUrl(indexFile, context);
379
+ }
380
+ }
381
+
382
+ // Try src/ directory
383
+ const srcDir = path.join(workspacePkg, "src");
384
+ for (const ext of [".ts", ".ui", ".uix", ".js"]) {
385
+ const srcIndex = path.join(srcDir, `index${ext}`);
386
+ if (await context.fileExists(srcIndex)) {
387
+ console.log(
388
+ `[SWITE] Found unbuilt workspace package ${pkgName} at ${srcIndex}`,
389
+ );
390
+ return await toUrl(srcIndex, context);
391
+ }
392
+ }
393
+
394
+ console.warn(
395
+ `[SWITE] Entry point not found for ${pkgName} at ${fullPath}, using fallback`,
396
+ );
397
+ return shouldUseCdnFallback(pkgName)
398
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
399
+ : `/node_modules/${specifier}`;
400
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * CDN fallback policy.
3
+ *
4
+ * Swite can fall back to jsDelivr (+esm) for packages it can't resolve locally.
5
+ * This must be safe and project-agnostic:
6
+ * - Unscoped packages (e.g. "react") are usually public on npm; allow by default.
7
+ * - Scoped packages (e.g. "@scope/pkg") may be private; do NOT CDN-fallback by default.
8
+ *
9
+ * Opt-in:
10
+ * - Set `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes to allow,
11
+ * e.g. "@types,@tanstack".
12
+ */
13
+
14
+ function getScope(specifierOrPkg: string): string | null {
15
+ if (!specifierOrPkg.startsWith("@")) return null;
16
+ const firstSlash = specifierOrPkg.indexOf("/");
17
+ if (firstSlash === -1) return null;
18
+ return specifierOrPkg.slice(0, firstSlash); // "@scope"
19
+ }
20
+
21
+ function parseAllowList(): Set<string> {
22
+ const raw = process.env.SWITE_CDN_FALLBACK_SCOPES || "";
23
+ const scopes = raw
24
+ .split(",")
25
+ .map((s) => s.trim())
26
+ .filter(Boolean)
27
+ .map((s) => (s.startsWith("@") ? s : `@${s}`));
28
+ return new Set(scopes);
29
+ }
30
+
31
+ export function shouldUseCdnFallback(specifierOrPkg: string): boolean {
32
+ const scope = getScope(specifierOrPkg);
33
+ if (!scope) return true; // unscoped: allow by default
34
+ const allow = parseAllowList();
35
+ return allow.has(scope);
36
+ }
37
+
@@ -0,0 +1,190 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { promises as fs } from "node:fs";
8
+ import path from "node:path";
9
+ import { findWorkspaceRoot } from "../../kernel/workspace.js";
10
+ import { findSwissLibMonorepo } from "../../kernel/package-finder.js";
11
+
12
+ export interface PathResolverContext {
13
+ root: string;
14
+ workspaceRoot: string | null;
15
+ }
16
+
17
+ /**
18
+ * Resolve file path from URL, handling SWISS packages, workspace packages, and app files
19
+ */
20
+ export async function resolveFilePath(
21
+ url: string,
22
+ root: string,
23
+ workspaceRoot: string | null = null,
24
+ ): Promise<string> {
25
+ // /node_modules/ URLs: walk up from app root until we find the package.
26
+ // pnpm may place deps at the app root, one level up (workspace pkg), or at
27
+ // the monorepo root depending on hoisting config and pnpm version.
28
+ if (url.startsWith("/node_modules/")) {
29
+ const urlPath = url.startsWith("/") ? url.slice(1) : url;
30
+
31
+ // Walk up the directory tree from root, trying node_modules at each level
32
+ let current = path.resolve(root);
33
+ const visited = new Set<string>();
34
+ for (let i = 0; i < 8; i++) {
35
+ const candidate = path.join(current, urlPath);
36
+ if (!visited.has(candidate)) {
37
+ visited.add(candidate);
38
+ try {
39
+ const resolved = await fs.realpath(candidate);
40
+ await fs.access(resolved);
41
+ return resolved;
42
+ } catch {
43
+ // try parent level
44
+ }
45
+ }
46
+ const parent = path.dirname(current);
47
+ if (parent === current) break; // filesystem root
48
+ current = parent;
49
+ }
50
+
51
+ // Explicit workspace root (covers hoisted-to-root installs)
52
+ const wsRoot = workspaceRoot || (await findWorkspaceRoot(root));
53
+ if (wsRoot) {
54
+ const wsPath = path.join(wsRoot, urlPath);
55
+ if (!visited.has(wsPath)) {
56
+ try {
57
+ const resolved = await fs.realpath(wsPath);
58
+ await fs.access(resolved);
59
+ return resolved;
60
+ } catch {
61
+ // not found there either
62
+ }
63
+ }
64
+ }
65
+
66
+ return path.join(path.resolve(root), urlPath); // fallback; handler will 404
67
+ }
68
+
69
+ // /swiss-packages/ URLs point to files in the co-located framework monorepo's packages/ dir
70
+ if (url.startsWith("/swiss-packages/")) {
71
+ const relativePath = url.replace(/^\/swiss-packages\//, "");
72
+ const monorepo = await findSwissLibMonorepo(root);
73
+ if (monorepo) {
74
+ const fullPath = path.join(monorepo, "packages", relativePath);
75
+ try {
76
+ await fs.access(fullPath);
77
+ return fullPath;
78
+ } catch {
79
+ return fullPath; // Return anyway; handler will 404 if missing
80
+ }
81
+ }
82
+ // No co-located monorepo found — return a path that will 404 cleanly
83
+ console.warn(`[file-path-resolver] No framework monorepo found for /swiss-packages/${relativePath}`);
84
+ return path.join(root, "node_modules", relativePath);
85
+ }
86
+
87
+ // Workspace-level directories: always resolve from workspace root
88
+ // Updated: lib/ now contains all packages (moved from packages/)
89
+ if (
90
+ url.startsWith("/lib/") ||
91
+ url.startsWith("/libraries/") ||
92
+ url.startsWith("/packages/") ||
93
+ url.startsWith("/modules/")
94
+ ) {
95
+ let wsRoot = workspaceRoot;
96
+ if (!wsRoot) {
97
+ wsRoot = await findWorkspaceRoot(root);
98
+ console.log(`[file-path-resolver] Detected workspace root: ${wsRoot} (from app root: ${root})`);
99
+ }
100
+
101
+ // Normalize URL: path.join with leading slash is wrong on Windows (treats as drive root)
102
+ const urlPath = url.startsWith("/") ? url.slice(1) : url;
103
+
104
+ // CRITICAL: For /lib/ paths, we MUST find the SWS root (which has lib/ directory)
105
+ // Start from app root and walk up until we find a directory with both pnpm-workspace.yaml AND lib/
106
+ if (url.startsWith("/lib/")) {
107
+ let current = root;
108
+ for (let i = 0; i < 10; i++) {
109
+ const workspaceFile = path.join(current, "pnpm-workspace.yaml");
110
+ const libDir = path.join(current, "lib");
111
+ try {
112
+ await fs.access(workspaceFile);
113
+ await fs.access(libDir);
114
+ // Found SWS root!
115
+ const resolved = path.join(current, urlPath);
116
+ console.log(`[file-path-resolver] Found SWS root with lib/: ${current}`);
117
+ console.log(`[file-path-resolver] Resolving ${url} from SWS root: ${current} -> ${resolved}`);
118
+ return resolved;
119
+ } catch {
120
+ // Continue searching up
121
+ }
122
+ const parent = path.dirname(current);
123
+ if (parent === current) break;
124
+ current = parent;
125
+ }
126
+ }
127
+
128
+ // For other paths, use detected workspace root
129
+ if (wsRoot) {
130
+ let resolved = path.join(wsRoot, urlPath);
131
+ // Dev fallback: if URL is /packages/.../dist/... and file doesn't exist, try src/ (unbuilt workspace packages)
132
+ if (
133
+ (url.startsWith("/packages/") || url.startsWith("/lib/")) &&
134
+ url.includes("/dist/")
135
+ ) {
136
+ try {
137
+ await fs.access(resolved);
138
+ } catch {
139
+ const srcUrl = urlPath.replace("/dist/", "/src/").replace(/\.js$/, ".ts");
140
+ const srcResolved = path.join(wsRoot, srcUrl);
141
+ try {
142
+ await fs.access(srcResolved);
143
+ console.log(`[file-path-resolver] dist not found, serving src: ${resolved} -> ${srcResolved}`);
144
+ return srcResolved;
145
+ } catch {
146
+ // Keep original resolved; handler will 404
147
+ }
148
+ }
149
+ }
150
+ console.log(`[file-path-resolver] Resolving ${url} from workspace root: ${wsRoot} -> ${resolved}`);
151
+ return resolved;
152
+ } else {
153
+ console.warn(`[file-path-resolver] No workspace root found, using app root: ${root}`);
154
+ return path.join(root, urlPath);
155
+ }
156
+ }
157
+
158
+ // For app files, check if URL already includes the app path
159
+ const wsRoot = workspaceRoot || (await findWorkspaceRoot(root));
160
+ if (wsRoot) {
161
+ const appRelativeToWorkspace = path
162
+ .relative(wsRoot, root)
163
+ .replace(/\\/g, "/");
164
+ if (url.startsWith(`/${appRelativeToWorkspace}/`)) {
165
+ // URL already includes app path, use workspace root
166
+ return path.join(wsRoot, url);
167
+ } else if (
168
+ url.startsWith("/src/") ||
169
+ url.startsWith("/public/") ||
170
+ url.startsWith("/assets/")
171
+ ) {
172
+ // App-specific paths (src/, public/, assets/) - resolve from app root
173
+ return path.join(root, url);
174
+ } else if (url.startsWith("/")) {
175
+ // Other absolute URLs, try workspace root first, then app root
176
+ const workspacePath = path.join(wsRoot, url);
177
+ try {
178
+ await fs.access(workspacePath);
179
+ return workspacePath;
180
+ } catch {
181
+ return path.join(root, url);
182
+ }
183
+ } else {
184
+ // Relative to app root
185
+ return path.join(root, url);
186
+ }
187
+ } else {
188
+ return path.join(root, url);
189
+ }
190
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Centralised /swiss-lib/ → /swiss-packages/ path fixup.
3
+ *
4
+ * Root cause: the UiCompiler emits absolute `/swiss-lib/` paths in some code
5
+ * paths (compiler was written against an older directory structure). Until the
6
+ * compiler is fixed at source this single function is the authoritative fixup.
7
+ * Apply it once per compilation, before passing code to the import rewriter.
8
+ *
9
+ * All seven previous fixup locations across ui-handler, uix-handler, and
10
+ * import-rewriter have been removed in favour of this call.
11
+ */
12
+ export function fixSwissLibPaths(code: string): string {
13
+ if (!code.includes('/swiss-lib/')) return code;
14
+ // More-specific pattern first so `/swiss-lib/packages/` doesn't leave a
15
+ // dangling `/swiss-packages/packages/` if the replacement ran twice.
16
+ return code
17
+ .replace(/\/swiss-lib\/packages\//g, '/swiss-packages/')
18
+ .replace(/\/swiss-lib\//g, '/swiss-packages/');
19
+ }