@swissjs/swite 0.3.1 → 0.3.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 (49) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cli.js +0 -0
  3. package/dist/config/config.d.ts +11 -0
  4. package/dist/config/config.d.ts.map +1 -1
  5. package/dist/dev-engine/handlers/base-handler.d.ts +3 -1
  6. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  7. package/dist/dev-engine/handlers/base-handler.js +1 -1
  8. package/dist/dev-engine/handlers/node-module-handler.d.ts.map +1 -1
  9. package/dist/dev-engine/handlers/node-module-handler.js +30 -41
  10. package/dist/dev-engine/middleware/middleware-setup.d.ts +1 -0
  11. package/dist/dev-engine/middleware/middleware-setup.d.ts.map +1 -1
  12. package/dist/dev-engine/server.d.ts.map +1 -1
  13. package/dist/dev-engine/server.js +4 -0
  14. package/dist/kernel/package-finder.d.ts +7 -7
  15. package/dist/kernel/package-finder.d.ts.map +1 -1
  16. package/dist/kernel/package-finder.js +56 -40
  17. package/dist/resolution/bare-import-resolver.d.ts.map +1 -1
  18. package/dist/resolution/bare-import-resolver.js +13 -4
  19. package/dist/resolution/path/file-path-resolver.d.ts +2 -1
  20. package/dist/resolution/path/file-path-resolver.d.ts.map +1 -1
  21. package/dist/resolution/path/file-path-resolver.js +36 -9
  22. package/docs/architecture/build-pipeline.md +97 -0
  23. package/docs/architecture/dev-server.md +87 -0
  24. package/docs/architecture/hmr.md +78 -0
  25. package/docs/architecture/import-rewriting.md +101 -0
  26. package/docs/architecture/index.md +16 -0
  27. package/docs/architecture/python-integration.md +93 -0
  28. package/docs/architecture/resolution.md +92 -0
  29. package/docs/cli/build.md +78 -0
  30. package/docs/cli/dev.md +90 -0
  31. package/docs/cli/index.md +15 -0
  32. package/docs/cli/start.md +45 -0
  33. package/docs/development/contributing.md +74 -0
  34. package/docs/development/index.md +12 -0
  35. package/docs/development/internals.md +101 -0
  36. package/docs/guide/configuration.md +89 -0
  37. package/docs/guide/index.md +13 -0
  38. package/docs/guide/project-structure.md +75 -0
  39. package/docs/guide/quickstart.md +113 -0
  40. package/docs/index.md +16 -0
  41. package/package.json +15 -24
  42. package/src/config/config.ts +11 -0
  43. package/src/dev-engine/handlers/base-handler.ts +4 -2
  44. package/src/dev-engine/handlers/node-module-handler.ts +51 -78
  45. package/src/dev-engine/middleware/middleware-setup.ts +1 -0
  46. package/src/dev-engine/server.ts +38 -33
  47. package/src/kernel/package-finder.ts +59 -43
  48. package/src/resolution/bare-import-resolver.ts +14 -4
  49. package/src/resolution/path/file-path-resolver.ts +44 -10
@@ -7,14 +7,15 @@
7
7
  import type { Response } from "express";
8
8
  import { promises as fs } from "node:fs";
9
9
  import * as path from "node:path";
10
- import chalk from "chalk";
11
- import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
12
- import { BaseHandler, type HandlerContext } from "./base-handler.js";
13
- import { UIHandler } from "./ui-handler.js";
14
- import { UIXHandler } from "./uix-handler.js";
15
- import { TSHandler } from "./ts-handler.js";
16
- import { findWorkspaceRoot } from "../../kernel/workspace.js";
17
- import { shouldUseCdnFallback } from "../../resolution/cdn/cdn-fallback.js";
10
+ import chalk from "chalk";
11
+ import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
12
+ import { BaseHandler, type HandlerContext } from "./base-handler.js";
13
+ import { UIHandler } from "./ui-handler.js";
14
+ import { UIXHandler } from "./uix-handler.js";
15
+ import { TSHandler } from "./ts-handler.js";
16
+ import { findWorkspaceRoot } from "../../kernel/workspace.js";
17
+ import { findPackage } from "../../kernel/package-finder.js";
18
+ import { shouldUseCdnFallback } from "../../resolution/cdn/cdn-fallback.js";
18
19
 
