@decohere/workspace 0.0.1-dev.125 → 0.0.1-dev.5
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/package.json +3 -13
- package/src/const.ts +19 -0
- package/src/errors.ts +1 -0
- package/src/factories/config.factory.ts +21 -0
- package/src/factories/find-workspace-root.ts +32 -0
- package/src/factories/project.factory.ts +75 -0
- package/src/factories/workspace.factory.ts +44 -0
- package/{types/src/index.d.ts → src/index.ts} +7 -1
- package/src/models/has-package-json.mixin.ts +30 -0
- package/src/models/has-tsconfig.mixin.ts +48 -0
- package/src/models/package-aliases.ts +130 -0
- package/src/models/package-json.model.ts +231 -0
- package/src/models/project-file-event.ts +9 -0
- package/src/models/project.model.ts +340 -0
- package/src/models/source-extensions.ts +59 -0
- package/src/models/tsconfig.model.ts +81 -0
- package/src/models/workspace-config.type.ts +57 -0
- package/src/models/workspace.model.ts +178 -0
- package/tests/find-workspace-root.spec.ts +30 -0
- package/tests/package-aliases.spec.ts +160 -0
- package/tests/package-json.model.spec.ts +78 -0
- package/tests/project.factory.spec.ts +100 -0
- package/tests/source-extensions.spec.ts +33 -0
- package/tests/workspace.factory.spec.ts +147 -0
- package/LICENSE +0 -15
- package/commonjs/src/const.js +0 -69
- package/commonjs/src/errors.js +0 -12
- package/commonjs/src/factories/config.factory.js +0 -90
- package/commonjs/src/factories/find-workspace-root.js +0 -37
- package/commonjs/src/factories/project.factory.js +0 -73
- package/commonjs/src/factories/workspace.factory.js +0 -50
- package/commonjs/src/index.js +0 -74
- package/commonjs/src/models/has-package-json.mixin.js +0 -50
- package/commonjs/src/models/has-tsconfig.mixin.js +0 -75
- package/commonjs/src/models/package-aliases.js +0 -112
- package/commonjs/src/models/package-json.model.js +0 -202
- package/commonjs/src/models/project-file-event.js +0 -4
- package/commonjs/src/models/project.model.js +0 -316
- package/commonjs/src/models/source-extensions.js +0 -81
- package/commonjs/src/models/tsconfig.model.js +0 -97
- package/commonjs/src/models/workspace-config.type.js +0 -7
- package/commonjs/src/models/workspace.model.js +0 -153
- package/es6/src/const.js +0 -36
- package/es6/src/errors.js +0 -2
- package/es6/src/factories/config.factory.js +0 -34
- package/es6/src/factories/find-workspace-root.js +0 -26
- package/es6/src/factories/project.factory.js +0 -58
- package/es6/src/factories/workspace.factory.js +0 -35
- package/es6/src/index.js +0 -14
- package/es6/src/models/has-package-json.mixin.js +0 -35
- package/es6/src/models/has-tsconfig.mixin.js +0 -60
- package/es6/src/models/package-aliases.js +0 -97
- package/es6/src/models/package-json.model.js +0 -192
- package/es6/src/models/project-file-event.js +0 -1
- package/es6/src/models/project.model.js +0 -301
- package/es6/src/models/source-extensions.js +0 -66
- package/es6/src/models/tsconfig.model.js +0 -74
- package/es6/src/models/workspace-config.type.js +0 -4
- package/es6/src/models/workspace.model.js +0 -143
- package/types/src/const.d.ts +0 -8
- package/types/src/errors.d.ts +0 -2
- package/types/src/factories/config.factory.d.ts +0 -6
- package/types/src/factories/find-workspace-root.d.ts +0 -6
- package/types/src/factories/project.factory.d.ts +0 -8
- package/types/src/factories/workspace.factory.d.ts +0 -9
- package/types/src/models/has-package-json.mixin.d.ts +0 -9
- package/types/src/models/has-tsconfig.mixin.d.ts +0 -11
- package/types/src/models/package-aliases.d.ts +0 -28
- package/types/src/models/package-json.model.d.ts +0 -40
- package/types/src/models/project-file-event.d.ts +0 -9
- package/types/src/models/project.model.d.ts +0 -64
- package/types/src/models/source-extensions.d.ts +0 -22
- package/types/src/models/tsconfig.model.d.ts +0 -11
- package/types/src/models/workspace-config.type.d.ts +0 -52
- package/types/src/models/workspace.model.d.ts +0 -40
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { injectable } from "@decohere/di/decorators";
|
|
2
|
+
import { collections } from "@decohere/core/collections";
|
|
3
|
+
import { mixwith } from "@decohere/core/extensibility";
|
|
4
|
+
import type { FileEx } from "@decohere/core/io";
|
|
5
|
+
import type { Subscription } from "rxjs";
|
|
6
|
+
import { Subject } from "rxjs";
|
|
7
|
+
import { WorkspaceConfigurationError } from "../errors.js";
|
|
8
|
+
import { HasPackageJson } from "./has-package-json.mixin.js";
|
|
9
|
+
import { HasTsConfig } from "./has-tsconfig.mixin.js";
|
|
10
|
+
import { type PackageAliases, packageAliases } from "./package-aliases.js";
|
|
11
|
+
import { PackageJson } from "./package-json.model.js";
|
|
12
|
+
import { Project, type ProjectSeed } from "./project.model.js";
|
|
13
|
+
import type { ProjectFileEvent } from "./project-file-event.js";
|
|
14
|
+
import type { WorkspaceOptions } from "./workspace-config.type.js";
|
|
15
|
+
|
|
16
|
+
export type WorkspaceInitOptions = {
|
|
17
|
+
packageJson?: PackageJson;
|
|
18
|
+
config: Partial<WorkspaceOptions>;
|
|
19
|
+
projects: ProjectSeed[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
@injectable()
|
|
23
|
+
export class Workspace extends mixwith(HasPackageJson, HasTsConfig) {
|
|
24
|
+
protected packageJson?: PackageJson;
|
|
25
|
+
readonly #name: string;
|
|
26
|
+
readonly #config: Partial<WorkspaceOptions>;
|
|
27
|
+
readonly #projectSeeds: readonly ProjectSeed[] = [];
|
|
28
|
+
readonly #projectCache = new Map<string, readonly Project[]>();
|
|
29
|
+
/**
|
|
30
|
+
* Each project's source file paths as a set, so `findProject` is a lookup rather than a scan
|
|
31
|
+
* of every file: the Vitest plugin asks once per transformed file (#517). Keyed by the list
|
|
32
|
+
* `getAllFiles` returns, so a new list (after its cache is flushed) gets a new set.
|
|
33
|
+
*/
|
|
34
|
+
readonly #sourcePaths = new WeakMap<readonly FileEx[], Set<string>>();
|
|
35
|
+
readonly #projectFileChangedSubject = new Subject<ProjectFileEvent>();
|
|
36
|
+
readonly #projectWatchSubscriptions = new Map<string, Subscription>();
|
|
37
|
+
#watchedProjects: readonly Project[] = [];
|
|
38
|
+
|
|
39
|
+
public constructor(options: Partial<WorkspaceInitOptions> = {}) {
|
|
40
|
+
super();
|
|
41
|
+
const { packageJson, config, projects } = options;
|
|
42
|
+
this.#name = packageJson?.name ?? "WORKSPACE";
|
|
43
|
+
this.packageJson = packageJson ?? new PackageJson();
|
|
44
|
+
this.#config = config ?? {};
|
|
45
|
+
this.#projectSeeds = projects ?? [];
|
|
46
|
+
this.validateWorkspace();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public get name() {
|
|
50
|
+
return this.#name;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected get projectSeeds(): readonly ProjectSeed[] {
|
|
54
|
+
return this.#projectSeeds;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public getProjects(configuration: string): readonly Project[] {
|
|
58
|
+
const cached = this.#projectCache.get(configuration);
|
|
59
|
+
if (cached != null) {
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const projects = this.#projectSeeds.map(
|
|
64
|
+
(seed) => new Project(seed.name, seed.config ?? ({} as never), configuration),
|
|
65
|
+
);
|
|
66
|
+
this.#projectCache.set(configuration, projects);
|
|
67
|
+
return projects;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public get rootDir(): string {
|
|
71
|
+
return this.#config.rootDir ?? "./";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public get packageJsonDirectory(): string {
|
|
75
|
+
return this.rootDir;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected get tsConfigDirectory(): string {
|
|
79
|
+
return this.rootDir;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected get fallbackTsConfigDirectory(): string | undefined {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Package-name aliases for this configuration: workspace convention, then
|
|
88
|
+
* workspace `tsconfig.json` `paths` overrides. Forge and Flux must use this
|
|
89
|
+
* instead of assembling the map themselves.
|
|
90
|
+
*/
|
|
91
|
+
public getPackageAliases(configuration: string): PackageAliases {
|
|
92
|
+
const tsconfig = this.findTsConfigPath() != null ? this.getTsConfigSync() : undefined;
|
|
93
|
+
const baseUrl =
|
|
94
|
+
typeof tsconfig?.compilerOptions?.baseUrl === "string" &&
|
|
95
|
+
tsconfig.compilerOptions.baseUrl.length > 0
|
|
96
|
+
? tsconfig.compilerOptions.baseUrl
|
|
97
|
+
: this.rootDir;
|
|
98
|
+
|
|
99
|
+
return packageAliases.build(this.getProjects(configuration), {
|
|
100
|
+
baseUrl,
|
|
101
|
+
tsconfigPaths: tsconfig?.compilerOptions?.paths as Record<string, string[]> | undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public async watch(configuration: string) {
|
|
106
|
+
this.stopWatching();
|
|
107
|
+
const projects = this.getProjects(configuration);
|
|
108
|
+
this.#watchedProjects = projects;
|
|
109
|
+
|
|
110
|
+
for (const project of projects) {
|
|
111
|
+
await project.watch(configuration);
|
|
112
|
+
const subscription = project.projectFileChanged$.subscribe((evt) =>
|
|
113
|
+
this.#projectFileChangedSubject.next(evt),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
this.#projectWatchSubscriptions.set(project.projectDirectory, subscription);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public stopWatching() {
|
|
121
|
+
for (const subscription of this.#projectWatchSubscriptions.values()) {
|
|
122
|
+
subscription.unsubscribe();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const project of this.#watchedProjects) {
|
|
126
|
+
project.stopWatching();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.#watchedProjects = [];
|
|
130
|
+
this.#projectWatchSubscriptions.clear();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public [Symbol.dispose]() {
|
|
134
|
+
this.stopWatching();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public get projectFileChanged$() {
|
|
138
|
+
return this.#projectFileChangedSubject.asObservable();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public async findProject(
|
|
142
|
+
filePath: string,
|
|
143
|
+
buildConfig: string,
|
|
144
|
+
): Promise<Project | undefined> {
|
|
145
|
+
for (const project of this.getProjects(buildConfig)) {
|
|
146
|
+
if ((await this.sourcePathsOf(project)).has(filePath)) {
|
|
147
|
+
return project;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async sourcePathsOf(project: Project): Promise<Set<string>> {
|
|
154
|
+
const files = await project.getAllFiles();
|
|
155
|
+
const cached = this.#sourcePaths.get(files);
|
|
156
|
+
if (cached != null) return cached;
|
|
157
|
+
const paths = new Set(files.map((file) => file.fullPath));
|
|
158
|
+
this.#sourcePaths.set(files, paths);
|
|
159
|
+
return paths;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private validateWorkspace() {
|
|
163
|
+
const errors = this.findDuplicateProjectSeeds(this.#projectSeeds);
|
|
164
|
+
if (errors.length > 0) {
|
|
165
|
+
throw new WorkspaceConfigurationError(errors.join("\n"));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private findDuplicateProjectSeeds(seeds: readonly ProjectSeed[]): string[] {
|
|
170
|
+
const duplicates = collections.findDuplicatesByKey([...seeds], "name");
|
|
171
|
+
return duplicates.map(
|
|
172
|
+
([name, group]) =>
|
|
173
|
+
`Duplicate project name "${name}" found at: ${group
|
|
174
|
+
.map((seed) => seed.config?.projectDir ?? name)
|
|
175
|
+
.join(", ")}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resetMemfs } from "@decohere/core/testing";
|
|
3
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { findWorkspaceRoot } from "../src/factories/find-workspace-root.js";
|
|
5
|
+
|
|
6
|
+
describe("findWorkspaceRoot", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
resetMemfs();
|
|
9
|
+
mkdirSync("/ws/projects/api/src", { recursive: true });
|
|
10
|
+
writeFileSync("/ws/package.json", "{}");
|
|
11
|
+
writeFileSync("/ws/projects/api/package.json", "{}");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("finds the root from a package directory below it", () => {
|
|
15
|
+
expect(findWorkspaceRoot("/ws/projects/api/src")).toBe("/ws");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns the root itself", () => {
|
|
19
|
+
expect(findWorkspaceRoot("/ws")).toBe("/ws");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("skips a package.json that has no projects/ folder (a package, not the root)", () => {
|
|
23
|
+
expect(findWorkspaceRoot("/ws/projects/api")).toBe("/ws");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("falls back to the start directory outside any workspace", () => {
|
|
27
|
+
mkdirSync("/elsewhere/deep", { recursive: true });
|
|
28
|
+
expect(findWorkspaceRoot("/elsewhere/deep")).toBe("/elsewhere/deep");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
PackageJson,
|
|
4
|
+
packageAliases,
|
|
5
|
+
Workspace,
|
|
6
|
+
WorkspaceDefaults,
|
|
7
|
+
} from "@decohere/workspace";
|
|
8
|
+
import { resetMemfs } from "@decohere/core/testing";
|
|
9
|
+
import { vol } from "memfs";
|
|
10
|
+
import { afterEach, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
resetMemfs();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function libWorkspace(rootDir = "/ws") {
|
|
17
|
+
// Aliases point at entries that exist.
|
|
18
|
+
vol.mkdirSync(`${rootDir}/projects/lib/src`, { recursive: true });
|
|
19
|
+
vol.writeFileSync(`${rootDir}/projects/lib/src/index.ts`, "");
|
|
20
|
+
return new Workspace({
|
|
21
|
+
config: { ...WorkspaceDefaults, rootDir },
|
|
22
|
+
projects: [
|
|
23
|
+
{
|
|
24
|
+
name: "@hmr/lib",
|
|
25
|
+
config: {
|
|
26
|
+
...WorkspaceDefaults,
|
|
27
|
+
rootDir,
|
|
28
|
+
projectDir: "projects/lib",
|
|
29
|
+
packageJson: new PackageJson({ name: "@hmr/lib", main: "src/index.ts" }),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it("maps workspace package names to source entries relative to the workspace root", () => {
|
|
37
|
+
const aliases = libWorkspace().getPackageAliases("debug");
|
|
38
|
+
expect(aliases.baseUrl).toBe(path.resolve("/ws"));
|
|
39
|
+
expect(aliases.paths["@hmr/lib"]).toEqual(["projects/lib/src/index.ts"]);
|
|
40
|
+
expect(aliases.paths["@hmr/lib/*"]).toEqual(["projects/lib/src/*"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("lets workspace tsconfig paths override convention aliases", () => {
|
|
44
|
+
vol.fromJSON({
|
|
45
|
+
"/ws/tsconfig.json": JSON.stringify({
|
|
46
|
+
compilerOptions: {
|
|
47
|
+
baseUrl: "/ws",
|
|
48
|
+
paths: { "@hmr/lib": ["projects/lib/src/override.ts"] },
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const aliases = libWorkspace().getPackageAliases("debug");
|
|
54
|
+
expect(aliases.paths["@hmr/lib"]).toEqual(["projects/lib/src/override.ts"]);
|
|
55
|
+
expect(aliases.paths["@hmr/lib/*"]).toEqual(["projects/lib/src/*"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const aliasPaths = {
|
|
59
|
+
"@hmr/lib": ["projects/lib/src/index.ts"],
|
|
60
|
+
"@hmr/lib/*": ["projects/lib/src/*"],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
it("does not treat relative specifiers as packages", () => {
|
|
64
|
+
expect(packageAliases.resolvePath("./dep-a.js", aliasPaths, "/workspace")).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("maps an exact package name to its source entry", () => {
|
|
68
|
+
expect(packageAliases.resolvePath("@hmr/lib", aliasPaths, "/workspace")).toBe(
|
|
69
|
+
path.resolve("/workspace/projects/lib/src/index.ts"),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("maps a package subpath through the name/* alias", () => {
|
|
74
|
+
expect(packageAliases.resolvePath("@hmr/lib/other", aliasPaths, "/workspace")).toBe(
|
|
75
|
+
path.resolve("/workspace/projects/lib/src/other.ts"),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("maps a .js alias target to the corresponding .ts source", () => {
|
|
80
|
+
expect(
|
|
81
|
+
packageAliases.resolvePath(
|
|
82
|
+
"@hmr/lib",
|
|
83
|
+
{ "@hmr/lib": ["projects/lib/src/index.js"] },
|
|
84
|
+
"/workspace",
|
|
85
|
+
),
|
|
86
|
+
).toBe(path.resolve("/workspace/projects/lib/src/index.ts"));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("ignores node builtins, internals, and empty specifiers", () => {
|
|
90
|
+
expect(packageAliases.resolvePath("node:fs", aliasPaths, "/workspace")).toBeNull();
|
|
91
|
+
expect(packageAliases.resolvePath("#internal", aliasPaths, "/workspace")).toBeNull();
|
|
92
|
+
expect(packageAliases.resolvePath("", aliasPaths, "/workspace")).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// #518: subpaths resolve from the package's exports and by convention, without tsconfig paths.
|
|
96
|
+
function publishedLayoutWorkspace() {
|
|
97
|
+
vol.fromJSON({
|
|
98
|
+
"/ws/projects/web/src/index.ts": "",
|
|
99
|
+
"/ws/projects/web/src/auth/auth.extensions.ts": "",
|
|
100
|
+
"/ws/projects/web/src/http/index.ts": "",
|
|
101
|
+
"/ws/projects/util/src/io/index.ts": "",
|
|
102
|
+
});
|
|
103
|
+
const project = (name: string, projectDir: string, packageJson: PackageJson) => ({
|
|
104
|
+
name,
|
|
105
|
+
config: { ...WorkspaceDefaults, rootDir: "/ws", projectDir, packageJson },
|
|
106
|
+
});
|
|
107
|
+
return new Workspace({
|
|
108
|
+
config: { ...WorkspaceDefaults, rootDir: "/ws" },
|
|
109
|
+
projects: [
|
|
110
|
+
// `main` and `exports` describe the published layout, as for a released package.
|
|
111
|
+
project(
|
|
112
|
+
"@t/web",
|
|
113
|
+
"projects/web",
|
|
114
|
+
new PackageJson({
|
|
115
|
+
name: "@t/web",
|
|
116
|
+
main: "index.js",
|
|
117
|
+
exports: {
|
|
118
|
+
".": { types: "./types/src/index.d.ts", import: "./es6/src/index.js" },
|
|
119
|
+
"./auth": { import: "./es6/src/auth/auth.extensions.js" },
|
|
120
|
+
"./gone": { import: "./es6/src/gone/index.js" },
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
// Only subpaths, no root entry.
|
|
125
|
+
project(
|
|
126
|
+
"@t/util",
|
|
127
|
+
"projects/util",
|
|
128
|
+
new PackageJson({ name: "@t/util", main: "index.js" }),
|
|
129
|
+
),
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
it("maps a published-layout main and exported subpaths back to their sources", () => {
|
|
135
|
+
const { paths } = publishedLayoutWorkspace().getPackageAliases("debug");
|
|
136
|
+
expect(paths["@t/web"]).toEqual(["projects/web/src/index.ts"]);
|
|
137
|
+
expect(paths["@t/web/auth"]).toEqual(["projects/web/src/auth/auth.extensions.ts"]);
|
|
138
|
+
expect(paths["@t/web/*"]).toEqual(["projects/web/src/*"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("leaves out an exported subpath whose source does not exist", () => {
|
|
142
|
+
const { paths } = publishedLayoutWorkspace().getPackageAliases("debug");
|
|
143
|
+
expect(paths["@t/web/gone"]).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("gives a package with only subpaths no bare alias, and resolves its subpaths under src", () => {
|
|
147
|
+
const { paths, baseUrl } = publishedLayoutWorkspace().getPackageAliases("debug");
|
|
148
|
+
expect(paths["@t/util"]).toBeUndefined();
|
|
149
|
+
expect(paths["@t/util/*"]).toEqual(["projects/util/src/*"]);
|
|
150
|
+
expect(packageAliases.resolvePath("@t/util/io", paths, baseUrl)).toBe(
|
|
151
|
+
path.resolve("/ws/projects/util/src/io/index.ts"),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("resolves a wildcard subpath to the directory's index.ts when there is no <sub>.ts", () => {
|
|
156
|
+
const { paths, baseUrl } = publishedLayoutWorkspace().getPackageAliases("debug");
|
|
157
|
+
expect(packageAliases.resolvePath("@t/web/http", paths, baseUrl)).toBe(
|
|
158
|
+
path.resolve("/ws/projects/web/src/http/index.ts"),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { PackageJson } from "@decohere/workspace";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe("PackageJson repository parsing", () => {
|
|
5
|
+
it("parses github shorthand repository", () => {
|
|
6
|
+
const packageJson = new PackageJson({
|
|
7
|
+
name: "workspace",
|
|
8
|
+
version: "1.2.3",
|
|
9
|
+
repository: "github:typesmiths/decohere",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(packageJson.getRepositoryOwner()).toBe("typesmiths");
|
|
13
|
+
expect(packageJson.getRepositoryName()).toBe("decohere");
|
|
14
|
+
expect(packageJson.getRepositoryFullName()).toBe("typesmiths/decohere");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("parses git+https GitHub repository URL object", () => {
|
|
18
|
+
const packageJson = new PackageJson({
|
|
19
|
+
name: "workspace",
|
|
20
|
+
version: "1.2.3",
|
|
21
|
+
repository: {
|
|
22
|
+
type: "git",
|
|
23
|
+
url: "git+https://github.com/typesmiths/decohere.git",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(packageJson.getRepositoryOwner()).toBe("typesmiths");
|
|
28
|
+
expect(packageJson.getRepositoryName()).toBe("decohere");
|
|
29
|
+
expect(packageJson.getRepositoryFullName()).toBe("typesmiths/decohere");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("normalizes owner and repository name to lowercase", () => {
|
|
33
|
+
const packageJson = new PackageJson({
|
|
34
|
+
name: "workspace",
|
|
35
|
+
version: "1.2.3",
|
|
36
|
+
repository: "github:TypeSmiths/DecoHere",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(packageJson.getRepositoryOwner()).toBe("typesmiths");
|
|
40
|
+
expect(packageJson.getRepositoryName()).toBe("decohere");
|
|
41
|
+
expect(packageJson.getRepositoryFullName()).toBe("typesmiths/decohere");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("parses non-GitHub repository URL values", () => {
|
|
45
|
+
const packageJson = new PackageJson({
|
|
46
|
+
name: "workspace",
|
|
47
|
+
version: "1.2.3",
|
|
48
|
+
repository: "https://gitlab.com/typesmiths/decohere.git",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(packageJson.getRepositoryOwner()).toBe("typesmiths");
|
|
52
|
+
expect(packageJson.getRepositoryName()).toBe("decohere");
|
|
53
|
+
expect(packageJson.getRepositoryFullName()).toBe("typesmiths/decohere");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("parses SSH repository URL values", () => {
|
|
57
|
+
const packageJson = new PackageJson({
|
|
58
|
+
name: "workspace",
|
|
59
|
+
version: "1.2.3",
|
|
60
|
+
repository: "git@gitlab.com:typesmiths/decohere.git",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(packageJson.getRepositoryOwner()).toBe("typesmiths");
|
|
64
|
+
expect(packageJson.getRepositoryName()).toBe("decohere");
|
|
65
|
+
expect(packageJson.getRepositoryFullName()).toBe("typesmiths/decohere");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns undefined when repository is missing", () => {
|
|
69
|
+
const packageJson = new PackageJson({
|
|
70
|
+
name: "workspace",
|
|
71
|
+
version: "1.2.3",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(packageJson.getRepositoryOwner()).toBeUndefined();
|
|
75
|
+
expect(packageJson.getRepositoryName()).toBeUndefined();
|
|
76
|
+
expect(packageJson.getRepositoryFullName()).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ProjectFactory, type WorkspaceOptions } from "@decohere/workspace";
|
|
2
|
+
import { resetMemfs } from "@decohere/core/testing";
|
|
3
|
+
import { vol } from "memfs";
|
|
4
|
+
import { afterEach, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const baseOptions: WorkspaceOptions = {
|
|
7
|
+
rootDir: ".",
|
|
8
|
+
outDir: "dist",
|
|
9
|
+
includeProjects: ["projects/**/package.json"],
|
|
10
|
+
ignoreProjects: ["**/node_modules/**", "**/__fixtures__/**"],
|
|
11
|
+
copyFiles: [],
|
|
12
|
+
configurations: [{ name: "debug" }],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
resetMemfs();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("builds projects from discovered package.json files", async () => {
|
|
20
|
+
vol.fromJSON({
|
|
21
|
+
"projects/path/to/nested1/package.json": JSON.stringify({ name: "MOCK_PROJ" }),
|
|
22
|
+
"projects/path/to/nested2/package.json": JSON.stringify({ name: "MOCK_PROJ2" }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const projects = await ProjectFactory.getProjects(baseOptions, "debug");
|
|
26
|
+
expect(projects).toHaveLength(2);
|
|
27
|
+
expect(projects.map((p) => p.name)).toEqual(["MOCK_PROJ", "MOCK_PROJ2"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("detects projects using include patterns", async () => {
|
|
31
|
+
vol.fromJSON({
|
|
32
|
+
"projects/package.json": JSON.stringify({ name: "NOT_INCLUDED" }),
|
|
33
|
+
"MOCK_DIR/package.json": JSON.stringify({ name: "MOCK_PROJ1" }),
|
|
34
|
+
"MOCK_DIR1/package.json": JSON.stringify({ name: "MOCK_PROJ2" }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const config: WorkspaceOptions = {
|
|
38
|
+
...baseOptions,
|
|
39
|
+
includeProjects: ["MOCK_DIR/**", "MOCK_DIR1/**"],
|
|
40
|
+
};
|
|
41
|
+
const projects = await ProjectFactory.getProjects(config, "debug");
|
|
42
|
+
expect(projects).toHaveLength(2);
|
|
43
|
+
expect(projects).toEqual(
|
|
44
|
+
expect.arrayContaining([
|
|
45
|
+
expect.objectContaining({ name: "MOCK_PROJ1" }),
|
|
46
|
+
expect.objectContaining({ name: "MOCK_PROJ2" }),
|
|
47
|
+
]),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("ignores projects when ignore patterns overlap include patterns", async () => {
|
|
52
|
+
vol.fromJSON({
|
|
53
|
+
"MOCK_DIR/package.json": JSON.stringify({ name: "MOCK_PROJ" }),
|
|
54
|
+
"MOCK_DIR/IGNORE/package.json": JSON.stringify({ name: "IGNORE" }),
|
|
55
|
+
"MOCK_DIR/IGNORE1/package.json": JSON.stringify({ name: "IGNORE" }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const config: WorkspaceOptions = {
|
|
59
|
+
...baseOptions,
|
|
60
|
+
includeProjects: ["MOCK_DIR/**"],
|
|
61
|
+
ignoreProjects: ["MOCK_DIR/IGNORE/**", "MOCK_DIR/IGNORE1/**"],
|
|
62
|
+
};
|
|
63
|
+
const projects = await ProjectFactory.getProjects(config, "debug");
|
|
64
|
+
expect(projects).toHaveLength(1);
|
|
65
|
+
expect(projects[0]?.name).toBe("MOCK_PROJ");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("ignores projects by exact file path", async () => {
|
|
69
|
+
vol.fromJSON({
|
|
70
|
+
"MOCK_DIR/package.json": JSON.stringify({ name: "MOCK_PROJ1" }),
|
|
71
|
+
"MOCK_DIR/IGNORE/PROJ_1/package.json": JSON.stringify({ name: "MOCK_PROJ2" }),
|
|
72
|
+
"MOCK_DIR/IGNORE/PROJ_2/package.json": JSON.stringify({ name: "IGNORED" }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const config: WorkspaceOptions = {
|
|
76
|
+
...baseOptions,
|
|
77
|
+
includeProjects: ["MOCK_DIR/**"],
|
|
78
|
+
ignoreProjects: ["MOCK_DIR/IGNORE/PROJ_2/package.json"],
|
|
79
|
+
};
|
|
80
|
+
const projects = await ProjectFactory.getProjects(config, "debug");
|
|
81
|
+
expect(projects).toHaveLength(1);
|
|
82
|
+
expect(projects.map((p) => p.name)).toEqual(["MOCK_PROJ1"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("excludes package.json nested under another package.json", async () => {
|
|
86
|
+
vol.fromJSON({
|
|
87
|
+
"projects/root/package.json": JSON.stringify({ name: "ROOT" }),
|
|
88
|
+
"projects/root/fixtures/bench/lib-a/package.json": JSON.stringify({ name: "BENCH_A" }),
|
|
89
|
+
"projects/root/fixtures/bench/lib-b/package.json": JSON.stringify({ name: "BENCH_B" }),
|
|
90
|
+
"projects/standalone/package.json": JSON.stringify({ name: "STANDALONE" }),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const config: WorkspaceOptions = {
|
|
94
|
+
...baseOptions,
|
|
95
|
+
includeProjects: ["projects/**/package.json"],
|
|
96
|
+
};
|
|
97
|
+
const projects = await ProjectFactory.getProjects(config, "debug");
|
|
98
|
+
|
|
99
|
+
expect(projects.map((p) => p.name)).toEqual(["ROOT", "STANDALONE"]);
|
|
100
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { sourceExtensions } from "@decohere/workspace";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe("sourceExtensions (#518)", () => {
|
|
5
|
+
it("maps an import's extension to the sources it can name", () => {
|
|
6
|
+
expect(sourceExtensions.candidates("/a/b.js")).toEqual(["/a/b.ts", "/a/b.tsx"]);
|
|
7
|
+
expect(sourceExtensions.candidates("/a/b.mjs")).toEqual(["/a/b.mts"]);
|
|
8
|
+
expect(sourceExtensions.candidates("/a/b.cjs")).toEqual(["/a/b.cts"]);
|
|
9
|
+
expect(sourceExtensions.candidates("/a/b.jsx")).toEqual(["/a/b.tsx"]);
|
|
10
|
+
expect(sourceExtensions.candidates("/a/b")).toEqual(["/a/b.ts", "/a/b.tsx"]);
|
|
11
|
+
expect(sourceExtensions.candidates("/a/b.mts")).toEqual(["/a/b.mts"]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("treats declaration files as not sources", () => {
|
|
15
|
+
expect(sourceExtensions.isSource("/a/b.ts")).toBe(true);
|
|
16
|
+
expect(sourceExtensions.isSource("/a/b.tsx")).toBe(true);
|
|
17
|
+
expect(sourceExtensions.isSource("/a/b.d.ts")).toBe(false);
|
|
18
|
+
expect(sourceExtensions.isSource("/a/b.d.mts")).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("names build output and module kind by source extension", () => {
|
|
22
|
+
expect(sourceExtensions.outputExtension("/a/b.mts")).toBe(".mjs");
|
|
23
|
+
expect(sourceExtensions.outputExtension("/a/b.cts")).toBe(".cjs");
|
|
24
|
+
expect(sourceExtensions.outputExtension("/a/b.tsx")).toBe(".js");
|
|
25
|
+
expect(sourceExtensions.outputPath("src/a.mts")).toBe("src/a.mjs");
|
|
26
|
+
expect(sourceExtensions.outputPath("src/a.cts")).toBe("src/a.cjs");
|
|
27
|
+
expect(sourceExtensions.outputPath("src/a.tsx")).toBe("src/a.js");
|
|
28
|
+
expect(sourceExtensions.outputPath("src/a.ts")).toBe("src/a.js");
|
|
29
|
+
expect(sourceExtensions.outputPath("bin/cli")).toBe("bin/cli.js");
|
|
30
|
+
expect(sourceExtensions.isCommonJs("/a/b.cts")).toBe(true);
|
|
31
|
+
expect(sourceExtensions.isCommonJs("/a/b.mts")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|