-
Notifications
You must be signed in to change notification settings - Fork 168
Expand file tree
/
Copy pathworkspace.ts
More file actions
215 lines (198 loc) · 7.04 KB
/
workspace.ts
File metadata and controls
215 lines (198 loc) · 7.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import fs from 'node:fs';
import path from 'node:path';
import { globSync } from 'glob';
import { minimatch } from 'minimatch';
import { Scalar, YAMLSeq } from 'yaml';
import { detectWorkspace as detectWorkspaceBinding } from '../../binding/index.js';
import {
DependencyType,
PackageManager,
type WorkspaceInfo,
type WorkspaceInfoOptional,
type WorkspacePackage,
} from '../types/index.ts';
import { editJsonFile, readJsonFile } from './json.ts';
import { getScopeFromPackageName } from './package.ts';
import { editYamlFile, readYamlFile } from './yaml.ts';
// npm/yarn use an array; Bun catalogs and Yarn classic nohoist use an object with `packages`.
export type NpmWorkspaces = string[] | { packages?: string[]; catalog?: Record<string, string> };
export function findPackageJsonFilesFromPatterns(patterns: string[], cwd: string): string[] {
if (patterns.length === 0) {
return [];
}
return globSync(
patterns.map((pattern) => `${pattern}/package.json`),
{ absolute: true, cwd },
);
}
// Detect if we're in a monorepo and get workspace info
export async function detectWorkspace(rootDir: string): Promise<WorkspaceInfoOptional> {
const bindingResult = await detectWorkspaceBinding(rootDir);
const result: WorkspaceInfoOptional = {
rootDir,
packageManager: undefined,
packageManagerVersion: 'latest',
isMonorepo: false,
monorepoScope: '',
workspacePatterns: [],
parentDirs: [],
packages: [],
};
if (bindingResult.packageManagerName) {
result.packageManager = bindingResult.packageManagerName as PackageManager;
}
if (bindingResult.packageManagerVersion) {
result.packageManagerVersion = bindingResult.packageManagerVersion;
}
if (bindingResult.isMonorepo) {
result.isMonorepo = bindingResult.isMonorepo;
}
if (bindingResult.root) {
// automatically correct the root directory from cwd
result.rootDir = bindingResult.root;
}
// Extract parent directories from workspace patterns
if (result.isMonorepo) {
const pnpmWorkspaceFile = path.join(result.rootDir, 'pnpm-workspace.yaml');
const packageJsonFile = path.join(result.rootDir, 'package.json');
if (fs.existsSync(pnpmWorkspaceFile)) {
const workspaceConfig = readYamlFile(pnpmWorkspaceFile) as { packages?: string[] };
if (Array.isArray(workspaceConfig.packages)) {
result.workspacePatterns = workspaceConfig.packages;
}
} else if (fs.existsSync(packageJsonFile)) {
// Check for npm/yarn/bun workspace (array or object form)
const pkg = readJsonFile(packageJsonFile) as { workspaces?: NpmWorkspaces };
if (Array.isArray(pkg.workspaces)) {
result.workspacePatterns = pkg.workspaces;
} else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
result.workspacePatterns = pkg.workspaces.packages;
}
}
const dirs = new Set<string>();
for (const pattern of result.workspacePatterns) {
// Extract directory from patterns like "apps/*", "packages/*", "foo/bar/*", "website", etc
if (!pattern.endsWith('*')) {
continue;
}
// Extract the directory name, ignore the wildcard
const dir = pattern.replace(/\/\*{1,2}$/, '');
if (dir) {
dirs.add(dir);
}
}
// eslint-disable-next-line unicorn/no-array-sort -- safe: sorting a fresh Array.from copy
result.parentDirs = Array.from(dirs).sort();
// Extract the scope from the package.json
const pkg = readJsonFile(packageJsonFile) as { name?: string };
if (pkg.name) {
result.monorepoScope = getScopeFromPackageName(pkg.name);
}
result.packages = discoverWorkspacePackages(result.workspacePatterns, result.rootDir);
}
return result;
}
// Discover all workspace packages
export function discoverWorkspacePackages(
workspacePatterns: string[],
rootDir: string,
): WorkspacePackage[] {
const packages: WorkspacePackage[] = [];
if (workspacePatterns.length === 0) {
return packages;
}
// Find all package.json files in the workspace
const packageJsonRelativePaths = globSync(
workspacePatterns.map((pattern) => `${pattern}/package.json`),
{
absolute: false,
cwd: rootDir,
ignore: ['**/node_modules/**'],
},
);
for (const packageJsonRelativePath of packageJsonRelativePaths) {
const packageJsonPath = path.join(rootDir, packageJsonRelativePath);
const pkg = readJsonFile(packageJsonPath) as {
name?: string;
description?: string;
version?: string;
dependencies?: Record<string, string>;
keywords?: string[];
};
if (!pkg.name) {
continue;
}
const isTemplatePackage =
pkg.keywords?.includes('vite-plus-template') ||
pkg.keywords?.includes('bingo-template') ||
!!pkg.dependencies?.bingo;
packages.push({
name: pkg.name,
path: path.dirname(packageJsonRelativePath),
description: pkg.description,
version: pkg.version,
isTemplatePackage,
});
}
return packages;
}
// Update package.json with workspace dependencies
export function updatePackageJsonWithDeps(
rootDir: string,
projectDir: string,
dependencies: string[],
dependencyType: DependencyType,
) {
const packageJsonPath = path.join(rootDir, projectDir, 'package.json');
editJsonFile<{ [key in DependencyType]?: Record<string, string> }>(packageJsonPath, (pkg) => {
if (!pkg[dependencyType]) {
pkg[dependencyType] = {};
}
for (const dep of dependencies) {
pkg[dependencyType][dep] = 'workspace:*';
}
return pkg;
});
}
// Update workspace configuration to include new project
export function updateWorkspaceConfig(projectPath: string, workspaceInfo: WorkspaceInfo) {
// Check if project path matches any workspace pattern
for (const pattern of workspaceInfo.workspacePatterns) {
if (minimatch(projectPath, pattern)) {
return;
}
}
// Derive pattern from project path (e.g., "packages/my-app" -> "packages/*", "website" -> "website", "foo/bar/app" -> "foo/bar/*")
let pattern = path.dirname(projectPath);
if (!pattern) {
// "website" -> "website"
pattern = projectPath;
} else {
// "foo/bar/app" -> "foo/bar/*"
pattern = `${pattern}/*`;
}
if (workspaceInfo.packageManager === PackageManager.pnpm) {
editYamlFile(path.join(workspaceInfo.rootDir, 'pnpm-workspace.yaml'), (doc) => {
let packages = doc.getIn(['packages']) as YAMLSeq<Scalar<string>>;
if (!packages) {
packages = new YAMLSeq<Scalar<string>>();
}
packages.add(new Scalar(pattern));
doc.setIn(['packages'], packages);
});
} else {
// Update package.json workspaces (array or object form)
editJsonFile<{ workspaces?: NpmWorkspaces }>(
path.join(workspaceInfo.rootDir, 'package.json'),
(pkg) => {
if (pkg.workspaces && !Array.isArray(pkg.workspaces)) {
// Preserve object form (e.g., Bun catalogs, Yarn classic nohoist)
pkg.workspaces.packages = [...(pkg.workspaces.packages || []), pattern];
} else {
pkg.workspaces = [...(pkg.workspaces || []), pattern];
}
return pkg;
},
);
}
}