19
20
  export class NodeModuleHandler extends BaseHandler {
20
21
  private uiHandler: UIHandler;
@@ -76,76 +77,39 @@ export class NodeModuleHandler extends BaseHandler {
76
77
  current = parent;
77
78
  }
78
79
  }
80
+ // CONSOLIDATED DISCOVERY: Use the Generalized Package Finder
81
+ // This follows our "Local-First" priority: Siblings > Local node_modules > Workspace node_modules
79
82
  if (!filePath) {
80
- // Try workspace root node_modules
81
- if (workspaceRoot) {
82
- const workspaceNodeModulesPath = path.join(workspaceRoot, urlPath);
83
- console.log(
84
- chalk.blue(
85
- `[node_modules] Trying workspace path: ${workspaceNodeModulesPath}`,
86
- ),
87
- );
88
- try {
89
- // Try to resolve symlinks first (realpath works even if path is a symlink)
90
- const resolvedPath = await fs.realpath(workspaceNodeModulesPath);
91
- console.log(
92
- chalk.blue(`[node_modules] Resolved to: ${resolvedPath}`),
93
- );
94
- // Verify the resolved path exists
95
- await fs.access(resolvedPath);
96
- filePath = resolvedPath;
97
- console.log(
98
- chalk.green(`[node_modules] ✓ Found in workspace: ${urlPath}`),
99
- );
100
- } catch (err2) {
101
- console.log(
102
- chalk.yellow(
103
- `[node_modules] Workspace path failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
104
- ),
105
- );
106
- // Try swiss-lib monorepo node_modules (dynamically found)
107
- const { findSwissLibMonorepo } = await import("../../kernel/package-finder.js");
108
- const swissLib = await findSwissLibMonorepo(this.context.root);
109
- if (swissLib) {
110
- const swissNodeModulesPath = path.join(swissLib, urlPath);
111
- console.log(
112
- chalk.blue(
113
- `[node_modules] Trying swiss-lib path: ${swissNodeModulesPath}`,
114
- ),
115
- );
116
- try {
117
- // Try to resolve symlinks first (realpath works even if path is a symlink)
118
- const resolvedPath = await fs.realpath(swissNodeModulesPath);
119
- console.log(
120
- chalk.blue(`[node_modules] Resolved to: ${resolvedPath}`),
121
- );
122
- // Verify the resolved path exists
123
- await fs.access(resolvedPath);
124
- filePath = resolvedPath;
125
- console.log(
126
- chalk.green(
127
- `[node_modules] ✓ Found in swiss-lib monorepo: ${urlPath}`,
128
- ),
129
- );
130
- } catch (err3) {
131
- console.log(
132
- chalk.yellow(
133
- `[node_modules] swiss-lib path failed: ${err3 instanceof Error ? err3.message : String(err3)}`,
134
- ),
135
- );
136
- // File not found in any location, will trigger case-insensitive search below
137
- filePath = path.join(this.context.root, urlPath);
83
+ const urlParts = urlPath.split("/");
84
+ const packageName = urlParts[1].startsWith("@") ? `${urlParts[1]}/${urlParts[2]}` : urlParts[1];
85
+ const remainingPath = urlParts[1].startsWith("@") ? urlParts.slice(3).join("/") : urlParts.slice(2).join("/");
86
+
87
+ const location = await findPackage(packageName, this.context.root, workspaceRoot);
88
+
89
+ if (location) {
90
+ filePath = path.join(location.path, remainingPath);
91
+ console.log(chalk.green(`[node_modules] ✓ Found ${packageName} via ${location.type}: ${filePath}`));
92
+
93
+ // Re-use dist -> src fallback for local siblings
94
+ if (location.type !== 'node_modules' && filePath.includes("/dist/")) {
95
+ const srcPath = filePath.replace("/dist/", "/src/").replace(/\.[mc]?js$/, ".ts");
96
+ try {
97
+ await fs.access(srcPath);
98
+ console.log(chalk.yellow(`[node_modules] Intercept: Serving local source instead of dist: ${srcPath}`));
99
+ filePath = srcPath;
100
+
101
+ if (srcPath.endsWith(".ts")) {
102
+ return await this.tsHandler.handle(url.replace(/\.[mc]?js$/, ".ts"), res);
138
103
  }
139
- } else {
140
- // File not found in any location, will trigger case-insensitive search below
141
- filePath = path.join(this.context.root, urlPath);
142
- }
104
+ } catch { /* Fallback to original filePath */ }
143
105
  }
144
- } else {
145
- filePath = path.join(this.context.root, urlPath);
146
106
  }
147
107
  }
148
108
 
109
+ if (!filePath) {
110
+ filePath = path.join(this.context.root, urlPath);
111
+ }
112
+
149
113
  console.log(
150
114
  chalk.gray(`[node_modules] Resolving: ${url} -> ${filePath}`),
151
115
  );
@@ -331,9 +295,18 @@ export class NodeModuleHandler extends BaseHandler {
331
295
  pkgName = firstSlash === -1 ? after : after.slice(0, firstSlash);
332
296
  }
333
297
 
334
- if (!pkgName || pkgName === "." || pkgName === "..") return null;
335
- if (!shouldUseCdnFallback(pkgName)) return null;
336
- // jsDelivr +esm serves ESM build; works for reflect-metadata and most npm packages
337
- return `https://cdn.jsdelivr.net/npm/${pkgName}/+esm`;
338
- }
339
- }
298
+ if (!pkgName || pkgName === "." || pkgName === "..") return null;
299
+
300
+ // Never redirect internal/private scoped packages to public CDNs
301
+ const internalScopes = this.context.userConfig?.internalScopes || [];
302
+ const isInternal = internalScopes.some(scope => pkgName === scope || pkgName.startsWith(scope + "/"));
303
+ if (isInternal) {
304
+ console.log(chalk.red(`[node_modules] CDN Blocked: Internal scope package ${pkgName} cannot be served from jsDelivr.`));
305
+ return null;
306
+ }
307
+
308
+ if (!shouldUseCdnFallback(pkgName)) return null;
309
+ // jsDelivr +esm serves ESM build; works for reflect-metadata and most npm packages
310
+ return `https://cdn.jsdelivr.net/npm/${pkgName}/+esm`;
311
+ }
312
+ }
@@ -35,6 +35,7 @@ export interface MiddlewareConfig {
35
35
  publicDir: string;
36
36
  resolver: ModuleResolver;
37
37
  hmr: HMREngine;
38
+ userConfig?: import("../../config/config.js").SwiteUserConfig;
38
39
  }
39
40
 
40
41
  export interface MiddlewareResult {
@@ -16,20 +16,21 @@ import chalk from "chalk";
16
16
  import { setupMiddleware } from "./middleware/middleware-setup.js";
17
17
  import { buildSymlinkRegistry } from "../resolution/symlink-registry.js";
18
18
  import { findSwissLibMonorepo } from "../kernel/package-finder.js";
19
+ import { loadUserConfig } from "../config/config-loader.js";
19
20
 
20
- export interface SwiteConfig {
21
- root: string;
22
- // Workspace/monorepo root. When set, Swite will use this for resolving
23
- // node_modules, workspace packages, and import-map generation.
24
- // This avoids relying on auto-detection (pnpm-workspace.yaml) which may not
25
- // exist in some deployment contexts.
26
- rootDir?: string;
27
- publicDir: string;
28
- port: number;
29
- host: string;
30
- open: boolean;
31
- hmrPort?: number; // Optional HMR WebSocket port
32
- }
21
+ export interface SwiteConfig {
22
+ root: string;
23
+ // Workspace/monorepo root. When set, Swite will use this for resolving
24
+ // node_modules, workspace packages, and import-map generation.
25
+ // This avoids relying on auto-detection (pnpm-workspace.yaml) which may not
26
+ // exist in some deployment contexts.
27
+ rootDir?: string;
28
+ publicDir: string;
29
+ port: number;
30
+ host: string;
31
+ open: boolean;
32
+ hmrPort?: number; // Optional HMR WebSocket port
33
+ }
33
34
 
34
35
  export class SwiteServer {
35
36
  private app = express();
@@ -55,16 +56,16 @@ export class SwiteServer {
55
56
  this.hmr = new HMREngine(this.config.root, this.config.hmrPort);
56
57
  }
57
58
 
58
- // CG-03: find workspace root by walking up from startDir
59
- private async findWorkspaceRoot(startDir: string): Promise<string | null> {
60
- if (this.config.rootDir) {
61
- return path.resolve(this.config.rootDir);
62
- }
63
- let current = startDir;
64
- for (let i = 0; i < 6; i++) {
65
- try {
66
- await fs.access(path.join(current, "pnpm-workspace.yaml"));
67
- return current;
59
+ // CG-03: find workspace root by walking up from startDir
60
+ private async findWorkspaceRoot(startDir: string): Promise<string | null> {
61
+ if (this.config.rootDir) {
62
+ return path.resolve(this.config.rootDir);
63
+ }
64
+ let current = startDir;
65
+ for (let i = 0; i < 6; i++) {
66
+ try {
67
+ await fs.access(path.join(current, "pnpm-workspace.yaml"));
68
+ return current;
68
69
  } catch {}
69
70
  try {
70
71
  const pkgJson = JSON.parse(
@@ -110,16 +111,20 @@ export class SwiteServer {
110
111
  }
111
112
  console.timeEnd("Symlink Registry");
112
113
 
113
- // Setup middleware
114
- console.time("Middleware Setup");
115
- const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
116
- const middlewareResult = await setupMiddleware(this.app, {
117
- root: this.config.root,
118
- workspaceRoot,
119
- publicDir: this.config.publicDir,
120
- resolver: this.resolver,
121
- hmr: this.hmr,
122
- });
114
+ // Load user config (swiss.config.ts) so internalScopes etc. flow into handlers
115
+ const userConfig = await loadUserConfig(this.config.root);
116
+
117
+ // Setup middleware
118
+ console.time("Middleware Setup");
119
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
120
+ const middlewareResult = await setupMiddleware(this.app, {
121
+ root: this.config.root,
122
+ workspaceRoot,
123
+ publicDir: this.config.publicDir,
124
+ resolver: this.resolver,
125
+ hmr: this.hmr,
126
+ userConfig,
127
+ });
123
128
  this.routes = middlewareResult.routes;
124
129
  this.routeScanner = middlewareResult.routeScanner;
125
130
  this.routeWatcher = middlewareResult.routeWatcher;
@@ -18,55 +18,54 @@ export interface PackageLocation {
18
18
  }
19
19
 
20
20
  /**
21
- * Find a co-located framework monorepo by scanning sibling directories at each
22
- * ancestor level for any workspace root (pnpm-workspace.yaml) that also has a
23
- * packages/ directory. Works for any framework directory name.
21
+ * Find any sibling monorepo by searching for its package.json
24
22
  */
25
- export async function findSwissLibMonorepo(startPath: string): Promise<string | null> {
26
- let current = path.resolve(startPath);
27
-
23
+ export async function findSiblingRepository(startPath: string, repoName: string): Promise<string | null> {
24
+ let current = startPath;
28
25
  for (let i = 0; i < 20; i++) {
29
- const parent = path.dirname(current);
30
- if (parent === current) break;
26
+ const siblingPath = path.join(current, repoName);
27
+ const pkgJson = path.join(siblingPath, "package.json");
28
+ if (await fileExists(pkgJson)) {
29
+ return siblingPath;
30
+ }
31
31
 
32
- // Scan siblings of `current` at this parent level
33
32
  try {
34
- const entries = await fs.readdir(parent, { withFileTypes: true });
33
+ const entries = await fs.readdir(current, { withFileTypes: true });
35
34
  for (const entry of entries) {
36
- if (entry.name === "node_modules") continue;
37
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
38
- const sibling = path.join(parent, entry.name);
39
- if (path.resolve(sibling) === path.resolve(current)) continue; // skip self
40
-
41
- if (
42
- await fileExists(path.join(sibling, "pnpm-workspace.yaml")) &&
43
- await fileExists(path.join(sibling, "packages"))
44
- ) {
45
- return sibling;
35
+ if (entry.name === repoName && (entry.isDirectory() || entry.isSymbolicLink())) {
36
+ const subDir = path.join(current, entry.name);
37
+ if (await fileExists(path.join(subDir, "package.json"))) {
38
+ return subDir;
39
+ }
46
40
  }
47
41
  }
48
- } catch {
49
- // Skip on permission errors
50
- }
42
+ } catch { /* Continue */ }
51
43
 
52
- current = parent;
44
+ current = path.dirname(current);
45
+ if (current === path.dirname(current)) break;
53
46
  }
54
-
55
47
  return null;
56
48
  }
57
49
 
58
50
  /**
59
- * Find a specific package by name, searching in:
60
- * 1. node_modules (local and workspace)
61
- * 2. swiss-lib/packages (if found)
62
- * 3. workspace packages (lib/, packages/, modules/)
51
+ * Backward compatibility wrapper for finding swiss-lib
52
+ */
53
+ export async function findSwissLibMonorepo(startPath: string): Promise<string | null> {
54
+ return findSiblingRepository(startPath, 'swiss-lib');
55
+ }
56
+
57
+ /**
58
+ * Find a specific package by name, with priority given based on environment.
59
+ * In development, we prioritize local sibling source code.
63
60
  */
64
61
  export async function findPackage(
65
62
  packageName: string,
66
63
  startPath: string,
67
64
  workspaceRoot?: string | null
68
65
  ): Promise<PackageLocation | null> {
69
- // 1. Check local node_modules
66
+ const isDev = process.env.NODE_ENV !== 'production';
67
+
68
+ // 1. Check local node_modules (Standard resolution) - HIGHEST PRIORITY in Remote-First
70
69
  const localNodeModules = path.join(startPath, "node_modules", packageName);
71
70
  if (await fileExists(path.join(localNodeModules, "package.json"))) {
72
71
  return { path: localNodeModules, type: 'node_modules' };
@@ -79,7 +78,7 @@ export async function findPackage(
79
78
  return { path: workspaceNodeModules, type: 'node_modules' };
80
79
  }
81
80
  }
82
-
81
+
83
82
  // 3. Check co-located framework monorepo packages/ for any scoped package
84
83
  if (packageName.startsWith("@")) {
85
84
  const monorepo = await findSwissLibMonorepo(startPath);
@@ -92,24 +91,41 @@ export async function findPackage(
92
91
  }
93
92
  }
94
93
 
95
- // 4. Check workspace packages (lib/, packages/, modules/)
94
+ // 4. In dev: broader sibling scan across parent directories
95
+ if (isDev && packageName.includes("/")) {
96
+ const parts = packageName.split("/");
97
+ const unscoped = parts[parts.length - 1];
98
+ const parentDirs = [
99
+ path.join(startPath, ".."),
100
+ path.join(startPath, "../.."),
101
+ path.join(startPath, "../../.."),
102
+ ];
103
+ for (const parent of parentDirs) {
104
+ try {
105
+ const potentialRepos = await fs.readdir(parent);
106
+ for (const repo of potentialRepos) {
107
+ const siblingPath = path.join(parent, repo);
108
+ const packagePath = path.join(siblingPath, "packages", unscoped);
109
+ if (await fileExists(path.join(packagePath, "package.json"))) {
110
+ console.log(`[package-finder] Dev Intercept: Serving ${packageName} from local source: ${packagePath}`);
111
+ return { path: packagePath, type: 'swiss-lib' };
112
+ }
113
+ }
114
+ } catch { /* Continue */ }
115
+ }
116
+ }
117
+
118
+ // 5. Fallback search in internal workspace packages (lib/, packages/, modules/)
96
119
  if (workspaceRoot) {
97
120
  const packageDirs = ["lib", "packages", "modules", "libraries", "apps"];
98
121
  for (const dir of packageDirs) {
99
122
  const searchDir = path.join(workspaceRoot, dir);
100
123
  if (!(await fileExists(searchDir))) continue;
101
124
 
102
- // Try scoped package name
103
- if (packageName.startsWith("@")) {
104
- const unscoped = packageName.split("/")[1];
105
- const packagePath = path.join(searchDir, unscoped);
106
- if (await fileExists(path.join(packagePath, "package.json"))) {
107
- return { path: packagePath, type: 'workspace' };
108
- }
109
- }
110
-
111
- // Try full package name
112
- const packagePath = path.join(searchDir, packageName);
125
+ const parts = packageName.split("/");
126
+ const unscoped = parts.length > 1 ? parts[1] : parts[0];
127
+ const packagePath = path.join(searchDir, unscoped);
128
+
113
129
  if (await fileExists(path.join(packagePath, "package.json"))) {
114
130
  return { path: packagePath, type: 'workspace' };
115
131
  }
@@ -47,7 +47,7 @@ export async function resolveBareImport(
47
47
  nodeModulesLocations.push(path.join(workspaceRoot, "node_modules"));
48
48
  }
49
49
 
50
- // Add swiss-lib monorepo node_modules
50
+ // Add monorepo node_modules if present
51
51
  const swissLib = await findSwissLibMonorepo(context.root);
52
52
  if (swissLib) {
53
53
  const swissNodeModules = path.join(swissLib, "node_modules");
@@ -209,9 +209,19 @@ export async function resolveBareImport(
209
209
 
210
210
  const fullPath = path.join(pkgDir, entryPoint);
211
211
 
212
- // Try the exact path first
213
- if (await context.fileExists(fullPath)) {
214
- return await toUrl(fullPath, context);
212
+ // Try a few Swiss-specific variations of the entry point before CDN fallback
213
+ const swissVariations = [
214
+ fullPath,
215
+ fullPath.replace(/\.(js|mjs|ts)$/, '.ui'),
216
+ fullPath.replace(/\.(js|mjs|ts)$/, '.uix'),
217
+ path.join(pkgDir, 'src/index.ui'),
218
+ path.join(pkgDir, 'src/index.uix'),
219
+ ];
220
+
221
+ for (const v of swissVariations) {
222
+ if (await context.fileExists(v)) {
223
+ return await toUrl(v, context);
224
+ }
215
225
  }
216
226
 
217
227
  // Try with extensions
@@ -7,11 +7,12 @@
7
7
  import { promises as fs } from "node:fs";
8
8
  import path from "node:path";
9
9
  import { findWorkspaceRoot } from "../../kernel/workspace.js";
10
- import { findSwissLibMonorepo } from "../../kernel/package-finder.js";
10
+ import { findSwissLibMonorepo, findPackage } from "../../kernel/package-finder.js";
11
11
 
12
12
  export interface PathResolverContext {
13
13
  root: string;
14
14
  workspaceRoot: string | null;
15
+ userConfig?: any; // SwiteUserConfig
15
16
  }
16
17
 
17
18
  /**
@@ -21,15 +22,52 @@ export async function resolveFilePath(
21
22
  url: string,
22
23
  root: string,
23
24
  workspaceRoot: string | null = null,
25
+ userConfig?: any
24
26
  ): Promise<string> {
27
+ // Consolidate workspace root discovery for consistency across resolution blocks
28
+ const wsRoot = workspaceRoot || (await findWorkspaceRoot(root));
29
+
25
30
  // /node_modules/ URLs: walk up from app root until we find the package.
26
31
  // pnpm may place deps at the app root, one level up (workspace pkg), or at
27
32
  // the monorepo root depending on hoisting config and pnpm version.
28
33
  if (url.startsWith("/node_modules/")) {
29
34
  const urlPath = url.startsWith("/") ? url.slice(1) : url;
35
+ const parts = urlPath.split("/");
36
+ // Handle @scoped/package or standard-package
37
+ const packageName = parts[1].startsWith("@") ? `${parts[1]}/${parts[2]}` : parts[1];
38
+ // NEW: PNPM-aware Interceptor. Check if this request is for an "internal" scope package.
39
+ // In development, we always prioritize local siblings if they exist.
40
+ const internalScopes = userConfig?.internalScopes || [];
41
+ const match = internalScopes.length > 0
42
+ ? url.match(new RegExp(`(${internalScopes.join("|")})\/([^/]+)`))
43
+ : null;
44
+
45
+ if (process.env.NODE_ENV !== 'production' && match) {
46
+ const packageName = match[0];
47
+ const remainingPath = url.split(match[0])[1];
48
+
49
+ const localLoc = await findPackage(packageName, root, wsRoot);
50
+ if (localLoc && localLoc.type !== 'node_modules') {
51
+ // Found local source! Redirect the base path
52
+ const fullPath = path.join(localLoc.path, remainingPath);
53
+
54
+ // Re-use workspace fallback logic for dist -> src transition
55
+ if (fullPath.includes("/dist/")) {
56
+ const srcPath = fullPath.replace("/dist/", "/src/").replace(/\.[mc]?js$/, ".ts");
57
+ try {
58
+ await fs.access(srcPath);
59
+ console.log(`[file-path-resolver] Intercept: ${packageName} redirecting to local src: ${srcPath}`);
60
+ return srcPath;
61
+ } catch { /* Fallback to dist if src not found */ }
62
+ }
63
+
64
+ console.log(`[file-path-resolver] Intercept: ${packageName} redirecting to local source: ${fullPath}`);
65
+ return fullPath;
66
+ }
67
+ }
30
68
 
31
- // Walk up the directory tree from root, trying node_modules at each level
32
- let current = path.resolve(root);
69
+ // Walk up the directory tree from root, trying node_modules at each level
70
+ let current = path.resolve(root);
33
71
  const visited = new Set<string>();
34
72
  for (let i = 0; i < 8; i++) {
35
73
  const candidate = path.join(current, urlPath);
@@ -49,7 +87,6 @@ export async function resolveFilePath(
49
87
  }
50
88
 
51
89
  // Explicit workspace root (covers hoisted-to-root installs)
52
- const wsRoot = workspaceRoot || (await findWorkspaceRoot(root));
53
90
  if (wsRoot) {
54
91
  const wsPath = path.join(wsRoot, urlPath);
55
92
  if (!visited.has(wsPath)) {
@@ -92,14 +129,12 @@ export async function resolveFilePath(
92
129
  url.startsWith("/packages/") ||
93
130
  url.startsWith("/modules/")
94
131
  ) {
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
- }
132
+ // Already detected wsRoot at function start
100
133
 
101
134
  // Normalize URL: path.join with leading slash is wrong on Windows (treats as drive root)
102
135
  const urlPath = url.startsWith("/") ? url.slice(1) : url;
136
+
137
+ // ...
103
138
 
104
139
  // CRITICAL: For /lib/ paths, we MUST find the SWS root (which has lib/ directory)
105
140
  // Start from app root and walk up until we find a directory with both pnpm-workspace.yaml AND lib/
@@ -156,7 +191,6 @@ export async function resolveFilePath(
156
191
  }
157
192
 
158
193
  // For app files, check if URL already includes the app path
159
- const wsRoot = workspaceRoot || (await findWorkspaceRoot(root));
160
194
  if (wsRoot) {
161
195
  const appRelativeToWorkspace = path
162
196
  .relative(wsRoot, root)