@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,198 @@
1
+ /*
2
+ * Module Resolver for SWITE
3
+ */
4
+
5
+ import path from "node:path";
6
+ import { promises as fs } from "node:fs";
7
+ import chalk from "chalk";
8
+ import type { ImportMap } from "../internal/generate-import-map.js";
9
+ import { findWorkspaceRoot } from "../kernel/workspace.js";
10
+ import { toUrl, type UrlResolverContext, type WorkspacePackageResolverContext } from "./url-resolver.js";
11
+ import { resolveWorkspacePackage } from "./workspace-package-resolver.js";
12
+ import { resolveBareImport, type BareImportResolverContext } from "./bare-import-resolver.js";
13
+
14
+ export class ModuleResolver {
15
+ private workspaceRoot: string | null = null;
16
+ private importMap: ImportMap | null = null;
17
+
18
+ constructor(private root: string) {}
19
+
20
+ /**
21
+ * Set pre-resolved import map (from build-time generation)
22
+ */
23
+ setImportMap(importMap: ImportMap | null): void {
24
+ this.importMap = importMap;
25
+ if (importMap) {
26
+ console.log(
27
+ chalk.green(
28
+ `[Resolver] Loaded import map with ${Object.keys(importMap).length - 2} entries`,
29
+ ),
30
+ );
31
+ }
32
+ }
33
+
34
+ private async getWorkspaceRoot(): Promise<string | null> {
35
+ if (this.workspaceRoot) return this.workspaceRoot;
36
+ this.workspaceRoot = await findWorkspaceRoot(this.root);
37
+ return this.workspaceRoot;
38
+ }
39
+
40
+ async resolve(specifier: string, importer: string): Promise<string> {
41
+ // Check import map first (fast path)
42
+ if (this.importMap && !specifier.startsWith(".") && !specifier.startsWith("/")) {
43
+ const mapped = this.importMap.imports[specifier];
44
+ if (mapped) {
45
+ console.log(
46
+ chalk.green(`[Resolver] ✅ Import map hit: ${specifier} -> ${mapped}`),
47
+ );
48
+ return mapped;
49
+ }
50
+ }
51
+
52
+ // CRITICAL: Skip variable references - they should never be resolved as modules
53
+ // Variables like def.componentUrl, someVar, etc. should be left as-is
54
+ // Only resolve actual module specifiers (bare imports starting with @ or valid package names)
55
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
56
+ // Check if this looks like a variable reference (property access, camelCase without @, etc.)
57
+ // Valid module specifiers: @scope/name, package-name, ./relative, /absolute
58
+ // Variable references: def.componentUrl, someVar, obj.prop, etc.
59
+ if (specifier.includes(".") && !specifier.startsWith("@")) {
60
+ // Property access pattern (def.componentUrl) - this is a variable, not a module
61
+ console.warn(
62
+ `[SWITE] resolve: Skipping variable reference: ${specifier}`,
63
+ );
64
+ return specifier; // Return as-is, don't try to resolve
65
+ }
66
+
67
+ // Additional check: if it doesn't look like a valid package name, it might be a variable
68
+ // Valid package names: start with letter or @, contain only alphanumeric, -, _, /
69
+ // Also allow file extensions at the end: .js, .ts, .ui, .uix, etc.
70
+ if (
71
+ !/^[@a-zA-Z][a-zA-Z0-9_/@-]*(\.(js|ts|ui|uix|mjs|cjs|jsx|tsx))?$/.test(
72
+ specifier,
73
+ )
74
+ ) {
75
+ console.warn(
76
+ `[SWITE] resolve: Invalid module specifier (likely variable): ${specifier}`,
77
+ );
78
+ return specifier; // Return as-is
79
+ }
80
+
81
+ const context: BareImportResolverContext = {
82
+ root: this.root,
83
+ getWorkspaceRoot: () => this.getWorkspaceRoot(),
84
+ fileExists: (p) => this.fileExists(p),
85
+ resolveWorkspacePackage: (pkgName) => this.resolveWorkspacePackage(pkgName),
86
+ };
87
+ return await resolveBareImport(specifier, context);
88
+ }
89
+
90
+ // Handle absolute paths (already URLs)
91
+ if (specifier.startsWith("/")) {
92
+ return specifier;
93
+ }
94
+
95
+ // Handle relative imports
96
+ // importer might be a URL path (/src/modules/index.ui) or file path
97
+ // Convert URL to file path if needed
98
+ let importerPath = importer;
99
+ if (importer.startsWith("/")) {
100
+ // URL path - convert to file path
101
+ // Prioritize app root for app files (src/, public/, assets/)
102
+ if (
103
+ importer.startsWith("/src/") ||
104
+ importer.startsWith("/public/") ||
105
+ importer.startsWith("/assets/")
106
+ ) {
107
+ // App-specific paths - resolve from app root
108
+ importerPath = path.join(this.root, importer);
109
+ } else {
110
+ // Other paths - try workspace root first (for libraries/, packages/)
111
+ const workspaceRoot = await this.getWorkspaceRoot();
112
+ if (workspaceRoot) {
113
+ const workspacePath = path.join(workspaceRoot, importer);
114
+ if (await this.fileExists(workspacePath)) {
115
+ importerPath = workspacePath;
116
+ } else {
117
+ // Fallback to app root
118
+ importerPath = path.join(this.root, importer);
119
+ }
120
+ } else {
121
+ // No workspace root, use app root
122
+ importerPath = path.join(this.root, importer);
123
+ }
124
+ }
125
+ }
126
+ const importerDir = path.dirname(importerPath);
127
+
128
+ // If specifier already has an extension (.ui, .uix, .ts, .js, etc.), try it first
129
+ // This preserves .ui/.uix extensions for SWISS files
130
+ const hasExtension = /\.(ui|uix|ts|tsx|js|jsx|mjs)$/.test(specifier);
131
+
132
+ if (hasExtension) {
133
+ const resolved = path.resolve(importerDir, specifier);
134
+ if (await this.fileExists(resolved)) {
135
+ return await this.toUrl(resolved);
136
+ }
137
+ }
138
+
139
+ // If no extension or file not found, try adding extensions
140
+ // Strip any existing extension from specifier (but preserve .ui/.uix if present)
141
+ const specifierWithoutExt = specifier.replace(/\.(js|ts|jsx|tsx|mjs)$/, "");
142
+ const resolved = path.resolve(importerDir, specifierWithoutExt);
143
+
144
+ // Try adding extensions (prioritize .ui and .uix for SWISS files)
145
+ const extensions = [".ui", ".uix", ".ts", ".tsx", ".js", ".jsx", ".mjs"];
146
+
147
+ for (const ext of extensions) {
148
+ const withExt = resolved + ext;
149
+ if (await this.fileExists(withExt)) {
150
+ return await this.toUrl(withExt);
151
+ }
152
+ }
153
+
154
+ // Try index files
155
+ for (const ext of extensions) {
156
+ const indexFile = path.join(resolved, `index${ext}`);
157
+ if (await this.fileExists(indexFile)) {
158
+ return await this.toUrl(indexFile);
159
+ }
160
+ }
161
+
162
+ // Return as-is if nothing found
163
+ return await this.toUrl(resolved);
164
+ }
165
+
166
+ private async fileExists(filePath: string): Promise<boolean> {
167
+ try {
168
+ await fs.access(filePath);
169
+ return true;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ private async resolveWorkspacePackage(
176
+ pkgName: string,
177
+ ): Promise<string | null> {
178
+ const context: WorkspacePackageResolverContext = {
179
+ root: this.root,
180
+ getWorkspaceRoot: () => this.getWorkspaceRoot(),
181
+ fileExists: (p) => this.fileExists(p),
182
+ };
183
+ return resolveWorkspacePackage(pkgName, context);
184
+ }
185
+
186
+ // OLD IMPLEMENTATION REMOVED - now uses resolver/workspace-package-resolver.ts
187
+
188
+ private async toUrl(filePath: string): Promise<string> {
189
+ const context: UrlResolverContext = {
190
+ root: this.root,
191
+ getWorkspaceRoot: () => this.getWorkspaceRoot(),
192
+ fileExists: (p) => this.fileExists(p),
193
+ };
194
+ return toUrl(filePath, context);
195
+ }
196
+
197
+ // OLD IMPLEMENTATION REMOVED - now uses resolver/url-resolver.ts
198
+ }
@@ -0,0 +1,237 @@
1
+ /*
2
+ * Import Rewriter for SWITE
3
+ *
4
+ * Design: collect-then-apply-right-to-left
5
+ *
6
+ * es-module-lexer gives positions {s, e} in the ORIGINAL string. The previous
7
+ * implementation tracked a running `offset` as replacements were applied, which
8
+ * accumulated errors when quote handling changed string lengths in unexpected
9
+ * ways and required three layers of fallback replacement logic.
10
+ *
11
+ * Instead we now:
12
+ * 1. Collect every replacement as {start, end, text} in original-string coordinates
13
+ * 2. Sort descending by start position
14
+ * 3. Apply right-to-left — each substitution cannot shift the position of any
15
+ * replacement to its left, so no offset tracking is needed at all.
16
+ */
17
+
18
+ import { init, parse } from "es-module-lexer";
19
+ import { ModuleResolver } from "../resolver.js";
20
+ import { promises as fs } from "node:fs";
21
+ import path from "node:path";
22
+ import chalk from "chalk";
23
+ import { shouldUseCdnFallback } from "../cdn/cdn-fallback.js";
24
+
25
+ interface Replacement {
26
+ start: number;
27
+ end: number;
28
+ text: string;
29
+ }
30
+
31
+ export async function rewriteImports(
32
+ code: string,
33
+ importer: string,
34
+ resolver: ModuleResolver,
35
+ ): Promise<string> {
36
+ await init;
37
+
38
+ try {
39
+ const [imports] = parse(code);
40
+ if (imports.length === 0) return code;
41
+
42
+ const replacements: Replacement[] = [];
43
+
44
+ for (const imp of imports) {
45
+ const { s: rawStart, e: rawEnd } = imp;
46
+ const rawSpecifier = code.slice(rawStart, rawEnd);
47
+
48
+ // Skip CSS imports — handled as static assets
49
+ if (rawSpecifier.includes(".css")) continue;
50
+
51
+ // Determine actual specifier string and the span in `code` that includes quotes
52
+ const { specifier, start, end } = resolveQuotedSpan(code, rawSpecifier, rawStart, rawEnd);
53
+ if (specifier === null) continue;
54
+
55
+ // Fix compiler bug: .uix/.ui imports emitted as .js or .tsx
56
+ if (
57
+ specifier.startsWith(".") &&
58
+ (specifier.endsWith(".js") || specifier.endsWith(".tsx")) &&
59
+ !specifier.includes("node_modules")
60
+ ) {
61
+ const newExt = await resolveExtensionFix(specifier, importer);
62
+ if (newExt) {
63
+ const base = specifier.endsWith(".tsx") ? specifier.slice(0, -4) : specifier.slice(0, -3);
64
+ replacements.push({ start, end, text: `"${base}${newExt}"` });
65
+ continue;
66
+ }
67
+ }
68
+
69
+ // Skip relative and absolute path imports (already resolved)
70
+ if (specifier.startsWith(".") || specifier.startsWith("/")) continue;
71
+
72
+ if (!/^[@a-zA-Z]/.test(specifier)) {
73
+ console.warn(`[SWITE] import-rewriter: Invalid specifier format: ${specifier}`);
74
+ continue;
75
+ }
76
+
77
+ // Resolve bare import
78
+ let resolved: string;
79
+ try {
80
+ resolved = await resolver.resolve(specifier, importer);
81
+ if (!resolved || resolved === specifier || (!resolved.startsWith("/") && !resolved.startsWith("http"))) {
82
+ console.warn(chalk.yellow(`[SWITE] import-rewriter: Resolver returned invalid result for ${specifier}, using CDN fallback`));
83
+ resolved = shouldUseCdnFallback(specifier)
84
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
85
+ : `/node_modules/${specifier}`;
86
+ }
87
+ } catch (error) {
88
+ console.error(chalk.red(`[SWITE] import-rewriter: Error resolving ${specifier}:`), error);
89
+ resolved = shouldUseCdnFallback(specifier)
90
+ ? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
91
+ : `/node_modules/${specifier}`;
92
+ }
93
+
94
+ // Prefer src/ over dist/ for workspace/swiss packages in dev
95
+ if (resolved.includes("/dist/") && (resolved.includes("/swiss-packages/") || resolved.includes("/packages/"))) {
96
+ resolved = resolved.replace("/dist/", "/src/").replace(/\.js$/, ".ts");
97
+ }
98
+
99
+ replacements.push({ start, end, text: `"${resolved}"` });
100
+ }
101
+
102
+ // Apply right-to-left so earlier positions are never shifted by later replacements
103
+ replacements.sort((a, b) => b.start - a.start);
104
+ let result = code;
105
+ for (const { start, end, text } of replacements) {
106
+ result = result.slice(0, start) + text + result.slice(end);
107
+ }
108
+
109
+ // Safety net: catch any bare scoped imports the lexer may have missed
110
+ const barePattern = /(?:import|from|export)\s+['"](@[^'"]+\/[^'"]+)[^'"]*['"]/g;
111
+ for (const match of Array.from(result.matchAll(barePattern))) {
112
+ const bareImport = match[1];
113
+ if (!bareImport.startsWith("/") && !bareImport.startsWith("http") && !bareImport.startsWith(".")) {
114
+ console.error(chalk.red(`[SWITE] import-rewriter: CRITICAL — bare import "${bareImport}" still present after rewriting`));
115
+ const replacement = shouldUseCdnFallback(bareImport)
116
+ ? `https://cdn.jsdelivr.net/npm/${bareImport}/+esm`
117
+ : `/node_modules/${bareImport}`;
118
+ result = result.replace(
119
+ new RegExp(bareImport.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
120
+ replacement,
121
+ );
122
+ }
123
+ }
124
+
125
+ // Regex fallback: fix relative .js/.tsx extension mismatches the lexer may have missed
126
+ const normalizedImporter = importer.replace(/\\/g, "/");
127
+ const isSwissPackage = normalizedImporter.includes("/swiss-packages/");
128
+ const isUixFile = normalizedImporter.endsWith(".uix") || normalizedImporter.endsWith(".ui");
129
+
130
+ result = result.replace(
131
+ /from\s+(["'])(\.\.?\/[^"']*?)(\.js|\.tsx)(\1)/g,
132
+ (match, quote, importPath, _ext, endQuote) => {
133
+ if (importPath.includes("node_modules") || !importPath.startsWith(".")) return match;
134
+ const isLibPath = normalizedImporter.includes("/lib/");
135
+ let newExt: string;
136
+ if (isSwissPackage || isLibPath) {
137
+ newExt = ".ts";
138
+ } else if (isUixFile) {
139
+ newExt = normalizedImporter.endsWith(".ui") ? ".ui" : ".uix";
140
+ } else {
141
+ newExt = ".ts";
142
+ }
143
+ return `from ${quote}${importPath}${newExt}${endQuote}`;
144
+ },
145
+ );
146
+
147
+ return result;
148
+ } catch (error) {
149
+ console.error(chalk.red(`[SWITE] import-rewriter: Error rewriting imports in ${importer}:`), error);
150
+ return code;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Given a raw specifier token from es-module-lexer, find the full quoted span
156
+ * in `code` (including the surrounding quote characters) and extract the clean
157
+ * specifier string. Returns `{specifier: null}` when the span cannot be found.
158
+ */
159
+ function resolveQuotedSpan(
160
+ code: string,
161
+ rawSpecifier: string,
162
+ rawStart: number,
163
+ rawEnd: number,
164
+ ): { specifier: string | null; start: number; end: number } {
165
+ const first = rawSpecifier[0];
166
+ const last = rawSpecifier[rawSpecifier.length - 1];
167
+
168
+ // Case 1: lexer returned the specifier WITH surrounding quotes
169
+ if ((first === '"' || first === "'") && first === last) {
170
+ return {
171
+ specifier: rawSpecifier.slice(1, -1),
172
+ start: rawStart,
173
+ end: rawEnd,
174
+ };
175
+ }
176
+
177
+ // Case 2: lexer returned the bare specifier; look one char back/forward for quotes
178
+ const charBefore = rawStart > 0 ? code[rawStart - 1] : "";
179
+ const charAfter = rawEnd < code.length ? code[rawEnd] : "";
180
+ if ((charBefore === '"' || charBefore === "'") && charBefore === charAfter) {
181
+ return {
182
+ specifier: rawSpecifier,
183
+ start: rawStart - 1,
184
+ end: rawEnd + 1,
185
+ };
186
+ }
187
+
188
+ // Case 3: search nearby for a quoted pattern
189
+ const escaped = rawSpecifier.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
190
+ const pattern = new RegExp(`(['"])${escaped}\\1`);
191
+ const match = pattern.exec(code);
192
+ if (match) {
193
+ return {
194
+ specifier: rawSpecifier,
195
+ start: match.index,
196
+ end: match.index + match[0].length,
197
+ };
198
+ }
199
+
200
+ console.warn(`[SWITE] import-rewriter: Could not find quotes for specifier: ${rawSpecifier}`);
201
+ return { specifier: null, start: rawStart, end: rawEnd };
202
+ }
203
+
204
+ /**
205
+ * Determine what extension a .js/.tsx import should be rewritten to,
206
+ * based on the importer's context and the actual files present on disk.
207
+ * Returns null when no rewrite is needed.
208
+ */
209
+ async function resolveExtensionFix(specifier: string, importer: string): Promise<string | null> {
210
+ const normalizedImporter = importer.replace(/\\/g, "/");
211
+ const isSwissPackage = normalizedImporter.includes("/swiss-packages/");
212
+ const isLibPath = normalizedImporter.includes("/lib/");
213
+ const isUixFile = normalizedImporter.endsWith(".uix") || normalizedImporter.endsWith(".ui");
214
+
215
+ if (isSwissPackage || isLibPath) return ".ts";
216
+
217
+ if (isUixFile) {
218
+ const base = specifier.endsWith(".tsx") ? specifier.slice(0, -4) : specifier.slice(0, -3);
219
+ const currentDir = path.dirname(importer);
220
+ const cleanPath = base.startsWith("./") ? base.slice(2) : base;
221
+ const uiPath = path.resolve(currentDir, cleanPath + ".ui");
222
+ const uixPath = path.resolve(currentDir, cleanPath + ".uix");
223
+ try {
224
+ await fs.access(uiPath);
225
+ return ".ui";
226
+ } catch {
227
+ try {
228
+ await fs.access(uixPath);
229
+ return ".uix";
230
+ } catch {
231
+ return ".ui";
232
+ }
233
+ }
234
+ }
235
+
236
+ return ".ts";
237
+ }
@@ -0,0 +1,114 @@
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
+
15
+ import { promises as fs } from "node:fs";
16
+ import path from "node:path";
17
+
18
+ // realpath (normalized, forward slashes) → browser URL prefix
19
+ const registry = new Map<string, string>();
20
+
21
+ export async function buildSymlinkRegistry(
22
+ nodeModulesDirs: string[]
23
+ ): Promise<void> {
24
+ registry.clear();
25
+ for (const dir of nodeModulesDirs) {
26
+ await scanNodeModulesDir(dir);
27
+ }
28
+ console.log(
29
+ `[SWITE] Symlink registry built: ${registry.size} entries from ${nodeModulesDirs.length} node_modules dirs`
30
+ );
31
+ }
32
+
33
+ async function scanNodeModulesDir(nodeModulesDir: string): Promise<void> {
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ let dirents: any[];
36
+ try {
37
+ dirents = await fs.readdir(nodeModulesDir, {
38
+ withFileTypes: true,
39
+ encoding: "utf8",
40
+ });
41
+ } catch {
42
+ return; // dir doesn't exist — skip silently
43
+ }
44
+
45
+ for (const dirent of dirents) {
46
+ if (dirent.name.startsWith(".")) continue;
47
+
48
+ if (dirent.name.startsWith("@")) {
49
+ // Scoped scope directory — scan one level deeper
50
+ const scopeDir = path.join(nodeModulesDir, dirent.name);
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ let scopedDirents: any[];
53
+ try {
54
+ scopedDirents = await fs.readdir(scopeDir, {
55
+ withFileTypes: true,
56
+ encoding: "utf8",
57
+ });
58
+ } catch {
59
+ continue;
60
+ }
61
+ for (const scoped of scopedDirents) {
62
+ if (scoped.isSymbolicLink()) {
63
+ await registerSymlink(
64
+ path.join(scopeDir, scoped.name),
65
+ `${dirent.name}/${scoped.name}`
66
+ );
67
+ }
68
+ }
69
+ } else if (dirent.isSymbolicLink()) {
70
+ await registerSymlink(
71
+ path.join(nodeModulesDir, dirent.name),
72
+ dirent.name
73
+ );
74
+ }
75
+ }
76
+ }
77
+
78
+ async function registerSymlink(
79
+ symlinkPath: string,
80
+ pkgName: string
81
+ ): Promise<void> {
82
+ try {
83
+ const realPath = await fs.realpath(symlinkPath);
84
+ const key = realPath.replace(/\\/g, "/");
85
+ const value = `/node_modules/${pkgName}`;
86
+ registry.set(key, value);
87
+ console.log(`[SWITE] Registry: ${pkgName}: ${key} → ${value}`);
88
+ } catch {
89
+ // broken symlink — ignore
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Look up an absolute filesystem path in the symlink registry.
95
+ *
96
+ * Returns the browser URL if absolutePath is, or is within, a package whose
97
+ * realpath was registered at startup. Returns null if not found.
98
+ *
99
+ * Example:
100
+ * /mnt/c/.../swiss-lib/packages/core/src/index.ts
101
+ * → /node_modules/@swissjs/core/src/index.ts
102
+ */
103
+ export function lookupInSymlinkRegistry(absolutePath: string): string | null {
104
+ const normalized = absolutePath.replace(/\\/g, "/");
105
+ for (const [realPkgPath, browserPrefix] of registry) {
106
+ if (normalized === realPkgPath) {
107
+ return browserPrefix;
108
+ }
109
+ if (normalized.startsWith(realPkgPath + "/")) {
110
+ return browserPrefix + normalized.slice(realPkgPath.length);
111
+ }
112
+ }
113
+ return null;
114
+ }