@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.
Files changed (163) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/publish.yml +50 -0
  4. package/.github/workflows/release.yml +53 -0
  5. package/BUILD_ANALYSIS.md +89 -0
  6. package/BUILD_STRATEGY.md +75 -0
  7. package/CHANGELOG.md +53 -0
  8. package/DIRECTIVE.md +488 -0
  9. package/__tests__/css-extraction.test.ts +261 -0
  10. package/__tests__/css-injection-integration.test.ts +247 -0
  11. package/__tests__/css-middleware.test.ts +191 -0
  12. package/__tests__/import-rewriter-bug.test.ts +135 -0
  13. package/dist/builder.d.ts +36 -0
  14. package/dist/builder.d.ts.map +1 -0
  15. package/dist/builder.js +772 -0
  16. package/dist/cache/compilation-cache.d.ts +33 -0
  17. package/dist/cache/compilation-cache.d.ts.map +1 -0
  18. package/dist/cache/compilation-cache.js +130 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +85 -0
  22. package/dist/config-loader.d.ts +8 -0
  23. package/dist/config-loader.d.ts.map +1 -0
  24. package/dist/config-loader.js +40 -0
  25. package/dist/config.d.ts +29 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +7 -0
  28. package/dist/dev/pythonDevManager.d.ts +12 -0
  29. package/dist/dev/pythonDevManager.d.ts.map +1 -0
  30. package/dist/dev/pythonDevManager.js +85 -0
  31. package/dist/env.d.ts +19 -0
  32. package/dist/env.d.ts.map +1 -0
  33. package/dist/env.js +112 -0
  34. package/dist/handlers/base-handler.d.ts +21 -0
  35. package/dist/handlers/base-handler.d.ts.map +1 -0
  36. package/dist/handlers/base-handler.js +38 -0
  37. package/dist/handlers/js-handler.d.ts +10 -0
  38. package/dist/handlers/js-handler.d.ts.map +1 -0
  39. package/dist/handlers/js-handler.js +87 -0
  40. package/dist/handlers/mjs-handler.d.ts +8 -0
  41. package/dist/handlers/mjs-handler.d.ts.map +1 -0
  42. package/dist/handlers/mjs-handler.js +44 -0
  43. package/dist/handlers/node-module-handler.d.ts +16 -0
  44. package/dist/handlers/node-module-handler.d.ts.map +1 -0
  45. package/dist/handlers/node-module-handler.js +267 -0
  46. package/dist/handlers/ts-handler.d.ts +11 -0
  47. package/dist/handlers/ts-handler.d.ts.map +1 -0
  48. package/dist/handlers/ts-handler.js +120 -0
  49. package/dist/handlers/ui-handler.d.ts +12 -0
  50. package/dist/handlers/ui-handler.d.ts.map +1 -0
  51. package/dist/handlers/ui-handler.js +182 -0
  52. package/dist/handlers/uix-handler.d.ts +12 -0
  53. package/dist/handlers/uix-handler.d.ts.map +1 -0
  54. package/dist/handlers/uix-handler.js +135 -0
  55. package/dist/hmr.d.ts +20 -0
  56. package/dist/hmr.d.ts.map +1 -0
  57. package/dist/hmr.js +265 -0
  58. package/dist/import-rewriter.d.ts +3 -0
  59. package/dist/import-rewriter.d.ts.map +1 -0
  60. package/dist/import-rewriter.js +351 -0
  61. package/dist/index.d.ts +14 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +13 -0
  64. package/dist/middleware/hmr-routes.d.ts +12 -0
  65. package/dist/middleware/hmr-routes.d.ts.map +1 -0
  66. package/dist/middleware/hmr-routes.js +97 -0
  67. package/dist/middleware/middleware-setup.d.ts +23 -0
  68. package/dist/middleware/middleware-setup.d.ts.map +1 -0
  69. package/dist/middleware/middleware-setup.js +596 -0
  70. package/dist/middleware/static-files.d.ts +15 -0
  71. package/dist/middleware/static-files.d.ts.map +1 -0
  72. package/dist/middleware/static-files.js +585 -0
  73. package/dist/proxy/SwiteProxyError.d.ts +6 -0
  74. package/dist/proxy/SwiteProxyError.d.ts.map +1 -0
  75. package/dist/proxy/SwiteProxyError.js +9 -0
  76. package/dist/proxy/proxyToPython.d.ts +28 -0
  77. package/dist/proxy/proxyToPython.d.ts.map +1 -0
  78. package/dist/proxy/proxyToPython.js +66 -0
  79. package/dist/resolver/bare-import-resolver.d.ts +9 -0
  80. package/dist/resolver/bare-import-resolver.d.ts.map +1 -0
  81. package/dist/resolver/bare-import-resolver.js +363 -0
  82. package/dist/resolver/symlink-registry.d.ts +13 -0
  83. package/dist/resolver/symlink-registry.d.ts.map +1 -0
  84. package/dist/resolver/symlink-registry.js +98 -0
  85. package/dist/resolver/url-resolver.d.ts +11 -0
  86. package/dist/resolver/url-resolver.d.ts.map +1 -0
  87. package/dist/resolver/url-resolver.js +268 -0
  88. package/dist/resolver/workspace-package-resolver.d.ts +10 -0
  89. package/dist/resolver/workspace-package-resolver.d.ts.map +1 -0
  90. package/dist/resolver/workspace-package-resolver.js +185 -0
  91. package/dist/resolver.d.ts +17 -0
  92. package/dist/resolver.d.ts.map +1 -0
  93. package/dist/resolver.js +191 -0
  94. package/dist/router/file-router.d.ts +19 -0
  95. package/dist/router/file-router.d.ts.map +1 -0
  96. package/dist/router/file-router.js +114 -0
  97. package/dist/server.d.ts +22 -0
  98. package/dist/server.d.ts.map +1 -0
  99. package/dist/server.js +122 -0
  100. package/dist/utils/cdn-fallback.d.ts +14 -0
  101. package/dist/utils/cdn-fallback.d.ts.map +1 -0
  102. package/dist/utils/cdn-fallback.js +36 -0
  103. package/dist/utils/file-path-resolver.d.ts +9 -0
  104. package/dist/utils/file-path-resolver.d.ts.map +1 -0
  105. package/dist/utils/file-path-resolver.js +187 -0
  106. package/dist/utils/generate-import-map-cli.d.ts +3 -0
  107. package/dist/utils/generate-import-map-cli.d.ts.map +1 -0
  108. package/dist/utils/generate-import-map-cli.js +32 -0
  109. package/dist/utils/generate-import-map.d.ts +21 -0
  110. package/dist/utils/generate-import-map.d.ts.map +1 -0
  111. package/dist/utils/generate-import-map.js +119 -0
  112. package/dist/utils/package-finder.d.ts +24 -0
  113. package/dist/utils/package-finder.d.ts.map +1 -0
  114. package/dist/utils/package-finder.js +161 -0
  115. package/dist/utils/package-registry.d.ts +36 -0
  116. package/dist/utils/package-registry.d.ts.map +1 -0
  117. package/dist/utils/package-registry.js +159 -0
  118. package/dist/utils/workspace.d.ts +6 -0
  119. package/dist/utils/workspace.d.ts.map +1 -0
  120. package/dist/utils/workspace.js +65 -0
  121. package/docs/IMPORT_REWRITING.md +164 -0
  122. package/docs/IMPORT_REWRITING_TROUBLESHOOTING.md +139 -0
  123. package/docs/PATH_RESOLUTION_GUIDE.md +221 -0
  124. package/package.json +49 -0
  125. package/src/adapters/proxy/SwiteProxyError.ts +12 -0
  126. package/src/adapters/proxy/proxyToPython.ts +88 -0
  127. package/src/build-engine/builder.ts +960 -0
  128. package/src/cli.ts +109 -0
  129. package/src/config/config-loader.ts +46 -0
  130. package/src/config/config.ts +34 -0
  131. package/src/config/env.ts +98 -0
  132. package/src/dev-engine/handlers/base-handler.ts +68 -0
  133. package/src/dev-engine/handlers/js-handler.ts +134 -0
  134. package/src/dev-engine/handlers/mjs-handler.ts +65 -0
  135. package/src/dev-engine/handlers/node-module-handler.ts +339 -0
  136. package/src/dev-engine/handlers/ts-handler.ts +143 -0
  137. package/src/dev-engine/handlers/ui-handler.ts +105 -0
  138. package/src/dev-engine/handlers/uix-handler.ts +90 -0
  139. package/src/dev-engine/hmr/hmr-client-template.ts +122 -0
  140. package/src/dev-engine/hmr/hmr.ts +173 -0
  141. package/src/dev-engine/middleware/hmr-routes.ts +120 -0
  142. package/src/dev-engine/middleware/middleware-setup.ts +351 -0
  143. package/src/dev-engine/middleware/static-files.ts +728 -0
  144. package/src/dev-engine/pythonDevManager.ts +116 -0
  145. package/src/dev-engine/router/file-router.ts +164 -0
  146. package/src/dev-engine/server.ts +152 -0
  147. package/src/index.ts +26 -0
  148. package/src/internal/cache/compilation-cache.ts +182 -0
  149. package/src/internal/generate-import-map-cli.ts +40 -0
  150. package/src/internal/generate-import-map.ts +154 -0
  151. package/src/kernel/package-finder.ts +164 -0
  152. package/src/kernel/package-registry.ts +198 -0
  153. package/src/kernel/workspace.ts +62 -0
  154. package/src/resolution/bare-import-resolver.ts +400 -0
  155. package/src/resolution/cdn/cdn-fallback.ts +37 -0
  156. package/src/resolution/path/file-path-resolver.ts +190 -0
  157. package/src/resolution/path/path-fixup.ts +19 -0
  158. package/src/resolution/resolver.ts +198 -0
  159. package/src/resolution/rewriting/import-rewriter.ts +237 -0
  160. package/src/resolution/symlink-registry.ts +114 -0
  161. package/src/resolution/url-resolver.ts +231 -0
  162. package/src/resolution/workspace-package-resolver.ts +94 -0
  163. package/tsconfig.json +37 -0
