@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.
- package/.changeset/config.json +11 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/publish.yml +50 -0
- package/.github/workflows/release.yml +53 -0
- package/BUILD_ANALYSIS.md +89 -0
- package/BUILD_STRATEGY.md +75 -0
- package/CHANGELOG.md +53 -0
- package/DIRECTIVE.md +488 -0
- package/__tests__/css-extraction.test.ts +261 -0
- package/__tests__/css-injection-integration.test.ts +247 -0
- package/__tests__/css-middleware.test.ts +191 -0
- package/__tests__/import-rewriter-bug.test.ts +135 -0
- package/dist/builder.d.ts +36 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +772 -0
- package/dist/cache/compilation-cache.d.ts +33 -0
- package/dist/cache/compilation-cache.d.ts.map +1 -0
- package/dist/cache/compilation-cache.js +130 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +85 -0
- package/dist/config-loader.d.ts +8 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +40 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +7 -0
- package/dist/dev/pythonDevManager.d.ts +12 -0
- package/dist/dev/pythonDevManager.d.ts.map +1 -0
- package/dist/dev/pythonDevManager.js +85 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +112 -0
- package/dist/handlers/base-handler.d.ts +21 -0
- package/dist/handlers/base-handler.d.ts.map +1 -0
- package/dist/handlers/base-handler.js +38 -0
- package/dist/handlers/js-handler.d.ts +10 -0
- package/dist/handlers/js-handler.d.ts.map +1 -0
- package/dist/handlers/js-handler.js +87 -0
- package/dist/handlers/mjs-handler.d.ts +8 -0
- package/dist/handlers/mjs-handler.d.ts.map +1 -0
- package/dist/handlers/mjs-handler.js +44 -0
- package/dist/handlers/node-module-handler.d.ts +16 -0
- package/dist/handlers/node-module-handler.d.ts.map +1 -0
- package/dist/handlers/node-module-handler.js +267 -0
- package/dist/handlers/ts-handler.d.ts +11 -0
- package/dist/handlers/ts-handler.d.ts.map +1 -0
- package/dist/handlers/ts-handler.js +120 -0
- package/dist/handlers/ui-handler.d.ts +12 -0
- package/dist/handlers/ui-handler.d.ts.map +1 -0
- package/dist/handlers/ui-handler.js +182 -0
- package/dist/handlers/uix-handler.d.ts +12 -0
- package/dist/handlers/uix-handler.d.ts.map +1 -0
- package/dist/handlers/uix-handler.js +135 -0
- package/dist/hmr.d.ts +20 -0
- package/dist/hmr.d.ts.map +1 -0
- package/dist/hmr.js +265 -0
- package/dist/import-rewriter.d.ts +3 -0
- package/dist/import-rewriter.d.ts.map +1 -0
- package/dist/import-rewriter.js +351 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/middleware/hmr-routes.d.ts +12 -0
- package/dist/middleware/hmr-routes.d.ts.map +1 -0
- package/dist/middleware/hmr-routes.js +97 -0
- package/dist/middleware/middleware-setup.d.ts +23 -0
- package/dist/middleware/middleware-setup.d.ts.map +1 -0
- package/dist/middleware/middleware-setup.js +596 -0
- package/dist/middleware/static-files.d.ts +15 -0
- package/dist/middleware/static-files.d.ts.map +1 -0
- package/dist/middleware/static-files.js +585 -0
- package/dist/proxy/SwiteProxyError.d.ts +6 -0
- package/dist/proxy/SwiteProxyError.d.ts.map +1 -0
- package/dist/proxy/SwiteProxyError.js +9 -0
- package/dist/proxy/proxyToPython.d.ts +28 -0
- package/dist/proxy/proxyToPython.d.ts.map +1 -0
- package/dist/proxy/proxyToPython.js +66 -0
- package/dist/resolver/bare-import-resolver.d.ts +9 -0
- package/dist/resolver/bare-import-resolver.d.ts.map +1 -0
- package/dist/resolver/bare-import-resolver.js +363 -0
- package/dist/resolver/symlink-registry.d.ts +13 -0
- package/dist/resolver/symlink-registry.d.ts.map +1 -0
- package/dist/resolver/symlink-registry.js +98 -0
- package/dist/resolver/url-resolver.d.ts +11 -0
- package/dist/resolver/url-resolver.d.ts.map +1 -0
- package/dist/resolver/url-resolver.js +268 -0
- package/dist/resolver/workspace-package-resolver.d.ts +10 -0
- package/dist/resolver/workspace-package-resolver.d.ts.map +1 -0
- package/dist/resolver/workspace-package-resolver.js +185 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +191 -0
- package/dist/router/file-router.d.ts +19 -0
- package/dist/router/file-router.d.ts.map +1 -0
- package/dist/router/file-router.js +114 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +122 -0
- package/dist/utils/cdn-fallback.d.ts +14 -0
- package/dist/utils/cdn-fallback.d.ts.map +1 -0
- package/dist/utils/cdn-fallback.js +36 -0
- package/dist/utils/file-path-resolver.d.ts +9 -0
- package/dist/utils/file-path-resolver.d.ts.map +1 -0
- package/dist/utils/file-path-resolver.js +187 -0
- package/dist/utils/generate-import-map-cli.d.ts +3 -0
- package/dist/utils/generate-import-map-cli.d.ts.map +1 -0
- package/dist/utils/generate-import-map-cli.js +32 -0
- package/dist/utils/generate-import-map.d.ts +21 -0
- package/dist/utils/generate-import-map.d.ts.map +1 -0
- package/dist/utils/generate-import-map.js +119 -0
- package/dist/utils/package-finder.d.ts +24 -0
- package/dist/utils/package-finder.d.ts.map +1 -0
- package/dist/utils/package-finder.js +161 -0
- package/dist/utils/package-registry.d.ts +36 -0
- package/dist/utils/package-registry.d.ts.map +1 -0
- package/dist/utils/package-registry.js +159 -0
- package/dist/utils/workspace.d.ts +6 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +65 -0
- package/docs/IMPORT_REWRITING.md +164 -0
- package/docs/IMPORT_REWRITING_TROUBLESHOOTING.md +139 -0
- package/docs/PATH_RESOLUTION_GUIDE.md +221 -0
- package/package.json +49 -0
- package/src/adapters/proxy/SwiteProxyError.ts +12 -0
- package/src/adapters/proxy/proxyToPython.ts +88 -0
- package/src/build-engine/builder.ts +960 -0
- package/src/cli.ts +109 -0
- package/src/config/config-loader.ts +46 -0
- package/src/config/config.ts +34 -0
- package/src/config/env.ts +98 -0
- package/src/dev-engine/handlers/base-handler.ts +68 -0
- package/src/dev-engine/handlers/js-handler.ts +134 -0
- package/src/dev-engine/handlers/mjs-handler.ts +65 -0
- package/src/dev-engine/handlers/node-module-handler.ts +339 -0
- package/src/dev-engine/handlers/ts-handler.ts +143 -0
- package/src/dev-engine/handlers/ui-handler.ts +105 -0
- package/src/dev-engine/handlers/uix-handler.ts +90 -0
- package/src/dev-engine/hmr/hmr-client-template.ts +122 -0
- package/src/dev-engine/hmr/hmr.ts +173 -0
- package/src/dev-engine/middleware/hmr-routes.ts +120 -0
- package/src/dev-engine/middleware/middleware-setup.ts +351 -0
- package/src/dev-engine/middleware/static-files.ts +728 -0
- package/src/dev-engine/pythonDevManager.ts +116 -0
- package/src/dev-engine/router/file-router.ts +164 -0
- package/src/dev-engine/server.ts +152 -0
- package/src/index.ts +26 -0
- package/src/internal/cache/compilation-cache.ts +182 -0
- package/src/internal/generate-import-map-cli.ts +40 -0
- package/src/internal/generate-import-map.ts +154 -0
- package/src/kernel/package-finder.ts +164 -0
- package/src/kernel/package-registry.ts +198 -0
- package/src/kernel/workspace.ts +62 -0
- package/src/resolution/bare-import-resolver.ts +400 -0
- package/src/resolution/cdn/cdn-fallback.ts +37 -0
- package/src/resolution/path/file-path-resolver.ts +190 -0
- package/src/resolution/path/path-fixup.ts +19 -0
- package/src/resolution/resolver.ts +198 -0
- package/src/resolution/rewriting/import-rewriter.ts +237 -0
- package/src/resolution/symlink-registry.ts +114 -0
- package/src/resolution/url-resolver.ts +231 -0
- package/src/resolution/workspace-package-resolver.ts +94 -0
- 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
|
+
}
|