@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.
- package/CHANGELOG.md +30 -0
- package/DIRECTIVE.md +57 -2
- package/__tests__/import-rewriter-bug.test.ts +100 -113
- package/__tests__/security-r001-r002.test.ts +190 -0
- package/dist/build-engine/builder.js +9 -9
- package/dist/cli.js +0 -0
- package/dist/config/config.d.ts +0 -5
- package/dist/config/config.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
- package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.js +91 -0
- package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/ui-handler.js +2 -64
- package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/uix-handler.js +2 -58
- package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
- package/dist/dev-engine/hmr/hmr.d.ts +10 -1
- package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
- package/dist/dev-engine/hmr/hmr.js +40 -2
- package/dist/dev-engine/middleware/middleware-setup.js +4 -3
- package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
- package/dist/dev-engine/middleware/static-files.js +145 -62
- package/dist/dev-engine/pythonDevManager.js +1 -1
- package/dist/dev-engine/router/file-router.d.ts.map +1 -1
- package/dist/dev-engine/router/file-router.js +2 -29
- package/dist/dev-engine/server.d.ts +7 -0
- package/dist/dev-engine/server.d.ts.map +1 -1
- package/dist/dev-engine/server.js +31 -3
- package/dist/kernel/package-finder.d.ts +0 -8
- package/dist/kernel/package-finder.d.ts.map +1 -1
- package/dist/kernel/package-finder.js +2 -2
- package/dist/kernel/package-registry.d.ts +6 -0
- package/dist/kernel/package-registry.d.ts.map +1 -1
- package/dist/kernel/package-registry.js +8 -0
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +12 -9
- package/docs/architecture/build-pipeline.md +97 -97
- package/docs/architecture/dev-server.md +87 -87
- package/docs/architecture/hmr.md +78 -78
- package/docs/architecture/import-rewriting.md +101 -101
- package/docs/architecture/index.md +16 -16
- package/docs/architecture/python-integration.md +93 -93
- package/docs/architecture/resolution.md +92 -92
- package/docs/cli/build.md +78 -78
- package/docs/cli/dev.md +90 -90
- package/docs/cli/index.md +15 -15
- package/docs/cli/start.md +45 -45
- package/docs/development/contributing.md +74 -74
- package/docs/development/index.md +12 -12
- package/docs/development/internals.md +101 -101
- package/docs/guide/configuration.md +89 -89
- package/docs/guide/index.md +13 -13
- package/docs/guide/project-structure.md +75 -75
- package/docs/guide/quickstart.md +113 -113
- package/docs/index.md +16 -16
- package/package.json +29 -16
- package/src/build-engine/builder.ts +9 -9
- package/src/config/config.ts +0 -5
- package/src/config/env.ts +98 -98
- package/src/dev-engine/handlers/base-handler.ts +109 -0
- package/src/dev-engine/handlers/ui-handler.ts +30 -110
- package/src/dev-engine/handlers/uix-handler.ts +21 -95
- package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
- package/src/dev-engine/hmr/hmr.ts +46 -1
- package/src/dev-engine/middleware/middleware-setup.ts +354 -354
- package/src/dev-engine/middleware/static-files.ts +203 -121
- package/src/dev-engine/pythonDevManager.ts +1 -1
- package/src/dev-engine/router/file-router.ts +2 -45
- package/src/dev-engine/server.ts +33 -3
- package/src/kernel/package-finder.ts +2 -2
- package/src/kernel/package-registry.ts +9 -0
- package/src/kernel/workspace.ts +8 -10
- package/src/resolution/cdn/cdn-fallback.ts +40 -40
- package/src/resolution/path/path-fixup.ts +27 -27
- package/src/resolution/rewriting/import-rewriter.ts +237 -237
- 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
|
+
}
|