@@ -0,0 +1,154 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Generate pre-resolved import maps at build time
5
+ * Licensed under the MIT License.
6
+ */
7
+
8
+ import { promises as fs } from "node:fs";
9
+ import path from "node:path";
10
+ import { ModuleResolver } from "../resolution/resolver.js";
11
+ import chalk from "chalk";
12
+ import { getPackageRegistry } from "../kernel/package-registry.js";
13
+ import { findSwissLibMonorepo } from "../kernel/package-finder.js";
14
+
15
+ export interface ImportMap {
16
+ version: string;
17
+ generated: number;
18
+ imports: {
19
+ [specifier: string]: string; // "@package/name" -> "/path/to/resolved/file"
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Scan workspace and generate import map
25
+ * This pre-resolves all known packages to eliminate runtime scanning
26
+ */
27
+ export async function generateImportMap(
28
+ root: string,
29
+ workspaceRoot: string | null,
30
+ ): Promise<ImportMap> {
31
+ const resolver = new ModuleResolver(root);
32
+ const importMap: ImportMap = {
33
+ version: "1.0",
34
+ generated: Date.now(),
35
+ imports: {},
36
+ };
37
+
38
+ console.log(chalk.blue("[ImportMap] Using dynamic package registry..."));
39
+
40
+ // Use dynamic package registry instead of manual scanning
41
+ const registry = getPackageRegistry();
42
+ const scanRoot = workspaceRoot || root;
43
+
44
+ if (!scanRoot) {
45
+ console.warn(chalk.yellow("[ImportMap] No workspace root or app root provided, cannot scan packages"));
46
+ return importMap;
47
+ }
48
+
49
+ // Add swiss-lib monorepo if it exists
50
+ const additionalRoots: string[] = [];
51
+ const swissLib = await findSwissLibMonorepo(root);
52
+ if (swissLib) {
53
+ try {
54
+ const swissPackageJson = path.join(swissLib, "package.json");
55
+ await fs.access(swissPackageJson);
56
+ additionalRoots.push(swissLib);
57
+ console.log(chalk.blue("[ImportMap] Including swiss-lib monorepo..."));
58
+ } catch {
59
+ // swiss-lib monorepo not accessible, skip
60
+ }
61
+ }
62
+
63
+ // Scan workspace using registry
64
+ try {
65
+ await registry.scanWorkspace(scanRoot, additionalRoots);
66
+ } catch (error: any) {
67
+ console.error(chalk.red(`[ImportMap] Error scanning workspace: ${error.message}`));
68
+ return importMap;
69
+ }
70
+
71
+ // Get all discovered packages
72
+ const packages = registry.getAllPackages().map(pkg => ({
73
+ name: pkg.name,
74
+ path: pkg.path,
75
+ }));
76
+
77
+ console.log(
78
+ chalk.blue(`[ImportMap] Resolving ${packages.length} packages...`),
79
+ );
80
+
81
+ // Resolve each package
82
+ let resolved = 0;
83
+ for (const pkg of packages) {
84
+ try {
85
+ // Resolve main export
86
+ const resolvedPath = await resolver.resolve(pkg.name, "");
87
+ if (resolvedPath && !resolvedPath.startsWith("http")) {
88
+ importMap.imports[pkg.name] = resolvedPath;
89
+ resolved++;
90
+ }
91
+
92
+ // Also resolve common subpaths (components, tokens, etc.)
93
+ const commonSubpaths = [
94
+ "/components",
95
+ "/tokens",
96
+ "/context",
97
+ "/shell",
98
+ "/jsx-runtime",
99
+ "/jsx-dev-runtime",
100
+ ];
101
+
102
+ for (const subpath of commonSubpaths) {
103
+ try {
104
+ const subpathSpecifier = `${pkg.name}${subpath}`;
105
+ const subpathResolved = await resolver.resolve(subpathSpecifier, "");
106
+ if (subpathResolved && !subpathResolved.startsWith("http")) {
107
+ importMap.imports[subpathSpecifier] = subpathResolved;
108
+ resolved++;
109
+ }
110
+ } catch {
111
+ // Subpath doesn't exist, skip
112
+ }
113
+ }
114
+ } catch (error) {
115
+ console.warn(
116
+ chalk.yellow(`[ImportMap] Failed to resolve ${pkg.name}:`, error),
117
+ );
118
+ }
119
+ }
120
+
121
+ console.log(
122
+ chalk.green(
123
+ `[ImportMap] ✅ Generated import map with ${resolved} entries`,
124
+ ),
125
+ );
126
+
127
+ return importMap;
128
+ }
129
+
130
+ /**
131
+ * Save import map to file
132
+ */
133
+ export async function saveImportMap(
134
+ importMap: ImportMap,
135
+ outputPath: string,
136
+ ): Promise<void> {
137
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
138
+ await fs.writeFile(outputPath, JSON.stringify(importMap, null, 2), "utf-8");
139
+ console.log(chalk.green(`[ImportMap] ✅ Saved to ${outputPath}`));
140
+ }
141
+
142
+ /**
143
+ * Load import map from file
144
+ */
145
+ export async function loadImportMap(
146
+ filePath: string,
147
+ ): Promise<ImportMap | null> {
148
+ try {
149
+ const content = await fs.readFile(filePath, "utf-8");
150
+ return JSON.parse(content) as ImportMap;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
@@ -0,0 +1,164 @@
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
+
10
+ /**
11
+ * Dynamically find package directories by searching up the file tree
12
+ * No hardcoded paths - works from any directory structure
13
+ */
14
+
15
+ export interface PackageLocation {
16
+ path: string;
17
+ type: 'swiss-lib' | 'workspace' | 'node_modules';
18
+ }
19
+
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.
24
+ */
25
+ export async function findSwissLibMonorepo(startPath: string): Promise<string | null> {
26
+ let current = path.resolve(startPath);
27
+
28
+ for (let i = 0; i < 20; i++) {
29
+ const parent = path.dirname(current);
30
+ if (parent === current) break;
31
+
32
+ // Scan siblings of `current` at this parent level
33
+ try {
34
+ const entries = await fs.readdir(parent, { withFileTypes: true });
35
+ 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;
46
+ }
47
+ }
48
+ } catch {
49
+ // Skip on permission errors
50
+ }
51
+
52
+ current = parent;
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ /**
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/)
63
+ */
64
+ export async function findPackage(
65
+ packageName: string,
66
+ startPath: string,
67
+ workspaceRoot?: string | null
68
+ ): Promise<PackageLocation | null> {
69
+ // 1. Check local node_modules
70
+ const localNodeModules = path.join(startPath, "node_modules", packageName);
71
+ if (await fileExists(path.join(localNodeModules, "package.json"))) {
72
+ return { path: localNodeModules, type: 'node_modules' };
73
+ }
74
+
75
+ // 2. Check workspace root node_modules
76
+ if (workspaceRoot) {
77
+ const workspaceNodeModules = path.join(workspaceRoot, "node_modules", packageName);
78
+ if (await fileExists(path.join(workspaceNodeModules, "package.json"))) {
79
+ return { path: workspaceNodeModules, type: 'node_modules' };
80
+ }
81
+ }
82
+
83
+ // 3. Check co-located framework monorepo packages/ for any scoped package
84
+ if (packageName.startsWith("@")) {
85
+ const monorepo = await findSwissLibMonorepo(startPath);
86
+ if (monorepo) {
87
+ const shortName = packageName.split("/")[1];
88
+ const monorepoPackage = path.join(monorepo, "packages", shortName);
89
+ if (await fileExists(path.join(monorepoPackage, "package.json"))) {
90
+ return { path: monorepoPackage, type: 'swiss-lib' };
91
+ }
92
+ }
93
+ }
94
+
95
+ // 4. Check workspace packages (lib/, packages/, modules/)
96
+ if (workspaceRoot) {
97
+ const packageDirs = ["lib", "packages", "modules", "libraries", "apps"];
98
+ for (const dir of packageDirs) {
99
+ const searchDir = path.join(workspaceRoot, dir);
100
+ if (!(await fileExists(searchDir))) continue;
101
+
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);
113
+ if (await fileExists(path.join(packagePath, "package.json"))) {
114
+ return { path: packagePath, type: 'workspace' };
115
+ }
116
+ }
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ /**
123
+ * Find all possible workspace roots by searching up the tree
124
+ */
125
+ export async function findWorkspaceRoots(startPath: string): Promise<string[]> {
126
+ const roots: string[] = [];
127
+ let current = startPath;
128
+
129
+ for (let i = 0; i < 20; i++) {
130
+ const workspaceFile = path.join(current, "pnpm-workspace.yaml");
131
+ const packageJson = path.join(current, "package.json");
132
+
133
+ try {
134
+ if (await fileExists(workspaceFile)) {
135
+ roots.push(current);
136
+ } else if (await fileExists(packageJson)) {
137
+ const pkg = JSON.parse(await fs.readFile(packageJson, "utf-8"));
138
+ if (pkg?.workspaces) {
139
+ roots.push(current);
140
+ }
141
+ }
142
+ } catch {
143
+ // Continue
144
+ }
145
+
146
+ const parent = path.dirname(current);
147
+ if (parent === current) break;
148
+ current = parent;
149
+ }
150
+
151
+ return roots;
152
+ }
153
+
154
+ /**
155
+ * Check if a file exists
156
+ */
157
+ async function fileExists(filePath: string): Promise<boolean> {
158
+ try {
159
+ await fs.access(filePath);
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
@@ -0,0 +1,198 @@
1
+ /*
2
+ * Package Registry - Dynamic package discovery
3
+ * Scans workspace to find all packages and caches their locations
4
+ * No hardcoded paths - discovers packages by scanning package.json files
5
+ */
6
+
7
+ import { promises as fs } from "node:fs";
8
+ import path from "node:path";
9
+ import chalk from "chalk";
10
+
11
+ export interface PackageInfo {
12
+ name: string;
13
+ path: string; // Full file system path to package directory
14
+ packageJson: any;
15
+ }
16
+
17
+ export class PackageRegistry {
18
+ private packages = new Map<string, PackageInfo>();
19
+ private scanned = false;
20
+ private scanRoots: string[] = [];
21
+
22
+ /**
23
+ * Scan workspace for all packages
24
+ */
25
+ async scanWorkspace(
26
+ workspaceRoot: string,
27
+ additionalRoots: string[] = []
28
+ ): Promise<void> {
29
+ if (this.scanned) {
30
+ return; // Already scanned
31
+ }
32
+
33
+ // Validate workspace root exists
34
+ if (!workspaceRoot) {
35
+ console.warn(chalk.yellow("[PackageRegistry] No workspace root provided, skipping scan"));
36
+ return;
37
+ }
38
+
39
+ try {
40
+ const rootStat = await fs.stat(workspaceRoot);
41
+ if (!rootStat.isDirectory()) {
42
+ console.warn(chalk.yellow(`[PackageRegistry] Workspace root is not a directory: ${workspaceRoot}`));
43
+ return;
44
+ }
45
+ } catch (error: any) {
46
+ console.warn(chalk.yellow(`[PackageRegistry] Cannot access workspace root ${workspaceRoot}:`, error.message));
47
+ return;
48
+ }
49
+
50
+ this.scanRoots = [workspaceRoot, ...additionalRoots.filter(root => root && root !== workspaceRoot)];
51
+ console.log(chalk.blue(`[PackageRegistry] Scanning workspace for packages...`));
52
+ console.log(chalk.gray(`[PackageRegistry] Roots: ${this.scanRoots.join(", ")}`));
53
+
54
+ for (const root of this.scanRoots) {
55
+ if (root) {
56
+ await this.scanDirectory(root);
57
+ }
58
+ }
59
+
60
+ this.scanned = true;
61
+ console.log(
62
+ chalk.green(
63
+ `[PackageRegistry] ✅ Found ${this.packages.size} packages`
64
+ )
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Recursively scan directory for package.json files
70
+ */
71
+ private async scanDirectory(dir: string, depth: number = 0): Promise<void> {
72
+ if (depth > 15) return; // Prevent infinite recursion
73
+
74
+ // Validate directory exists and is accessible
75
+ try {
76
+ const dirStat = await fs.stat(dir);
77
+ if (!dirStat.isDirectory()) {
78
+ return;
79
+ }
80
+ } catch (error: any) {
81
+ // Directory doesn't exist or permission denied, skip silently
82
+ if (error.code === "ENOENT" || error.code === "EACCES") {
83
+ return;
84
+ }
85
+ console.warn(chalk.yellow(`[PackageRegistry] Cannot access ${dir}:`, error.message));
86
+ return;
87
+ }
88
+
89
+ try {
90
+ const entries = await fs.readdir(dir, { withFileTypes: true });
91
+
92
+ for (const entry of entries) {
93
+ if (entry.isDirectory()) {
94
+ // Skip common directories that shouldn't be scanned
95
+ if (
96
+ entry.name === "node_modules" ||
97
+ entry.name === "dist" ||
98
+ entry.name === ".git" ||
99
+ entry.name === ".swite" ||
100
+ entry.name.startsWith(".")
101
+ ) {
102
+ continue;
103
+ }
104
+
105
+ const packageJsonPath = path.join(dir, entry.name, "package.json");
106
+
107
+ try {
108
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
109
+ const packageJson = JSON.parse(packageJsonContent);
110
+
111
+ if (packageJson.name) {
112
+ const packagePath = path.join(dir, entry.name);
113
+ const packageInfo: PackageInfo = {
114
+ name: packageJson.name,
115
+ path: packagePath,
116
+ packageJson,
117
+ };
118
+
119
+ // Store package - if duplicate name, prefer the one found first (or closest to workspace root)
120
+ if (!this.packages.has(packageJson.name)) {
121
+ this.packages.set(packageJson.name, packageInfo);
122
+ console.log(
123
+ chalk.gray(
124
+ `[PackageRegistry] Found: ${packageJson.name} at ${packagePath}`
125
+ )
126
+ );
127
+ } else {
128
+ // Log duplicate but don't overwrite (first found wins)
129
+ console.log(
130
+ chalk.yellow(
131
+ `[PackageRegistry] Duplicate package ${packageJson.name} found at ${packagePath}, keeping first`
132
+ )
133
+ );
134
+ }
135
+ }
136
+ } catch (error: any) {
137
+ // Not a package.json or invalid JSON, continue scanning
138
+ // Silently ignore - this is expected for non-package directories
139
+ }
140
+
141
+ // Recurse into subdirectories (but skip if we found a package.json here)
142
+ // This allows nested package layouts (e.g. packages/foo/modules/bar)
143
+ await this.scanDirectory(path.join(dir, entry.name), depth + 1);
144
+ }
145
+ }
146
+ } catch (error: any) {
147
+ // Directory read error, log but don't fail
148
+ if (error.code !== "ENOENT" && error.code !== "EACCES") {
149
+ console.warn(chalk.yellow(`[PackageRegistry] Error reading directory ${dir}:`, error.message));
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Find package by name
156
+ */
157
+ findPackage(packageName: string): PackageInfo | null {
158
+ return this.packages.get(packageName) || null;
159
+ }
160
+
161
+ /**
162
+ * Get all packages
163
+ */
164
+ getAllPackages(): PackageInfo[] {
165
+ return Array.from(this.packages.values());
166
+ }
167
+
168
+ /**
169
+ * Clear cache and rescan
170
+ */
171
+ async rescan(): Promise<void> {
172
+ const roots = [...this.scanRoots];
173
+ this.packages.clear();
174
+ this.scanned = false;
175
+ this.scanRoots = [];
176
+
177
+ if (roots.length > 0) {
178
+ await this.scanWorkspace(roots[0], roots.slice(1));
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Get package count
184
+ */
185
+ getPackageCount(): number {
186
+ return this.packages.size;
187
+ }
188
+ }
189
+
190
+ // Singleton instance
191
+ let registryInstance: PackageRegistry | null = null;
192
+
193
+ export function getPackageRegistry(): PackageRegistry {
194
+ if (!registryInstance) {
195
+ registryInstance = new PackageRegistry();
196
+ }
197
+ return registryInstance;
198
+ }
@@ -0,0 +1,62 @@
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
+
10
+ /**
11
+ * Find the workspace root by looking for pnpm-workspace.yaml or package.json with workspaces
12
+ * Updated: Now also checks for lib/ directory to ensure we find the correct SWS root
13
+ */
14
+ export async function findWorkspaceRoot(root: string): Promise<string | null> {
15
+ let current = root;
16
+ for (let i = 0; i < 10; i++) { // Increased from 5 to 10 to go higher up
17
+ const workspaceFile = path.join(current, "pnpm-workspace.yaml");
18
+ const packageJson = path.join(current, "package.json");
19
+ const libDir = path.join(current, "lib");
20
+
21
+ try {
22
+ await fs.access(workspaceFile);
23
+ // Accept root if it has lib/ (SWS with lib/) or packages/ (SWS with packages/ at root)
24
+ const packagesDir = path.join(current, "packages");
25
+ try {
26
+ await fs.access(libDir);
27
+ console.log(`[workspace] Found workspace root with lib/: ${current}`);
28
+ return current;
29
+ } catch {
30
+ try {
31
+ await fs.access(packagesDir);
32
+ console.log(`[workspace] Found workspace root with packages/: ${current}`);
33
+ return current;
34
+ } catch {
35
+ // Workspace file exists but no lib/ or packages/, continue searching up
36
+ console.log(`[workspace] Found workspace file at ${current} but no lib/ or packages/, continuing search...`);
37
+ }
38
+ }
39
+ } catch {
40
+ try {
41
+ const pkgJson = JSON.parse(await fs.readFile(packageJson, "utf-8"));
42
+ if (pkgJson?.workspaces) {
43
+ // Also check for lib/ when package.json has workspaces
44
+ try {
45
+ await fs.access(libDir);
46
+ console.log(`[workspace] Found workspace root with lib/ (via package.json): ${current}`);
47
+ return current;
48
+ } catch {
49
+ // Has workspaces but no lib/, continue searching
50
+ }
51
+ }
52
+ } catch {
53
+ // Continue searching
54
+ }
55
+ }
56
+ const parent = path.dirname(current);
57
+ if (parent === current) break;
58
+ current = parent;
59
+ }
60
+ console.warn(`[workspace] No workspace root found starting from: ${root}`);
61
+ return null;
62
+ }