@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,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
|
+
}
|