@decohere/workspace 0.0.1-dev.126 → 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/README.md +1 -1
- 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,81 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { io } from "@decohere/core/io";
|
|
4
|
+
import ts, { type CompilerOptions } from "typescript";
|
|
5
|
+
import { WorkspaceConfigurationError } from "../errors.js";
|
|
6
|
+
|
|
7
|
+
const host = ts.sys;
|
|
8
|
+
|
|
9
|
+
host.readFile = (file) => {
|
|
10
|
+
return fs.readFileSync(file, { encoding: "utf8" }) as string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
host.readDirectory = (directoryName) => {
|
|
14
|
+
return fs
|
|
15
|
+
.readdirSync(directoryName, { withFileTypes: true })
|
|
16
|
+
.filter((entry) => entry.isDirectory())
|
|
17
|
+
.map((entry) => entry.name);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_COMPILER_OPTIONS: CompilerOptions = {
|
|
21
|
+
emitDeclarationOnly: true,
|
|
22
|
+
declaration: true,
|
|
23
|
+
baseUrl: "./",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class Tsconfig {
|
|
27
|
+
public static default() {
|
|
28
|
+
return new Tsconfig();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static async parse(filePath: string) {
|
|
32
|
+
if (!(await io.fileExists(filePath))) {
|
|
33
|
+
throw new WorkspaceConfigurationError(`TSConfig not found ${filePath}`);
|
|
34
|
+
}
|
|
35
|
+
const configFileText = await io.readFile(filePath);
|
|
36
|
+
const result = ts.parseConfigFileTextToJson(filePath, configFileText);
|
|
37
|
+
if (result.error) {
|
|
38
|
+
const errorMessage = result.error.messageText.toString();
|
|
39
|
+
throw new WorkspaceConfigurationError(`Error parsing ${filePath}. ${errorMessage}`);
|
|
40
|
+
}
|
|
41
|
+
const dirName = path.resolve(path.dirname(filePath));
|
|
42
|
+
const configParseResult = ts.parseJsonConfigFileContent(result.config, host, dirName);
|
|
43
|
+
return new Tsconfig({
|
|
44
|
+
compilerOptions: configParseResult.options,
|
|
45
|
+
errors: configParseResult.errors,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static parseSync(filePath: string) {
|
|
50
|
+
if (!Tsconfig.fileExistsSync(filePath)) {
|
|
51
|
+
throw new WorkspaceConfigurationError(`TSConfig not found ${filePath}`);
|
|
52
|
+
}
|
|
53
|
+
const configFileText = fs.readFileSync(filePath, "utf8");
|
|
54
|
+
const result = ts.parseConfigFileTextToJson(filePath, configFileText);
|
|
55
|
+
if (result.error) {
|
|
56
|
+
const errorMessage = result.error.messageText.toString();
|
|
57
|
+
throw new WorkspaceConfigurationError(`Error parsing ${filePath}. ${errorMessage}`);
|
|
58
|
+
}
|
|
59
|
+
const dirName = path.resolve(path.dirname(filePath));
|
|
60
|
+
const configParseResult = ts.parseJsonConfigFileContent(result.config, host, dirName);
|
|
61
|
+
return new Tsconfig({
|
|
62
|
+
compilerOptions: configParseResult.options,
|
|
63
|
+
errors: configParseResult.errors,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static fileExistsSync(filePath: string): boolean {
|
|
68
|
+
try {
|
|
69
|
+
return fs.statSync(filePath).isFile();
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public constructor(data: Partial<Tsconfig> = { compilerOptions: DEFAULT_COMPILER_OPTIONS }) {
|
|
76
|
+
Object.assign(this, data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public readonly compilerOptions: CompilerOptions;
|
|
80
|
+
public readonly errors: readonly ts.Diagnostic[];
|
|
81
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic configuration for a workspace build profile.
|
|
3
|
+
* Concrete tools (e.g. @decohere/forge) extend this with tool-specific fields.
|
|
4
|
+
*/
|
|
5
|
+
export type Configuration = {
|
|
6
|
+
name: string;
|
|
7
|
+
include?: string[];
|
|
8
|
+
ignore?: string[];
|
|
9
|
+
/** Extra globs to copy into the configuration outDir (non-transpiled assets). */
|
|
10
|
+
copyFiles?: string[];
|
|
11
|
+
/** If true, the output is packed into a folder per project directory. */
|
|
12
|
+
pack?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DockerConfig = {
|
|
16
|
+
image?: string;
|
|
17
|
+
dockerfile?: string;
|
|
18
|
+
buildArgs?: Record<string, string>;
|
|
19
|
+
secrets?: Record<string, string>;
|
|
20
|
+
tags?: string[];
|
|
21
|
+
push?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The UI a deployable serves from its API (`withStatic` in `@decohere/http`). `forge package` copies the
|
|
26
|
+
* UI project's build output into the deployable's `.package/` folder.
|
|
27
|
+
*/
|
|
28
|
+
export type UiConfig = {
|
|
29
|
+
/** Package name of the UI project, e.g. `@app/shop.web`. */
|
|
30
|
+
project: string;
|
|
31
|
+
/** The UI project's build output folder, relative to that project. Defaults to `dist`. */
|
|
32
|
+
output?: string;
|
|
33
|
+
/** Where it lands inside `.package/`, which is also the folder the API serves. Defaults to `ui`. */
|
|
34
|
+
to?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type WorkspaceFactoryOptions = {
|
|
38
|
+
rootDir?: string;
|
|
39
|
+
configFile?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ProjectOptions = {
|
|
43
|
+
include?: string[];
|
|
44
|
+
ignore?: string[];
|
|
45
|
+
copyFiles: string[];
|
|
46
|
+
configurations: Configuration[];
|
|
47
|
+
docker?: DockerConfig;
|
|
48
|
+
ui?: UiConfig;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type WorkspaceOptions = {
|
|
52
|
+
rootDir?: string;
|
|
53
|
+
outDir: string;
|
|
54
|
+
includeProjects: string[];
|
|
55
|
+
ignoreProjects: string[];
|
|
56
|
+
projects?: Record<string, Partial<ProjectOptions>>;
|
|
57
|
+
} & ProjectOptions;
|
|
@@ -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
|
+
});
|