@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,231 @@
|
|
|
1
|
+
export type RepositoryReference =
|
|
2
|
+
| string
|
|
3
|
+
| {
|
|
4
|
+
type?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
directory?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PackageExports = string | { [key: string]: PackageExports };
|
|
10
|
+
|
|
11
|
+
type ParsedRepository = {
|
|
12
|
+
owner: string;
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class PackageJson {
|
|
17
|
+
private static readonly PACKAGE_JSON_KEY_ORDER = [
|
|
18
|
+
"$schema",
|
|
19
|
+
"name",
|
|
20
|
+
"displayName",
|
|
21
|
+
"version",
|
|
22
|
+
"description",
|
|
23
|
+
"keywords",
|
|
24
|
+
"homepage",
|
|
25
|
+
"bugs",
|
|
26
|
+
"repository",
|
|
27
|
+
"license",
|
|
28
|
+
"author",
|
|
29
|
+
"contributors",
|
|
30
|
+
"funding",
|
|
31
|
+
"type",
|
|
32
|
+
"main",
|
|
33
|
+
"module",
|
|
34
|
+
"types",
|
|
35
|
+
"exports",
|
|
36
|
+
"files",
|
|
37
|
+
"bin",
|
|
38
|
+
"man",
|
|
39
|
+
"directories",
|
|
40
|
+
"scripts",
|
|
41
|
+
"dependencies",
|
|
42
|
+
"devDependencies",
|
|
43
|
+
"peerDependencies",
|
|
44
|
+
"peerDependenciesMeta",
|
|
45
|
+
"optionalDependencies",
|
|
46
|
+
"bundledDependencies",
|
|
47
|
+
"engines",
|
|
48
|
+
"os",
|
|
49
|
+
"cpu",
|
|
50
|
+
"private",
|
|
51
|
+
"publishConfig",
|
|
52
|
+
"workspaces",
|
|
53
|
+
"packageManager",
|
|
54
|
+
"forge",
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
public constructor(data?: Partial<PackageJson>) {
|
|
58
|
+
Object.assign(this, data);
|
|
59
|
+
this.dependencies = this.dependencies ?? {};
|
|
60
|
+
this.devDependencies = this.devDependencies ?? {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public sort(): this {
|
|
64
|
+
const sorted = this.toSortedPackageJsonRecord();
|
|
65
|
+
Object.assign(this, sorted);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public withVersion(version: string): PackageJson {
|
|
70
|
+
return new PackageJson({ ...this, version });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public withDependencies(dependencies: Record<string, string>): PackageJson {
|
|
74
|
+
return new PackageJson({ ...this, dependencies });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public withDevDependencies(devDependencies: Record<string, string>): PackageJson {
|
|
78
|
+
return new PackageJson({ ...this, devDependencies });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public toJSON(): string {
|
|
82
|
+
return `${JSON.stringify(this.toSortedPackageJsonRecord(), null, 2)}\n`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public getRepositoryOwner() {
|
|
86
|
+
return this.parseRepository()?.owner;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public getRepositoryName() {
|
|
90
|
+
return this.parseRepository()?.name;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public getRepositoryFullName() {
|
|
94
|
+
const parsed = this.parseRepository();
|
|
95
|
+
if (parsed == null) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return `${parsed.owner}/${parsed.name}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private sortDependencyMap(input: Record<string, string>) {
|
|
103
|
+
return Object.fromEntries(
|
|
104
|
+
Object.entries(input).sort(([left], [right]) => left.localeCompare(right, "en")),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private sortPackageJsonKeys(source: Record<string, unknown>) {
|
|
109
|
+
const sorted = Object.create(null) as Record<string, unknown>;
|
|
110
|
+
|
|
111
|
+
for (const key of PackageJson.PACKAGE_JSON_KEY_ORDER) {
|
|
112
|
+
if (key in source) {
|
|
113
|
+
sorted[key] = source[key];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const remainingKeys = Object.keys(source)
|
|
118
|
+
.filter((key) => !PackageJson.PACKAGE_JSON_KEY_ORDER.includes(key as never))
|
|
119
|
+
.sort();
|
|
120
|
+
|
|
121
|
+
for (const key of remainingKeys) {
|
|
122
|
+
sorted[key] = source[key];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return sorted;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private toSortedPackageJsonRecord() {
|
|
129
|
+
const clone = { ...this } as Record<string, unknown>;
|
|
130
|
+
if (clone.dependencies) {
|
|
131
|
+
clone.dependencies = this.sortDependencyMap(
|
|
132
|
+
clone.dependencies as Record<string, string>,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (clone.devDependencies) {
|
|
136
|
+
clone.devDependencies = this.sortDependencyMap(
|
|
137
|
+
clone.devDependencies as Record<string, string>,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return this.sortPackageJsonKeys(clone);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private repositoryReferenceToString(repository?: RepositoryReference) {
|
|
144
|
+
if (typeof repository === "string") {
|
|
145
|
+
return repository;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (repository == null) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return String(repository.url ?? "");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private parseRepository(): ParsedRepository | undefined {
|
|
156
|
+
const repositoryValue = this.repositoryReferenceToString(this.repository);
|
|
157
|
+
if (repositoryValue === "") {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const trimmed = repositoryValue.trim();
|
|
162
|
+
|
|
163
|
+
if (!trimmed.includes("://") && !trimmed.startsWith("git@")) {
|
|
164
|
+
const shorthand = /^(?:[a-z0-9-]+:)?([^/\s]+)\/([^\s]+)$/i.exec(trimmed);
|
|
165
|
+
if (shorthand) {
|
|
166
|
+
return {
|
|
167
|
+
owner: shorthand[1].toLowerCase(),
|
|
168
|
+
name: this.sanitizeRepositoryName(shorthand[2]),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const ssh = /^git@[^:]+:([^/\s]+)\/([^\s]+)$/i.exec(trimmed);
|
|
174
|
+
if (ssh) {
|
|
175
|
+
return {
|
|
176
|
+
owner: ssh[1].toLowerCase(),
|
|
177
|
+
name: this.sanitizeRepositoryName(ssh[2]),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const urlRepository = this.parseRepositoryFromUrl(trimmed);
|
|
182
|
+
if (urlRepository != null) {
|
|
183
|
+
return urlRepository;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private parseRepositoryFromUrl(repository: string): ParsedRepository | undefined {
|
|
190
|
+
const normalized = repository.replace(/^git\+/, "");
|
|
191
|
+
let url: URL;
|
|
192
|
+
try {
|
|
193
|
+
url = new URL(normalized);
|
|
194
|
+
} catch {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
199
|
+
if (segments.length < 2) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const owner = segments[0]?.toLowerCase();
|
|
204
|
+
const name = this.sanitizeRepositoryName(segments[1] ?? "");
|
|
205
|
+
if (owner == null || owner === "" || name === "") {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
owner,
|
|
211
|
+
name,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private sanitizeRepositoryName(name: string) {
|
|
216
|
+
return name.replace(/\.git$/i, "").toLowerCase();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public readonly name: string;
|
|
220
|
+
public version: string;
|
|
221
|
+
public readonly repository?: RepositoryReference;
|
|
222
|
+
public readonly main: string;
|
|
223
|
+
/** `exports` as written in package.json: a path, or a map of subpaths (values may be condition maps). */
|
|
224
|
+
public readonly exports?: PackageExports;
|
|
225
|
+
public readonly peerDependencies?: Record<string, string>;
|
|
226
|
+
public readonly optionalDependencies?: Record<string, string>;
|
|
227
|
+
public readonly scripts: Record<string, string>;
|
|
228
|
+
public dependencies: Record<string, string> = {};
|
|
229
|
+
public devDependencies: Record<string, string> = {};
|
|
230
|
+
public readonly private?: boolean;
|
|
231
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { cache, memoize } from "@decohere/core/caching";
|
|
4
|
+
import { mixwith } from "@decohere/core/extensibility";
|
|
5
|
+
import { type FileEx, io, type Watcher } from "@decohere/core/io";
|
|
6
|
+
import picomatch from "picomatch";
|
|
7
|
+
import { Subject } from "rxjs";
|
|
8
|
+
import { WorkspaceConfigurationError } from "../errors.js";
|
|
9
|
+
import { HasPackageJson } from "./has-package-json.mixin.js";
|
|
10
|
+
import { HasTsConfig } from "./has-tsconfig.mixin.js";
|
|
11
|
+
import { type PackageExports, PackageJson } from "./package-json.model.js";
|
|
12
|
+
import type { ProjectFileEvent } from "./project-file-event.js";
|
|
13
|
+
import { sourceExtensions } from "./source-extensions.js";
|
|
14
|
+
import type { Configuration, WorkspaceOptions } from "./workspace-config.type.js";
|
|
15
|
+
|
|
16
|
+
export type ProjectConfig = {
|
|
17
|
+
projectDir?: string;
|
|
18
|
+
packageJson?: PackageJson;
|
|
19
|
+
} & WorkspaceOptions;
|
|
20
|
+
|
|
21
|
+
export type ProjectSeed = {
|
|
22
|
+
name: string;
|
|
23
|
+
config?: ProjectConfig;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class Project extends mixwith(HasPackageJson, HasTsConfig) {
|
|
27
|
+
protected packageJson?: PackageJson;
|
|
28
|
+
readonly #projectFileChangedSubject = new Subject<ProjectFileEvent>();
|
|
29
|
+
readonly #sourceFiles = new Map<string, FileEx>();
|
|
30
|
+
readonly #projectConfig: ProjectConfig;
|
|
31
|
+
readonly #name: string;
|
|
32
|
+
readonly #projectDirectory: string;
|
|
33
|
+
readonly #selectedConfiguration: Configuration;
|
|
34
|
+
#hasLoadedSourceFiles = false;
|
|
35
|
+
#watcher?: Watcher;
|
|
36
|
+
|
|
37
|
+
public constructor(
|
|
38
|
+
name: string,
|
|
39
|
+
config: ProjectConfig = {} as ProjectConfig,
|
|
40
|
+
selectedConfigurationName?: string,
|
|
41
|
+
) {
|
|
42
|
+
super();
|
|
43
|
+
this.#name = name;
|
|
44
|
+
this.#projectDirectory = config?.projectDir ?? name;
|
|
45
|
+
this.#projectConfig = config;
|
|
46
|
+
this.#selectedConfiguration = this.resolveConfiguration(selectedConfigurationName);
|
|
47
|
+
this.packageJson = config.packageJson ?? new PackageJson();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public get name() {
|
|
51
|
+
return this.#name;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public get isPrivate() {
|
|
55
|
+
return this.packageJson?.private === true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public toString() {
|
|
59
|
+
return `Project(${this.#name})`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public get projectDirectory() {
|
|
63
|
+
return this.#projectDirectory;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get projectRoot() {
|
|
67
|
+
return path.resolve(this.#projectConfig.rootDir ?? "./", this.#projectDirectory);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public get configuration() {
|
|
71
|
+
return this.#selectedConfiguration;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected get packageJsonDirectory(): string {
|
|
75
|
+
return this.#absoluteProjectDir;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected get tsConfigDirectory(): string {
|
|
79
|
+
return this.#absoluteProjectDir;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected get fallbackTsConfigDirectory(): string | undefined {
|
|
83
|
+
return this.#projectConfig.rootDir;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public get rootDir(): string {
|
|
87
|
+
return this.#projectConfig.rootDir ?? "./";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get #absoluteProjectDir(): string {
|
|
91
|
+
return this.projectRoot;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public get include() {
|
|
95
|
+
return this.#selectedConfiguration.include ?? this.#projectConfig.include ?? [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public get ignore() {
|
|
99
|
+
const baseIgnore = this.#projectConfig.ignore ?? [];
|
|
100
|
+
const selectedIgnore = this.#selectedConfiguration.ignore ?? [];
|
|
101
|
+
return [...baseIgnore, ...selectedIgnore];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public get copyFiles() {
|
|
105
|
+
const base = this.#projectConfig.copyFiles ?? [];
|
|
106
|
+
const selected = this.#selectedConfiguration.copyFiles ?? [];
|
|
107
|
+
return [...base, ...selected];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public get dockerConfig() {
|
|
111
|
+
return this.#projectConfig.docker;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public get uiConfig() {
|
|
115
|
+
return this.#projectConfig.ui;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public get configurations(): Configuration[] {
|
|
119
|
+
return this.#projectConfig.configurations ?? [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public get projectFileChanged$() {
|
|
123
|
+
return this.#projectFileChangedSubject.asObservable();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@memoize()
|
|
127
|
+
public async getAllFiles() {
|
|
128
|
+
const files = await io.findFiles(this.include, {
|
|
129
|
+
cwd: this.#absoluteProjectDir,
|
|
130
|
+
ignore: this.ignore,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.#sourceFiles.clear();
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
this.#sourceFiles.set(file.relativePath, file);
|
|
136
|
+
}
|
|
137
|
+
this.#hasLoadedSourceFiles = true;
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public isSourceFile(file: FileEx) {
|
|
142
|
+
return this.#sourceFiles.has(file.relativePath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public async watch(configuration: string) {
|
|
146
|
+
if (this.#watcher != null) {
|
|
147
|
+
return this.#watcher;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!this.#hasLoadedSourceFiles) {
|
|
151
|
+
await this.getAllFiles();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const watcher = io.watch(this.#absoluteProjectDir, { ignoreInitial: true });
|
|
155
|
+
this.#watcher = watcher;
|
|
156
|
+
|
|
157
|
+
watcher.on(io.WatchEvents.add, (file) => {
|
|
158
|
+
const fileEx = file as FileEx;
|
|
159
|
+
this.updateSourceFileCache(io.WatchEvents.add, fileEx);
|
|
160
|
+
this.#projectFileChangedSubject.next({
|
|
161
|
+
project: this,
|
|
162
|
+
configuration,
|
|
163
|
+
eventName: io.WatchEvents.add,
|
|
164
|
+
file: fileEx,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
watcher.on(io.WatchEvents.change, (file) => {
|
|
169
|
+
const fileEx = file as FileEx;
|
|
170
|
+
this.updateSourceFileCache(io.WatchEvents.change, fileEx);
|
|
171
|
+
this.#projectFileChangedSubject.next({
|
|
172
|
+
project: this,
|
|
173
|
+
configuration,
|
|
174
|
+
eventName: io.WatchEvents.change,
|
|
175
|
+
file: fileEx,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
watcher.on(io.WatchEvents.unlink, (file) => {
|
|
180
|
+
const fileEx = file as FileEx;
|
|
181
|
+
this.updateSourceFileCache(io.WatchEvents.unlink, fileEx);
|
|
182
|
+
this.#projectFileChangedSubject.next({
|
|
183
|
+
project: this,
|
|
184
|
+
configuration,
|
|
185
|
+
eventName: io.WatchEvents.unlink,
|
|
186
|
+
file: fileEx,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return watcher;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public stopWatching() {
|
|
194
|
+
this.#watcher?.close();
|
|
195
|
+
this.#watcher = undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@cache()
|
|
199
|
+
public getRootOut() {
|
|
200
|
+
return path.join(this.getWorkspaceOut(), this.#selectedConfiguration.name);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@cache()
|
|
204
|
+
public getOutDir(target = "") {
|
|
205
|
+
const targetDir = this.#selectedConfiguration.pack
|
|
206
|
+
? path.join(this.projectDirectory, target)
|
|
207
|
+
: path.join(this.projectDirectory);
|
|
208
|
+
|
|
209
|
+
const out = path.join(this.getRootOut(), targetDir);
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public getConfiguration(name: string) {
|
|
214
|
+
return this.configurations.find((b) => b.name === name);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The package's entry source file. A TypeScript `main` (the workspace convention,
|
|
219
|
+
* `src/index.ts`) is used as written. A `main` or `exports["."]` that describes the published
|
|
220
|
+
* layout (`index.js`, `./es6/src/index.js`) is mapped back to the source it was built from,
|
|
221
|
+
* falling back to `src/index.ts` (#518).
|
|
222
|
+
*/
|
|
223
|
+
public getSourceEntryPath(): string {
|
|
224
|
+
const projectRoot = path.join(this.rootDir, this.projectDirectory);
|
|
225
|
+
const main = this.packageJson?.main;
|
|
226
|
+
if (main != null && sourceExtensions.isSource(main)) {
|
|
227
|
+
return path.join(projectRoot, main);
|
|
228
|
+
}
|
|
229
|
+
const exports = this.packageJson?.exports;
|
|
230
|
+
const root =
|
|
231
|
+
exports == null
|
|
232
|
+
? undefined
|
|
233
|
+
: typeof exports === "string"
|
|
234
|
+
? exports
|
|
235
|
+
: (exports["."] ??
|
|
236
|
+
(Object.keys(exports).some((k) => k.startsWith(".")) ? undefined : exports));
|
|
237
|
+
for (const built of [root == null ? undefined : firstExportTarget(root), main]) {
|
|
238
|
+
const fromSrc = built?.match(/(?:^|\/)(src\/.+)$/)?.[1];
|
|
239
|
+
const source =
|
|
240
|
+
fromSrc == null ? undefined : sourceFileFor(path.join(projectRoot, fromSrc));
|
|
241
|
+
if (source != null) return source;
|
|
242
|
+
}
|
|
243
|
+
const conventional = path.join(projectRoot, "src/index.ts");
|
|
244
|
+
if (main == null || existsSync(conventional)) return conventional;
|
|
245
|
+
return path.join(projectRoot, main);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The source file each subpath in package.json `exports` names, such as `./auth` →
|
|
250
|
+
* `<project>/src/auth/auth.extensions.ts` (#518). Exports point at the release layout
|
|
251
|
+
* (`./es6/src/auth/auth.extensions.js`); the source is the part from `src/`, with its
|
|
252
|
+
* TypeScript extension. Wildcard subpaths, and targets with no such source file, are left out.
|
|
253
|
+
*/
|
|
254
|
+
public getSourceExports(): Record<string, string> {
|
|
255
|
+
const exports = this.packageJson?.exports;
|
|
256
|
+
if (exports == null || typeof exports === "string") return {};
|
|
257
|
+
const projectRoot = path.join(this.rootDir, this.projectDirectory);
|
|
258
|
+
const sources: Record<string, string> = {};
|
|
259
|
+
for (const [subpath, target] of Object.entries(exports)) {
|
|
260
|
+
if (!subpath.startsWith("./") || subpath.includes("*")) continue;
|
|
261
|
+
const built = firstExportTarget(target);
|
|
262
|
+
const fromSrc = built?.match(/(?:^|\/)(src\/.+)$/)?.[1];
|
|
263
|
+
if (fromSrc == null) continue;
|
|
264
|
+
const source = sourceFileFor(path.join(projectRoot, fromSrc));
|
|
265
|
+
if (source != null) sources[subpath] = source;
|
|
266
|
+
}
|
|
267
|
+
return sources;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public [Symbol.dispose]() {
|
|
271
|
+
this.stopWatching();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private getWorkspaceOut() {
|
|
275
|
+
const out = path.join(this.#projectConfig.rootDir ?? "./", this.#projectConfig.outDir);
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private updateSourceFileCache(eventName: io.WatchEventName, file: FileEx) {
|
|
280
|
+
if (!this.#hasLoadedSourceFiles) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
switch (eventName) {
|
|
285
|
+
case io.WatchEvents.unlink:
|
|
286
|
+
this.#sourceFiles.delete(file.relativePath);
|
|
287
|
+
return;
|
|
288
|
+
case io.WatchEvents.add:
|
|
289
|
+
case io.WatchEvents.change:
|
|
290
|
+
if (this.matchesSourcePattern(file.relativePath)) {
|
|
291
|
+
this.#sourceFiles.set(file.relativePath, file);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.#sourceFiles.delete(file.relativePath);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private matchesSourcePattern(relativePath: string) {
|
|
301
|
+
if (picomatch.isMatch(relativePath, this.ignore)) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return picomatch.isMatch(relativePath, this.include);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private resolveConfiguration(configurationName?: string): Configuration {
|
|
309
|
+
const resolvedConfigurationName = configurationName ?? "debug";
|
|
310
|
+
|
|
311
|
+
const configuration = this.configurations.find(
|
|
312
|
+
(c) => c.name === resolvedConfigurationName,
|
|
313
|
+
);
|
|
314
|
+
if (configuration == null) {
|
|
315
|
+
throw new WorkspaceConfigurationError(
|
|
316
|
+
`Configuration "${resolvedConfigurationName}" was not found for project "${this.name}"`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return configuration;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The first path an exports value gives, preferring the conditions that name runtime code. */
|
|
325
|
+
function firstExportTarget(value: PackageExports): string | undefined {
|
|
326
|
+
if (typeof value === "string") return value;
|
|
327
|
+
for (const condition of ["import", "default", "require", "node", "types"]) {
|
|
328
|
+
const nested = value[condition];
|
|
329
|
+
if (nested != null) return firstExportTarget(nested);
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The TypeScript source a built or declaration path came from, if it exists. */
|
|
335
|
+
function sourceFileFor(builtPath: string): string | undefined {
|
|
336
|
+
// A declaration names the file its JavaScript sits next to: `x.d.ts` is `x.js`.
|
|
337
|
+
const declaration = builtPath.match(/^(.*)\.d\.([mc]?)ts$/);
|
|
338
|
+
const built = declaration == null ? builtPath : `${declaration[1]}.${declaration[2]}js`;
|
|
339
|
+
return sourceExtensions.candidates(built).find((candidate) => existsSync(candidate));
|
|
340
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/** TypeScript source extensions a workspace compiles (#518). */
|
|
4
|
+
const SOURCE = [".ts", ".tsx", ".mts", ".cts"] as const;
|
|
5
|
+
|
|
6
|
+
/** The sources an import's extension can name: `./a.js` is `a.ts` or `a.tsx`, `./a.mjs` is `a.mts`. */
|
|
7
|
+
const SOURCES_FOR: Record<string, readonly string[]> = {
|
|
8
|
+
".js": [".ts", ".tsx"],
|
|
9
|
+
".jsx": [".tsx"],
|
|
10
|
+
".mjs": [".mts"],
|
|
11
|
+
".cjs": [".cts"],
|
|
12
|
+
"": [".ts", ".tsx"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Whether a path is a TypeScript source (not a declaration file). */
|
|
16
|
+
function isSource(file: string): boolean {
|
|
17
|
+
return SOURCE.some((ext) => file.endsWith(ext)) && !/\.d\.[mc]?ts$/.test(file);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The source files a path written in an import (or a built path) could name, most likely first:
|
|
22
|
+
* itself when it is already a source, else the same stem with each matching source extension.
|
|
23
|
+
*/
|
|
24
|
+
function candidates(file: string): string[] {
|
|
25
|
+
if (isSource(file)) return [file];
|
|
26
|
+
const ext = path.extname(file);
|
|
27
|
+
const known = SOURCES_FOR[ext];
|
|
28
|
+
if (known == null) return [`${file}.ts`, `${file}.tsx`];
|
|
29
|
+
const stem = ext === "" ? file : file.slice(0, -ext.length);
|
|
30
|
+
return known.map((sourceExt) => `${stem}${sourceExt}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The extension a source builds to: `.mts` to `.mjs`, `.cts` to `.cjs`, the rest to `.js`. */
|
|
34
|
+
function outputExtension(file: string): string {
|
|
35
|
+
if (file.endsWith(".mts")) return ".mjs";
|
|
36
|
+
if (file.endsWith(".cts")) return ".cjs";
|
|
37
|
+
return ".js";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The path a source builds to: `a.mts` to `a.mjs`, `a.cts` to `a.cjs`, `a.ts` and `a.tsx` to `a.js`. */
|
|
41
|
+
function outputPath(file: string): string {
|
|
42
|
+
const ext = path.extname(file);
|
|
43
|
+
const stem = ext === "" ? file : file.slice(0, -ext.length);
|
|
44
|
+
return `${stem}${outputExtension(file)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether a source is CommonJS (`.cts`); everything else compiles as an ES module. */
|
|
48
|
+
function isCommonJs(file: string): boolean {
|
|
49
|
+
return file.endsWith(".cts");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const sourceExtensions = {
|
|
53
|
+
SOURCE,
|
|
54
|
+
isSource,
|
|
55
|
+
candidates,
|
|
56
|
+
outputExtension,
|
|
57
|
+
outputPath,
|
|
58
|
+
isCommonJs,
|
|
59
|
+
};
|