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