@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,400 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Bare Import Resolver - Resolves bare module specifiers (@swissjs/core, etc.)
|
|
3
|
+
* Extracted from resolver.ts for modularity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { promises as fs } from "node:fs";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { findSwissLibMonorepo } from "../kernel/package-finder.js";
|
|
10
|
+
import { shouldUseCdnFallback } from "./cdn/cdn-fallback.js";
|
|
11
|
+
import type { UrlResolverContext, WorkspacePackageResolverContext } from "./url-resolver.js";
|
|
12
|
+
import { resolveWorkspacePackage } from "./workspace-package-resolver.js";
|
|
13
|
+
import { toUrl } from "./url-resolver.js";
|
|
14
|
+
|
|
15
|
+
export interface BareImportResolverContext extends UrlResolverContext {
|
|
16
|
+
resolveWorkspacePackage: (pkgName: string) => Promise<string | null>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve bare import specifier (e.g., @swissjs/core, react, etc.)
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveBareImport(
|
|
23
|
+
specifier: string,
|
|
24
|
+
context: BareImportResolverContext
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
|
|
27
|
+
// Extract package name outside the try/catch so fallback logic can reference it.
|
|
28
|
+
// This must stay project-agnostic: works for both scoped and unscoped packages.
|
|
29
|
+
const parts = specifier.split("/");
|
|
30
|
+
const isScoped = specifier.startsWith("@");
|
|
31
|
+
const pkgName = isScoped ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
32
|
+
const subPath = isScoped ? parts.slice(2).join("/") : parts.slice(1).join("/");
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Find package.json - check multiple node_modules locations
|
|
36
|
+
let pkgDir: string | null = null;
|
|
37
|
+
let pkgJsonPath: string | null = null;
|
|
38
|
+
|
|
39
|
+
const nodeModulesLocations: string[] = [
|
|
40
|
+
path.join(context.root, "node_modules"),
|
|
41
|
+
path.join(path.dirname(context.root), "node_modules"),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Add workspace root node_modules
|
|
45
|
+
const workspaceRoot = await context.getWorkspaceRoot();
|
|
46
|
+
if (workspaceRoot) {
|
|
47
|
+
nodeModulesLocations.push(path.join(workspaceRoot, "node_modules"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add swiss-lib monorepo node_modules
|
|
51
|
+
const swissLib = await findSwissLibMonorepo(context.root);
|
|
52
|
+
if (swissLib) {
|
|
53
|
+
const swissNodeModules = path.join(swissLib, "node_modules");
|
|
54
|
+
if (await context.fileExists(swissNodeModules)) {
|
|
55
|
+
nodeModulesLocations.push(swissNodeModules);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Try each location
|
|
60
|
+
for (const nodeModulesPath of nodeModulesLocations) {
|
|
61
|
+
const testPkgDir = path.join(nodeModulesPath, pkgName);
|
|
62
|
+
const testPkgJsonPath = path.join(testPkgDir, "package.json");
|
|
63
|
+
if (await context.fileExists(testPkgJsonPath)) {
|
|
64
|
+
pkgDir = testPkgDir;
|
|
65
|
+
pkgJsonPath = testPkgJsonPath;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!pkgJsonPath || !pkgDir) {
|
|
71
|
+
const workspacePkg = await context.resolveWorkspacePackage(pkgName);
|
|
72
|
+
if (workspacePkg) {
|
|
73
|
+
return await resolveWorkspacePackageEntry(
|
|
74
|
+
workspacePkg,
|
|
75
|
+
pkgName,
|
|
76
|
+
subPath,
|
|
77
|
+
specifier,
|
|
78
|
+
context,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!shouldUseCdnFallback(pkgName)) {
|
|
83
|
+
console.warn(
|
|
84
|
+
`[SWITE] Package ${pkgName} not found anywhere. Scoped package detected; CDN fallback is disabled by default.`,
|
|
85
|
+
);
|
|
86
|
+
return `/node_modules/${specifier}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.warn(`[SWITE] Package ${pkgName} not found anywhere, using CDN fallback`);
|
|
90
|
+
return `https://cdn.jsdelivr.net/npm/${specifier}/+esm`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Continue with normal resolution if we found it
|
|
94
|
+
if (pkgJsonPath && pkgDir) {
|
|
95
|
+
return await resolveWorkspacePackageEntry(pkgDir, pkgName, subPath, specifier, context);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if this is a workspace package (symlinked)
|
|
99
|
+
let realPkgDir: string;
|
|
100
|
+
try {
|
|
101
|
+
realPkgDir = await fs.realpath(pkgDir);
|
|
102
|
+
} catch {
|
|
103
|
+
realPkgDir = pkgDir;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const workspacePkg = await context.resolveWorkspacePackage(pkgName);
|
|
107
|
+
if (workspacePkg) {
|
|
108
|
+
const normalizedWorkspacePkg = path
|
|
109
|
+
.resolve(workspacePkg)
|
|
110
|
+
.replace(/\\/g, "/")
|
|
111
|
+
.toLowerCase();
|
|
112
|
+
const normalizedRealPkgDir = path
|
|
113
|
+
.resolve(realPkgDir)
|
|
114
|
+
.replace(/\\/g, "/")
|
|
115
|
+
.toLowerCase();
|
|
116
|
+
|
|
117
|
+
// If the real path is the workspace package, use workspace resolution
|
|
118
|
+
if (
|
|
119
|
+
normalizedRealPkgDir === normalizedWorkspacePkg ||
|
|
120
|
+
normalizedRealPkgDir.includes(normalizedWorkspacePkg)
|
|
121
|
+
) {
|
|
122
|
+
return await resolveWorkspacePackageEntry(
|
|
123
|
+
workspacePkg,
|
|
124
|
+
pkgName,
|
|
125
|
+
subPath,
|
|
126
|
+
specifier,
|
|
127
|
+
context,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Read package.json for exports/main resolution
|
|
133
|
+
const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath!, "utf-8"));
|
|
134
|
+
|
|
135
|
+
// Handle exports field if present
|
|
136
|
+
if (pkgJson.exports) {
|
|
137
|
+
const exportKey = subPath ? `./${subPath}` : ".";
|
|
138
|
+
const exportEntry = pkgJson.exports[exportKey];
|
|
139
|
+
|
|
140
|
+
if (exportEntry) {
|
|
141
|
+
let entryPoint: string;
|
|
142
|
+
if (typeof exportEntry === "string") {
|
|
143
|
+
entryPoint = exportEntry;
|
|
144
|
+
} else if (exportEntry.import) {
|
|
145
|
+
entryPoint = exportEntry.import;
|
|
146
|
+
} else if (exportEntry.default) {
|
|
147
|
+
entryPoint = exportEntry.default;
|
|
148
|
+
} else {
|
|
149
|
+
entryPoint = exportEntry as unknown as string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const fullPath = path.join(pkgDir, entryPoint);
|
|
153
|
+
|
|
154
|
+
// Prefer source over dist
|
|
155
|
+
const normalizedFullPath = fullPath.replace(/\\/g, "/");
|
|
156
|
+
if (normalizedFullPath.includes("/dist/")) {
|
|
157
|
+
const srcFullPath = normalizedFullPath
|
|
158
|
+
.replace("/dist/", "/src/")
|
|
159
|
+
.replace(/\.js$/, ".ts");
|
|
160
|
+
if (await context.fileExists(srcFullPath)) {
|
|
161
|
+
return await toUrl(srcFullPath, context);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (await context.fileExists(fullPath)) {
|
|
166
|
+
return await toUrl(fullPath, context);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try extensions
|
|
170
|
+
for (const ext of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
|
|
171
|
+
const withExt = fullPath.replace(/\.(js|mjs|ts|ui|uix)$/, ext);
|
|
172
|
+
if (await context.fileExists(withExt)) {
|
|
173
|
+
return await toUrl(withExt, context);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Try case-insensitive match
|
|
178
|
+
const dir = path.dirname(fullPath);
|
|
179
|
+
const fileName = path.basename(fullPath);
|
|
180
|
+
try {
|
|
181
|
+
const files = await fs.readdir(dir);
|
|
182
|
+
const caseInsensitiveMatch = files.find(
|
|
183
|
+
(f) => f.toLowerCase() === fileName.toLowerCase(),
|
|
184
|
+
);
|
|
185
|
+
if (caseInsensitiveMatch) {
|
|
186
|
+
const correctedPath = path.join(dir, caseInsensitiveMatch);
|
|
187
|
+
if (await context.fileExists(correctedPath)) {
|
|
188
|
+
console.log(
|
|
189
|
+
chalk.yellow(
|
|
190
|
+
`[SWITE] Case-insensitive match for ${pkgName}: ${fileName} -> ${caseInsensitiveMatch}`,
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
return await toUrl(correctedPath, context);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// Directory doesn't exist, continue to fallback
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Determine entry point
|
|
203
|
+
let entryPoint: string;
|
|
204
|
+
if (subPath) {
|
|
205
|
+
entryPoint = subPath;
|
|
206
|
+
} else {
|
|
207
|
+
entryPoint = pkgJson.module || pkgJson.main || "index.js";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const fullPath = path.join(pkgDir, entryPoint);
|
|
211
|
+
|
|
212
|
+
// Try the exact path first
|
|
213
|
+
if (await context.fileExists(fullPath)) {
|
|
214
|
+
return await toUrl(fullPath, context);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Try with extensions
|
|
218
|
+
for (const ext of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
|
|
219
|
+
if (await context.fileExists(fullPath + ext)) {
|
|
220
|
+
return await toUrl(fullPath + ext, context);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fallback to CDN (jsDelivr; esm.sh returns 500 for some packages) when allowed.
|
|
225
|
+
console.warn(`[SWITE] Could not resolve ${specifier}, using fallback`);
|
|
226
|
+
return shouldUseCdnFallback(pkgName)
|
|
227
|
+
? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
|
|
228
|
+
: `/node_modules/${specifier}`;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.warn(`[SWITE] Error resolving ${specifier}:`, error);
|
|
231
|
+
return shouldUseCdnFallback(pkgName)
|
|
232
|
+
? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
|
|
233
|
+
: `/node_modules/${specifier}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolve entry point for workspace package
|
|
239
|
+
*/
|
|
240
|
+
async function resolveWorkspacePackageEntry(
|
|
241
|
+
workspacePkg: string,
|
|
242
|
+
pkgName: string,
|
|
243
|
+
subPath: string,
|
|
244
|
+
specifier: string,
|
|
245
|
+
context: BareImportResolverContext,
|
|
246
|
+
): Promise<string> {
|
|
247
|
+
const workspacePkgJson = JSON.parse(
|
|
248
|
+
await fs.readFile(path.join(workspacePkg, "package.json"), "utf-8"),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Handle exports field if present
|
|
252
|
+
if (workspacePkgJson.exports) {
|
|
253
|
+
const subPathWithoutExt = subPath
|
|
254
|
+
? subPath.replace(/\.(js|ts|ui|uix)$/, "")
|
|
255
|
+
: "";
|
|
256
|
+
let exportKey = subPathWithoutExt ? `./${subPathWithoutExt}` : ".";
|
|
257
|
+
|
|
258
|
+
// Try to find matching export
|
|
259
|
+
if (subPath && !workspacePkgJson.exports[exportKey]) {
|
|
260
|
+
if (subPathWithoutExt) {
|
|
261
|
+
const withoutExtKey = `./${subPathWithoutExt}`;
|
|
262
|
+
if (workspacePkgJson.exports[withoutExtKey]) {
|
|
263
|
+
exportKey = withoutExtKey;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!workspacePkgJson.exports[exportKey]) {
|
|
268
|
+
const subPathParts = subPathWithoutExt.split("/");
|
|
269
|
+
if (subPathParts.length > 1) {
|
|
270
|
+
const dirPath = subPathParts.slice(0, -1).join("/");
|
|
271
|
+
const dirExportKey = `./${dirPath}`;
|
|
272
|
+
if (workspacePkgJson.exports[dirExportKey]) {
|
|
273
|
+
exportKey = dirExportKey;
|
|
274
|
+
} else {
|
|
275
|
+
const firstDir = subPathParts[0];
|
|
276
|
+
const firstDirExportKey = `./${firstDir}`;
|
|
277
|
+
if (workspacePkgJson.exports[firstDirExportKey]) {
|
|
278
|
+
exportKey = firstDirExportKey;
|
|
279
|
+
} else {
|
|
280
|
+
const lastDir = subPathParts[subPathParts.length - 2];
|
|
281
|
+
const lastDirExportKey = `./${lastDir}`;
|
|
282
|
+
if (workspacePkgJson.exports[lastDirExportKey]) {
|
|
283
|
+
exportKey = lastDirExportKey;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const exportEntry = workspacePkgJson.exports[exportKey];
|
|
292
|
+
|
|
293
|
+
if (exportEntry) {
|
|
294
|
+
let entryPoint: string;
|
|
295
|
+
if (typeof exportEntry === "string") {
|
|
296
|
+
entryPoint = exportEntry;
|
|
297
|
+
} else if (exportEntry.import) {
|
|
298
|
+
entryPoint = exportEntry.import;
|
|
299
|
+
} else if (exportEntry.default) {
|
|
300
|
+
entryPoint = exportEntry.default;
|
|
301
|
+
} else {
|
|
302
|
+
entryPoint = exportEntry;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const normalizedEntryPoint = entryPoint.startsWith("./")
|
|
306
|
+
? entryPoint.slice(2)
|
|
307
|
+
: entryPoint;
|
|
308
|
+
const fullPath = path.join(workspacePkg, normalizedEntryPoint);
|
|
309
|
+
|
|
310
|
+
// Dev: prefer src over dist for workspace packages (unbuilt or dev mode)
|
|
311
|
+
const normalizedFull = fullPath.replace(/\\/g, "/");
|
|
312
|
+
if (normalizedFull.includes("/dist/")) {
|
|
313
|
+
const srcPath = fullPath.replace(/[/\\]dist[/\\]/, path.sep + "src" + path.sep).replace(/\.js$/i, ".ts");
|
|
314
|
+
if (await context.fileExists(srcPath)) {
|
|
315
|
+
return await toUrl(srcPath, context);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (await context.fileExists(fullPath)) {
|
|
320
|
+
return await toUrl(fullPath, context);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Try extensions
|
|
324
|
+
for (const ext of [".ui", ".uix", ".ts", ".js"]) {
|
|
325
|
+
const withExt = fullPath.replace(/\.(js|ts|ui|uix)$/, ext);
|
|
326
|
+
if (await context.fileExists(withExt)) {
|
|
327
|
+
return await toUrl(withExt, context);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fallback to old logic
|
|
334
|
+
let entryPoint: string;
|
|
335
|
+
if (subPath) {
|
|
336
|
+
entryPoint = subPath;
|
|
337
|
+
} else {
|
|
338
|
+
entryPoint =
|
|
339
|
+
workspacePkgJson.module || workspacePkgJson.main || "index.js";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const fullPath = path.join(workspacePkg, entryPoint);
|
|
343
|
+
|
|
344
|
+
// Dev: prefer src over dist for workspace packages
|
|
345
|
+
const normalizedFull = fullPath.replace(/\\/g, "/");
|
|
346
|
+
if (normalizedFull.includes("/dist/")) {
|
|
347
|
+
const srcPath = fullPath.replace(/[/\\]dist[/\\]/, path.sep + "src" + path.sep).replace(/\.js$/i, ".ts");
|
|
348
|
+
if (await context.fileExists(srcPath)) {
|
|
349
|
+
return await toUrl(srcPath, context);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (await context.fileExists(fullPath)) {
|
|
354
|
+
return await toUrl(fullPath, context);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Try extensions
|
|
358
|
+
const ext = path.extname(entryPoint);
|
|
359
|
+
if (ext) {
|
|
360
|
+
const basePath = fullPath.slice(0, -ext.length);
|
|
361
|
+
for (const tryExt of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
|
|
362
|
+
if (await context.fileExists(basePath + tryExt)) {
|
|
363
|
+
return await toUrl(basePath + tryExt, context);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
for (const tryExt of [".js", ".mjs", ".ts", ".ui", ".uix"]) {
|
|
368
|
+
if (await context.fileExists(fullPath + tryExt)) {
|
|
369
|
+
return await toUrl(fullPath + tryExt, context);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Try index files
|
|
375
|
+
for (const tryExt of [".js", ".ts", ".ui", ".uix"]) {
|
|
376
|
+
const indexFile = path.join(fullPath, `index${tryExt}`);
|
|
377
|
+
if (await context.fileExists(indexFile)) {
|
|
378
|
+
return await toUrl(indexFile, context);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Try src/ directory
|
|
383
|
+
const srcDir = path.join(workspacePkg, "src");
|
|
384
|
+
for (const ext of [".ts", ".ui", ".uix", ".js"]) {
|
|
385
|
+
const srcIndex = path.join(srcDir, `index${ext}`);
|
|
386
|
+
if (await context.fileExists(srcIndex)) {
|
|
387
|
+
console.log(
|
|
388
|
+
`[SWITE] Found unbuilt workspace package ${pkgName} at ${srcIndex}`,
|
|
389
|
+
);
|
|
390
|
+
return await toUrl(srcIndex, context);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.warn(
|
|
395
|
+
`[SWITE] Entry point not found for ${pkgName} at ${fullPath}, using fallback`,
|
|
396
|
+
);
|
|
397
|
+
return shouldUseCdnFallback(pkgName)
|
|
398
|
+
? `https://cdn.jsdelivr.net/npm/${specifier}/+esm`
|
|
399
|
+
: `/node_modules/${specifier}`;
|
|
400
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN fallback policy.
|
|
3
|
+
*
|
|
4
|
+
* Swite can fall back to jsDelivr (+esm) for packages it can't resolve locally.
|
|
5
|
+
* This must be safe and project-agnostic:
|
|
6
|
+
* - Unscoped packages (e.g. "react") are usually public on npm; allow by default.
|
|
7
|
+
* - Scoped packages (e.g. "@scope/pkg") may be private; do NOT CDN-fallback by default.
|
|
8
|
+
*
|
|
9
|
+
* Opt-in:
|
|
10
|
+
* - Set `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes to allow,
|
|
11
|
+
* e.g. "@types,@tanstack".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function getScope(specifierOrPkg: string): string | null {
|
|
15
|
+
if (!specifierOrPkg.startsWith("@")) return null;
|
|
16
|
+
const firstSlash = specifierOrPkg.indexOf("/");
|
|
17
|
+
if (firstSlash === -1) return null;
|
|
18
|
+
return specifierOrPkg.slice(0, firstSlash); // "@scope"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseAllowList(): Set<string> {
|
|
22
|
+
const raw = process.env.SWITE_CDN_FALLBACK_SCOPES || "";
|
|
23
|
+
const scopes = raw
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((s) => s.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((s) => (s.startsWith("@") ? s : `@${s}`));
|
|
28
|
+
return new Set(scopes);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function shouldUseCdnFallback(specifierOrPkg: string): boolean {
|
|
32
|
+
const scope = getScope(specifierOrPkg);
|
|
33
|
+
if (!scope) return true; // unscoped: allow by default
|
|
34
|
+
const allow = parseAllowList();
|
|
35
|
+
return allow.has(scope);
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
* SWITE - SWISS Development Server
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { promises as fs } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { findWorkspaceRoot } from "../../kernel/workspace.js";
|
|
10
|
+
import { findSwissLibMonorepo } from "../../kernel/package-finder.js";
|
|
11
|
+
|
|
12
|
+
export interface PathResolverContext {
|
|
13
|
+
root: string;
|
|
14
|
+
workspaceRoot: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve file path from URL, handling SWISS packages, workspace packages, and app files
|
|
19
|
+
*/
|
|
20
|
+
export async function resolveFilePath(
|
|
21
|
+
url: string,
|
|
22
|
+
root: string,
|
|
23
|
+
workspaceRoot: string | null = null,
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
// /node_modules/ URLs: walk up from app root until we find the package.
|
|
26
|
+
// pnpm may place deps at the app root, one level up (workspace pkg), or at
|
|
27
|
+
// the monorepo root depending on hoisting config and pnpm version.
|
|
28
|
+
if (url.startsWith("/node_modules/")) {
|
|
29
|
+
const urlPath = url.startsWith("/") ? url.slice(1) : url;
|
|
30
|
+
|
|
31
|
+
// Walk up the directory tree from root, trying node_modules at each level
|
|
32
|
+
let current = path.resolve(root);
|
|
33
|
+
const visited = new Set<string>();
|
|
34
|
+
for (let i = 0; i < 8; i++) {
|
|
35
|
+
const candidate = path.join(current, urlPath);
|
|
36
|
+
if (!visited.has(candidate)) {
|
|
37
|
+
visited.add(candidate);
|
|
38
|
+
try {
|
|
39
|
+
const resolved = await fs.realpath(candidate);
|
|
40
|
+
await fs.access(resolved);
|
|
41
|
+
return resolved;
|
|
42
|
+
} catch {
|
|
43
|
+
// try parent level
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const parent = path.dirname(current);
|
|
47
|
+
if (parent === current) break; // filesystem root
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Explicit workspace root (covers hoisted-to-root installs)
|
|
52
|
+
const wsRoot = workspaceRoot || (await findWorkspaceRoot(root));
|
|
53
|
+
if (wsRoot) {
|
|
54
|
+
const wsPath = path.join(wsRoot, urlPath);
|
|
55
|
+
if (!visited.has(wsPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const resolved = await fs.realpath(wsPath);
|
|
58
|
+
await fs.access(resolved);
|
|
59
|
+
return resolved;
|
|
60
|
+
} catch {
|
|
61
|
+
// not found there either
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return path.join(path.resolve(root), urlPath); // fallback; handler will 404
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// /swiss-packages/ URLs point to files in the co-located framework monorepo's packages/ dir
|
|
70
|
+
if (url.startsWith("/swiss-packages/")) {
|
|
71
|
+
const relativePath = url.replace(/^\/swiss-packages\//, "");
|
|
72
|
+
const monorepo = await findSwissLibMonorepo(root);
|
|
73
|
+
if (monorepo) {
|
|
74
|
+
const fullPath = path.join(monorepo, "packages", relativePath);
|
|
75
|
+
try {
|
|
76
|
+
await fs.access(fullPath);
|
|
77
|
+
return fullPath;
|
|
78
|
+
} catch {
|
|
79
|
+
return fullPath; // Return anyway; handler will 404 if missing
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// No co-located monorepo found — return a path that will 404 cleanly
|
|
83
|
+
console.warn(`[file-path-resolver] No framework monorepo found for /swiss-packages/${relativePath}`);
|
|
84
|
+
return path.join(root, "node_modules", relativePath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Workspace-level directories: always resolve from workspace root
|
|
88
|
+
// Updated: lib/ now contains all packages (moved from packages/)
|
|
89
|
+
if (
|
|
90
|
+
url.startsWith("/lib/") ||
|
|
91
|
+
url.startsWith("/libraries/") ||
|
|
92
|
+
url.startsWith("/packages/") ||
|
|
93
|
+
url.startsWith("/modules/")
|
|
94
|
+
) {
|
|
95
|
+
let wsRoot = workspaceRoot;
|
|
96
|
+
if (!wsRoot) {
|
|
97
|
+
wsRoot = await findWorkspaceRoot(root);
|
|
98
|
+
console.log(`[file-path-resolver] Detected workspace root: ${wsRoot} (from app root: ${root})`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Normalize URL: path.join with leading slash is wrong on Windows (treats as drive root)
|
|
102
|
+
const urlPath = url.startsWith("/") ? url.slice(1) : url;
|
|
103
|
+
|
|
104
|
+
// CRITICAL: For /lib/ paths, we MUST find the SWS root (which has lib/ directory)
|
|
105
|
+
// Start from app root and walk up until we find a directory with both pnpm-workspace.yaml AND lib/
|
|
106
|
+
if (url.startsWith("/lib/")) {
|
|
107
|
+
let current = root;
|
|
108
|
+
for (let i = 0; i < 10; i++) {
|
|
109
|
+
const workspaceFile = path.join(current, "pnpm-workspace.yaml");
|
|
110
|
+
const libDir = path.join(current, "lib");
|
|
111
|
+
try {
|
|
112
|
+
await fs.access(workspaceFile);
|
|
113
|
+
await fs.access(libDir);
|
|
114
|
+
// Found SWS root!
|
|
115
|
+
const resolved = path.join(current, urlPath);
|
|
116
|
+
console.log(`[file-path-resolver] Found SWS root with lib/: ${current}`);
|
|
117
|
+
console.log(`[file-path-resolver] Resolving ${url} from SWS root: ${current} -> ${resolved}`);
|
|
118
|
+
return resolved;
|
|
119
|
+
} catch {
|
|
120
|
+
// Continue searching up
|
|
121
|
+
}
|
|
122
|
+
const parent = path.dirname(current);
|
|
123
|
+
if (parent === current) break;
|
|
124
|
+
current = parent;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// For other paths, use detected workspace root
|
|
129
|
+
if (wsRoot) {
|
|
130
|
+
let resolved = path.join(wsRoot, urlPath);
|
|
131
|
+
// Dev fallback: if URL is /packages/.../dist/... and file doesn't exist, try src/ (unbuilt workspace packages)
|
|
132
|
+
if (
|
|
133
|
+
(url.startsWith("/packages/") || url.startsWith("/lib/")) &&
|
|
134
|
+
url.includes("/dist/")
|
|
135
|
+
) {
|
|
136
|
+
try {
|
|
137
|
+
await fs.access(resolved);
|
|
138
|
+
} catch {
|
|
139
|
+
const srcUrl = urlPath.replace("/dist/", "/src/").replace(/\.js$/, ".ts");
|
|
140
|
+
const srcResolved = path.join(wsRoot, srcUrl);
|
|
141
|
+
try {
|
|
142
|
+
await fs.access(srcResolved);
|
|
143
|
+
console.log(`[file-path-resolver] dist not found, serving src: ${resolved} -> ${srcResolved}`);
|
|
144
|
+
return srcResolved;
|
|
145
|
+
} catch {
|
|
146
|
+
// Keep original resolved; handler will 404
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
console.log(`[file-path-resolver] Resolving ${url} from workspace root: ${wsRoot} -> ${resolved}`);
|
|
151
|
+
return resolved;
|
|
152
|
+
} else {
|
|
153
|
+
console.warn(`[file-path-resolver] No workspace root found, using app root: ${root}`);
|
|
154
|
+
return path.join(root, urlPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// For app files, check if URL already includes the app path
|
|
159
|
+
const wsRoot = workspaceRoot || (await findWorkspaceRoot(root));
|
|
160
|
+
if (wsRoot) {
|
|
161
|
+
const appRelativeToWorkspace = path
|
|
162
|
+
.relative(wsRoot, root)
|
|
163
|
+
.replace(/\\/g, "/");
|
|
164
|
+
if (url.startsWith(`/${appRelativeToWorkspace}/`)) {
|
|
165
|
+
// URL already includes app path, use workspace root
|
|
166
|
+
return path.join(wsRoot, url);
|
|
167
|
+
} else if (
|
|
168
|
+
url.startsWith("/src/") ||
|
|
169
|
+
url.startsWith("/public/") ||
|
|
170
|
+
url.startsWith("/assets/")
|
|
171
|
+
) {
|
|
172
|
+
// App-specific paths (src/, public/, assets/) - resolve from app root
|
|
173
|
+
return path.join(root, url);
|
|
174
|
+
} else if (url.startsWith("/")) {
|
|
175
|
+
// Other absolute URLs, try workspace root first, then app root
|
|
176
|
+
const workspacePath = path.join(wsRoot, url);
|
|
177
|
+
try {
|
|
178
|
+
await fs.access(workspacePath);
|
|
179
|
+
return workspacePath;
|
|
180
|
+
} catch {
|
|
181
|
+
return path.join(root, url);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Relative to app root
|
|
185
|
+
return path.join(root, url);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
return path.join(root, url);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised /swiss-lib/ → /swiss-packages/ path fixup.
|
|
3
|
+
*
|
|
4
|
+
* Root cause: the UiCompiler emits absolute `/swiss-lib/` paths in some code
|
|
5
|
+
* paths (compiler was written against an older directory structure). Until the
|
|
6
|
+
* compiler is fixed at source this single function is the authoritative fixup.
|
|
7
|
+
* Apply it once per compilation, before passing code to the import rewriter.
|
|
8
|
+
*
|
|
9
|
+
* All seven previous fixup locations across ui-handler, uix-handler, and
|
|
10
|
+
* import-rewriter have been removed in favour of this call.
|
|
11
|
+
*/
|
|
12
|
+
export function fixSwissLibPaths(code: string): string {
|
|
13
|
+
if (!code.includes('/swiss-lib/')) return code;
|
|
14
|
+
// More-specific pattern first so `/swiss-lib/packages/` doesn't leave a
|
|
15
|
+
// dangling `/swiss-packages/packages/` if the replacement ran twice.
|
|
16
|
+
return code
|
|
17
|
+
.replace(/\/swiss-lib\/packages\//g, '/swiss-packages/')
|
|
18
|
+
.replace(/\/swiss-lib\//g, '/swiss-packages/');
|
|
19
|
+
}
|