@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,66 @@
1
+ import { SwiteProxyError } from "./SwiteProxyError.js";
2
+ let _pythonConfig = null;
3
+ let _productionMode = false;
4
+ /**
5
+ * Called by swite start on startup.
6
+ * Disables localhost fallback — PYTHON_SERVICE_URL is the only valid base URL.
7
+ */
8
+ export function setProductionMode() {
9
+ _productionMode = true;
10
+ }
11
+ /**
12
+ * Called by the swite dev process manager (S-03) on startup.
13
+ * Stores the resolved python service config for use by proxyToPython.
14
+ */
15
+ export function initPythonProxy(config) {
16
+ _pythonConfig = config;
17
+ }
18
+ /**
19
+ * Proxy a request from the Node server to the internal Python service.
20
+ *
21
+ * Resolves base URL from PYTHON_SERVICE_URL env var if set,
22
+ * otherwise falls back to http://localhost:{python.port} from config.
23
+ *
24
+ * Always injects X-Internal-Token header.
25
+ * Throws SwiteProxyError on non-2xx responses.
26
+ */
27
+ export async function proxyToPython(options) {
28
+ const envBaseUrl = process.env["PYTHON_SERVICE_URL"];
29
+ let baseUrl;
30
+ if (envBaseUrl) {
31
+ baseUrl = envBaseUrl.replace(/\/$/, "");
32
+ }
33
+ else if (!_productionMode && _pythonConfig) {
34
+ baseUrl = `http://localhost:${_pythonConfig.port}`;
35
+ }
36
+ else {
37
+ throw new Error(_productionMode
38
+ ? "PYTHON_SERVICE_URL is required in production mode but is not set."
39
+ : "Python service not configured. Call initPythonProxy() before using proxyToPython, or set PYTHON_SERVICE_URL.");
40
+ }
41
+ const token = process.env["INTERNAL_API_TOKEN"] ?? "";
42
+ const url = `${baseUrl}${options.path}`;
43
+ const requestHeaders = {
44
+ "X-Internal-Token": token,
45
+ ...options.headers,
46
+ };
47
+ if (options.body !== undefined) {
48
+ requestHeaders["Content-Type"] = "application/json";
49
+ }
50
+ const response = await fetch(url, {
51
+ method: options.method,
52
+ headers: requestHeaders,
53
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
54
+ });
55
+ if (!response.ok) {
56
+ let responseBody;
57
+ try {
58
+ responseBody = await response.json();
59
+ }
60
+ catch {
61
+ responseBody = await response.text();
62
+ }
63
+ throw new SwiteProxyError(response.status, `Python service responded with ${response.status} on ${options.method} ${options.path}`, responseBody);
64
+ }
65
+ return response.json();
66
+ }
@@ -0,0 +1,9 @@
1
+ import type { UrlResolverContext } from "./url-resolver.js";
2
+ export interface BareImportResolverContext extends UrlResolverContext {
3
+ resolveWorkspacePackage: (pkgName: string) => Promise<string | null>;
4
+ }
5
+ /**
6
+ * Resolve bare import specifier (e.g., @kibologic/core, react, etc.)
7
+ */
8
+ export declare function resolveBareImport(specifier: string, context: BareImportResolverContext): Promise<string>;
9
+ //# sourceMappingURL=bare-import-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bare-import-resolver.d.ts","sourceRoot":"","sources":["../../src/resolver/bare-import-resolver.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAmC,MAAM,mBAAmB,CAAC;AAI7F,MAAM,WAAW,yBAA0B,SAAQ,kBAAkB;IACnE,uBAAuB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACtE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CAqQjB"}
@@ -0,0 +1,363 @@
1
+ /*
2
+ * Bare Import Resolver - Resolves bare module specifiers (@kibologic/core, etc.)
3
+ * Extracted from resolver.ts for modularity
4
+ */
5
+ import path from "node:path";
6
+ import { promises as fs } from "node:fs";
7
+ import chalk from "chalk";
8
+ import { findSwissLibMonorepo } from "../utils/package-finder.js";
9
+ import { shouldUseCdnFallback } from "../utils/cdn-fallback.js";
10
+ import { resolveWorkspacePackage } from "./workspace-package-resolver.js";
11
+ import { toUrl } from "./url-resolver.js";
12
+ /**
13
+ * Resolve bare import specifier (e.g., @kibologic/core, react, etc.)
14
+ */
15
+ export async function resolveBareImport(specifier, context) {
16
+ console.log(`[SWITE] resolveBareImport CALLED: ${specifier}`);
17
+ // Extract package name outside the try/catch so fallback logic can reference it.
18
+ // This must stay project-agnostic: works for both scoped and unscoped packages.
19
+ const parts = specifier.split("/");
20
+ const isScoped = specifier.startsWith("@");
21
+ const pkgName = isScoped ? `${parts[0]}/${parts[1]}` : parts[0];
22
+ const subPath = isScoped ? parts.slice(2).join("/") : parts.slice(1).join("/");
23
+ try {
24
+ console.log(`[SWITE] resolveBareImport: ${specifier} -> pkgName: ${pkgName}, subPath: ${subPath}`);
25
+ // Find package.json - check multiple node_modules locations
26
+ let pkgDir = null;
27
+ let pkgJsonPath = null;
28
+ const nodeModulesLocations = [
29
+ path.join(context.root, "node_modules"),
30
+ path.join(path.dirname(context.root), "node_modules"),
31
+ ];
32
+ // Add workspace root node_modules
33
+ const workspaceRoot = await context.getWorkspaceRoot();
34
+ if (workspaceRoot) {
35
+ nodeModulesLocations.push(path.join(workspaceRoot, "node_modules"));
36
+ }
37
+ // Add swiss-lib monorepo node_modules
38
+ const swissLib = await findSwissLibMonorepo(context.root);
39
+ if (swissLib) {
40
+ const swissNodeModules = path.join(swissLib, "node_modules");
41
+ if (await context.fileExists(swissNodeModules)) {
42
+ nodeModulesLocations.push(swissNodeModules);
43
+ console.log(`[SWITE] Added swiss-lib monorepo node_modules for ${pkgName}`);
44
+ }
45
+ }
46
+ // Try each location
47
+ for (const nodeModulesPath of nodeModulesLocations) {
48
+ const testPkgDir = path.join(nodeModulesPath, pkgName);
49
+ const testPkgJsonPath = path.join(testPkgDir, "package.json");
50
+ if (await context.fileExists(testPkgJsonPath)) {
51
+ pkgDir = testPkgDir;
52
+ pkgJsonPath = testPkgJsonPath;
53
+ console.log(`[SWITE] Found ${pkgName} in ${nodeModulesPath}`);
54
+ break;
55
+ }
56
+ }
57
+ if (!pkgJsonPath || !pkgDir) {
58
+ console.log(`[SWITE] Package ${pkgName} not in node_modules, checking workspace...`);
59
+ // EMERGENCY FIX: For @kibologic/* packages, try direct path first
60
+ if (pkgName.startsWith('@kibologic/')) {
61
+ const pkgShortName = pkgName.replace('@kibologic/', '');
62
+ // Try relative path from SWS to swiss-lib
63
+ const potentialPaths = [
64
+ path.join(context.root, '../swiss-lib/packages', pkgShortName),
65
+ path.join(context.root, '../../swiss-lib/packages', pkgShortName),
66
+ path.join(context.root, '../../../swiss-lib/packages', pkgShortName),
67
+ ];
68
+ for (const candidatePath of potentialPaths) {
69
+ const candidatePkgJson = path.join(candidatePath, 'package.json');
70
+ if (await context.fileExists(candidatePkgJson)) {
71
+ console.log(`[SWITE] Emergency: Found ${pkgName} at ${candidatePath}`);
72
+ pkgDir = candidatePath;
73
+ pkgJsonPath = candidatePkgJson;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ // If still not found, try workspace resolver
79
+ if (!pkgJsonPath || !pkgDir) {
80
+ const workspacePkg = await context.resolveWorkspacePackage(pkgName);
81
+ if (workspacePkg) {
82
+ return await resolveWorkspacePackageEntry(workspacePkg, pkgName, subPath, specifier, context);
83
+ }
84
+ // Last resort: CDN fallback
85
+ if (!shouldUseCdnFallback(pkgName)) {
86
+ // Scoped packages may be private and are not necessarily available on public npm CDNs.
87
+ console.warn(`[SWITE] Package ${pkgName} not found anywhere. Scoped package detected; CDN fallback is disabled by default.`);
88
+ // Return a same-origin node_modules URL to make failures explicit and allow the
89
+ // node-module handler to try serving it.
90
+ return `/node_modules/${specifier}`;
91
+ }
92
+ console.warn(`[SWITE] Package ${pkgName} not found anywhere, using CDN fallback`);
93
+ return `https://cdn.jsdelivr.net/npm/${specifier}/+esm`;
94
+ }
95
+ }
96
+ // Continue with normal resolution if we found it
97
+ if (pkgJsonPath && pkgDir) {
98
+ return await resolveWorkspacePackageEntry(pkgDir, pkgName, subPath, specifier, context);
99
+ }
100
+ // Check if this is a workspace package (symlinked)
101
+ let realPkgDir;
102
+ try {
103
+ realPkgDir = await fs.realpath(pkgDir);
104
+ }
105
+ catch {
106
+ realPkgDir = pkgDir;
107
+ }
108
+ const workspacePkg = await context.resolveWorkspacePackage(pkgName);
109
+ console.log(`[SWITE] resolveBareImport (node_modules): workspacePkg=${workspacePkg}, realPkgDir=${realPkgDir}, pkgName=${pkgName}`);
110
+ if (workspacePkg) {
111
+ const normalizedWorkspacePkg = path
112
+ .resolve(workspacePkg)
113
+ .replace(/\\/g, "/")
114
+ .toLowerCase();
115
+ const normalizedRealPkgDir = path
116
+ .resolve(realPkgDir)
117
+ .replace(/\\/g, "/")
118
+ .toLowerCase();
119
+ // If the real path is the workspace package, use workspace resolution
120
+ if (normalizedRealPkgDir === normalizedWorkspacePkg ||
121
+ normalizedRealPkgDir.includes(normalizedWorkspacePkg)) {
122
+ console.log(`[SWITE] resolveBareImport (node_modules): Using workspace resolution for ${pkgName}`);
123
+ return await resolveWorkspacePackageEntry(workspacePkg, pkgName, subPath, specifier, context);
124
+ }
125
+ }
126
+ // Read package.json for exports/main resolution
127
+ const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8"));
128
+ // Handle exports field if present
129
+ if (pkgJson.exports) {
130
+ const exportKey = subPath ? `./${subPath}` : ".";
131
+ const exportEntry = pkgJson.exports[exportKey];
132
+ if (exportEntry) {
133
+ let entryPoint;
134
+ if (typeof exportEntry === "string") {
135
+ entryPoint = exportEntry;
136
+ }
137
+ else if (exportEntry.import) {
138
+ entryPoint = exportEntry.import;
139
+ }
140
+ else if (exportEntry.default) {
141
+ entryPoint = exportEntry.default;
142
+ }
143
+ else {
144
+ entryPoint = exportEntry;
145
+ }
146
+ const fullPath = path.join(pkgDir, entryPoint);
147
+ // Prefer source over dist
148
+ const normalizedFullPath = fullPath.replace(/\\/g, "/");
149
+ if (normalizedFullPath.includes("/dist/")) {
150
+ const srcFullPath = normalizedFullPath
151
+ .replace("/dist/", "/src/")
152
+ .replace(/\.js$/, ".ts");
153
+ if (await context.fileExists(srcFullPath)) {
154
+ console.log(`[SWITE] resolveBareImport: Using source file instead of dist: ${srcFullPath}`);
155
+ return await toUrl(srcFullPath, context);
156
+ }
157
+ }
158
+ if (await context.fileExists(fullPath)) {
159
+ return await toUrl(fullPath, context);
160
+ }
161
+ // Try extensions
162
+ for (const ext of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
163
+ const withExt = fullPath.replace(/\.(js|mjs|ts|ui|uix)$/, ext);
164
+ if (await context.fileExists(withExt)) {
165
+ return await toUrl(withExt, context);
166
+ }
167
+ }
168
+ // Try case-insensitive match
169
+ const dir = path.dirname(fullPath);
170
+ const fileName = path.basename(fullPath);
171
+ try {
172
+ const files = await fs.readdir(dir);
173
+ const caseInsensitiveMatch = files.find((f) => f.toLowerCase() === fileName.toLowerCase());
174
+ if (caseInsensitiveMatch) {
175
+ const correctedPath = path.join(dir, caseInsensitiveMatch);
176
+ if (await context.fileExists(correctedPath)) {
177
+ console.log(chalk.yellow(`[SWITE] Case-insensitive match for ${pkgName}: ${fileName} -> ${caseInsensitiveMatch}`));
178
+ return await toUrl(correctedPath, context);
179
+ }
180
+ }
181
+ }
182
+ catch {
183
+ // Directory doesn't exist, continue to fallback
184
+ }
185
+ }
186
+ }
187
+ // Determine entry point
188
+ let entryPoint;
189
+ if (subPath) {
190
+ entryPoint = subPath;
191
+ }
192
+ else {
193
+ entryPoint = pkgJson.module || pkgJson.main || "index.js";
194
+ }
195
+ const fullPath = path.join(pkgDir, entryPoint);
196
+ // Try the exact path first
197
+ if (await context.fileExists(fullPath)) {
198
+ return await toUrl(fullPath, context);
199
+ }
200
+ // Try with extensions
201
+ for (const ext of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
202
+ if (await context.fileExists(fullPath + ext)) {
203
+ return await toUrl(fullPath + ext, context);
204
+ }
205
+ }
206
+ // Fallback to CDN (jsDelivr; esm.sh returns 500 for some packages) when allowed.
207
+ console.warn(`[SWITE] Could not resolve ${specifier}, using fallback`);
208
+ return shouldUseCdnFallback(pkgName)
209
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
210
+ : `/node_modules/${specifier}`;
211
+ }
212
+ catch (error) {
213
+ console.warn(`[SWITE] Error resolving ${specifier}:`, error);
214
+ return shouldUseCdnFallback(pkgName)
215
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
216
+ : `/node_modules/${specifier}`;
217
+ }
218
+ }
219
+ /**
220
+ * Resolve entry point for workspace package
221
+ */
222
+ async function resolveWorkspacePackageEntry(workspacePkg, pkgName, subPath, specifier, context) {
223
+ const workspacePkgJson = JSON.parse(await fs.readFile(path.join(workspacePkg, "package.json"), "utf-8"));
224
+ // Handle exports field if present
225
+ if (workspacePkgJson.exports) {
226
+ const subPathWithoutExt = subPath
227
+ ? subPath.replace(/\.(js|ts|ui|uix)$/, "")
228
+ : "";
229
+ let exportKey = subPathWithoutExt ? `./${subPathWithoutExt}` : ".";
230
+ console.log(`[SWITE] resolveBareImport (workspace): pkgName=${pkgName}, subPath=${subPath}, subPathWithoutExt=${subPathWithoutExt}, exportKey=${exportKey}`);
231
+ // Try to find matching export
232
+ if (subPath && !workspacePkgJson.exports[exportKey]) {
233
+ if (subPathWithoutExt) {
234
+ const withoutExtKey = `./${subPathWithoutExt}`;
235
+ if (workspacePkgJson.exports[withoutExtKey]) {
236
+ exportKey = withoutExtKey;
237
+ }
238
+ }
239
+ if (!workspacePkgJson.exports[exportKey]) {
240
+ const subPathParts = subPathWithoutExt.split("/");
241
+ if (subPathParts.length > 1) {
242
+ const dirPath = subPathParts.slice(0, -1).join("/");
243
+ const dirExportKey = `./${dirPath}`;
244
+ if (workspacePkgJson.exports[dirExportKey]) {
245
+ exportKey = dirExportKey;
246
+ }
247
+ else {
248
+ const firstDir = subPathParts[0];
249
+ const firstDirExportKey = `./${firstDir}`;
250
+ if (workspacePkgJson.exports[firstDirExportKey]) {
251
+ exportKey = firstDirExportKey;
252
+ }
253
+ else {
254
+ const lastDir = subPathParts[subPathParts.length - 2];
255
+ const lastDirExportKey = `./${lastDir}`;
256
+ if (workspacePkgJson.exports[lastDirExportKey]) {
257
+ exportKey = lastDirExportKey;
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ const exportEntry = workspacePkgJson.exports[exportKey];
265
+ if (exportEntry) {
266
+ let entryPoint;
267
+ if (typeof exportEntry === "string") {
268
+ entryPoint = exportEntry;
269
+ }
270
+ else if (exportEntry.import) {
271
+ entryPoint = exportEntry.import;
272
+ }
273
+ else if (exportEntry.default) {
274
+ entryPoint = exportEntry.default;
275
+ }
276
+ else {
277
+ entryPoint = exportEntry;
278
+ }
279
+ const normalizedEntryPoint = entryPoint.startsWith("./")
280
+ ? entryPoint.slice(2)
281
+ : entryPoint;
282
+ const fullPath = path.join(workspacePkg, normalizedEntryPoint);
283
+ // Dev: prefer src over dist for workspace packages (unbuilt or dev mode)
284
+ const normalizedFull = fullPath.replace(/\\/g, "/");
285
+ if (normalizedFull.includes("/dist/")) {
286
+ const srcPath = fullPath.replace(/[/\\]dist[/\\]/, path.sep + "src" + path.sep).replace(/\.js$/i, ".ts");
287
+ if (await context.fileExists(srcPath)) {
288
+ console.log(`[SWITE] resolveBareImport (workspace): preferring src over dist: ${srcPath}`);
289
+ return await toUrl(srcPath, context);
290
+ }
291
+ }
292
+ if (await context.fileExists(fullPath)) {
293
+ return await toUrl(fullPath, context);
294
+ }
295
+ // Try extensions
296
+ for (const ext of [".ui", ".uix", ".ts", ".js"]) {
297
+ const withExt = fullPath.replace(/\.(js|ts|ui|uix)$/, ext);
298
+ if (await context.fileExists(withExt)) {
299
+ return await toUrl(withExt, context);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ // Fallback to old logic
305
+ let entryPoint;
306
+ if (subPath) {
307
+ entryPoint = subPath;
308
+ }
309
+ else {
310
+ entryPoint =
311
+ workspacePkgJson.module || workspacePkgJson.main || "index.js";
312
+ }
313
+ const fullPath = path.join(workspacePkg, entryPoint);
314
+ // Dev: prefer src over dist for workspace packages
315
+ const normalizedFull = fullPath.replace(/\\/g, "/");
316
+ if (normalizedFull.includes("/dist/")) {
317
+ const srcPath = fullPath.replace(/[/\\]dist[/\\]/, path.sep + "src" + path.sep).replace(/\.js$/i, ".ts");
318
+ if (await context.fileExists(srcPath)) {
319
+ console.log(`[SWITE] resolveBareImport (fallback): preferring src over dist: ${srcPath}`);
320
+ return await toUrl(srcPath, context);
321
+ }
322
+ }
323
+ if (await context.fileExists(fullPath)) {
324
+ return await toUrl(fullPath, context);
325
+ }
326
+ // Try extensions
327
+ const ext = path.extname(entryPoint);
328
+ if (ext) {
329
+ const basePath = fullPath.slice(0, -ext.length);
330
+ for (const tryExt of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
331
+ if (await context.fileExists(basePath + tryExt)) {
332
+ return await toUrl(basePath + tryExt, context);
333
+ }
334
+ }
335
+ }
336
+ else {
337
+ for (const tryExt of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
338
+ if (await context.fileExists(fullPath + tryExt)) {
339
+ return await toUrl(fullPath + tryExt, context);
340
+ }
341
+ }
342
+ }
343
+ // Try index files
344
+ for (const tryExt of [".js", ".ts", ".ui", ".uix"]) {
345
+ const indexFile = path.join(fullPath, `index${tryExt}`);
346
+ if (await context.fileExists(indexFile)) {
347
+ return await toUrl(indexFile, context);
348
+ }
349
+ }
350
+ // Try src/ directory
351
+ const srcDir = path.join(workspacePkg, "src");
352
+ for (const ext of [".ts", ".ui", ".uix", ".js"]) {
353
+ const srcIndex = path.join(srcDir, `index${ext}`);
354
+ if (await context.fileExists(srcIndex)) {
355
+ console.log(`[SWITE] Found unbuilt workspace package ${pkgName} at ${srcIndex}`);
356
+ return await toUrl(srcIndex, context);
357
+ }
358
+ }
359
+ console.warn(`[SWITE] Entry point not found for ${pkgName} at ${fullPath}, using fallback`);
360
+ return shouldUseCdnFallback(pkgName)
361
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
362
+ : `/node_modules/${specifier}`;
363
+ }
@@ -0,0 +1,13 @@
1
+ export declare function buildSymlinkRegistry(nodeModulesDirs: string[]): Promise<void>;
2
+ /**
3
+ * Look up an absolute filesystem path in the symlink registry.
4
+ *
5
+ * Returns the browser URL if absolutePath is, or is within, a package whose
6
+ * realpath was registered at startup. Returns null if not found.
7
+ *
8
+ * Example:
9
+ * /mnt/c/.../swiss-lib/packages/core/src/index.ts
10
+ * → /node_modules/@kibologic/core/src/index.ts
11
+ */
12
+ export declare function lookupInSymlinkRegistry(absolutePath: string): string | null;
13
+ //# sourceMappingURL=symlink-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symlink-registry.d.ts","sourceRoot":"","sources":["../../src/resolver/symlink-registry.ts"],"names":[],"mappings":"AAoBA,wBAAsB,oBAAoB,CACxC,eAAe,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CAQf;AA8DD;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW3E"}
@@ -0,0 +1,98 @@
1
+ /*
2
+ * Symlink Registry - CG-03 root cause fix
3
+ *
4
+ * fs.realpath() throughout Swite's handler chain resolves symlinks to absolute
5
+ * filesystem paths (e.g. /mnt/c/.../swiss-lib/packages/core/src/index.ts).
6
+ * These leak into toUrl() and hit the startsWith("/") early-return before the
7
+ * proper node_modules/swiss-lib handling logic is reached.
8
+ *
9
+ * At server startup we scan node_modules directories for symlinks and build a
10
+ * map: realpath → /node_modules/<pkg-name>
11
+ * toUrl() consults this registry FIRST and short-circuits back to the correct
12
+ * browser URL.
13
+ */
14
+ import { promises as fs } from "node:fs";
15
+ import path from "node:path";
16
+ // realpath (normalized, forward slashes) → browser URL prefix
17
+ const registry = new Map();
18
+ export async function buildSymlinkRegistry(nodeModulesDirs) {
19
+ registry.clear();
20
+ for (const dir of nodeModulesDirs) {
21
+ await scanNodeModulesDir(dir);
22
+ }
23
+ console.log(`[SWITE] Symlink registry built: ${registry.size} entries from ${nodeModulesDirs.length} node_modules dirs`);
24
+ }
25
+ async function scanNodeModulesDir(nodeModulesDir) {
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ let dirents;
28
+ try {
29
+ dirents = await fs.readdir(nodeModulesDir, {
30
+ withFileTypes: true,
31
+ encoding: "utf8",
32
+ });
33
+ }
34
+ catch {
35
+ return; // dir doesn't exist — skip silently
36
+ }
37
+ for (const dirent of dirents) {
38
+ if (dirent.name.startsWith("."))
39
+ continue;
40
+ if (dirent.name.startsWith("@")) {
41
+ // Scoped scope directory — scan one level deeper
42
+ const scopeDir = path.join(nodeModulesDir, dirent.name);
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ let scopedDirents;
45
+ try {
46
+ scopedDirents = await fs.readdir(scopeDir, {
47
+ withFileTypes: true,
48
+ encoding: "utf8",
49
+ });
50
+ }
51
+ catch {
52
+ continue;
53
+ }
54
+ for (const scoped of scopedDirents) {
55
+ if (scoped.isSymbolicLink()) {
56
+ await registerSymlink(path.join(scopeDir, scoped.name), `${dirent.name}/${scoped.name}`);
57
+ }
58
+ }
59
+ }
60
+ else if (dirent.isSymbolicLink()) {
61
+ await registerSymlink(path.join(nodeModulesDir, dirent.name), dirent.name);
62
+ }
63
+ }
64
+ }
65
+ async function registerSymlink(symlinkPath, pkgName) {
66
+ try {
67
+ const realPath = await fs.realpath(symlinkPath);
68
+ const key = realPath.replace(/\\/g, "/");
69
+ const value = `/node_modules/${pkgName}`;
70
+ registry.set(key, value);
71
+ console.log(`[SWITE] Registry: ${pkgName}: ${key} → ${value}`);
72
+ }
73
+ catch {
74
+ // broken symlink — ignore
75
+ }
76
+ }
77
+ /**
78
+ * Look up an absolute filesystem path in the symlink registry.
79
+ *
80
+ * Returns the browser URL if absolutePath is, or is within, a package whose
81
+ * realpath was registered at startup. Returns null if not found.
82
+ *
83
+ * Example:
84
+ * /mnt/c/.../swiss-lib/packages/core/src/index.ts
85
+ * → /node_modules/@kibologic/core/src/index.ts
86
+ */
87
+ export function lookupInSymlinkRegistry(absolutePath) {
88
+ const normalized = absolutePath.replace(/\\/g, "/");
89
+ for (const [realPkgPath, browserPrefix] of registry) {
90
+ if (normalized === realPkgPath) {
91
+ return browserPrefix;
92
+ }
93
+ if (normalized.startsWith(realPkgPath + "/")) {
94
+ return browserPrefix + normalized.slice(realPkgPath.length);
95
+ }
96
+ }
97
+ return null;
98
+ }
@@ -0,0 +1,11 @@
1
+ export interface UrlResolverContext {
2
+ root: string;
3
+ getWorkspaceRoot: () => Promise<string | null>;
4
+ fileExists: (filePath: string) => Promise<boolean>;
5
+ }
6
+ export type WorkspacePackageResolverContext = UrlResolverContext;
7
+ /**
8
+ * Convert file path to URL for browser
9
+ */
10
+ export declare function toUrl(filePath: string, context: UrlResolverContext): Promise<string>;
11
+ //# sourceMappingURL=url-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-resolver.d.ts","sourceRoot":"","sources":["../../src/resolver/url-resolver.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/C,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AAED,MAAM,MAAM,+BAA+B,GAAG,kBAAkB,CAAC;AAoBjE;;GAEG;AACH,wBAAsB,KAAK,CACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA4QjB"}