@ldlework/workmark 1.0.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.
@@ -0,0 +1,124 @@
1
+ import { dirname, join, relative } from "node:path";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { createJiti } from "jiti";
4
+ import ignore, { type Ignore } from "ignore";
5
+ import { Project } from "./project.js";
6
+ import type { IProject, IWorkspace, ProjectDef } from "./types.js";
7
+ import { jitiOptions } from "./jiti-options.js";
8
+
9
+ export class Workspace implements IWorkspace {
10
+ readonly root: string;
11
+ readonly projects: readonly IProject[];
12
+
13
+ private readonly byName: Map<string, IProject>;
14
+
15
+ constructor(root: string, projects: IProject[]) {
16
+ this.root = root;
17
+ this.projects = Object.freeze(projects);
18
+ this.byName = new Map(projects.map((p) => [p.name, p]));
19
+
20
+ // Validate unique names
21
+ if (this.byName.size !== projects.length) {
22
+ const seen = new Set<string>();
23
+ for (const p of projects) {
24
+ if (seen.has(p.name)) throw new Error(`Duplicate project name: "${p.name}"`);
25
+ seen.add(p.name);
26
+ }
27
+ }
28
+ }
29
+
30
+ get(name: string): IProject {
31
+ const p = this.byName.get(name);
32
+ if (!p) throw new Error(`Unknown project: "${name}"`);
33
+ return p;
34
+ }
35
+
36
+ withCapability(cap: string): IProject[] {
37
+ return this.projects.filter((p) => p.has(cap));
38
+ }
39
+
40
+ withTag(tag: string): IProject[] {
41
+ return this.projects.filter((p) => p.tags.includes(tag));
42
+ }
43
+ }
44
+
45
+ /** Build an Ignore instance from .gitignore (if present) + hardcoded defaults. */
46
+ function loadIgnore(root: string): Ignore {
47
+ const ig = ignore().add(["node_modules", "dist"]);
48
+ const gitignorePath = join(root, ".gitignore");
49
+ if (existsSync(gitignorePath)) {
50
+ ig.add(readFileSync(gitignorePath, "utf-8"));
51
+ }
52
+ return ig;
53
+ }
54
+
55
+ /** Recursively find all ws.ts files, respecting .gitignore rules. */
56
+ function findWsFiles(root: string): string[] {
57
+ const ig = loadIgnore(root);
58
+ const results: string[] = [];
59
+
60
+ function walk(dir: string): void {
61
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
62
+ const rel = relative(root, join(dir, entry.name));
63
+ if (entry.isDirectory()) {
64
+ if (!ig.ignores(rel + "/")) walk(join(dir, entry.name));
65
+ } else if (entry.name === "ws.ts") {
66
+ results.push(join(dir, entry.name));
67
+ }
68
+ }
69
+ }
70
+
71
+ walk(root);
72
+ return results;
73
+ }
74
+
75
+ /** Find the workspace root by walking up from cwd looking for .ws/. */
76
+ function findRoot(from: string): string {
77
+ let dir = from;
78
+ while (true) {
79
+ if (existsSync(join(dir, ".ws"))) {
80
+ return dir;
81
+ }
82
+ const parent = dirname(dir);
83
+ if (parent === dir) throw new Error("Could not find workspace root (.ws/ directory)");
84
+ dir = parent;
85
+ }
86
+ }
87
+
88
+ export async function loadWorkspace(from?: string): Promise<Workspace> {
89
+ const root = from ?? process.env.WORKSPACE_ROOT ?? findRoot(process.cwd());
90
+ const jiti = createJiti(root, jitiOptions());
91
+
92
+ // Recursively find all ws.ts files
93
+ const wsFiles = findWsFiles(root);
94
+
95
+ // Import each and build Project instances
96
+ const projects: Project[] = [];
97
+
98
+ for (const wsFile of wsFiles) {
99
+ const dir = dirname(wsFile);
100
+ let exported: unknown;
101
+ try {
102
+ exported = await jiti.import(wsFile);
103
+ } catch (err) {
104
+ // Log import failures — helps debug bad ws.ts files
105
+ console.error(`[workspace] Skipping ${wsFile}: ${(err as Error).message}`);
106
+ continue;
107
+ }
108
+
109
+ // jiti may return { default: ... } when interopDefault doesn't fully unwrap
110
+ if (exported && typeof exported === "object" && "default" in (exported as Record<string, unknown>)) {
111
+ exported = (exported as Record<string, unknown>).default;
112
+ }
113
+
114
+ const items: unknown[] = Array.isArray(exported) ? exported : [exported];
115
+
116
+ for (const item of items) {
117
+ if (item && typeof item === "object" && typeof (item as Record<string, unknown>).name === "string") {
118
+ projects.push(new Project(item as ProjectDef, dir));
119
+ }
120
+ }
121
+ }
122
+
123
+ return new Workspace(root, projects);
124
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }