@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decohere/workspace",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.5",
|
|
4
4
|
"description": "Workspace and project model primitives for monorepo tooling",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -10,15 +10,6 @@
|
|
|
10
10
|
"tsconfig",
|
|
11
11
|
"decohere"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://github.com/typesmiths/decohere/tree/main/projects/workspace#readme",
|
|
14
|
-
"bugs": {
|
|
15
|
-
"url": "https://github.com/typesmiths/decohere/issues"
|
|
16
|
-
},
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/typesmiths/decohere.git",
|
|
20
|
-
"directory": "projects/workspace"
|
|
21
|
-
},
|
|
22
13
|
"license": "ISC",
|
|
23
14
|
"author": "Typesmiths",
|
|
24
15
|
"type": "module",
|
|
@@ -33,9 +24,8 @@
|
|
|
33
24
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
34
25
|
},
|
|
35
26
|
"dependencies": {
|
|
36
|
-
"@decohere/
|
|
37
|
-
"@decohere/
|
|
38
|
-
"@decohere/devkit": "0.0.1-dev.125",
|
|
27
|
+
"@decohere/di": "0.0.1-dev.5",
|
|
28
|
+
"@decohere/core": "0.0.1-dev.5",
|
|
39
29
|
"picomatch": "^4.0.4",
|
|
40
30
|
"rxjs": "^7.8.2",
|
|
41
31
|
"typescript": "^6.0.3"
|
package/src/const.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const TS_CONFIG = "tsconfig.json";
|
|
2
|
+
export const PACKAGE_JSON = "package.json";
|
|
3
|
+
export const NODE_MODULES = "node_modules";
|
|
4
|
+
export const FIXTURES = "__fixtures__";
|
|
5
|
+
export const WORKSPACE_CONFIG = ".decohere.js";
|
|
6
|
+
export const PROJECTS_DIR = "projects";
|
|
7
|
+
|
|
8
|
+
import type { WorkspaceOptions } from "./models/workspace-config.type.js";
|
|
9
|
+
|
|
10
|
+
export const WorkspaceDefaults: WorkspaceOptions = {
|
|
11
|
+
rootDir: "./",
|
|
12
|
+
outDir: "dist",
|
|
13
|
+
includeProjects: [`${PROJECTS_DIR}/**/${PACKAGE_JSON}`],
|
|
14
|
+
ignoreProjects: [`**/${NODE_MODULES}/**`, `**/${FIXTURES}/**`, "**/dist/**"],
|
|
15
|
+
include: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
|
16
|
+
ignore: ["**/*.d.ts", "**/*.d.mts", "**/*.d.cts", `**/${NODE_MODULES}/**`],
|
|
17
|
+
copyFiles: [],
|
|
18
|
+
configurations: [{ name: "debug" }],
|
|
19
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class WorkspaceConfigurationError extends Error {}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { cache } from "@decohere/core/caching";
|
|
4
|
+
import { io } from "@decohere/core/io";
|
|
5
|
+
import { WORKSPACE_CONFIG } from "../const.js";
|
|
6
|
+
import type { WorkspaceOptions } from "../models/workspace-config.type.js";
|
|
7
|
+
|
|
8
|
+
class ConfigFactory {
|
|
9
|
+
@cache()
|
|
10
|
+
public async getUserConfig(configFile?: string): Promise<Partial<WorkspaceOptions>> {
|
|
11
|
+
const file = configFile ?? WORKSPACE_CONFIG;
|
|
12
|
+
if (!file || !(await io.fileExists(file))) {
|
|
13
|
+
return {} as WorkspaceOptions;
|
|
14
|
+
}
|
|
15
|
+
const fileUrl = pathToFileURL(path.resolve(file)).href;
|
|
16
|
+
const conf = (await import(fileUrl)).default as Partial<WorkspaceOptions>;
|
|
17
|
+
return conf;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const UserConfigFactory = new ConfigFactory();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { PACKAGE_JSON, PROJECTS_DIR } from "../const.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The workspace root for a command run from `startDir`: the nearest directory at or above
|
|
7
|
+
* it with a `package.json` and a `projects/` folder. Falls back to `startDir`, so running
|
|
8
|
+
* from a package directory (`projects/api`) still sees the whole workspace.
|
|
9
|
+
*/
|
|
10
|
+
export function findWorkspaceRoot(startDir: string): string {
|
|
11
|
+
const start = path.resolve(startDir);
|
|
12
|
+
let dir = start;
|
|
13
|
+
for (;;) {
|
|
14
|
+
if (
|
|
15
|
+
existsSync(path.join(dir, PACKAGE_JSON)) &&
|
|
16
|
+
isDirectory(path.join(dir, PROJECTS_DIR))
|
|
17
|
+
) {
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
const parent = path.dirname(dir);
|
|
21
|
+
if (parent === dir) return start;
|
|
22
|
+
dir = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isDirectory(fullPath: string): boolean {
|
|
27
|
+
try {
|
|
28
|
+
return statSync(fullPath).isDirectory();
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { type FileEx, io } from "@decohere/core/io";
|
|
3
|
+
import { merge } from "@decohere/core/objects";
|
|
4
|
+
import { WorkspaceConfigurationError } from "../errors.js";
|
|
5
|
+
import { PackageJson } from "../models/package-json.model.js";
|
|
6
|
+
import { Project, type ProjectConfig, type ProjectSeed } from "../models/project.model.js";
|
|
7
|
+
import type { WorkspaceOptions } from "../models/workspace-config.type.js";
|
|
8
|
+
|
|
9
|
+
export class ProjectFactory {
|
|
10
|
+
public static async getProjectSeeds(config: WorkspaceOptions) {
|
|
11
|
+
const { includeProjects, ignoreProjects, rootDir } = config;
|
|
12
|
+
const cwd = rootDir ?? "./";
|
|
13
|
+
|
|
14
|
+
const discoveredPackageJsonFiles = await io.findFiles(includeProjects, {
|
|
15
|
+
cwd,
|
|
16
|
+
ignore: ignoreProjects,
|
|
17
|
+
});
|
|
18
|
+
const packageJsonFiles = ProjectFactory.filterNestedPackageJsonFiles(
|
|
19
|
+
discoveredPackageJsonFiles,
|
|
20
|
+
);
|
|
21
|
+
const projects = await Promise.all(
|
|
22
|
+
packageJsonFiles.map((file) => ProjectFactory.projectFrom(file, config)),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static filterNestedPackageJsonFiles(files: readonly FileEx[]) {
|
|
29
|
+
const normalize = (filePath: string) => filePath.replaceAll("\\", "/");
|
|
30
|
+
const packageJsonSet = new Set(files.map((file) => normalize(file.relativePath)));
|
|
31
|
+
|
|
32
|
+
return files.filter((file) => {
|
|
33
|
+
const relativePath = normalize(file.relativePath);
|
|
34
|
+
const projectDir = path.posix.dirname(relativePath);
|
|
35
|
+
let dir = path.posix.dirname(projectDir);
|
|
36
|
+
while (dir !== "." && dir !== "/") {
|
|
37
|
+
if (packageJsonSet.has(`${dir}/package.json`)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
dir = path.posix.dirname(dir);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static async getProjects(config: WorkspaceOptions, configuration?: string) {
|
|
48
|
+
const seeds = await ProjectFactory.getProjectSeeds(config);
|
|
49
|
+
return seeds.map((seed) => new Project(seed.name, seed.config, configuration));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private static async projectFrom(
|
|
53
|
+
file: FileEx,
|
|
54
|
+
config: WorkspaceOptions,
|
|
55
|
+
): Promise<ProjectSeed> {
|
|
56
|
+
const packageJsonData = await io
|
|
57
|
+
.readJsonFile<PackageJson>(file.fullPath)
|
|
58
|
+
.catch((error) => {
|
|
59
|
+
throw new WorkspaceConfigurationError(
|
|
60
|
+
`Error reading package.json ${file.fullPath}`,
|
|
61
|
+
error,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
const name = packageJsonData.name;
|
|
65
|
+
const packageJson = new PackageJson(packageJsonData);
|
|
66
|
+
const projectDir = path.dirname(file.relativePath);
|
|
67
|
+
const mergedConfig = merge.withOptions<ProjectConfig>(
|
|
68
|
+
{ mergeArrays: false },
|
|
69
|
+
config,
|
|
70
|
+
config?.projects?.[name] ?? {},
|
|
71
|
+
{ projectDir, packageJson },
|
|
72
|
+
);
|
|
73
|
+
return { name, config: mergedConfig };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { io } from "@decohere/core/io";
|
|
3
|
+
import { merge } from "@decohere/core/objects";
|
|
4
|
+
import { PACKAGE_JSON, WorkspaceDefaults } from "../const.js";
|
|
5
|
+
import { PackageJson } from "../models/package-json.model.js";
|
|
6
|
+
import { Workspace } from "../models/workspace.model.js";
|
|
7
|
+
import type {
|
|
8
|
+
WorkspaceFactoryOptions,
|
|
9
|
+
WorkspaceOptions,
|
|
10
|
+
} from "../models/workspace-config.type.js";
|
|
11
|
+
import { UserConfigFactory } from "./config.factory.js";
|
|
12
|
+
import { ProjectFactory } from "./project.factory.js";
|
|
13
|
+
|
|
14
|
+
class Factory {
|
|
15
|
+
public async create(options: WorkspaceFactoryOptions & Partial<WorkspaceOptions> = {}) {
|
|
16
|
+
const config = await this.getMergedConfig(options);
|
|
17
|
+
const projectSeeds = await ProjectFactory.getProjectSeeds(config);
|
|
18
|
+
const packageJson = await this.getPackageJson(config.rootDir ?? "./");
|
|
19
|
+
const workspace = new Workspace({ packageJson, config, projects: projectSeeds });
|
|
20
|
+
return workspace;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private async getMergedConfig(
|
|
24
|
+
options: WorkspaceFactoryOptions & Partial<WorkspaceOptions>,
|
|
25
|
+
): Promise<WorkspaceOptions> {
|
|
26
|
+
const { configFile, ...rest } = options;
|
|
27
|
+
const userConfig = await UserConfigFactory.getUserConfig(configFile);
|
|
28
|
+
return merge.withOptions<WorkspaceOptions>(
|
|
29
|
+
{ mergeArrays: false },
|
|
30
|
+
WorkspaceDefaults,
|
|
31
|
+
userConfig,
|
|
32
|
+
rest,
|
|
33
|
+
) as WorkspaceOptions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async getPackageJson(rootDir: string) {
|
|
37
|
+
const packageJsonPath = path.join(rootDir, PACKAGE_JSON);
|
|
38
|
+
if (!(await io.fileExists(packageJsonPath))) return;
|
|
39
|
+
const data = await io.readJsonFile<Partial<PackageJson>>(packageJsonPath);
|
|
40
|
+
return new PackageJson(data);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const WorkspaceFactory = new Factory();
|
|
@@ -15,4 +15,10 @@ export { sourceExtensions } from "./models/source-extensions.js";
|
|
|
15
15
|
export type { ProjectFileEvent } from "./models/project-file-event.js";
|
|
16
16
|
export { DEFAULT_COMPILER_OPTIONS, Tsconfig } from "./models/tsconfig.model.js";
|
|
17
17
|
export { Workspace } from "./models/workspace.model.js";
|
|
18
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
Configuration,
|
|
20
|
+
ProjectOptions,
|
|
21
|
+
UiConfig,
|
|
22
|
+
WorkspaceFactoryOptions,
|
|
23
|
+
WorkspaceOptions,
|
|
24
|
+
} from "./models/workspace-config.type.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { invalidateMemoize, memoize } from "@decohere/core/caching";
|
|
3
|
+
import { io } from "@decohere/core/io";
|
|
4
|
+
import { PACKAGE_JSON } from "../const.js";
|
|
5
|
+
import { PackageJson } from "./package-json.model.js";
|
|
6
|
+
|
|
7
|
+
export abstract class HasPackageJson {
|
|
8
|
+
protected abstract packageJson?: PackageJson;
|
|
9
|
+
protected abstract get packageJsonDirectory(): string;
|
|
10
|
+
|
|
11
|
+
@memoize()
|
|
12
|
+
public async getPackageJson() {
|
|
13
|
+
return new PackageJson(
|
|
14
|
+
await io.readJsonFile(path.join(this.packageJsonDirectory, PACKAGE_JSON)),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public get packageJsonPath() {
|
|
19
|
+
return path.join(this.packageJsonDirectory, PACKAGE_JSON);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public async getVersion() {
|
|
23
|
+
return (await this.getPackageJson()).version;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async persistPackageJson(packageJson: PackageJson) {
|
|
27
|
+
await io.writeFile(this.packageJsonPath, packageJson.toJSON());
|
|
28
|
+
invalidateMemoize.evict(this, HasPackageJson.prototype.getPackageJson);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { memoize } from "@decohere/core/caching";
|
|
4
|
+
import { io } from "@decohere/core/io";
|
|
5
|
+
import { TS_CONFIG } from "../const.js";
|
|
6
|
+
import { Tsconfig } from "./tsconfig.model.js";
|
|
7
|
+
|
|
8
|
+
export abstract class HasTsConfig {
|
|
9
|
+
protected abstract get tsConfigDirectory(): string;
|
|
10
|
+
protected abstract get fallbackTsConfigDirectory(): string | undefined;
|
|
11
|
+
|
|
12
|
+
@memoize()
|
|
13
|
+
public async getTsConfig() {
|
|
14
|
+
for (const configPath of this.tsConfigPaths) {
|
|
15
|
+
if (await io.fileExists(configPath)) return Tsconfig.parse(configPath);
|
|
16
|
+
}
|
|
17
|
+
return Tsconfig.default();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@memoize()
|
|
21
|
+
public getTsConfigSync() {
|
|
22
|
+
const configPath = this.findTsConfigPath();
|
|
23
|
+
if (configPath != null) return Tsconfig.parseSync(configPath);
|
|
24
|
+
return Tsconfig.default();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** First existing `tsconfig.json` on this object, if any. */
|
|
28
|
+
protected findTsConfigPath(): string | undefined {
|
|
29
|
+
return this.tsConfigPaths.find((configPath) => HasTsConfig.fileExistsSync(configPath));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private get tsConfigPaths(): string[] {
|
|
33
|
+
const paths = [path.join(this.tsConfigDirectory, TS_CONFIG)];
|
|
34
|
+
const fallback = this.fallbackTsConfigDirectory;
|
|
35
|
+
if (fallback != null) {
|
|
36
|
+
paths.push(path.join(fallback, TS_CONFIG));
|
|
37
|
+
}
|
|
38
|
+
return paths;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private static fileExistsSync(filePath: string): boolean {
|
|
42
|
+
try {
|
|
43
|
+
return fs.statSync(filePath).isFile();
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Project } from "./project.model.js";
|
|
4
|
+
import { sourceExtensions } from "./source-extensions.js";
|
|
5
|
+
|
|
6
|
+
export type PackageAliases = {
|
|
7
|
+
/** Absolute base for SWC `jsc.paths` / specifier resolution. */
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
/** Package-name and `name/*` keys, values relative to `baseUrl`. */
|
|
10
|
+
paths: Record<string, string[]>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convention aliases from workspace packages, then tsconfig `paths` for the same keys.
|
|
15
|
+
* Prefer `Workspace.getPackageAliases` when a workspace is available.
|
|
16
|
+
*/
|
|
17
|
+
function build(
|
|
18
|
+
projects: readonly Project[],
|
|
19
|
+
options: { baseUrl: string; tsconfigPaths?: Record<string, string[]> },
|
|
20
|
+
): PackageAliases {
|
|
21
|
+
const baseUrl = path.resolve(options.baseUrl);
|
|
22
|
+
const paths = {
|
|
23
|
+
...conventionAliasPaths(projects, baseUrl),
|
|
24
|
+
...options.tsconfigPaths,
|
|
25
|
+
};
|
|
26
|
+
return { baseUrl, paths };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Bare import specifier that is not relative, absolute, or a node/file URL. */
|
|
30
|
+
function isBareSpecifier(specifier: string): boolean {
|
|
31
|
+
return (
|
|
32
|
+
specifier.length > 0 &&
|
|
33
|
+
!specifier.startsWith(".") &&
|
|
34
|
+
!specifier.startsWith("/") &&
|
|
35
|
+
!specifier.startsWith("node:") &&
|
|
36
|
+
!specifier.startsWith("file:") &&
|
|
37
|
+
!specifier.startsWith("data:") &&
|
|
38
|
+
!specifier.startsWith("#")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a bare specifier through package aliases (SWC-style `paths`
|
|
44
|
+
* relative to `baseUrl`) to an absolute `.ts` path. Longest key wins.
|
|
45
|
+
*/
|
|
46
|
+
function resolvePath(
|
|
47
|
+
specifier: string,
|
|
48
|
+
paths: Record<string, string[]>,
|
|
49
|
+
baseUrl: string,
|
|
50
|
+
): string | null {
|
|
51
|
+
if (!isBareSpecifier(specifier)) return null;
|
|
52
|
+
|
|
53
|
+
const resolvedBase = path.resolve(baseUrl);
|
|
54
|
+
const keys = Object.keys(paths).sort((a, b) => b.length - a.length);
|
|
55
|
+
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const target = paths[key]?.[0];
|
|
58
|
+
if (target == null) continue;
|
|
59
|
+
|
|
60
|
+
if (key.endsWith("/*")) {
|
|
61
|
+
const prefix = key.slice(0, -1);
|
|
62
|
+
if (!specifier.startsWith(prefix)) continue;
|
|
63
|
+
const mappedRoot = target.endsWith("/*") ? target.slice(0, -2) : target;
|
|
64
|
+
const subpath = specifier.slice(prefix.length);
|
|
65
|
+
return subpathSource(path.resolve(resolvedBase, mappedRoot, subpath));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (specifier === key) {
|
|
69
|
+
return toTsSourcePath(path.resolve(resolvedBase, target.replace(/\*$/, "")));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function conventionAliasPaths(
|
|
77
|
+
projects: readonly Project[],
|
|
78
|
+
baseUrl: string,
|
|
79
|
+
): Record<string, string[]> {
|
|
80
|
+
const aliases: Record<string, string[]> = {};
|
|
81
|
+
|
|
82
|
+
for (const project of projects) {
|
|
83
|
+
const name = project.name;
|
|
84
|
+
if (name.length === 0) continue;
|
|
85
|
+
|
|
86
|
+
const entry = path.resolve(project.getSourceEntryPath());
|
|
87
|
+
// A package with no root entry (only subpaths, like core) gets no bare alias, and its
|
|
88
|
+
// subpaths resolve under `src/`.
|
|
89
|
+
const hasEntry = existsSync(entry);
|
|
90
|
+
if (hasEntry) aliases[name] = [toPosixRelative(baseUrl, entry)];
|
|
91
|
+
const subpathRoot = hasEntry
|
|
92
|
+
? path.dirname(entry)
|
|
93
|
+
: path.resolve(project.rootDir, project.projectDirectory, "src");
|
|
94
|
+
if (existsSync(subpathRoot)) {
|
|
95
|
+
aliases[`${name}/*`] = [`${toPosixRelative(baseUrl, subpathRoot)}/*`];
|
|
96
|
+
}
|
|
97
|
+
// Subpaths the package exports, mapped to their sources (#518): these can name any file,
|
|
98
|
+
// not just `<sub>.ts` or `<sub>/index.ts` under the entry's directory.
|
|
99
|
+
for (const [subpath, source] of Object.entries(project.getSourceExports())) {
|
|
100
|
+
aliases[`${name}/${subpath.slice(2)}`] = [toPosixRelative(baseUrl, source)];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return aliases;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toPosixRelative(from: string, to: string): string {
|
|
108
|
+
const relative = path.relative(from, to);
|
|
109
|
+
if (relative === "") return ".";
|
|
110
|
+
return relative.split(path.sep).join("/");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The source file a wildcard subpath names: `<sub>` with a source extension (`.ts`, `.tsx`, or
|
|
115
|
+
* `.mts`/`.cts` for `.mjs`/`.cjs`), else the `index` source of the directory it names (as
|
|
116
|
+
* `@pkg/models` is `src/models/index.ts`), else `<sub>.ts` for the caller to report missing.
|
|
117
|
+
*/
|
|
118
|
+
function subpathSource(candidate: string): string {
|
|
119
|
+
const files = sourceExtensions.candidates(candidate);
|
|
120
|
+
const directory = candidate.replace(/\.([mc]?[jt]sx?)$/, "");
|
|
121
|
+
const indexes = sourceExtensions.candidates(path.join(directory, "index"));
|
|
122
|
+
return [...files, ...indexes].find((file) => existsSync(file)) ?? toTsSourcePath(candidate);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A target as a source path: itself when it is one, else its first source candidate. */
|
|
126
|
+
function toTsSourcePath(candidate: string): string {
|
|
127
|
+
return sourceExtensions.candidates(candidate)[0] ?? `${candidate}.ts`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const packageAliases = { build, isBareSpecifier, resolvePath };
|
|
@@ -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
|
+
}
|