@swissjs/swite 0.3.5 → 0.4.2

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 (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/DIRECTIVE.md +57 -2
  3. package/__tests__/import-rewriter-bug.test.ts +100 -113
  4. package/__tests__/security-r001-r002.test.ts +190 -0
  5. package/dist/build-engine/builder.js +9 -9
  6. package/dist/cli.js +0 -0
  7. package/dist/config/config.d.ts +0 -5
  8. package/dist/config/config.d.ts.map +1 -1
  9. package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
  10. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  11. package/dist/dev-engine/handlers/base-handler.js +91 -0
  12. package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
  13. package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
  14. package/dist/dev-engine/handlers/ui-handler.js +2 -64
  15. package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
  16. package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
  17. package/dist/dev-engine/handlers/uix-handler.js +2 -58
  18. package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
  19. package/dist/dev-engine/hmr/hmr.d.ts +10 -1
  20. package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
  21. package/dist/dev-engine/hmr/hmr.js +40 -2
  22. package/dist/dev-engine/middleware/middleware-setup.js +4 -3
  23. package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
  24. package/dist/dev-engine/middleware/static-files.js +145 -62
  25. package/dist/dev-engine/pythonDevManager.js +1 -1
  26. package/dist/dev-engine/router/file-router.d.ts.map +1 -1
  27. package/dist/dev-engine/router/file-router.js +2 -29
  28. package/dist/dev-engine/server.d.ts +7 -0
  29. package/dist/dev-engine/server.d.ts.map +1 -1
  30. package/dist/dev-engine/server.js +31 -3
  31. package/dist/kernel/package-finder.d.ts +0 -8
  32. package/dist/kernel/package-finder.d.ts.map +1 -1
  33. package/dist/kernel/package-finder.js +2 -2
  34. package/dist/kernel/package-registry.d.ts +6 -0
  35. package/dist/kernel/package-registry.d.ts.map +1 -1
  36. package/dist/kernel/package-registry.js +8 -0
  37. package/dist/kernel/workspace.d.ts.map +1 -1
  38. package/dist/kernel/workspace.js +12 -9
  39. package/docs/architecture/build-pipeline.md +97 -97
  40. package/docs/architecture/dev-server.md +87 -87
  41. package/docs/architecture/hmr.md +78 -78
  42. package/docs/architecture/import-rewriting.md +101 -101
  43. package/docs/architecture/index.md +16 -16
  44. package/docs/architecture/python-integration.md +93 -93
  45. package/docs/architecture/resolution.md +92 -92
  46. package/docs/cli/build.md +78 -78
  47. package/docs/cli/dev.md +90 -90
  48. package/docs/cli/index.md +15 -15
  49. package/docs/cli/start.md +45 -45
  50. package/docs/development/contributing.md +74 -74
  51. package/docs/development/index.md +12 -12
  52. package/docs/development/internals.md +101 -101
  53. package/docs/guide/configuration.md +89 -89
  54. package/docs/guide/index.md +13 -13
  55. package/docs/guide/project-structure.md +75 -75
  56. package/docs/guide/quickstart.md +113 -113
  57. package/docs/index.md +16 -16
  58. package/package.json +29 -16
  59. package/src/build-engine/builder.ts +9 -9
  60. package/src/config/config.ts +0 -5
  61. package/src/config/env.ts +98 -98
  62. package/src/dev-engine/handlers/base-handler.ts +109 -0
  63. package/src/dev-engine/handlers/ui-handler.ts +30 -110
  64. package/src/dev-engine/handlers/uix-handler.ts +21 -95
  65. package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
  66. package/src/dev-engine/hmr/hmr.ts +46 -1
  67. package/src/dev-engine/middleware/middleware-setup.ts +354 -354
  68. package/src/dev-engine/middleware/static-files.ts +203 -121
  69. package/src/dev-engine/pythonDevManager.ts +1 -1
  70. package/src/dev-engine/router/file-router.ts +2 -45
  71. package/src/dev-engine/server.ts +33 -3
  72. package/src/kernel/package-finder.ts +2 -2
  73. package/src/kernel/package-registry.ts +9 -0
  74. package/src/kernel/workspace.ts +8 -10
  75. package/src/resolution/cdn/cdn-fallback.ts +40 -40
  76. package/src/resolution/path/path-fixup.ts +27 -27
  77. package/src/resolution/rewriting/import-rewriter.ts +237 -237
  78. package/src/resolution/symlink-registry.ts +114 -114
@@ -1,237 +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
- }
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
+ }