Skip to content

Commit 3e8410b

Browse files
feat: Support for yarn workspaces (#493)
* feat: support for yarn workspaces * fix: should have newPrefix for every dependency * fix: remove console.log * fix: localPath was missing the src depencency folder * test: skip ignore files for now * fix circular depPath resolving problem * yarn workspaces test for .vscodeignore functionality * support yarn detection as well --------- Co-authored-by: João Moreno <[email protected]>
1 parent 7be15a9 commit 3e8410b

39 files changed

+1543
-96
lines changed

package-lock.json

+40-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"chalk": "^2.4.2",
4343
"cheerio": "^1.0.0-rc.9",
4444
"commander": "^6.2.1",
45+
"find-yarn-workspace-root": "^2.0.0",
4546
"glob": "^7.0.6",
4647
"hosted-git-info": "^4.0.2",
4748
"jsonc-parser": "^3.2.0",

src/npm.ts

+84-66
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as path from 'path';
22
import * as fs from 'fs';
33
import * as cp from 'child_process';
4-
import parseSemver from 'parse-semver';
5-
import { CancellationToken, log, nonnull } from './util';
4+
import findWorkspaceRoot from 'find-yarn-workspace-root';
5+
import { Manifest } from './manifest';
6+
import { readNodeManifest } from './package';
7+
import { CancellationToken, log } from './util';
68

79
const exists = (file: string) =>
810
fs.promises.stat(file).then(
@@ -62,51 +64,67 @@ async function checkNPM(cancellationToken?: CancellationToken): Promise<void> {
6264
}
6365
}
6466

65-
function getNpmDependencies(cwd: string): Promise<string[]> {
67+
function getNpmDependencies(cwd: string): Promise<SourceAndDestination[]> {
6668
return checkNPM()
6769
.then(() =>
6870
exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 })
6971
)
70-
.then(({ stdout }) => stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir)));
71-
}
72-
73-
interface YarnTreeNode {
74-
name: string;
75-
children: YarnTreeNode[];
72+
.then(({ stdout }) => stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir))
73+
.map(dir => {
74+
return {
75+
src: dir,
76+
dest: path.relative(cwd, dir)
77+
}
78+
}));
7679
}
7780

7881
export interface YarnDependency {
7982
name: string;
80-
path: string;
83+
path: SourceAndDestination;
8184
children: YarnDependency[];
8285
}
8386

84-
function asYarnDependency(prefix: string, tree: YarnTreeNode, prune: boolean): YarnDependency | null {
85-
if (prune && /@[\^~]/.test(tree.name)) {
86-
return null;
87-
}
88-
89-
let name: string;
90-
91-
try {
92-
const parseResult = parseSemver(tree.name);
93-
name = parseResult.name;
94-
} catch (err) {
95-
name = tree.name.replace(/^([^@+])@.*$/, '$1');
96-
}
97-
98-
const dependencyPath = path.join(prefix, name);
99-
const children: YarnDependency[] = [];
87+
export interface SourceAndDestination {
88+
src: string;
89+
dest: string;
90+
}
10091

101-
for (const child of tree.children || []) {
102-
const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), child, prune);
92+
async function asYarnDependencies(root: string, rootDependencies: string[]): Promise<YarnDependency[]> {
93+
const resolve = async (prefix: string, dependencies: string[], collected: Map<string, YarnDependency> = new Map()): Promise<YarnDependency[]> => await Promise.all(dependencies
94+
.map(async (name: string) => {
95+
let newPrefix = prefix, depPath = null, depManifest = null;
96+
while (!depManifest && root.length <= newPrefix.length) {
97+
depPath = path.join(newPrefix, 'node_modules', name);
98+
try {
99+
depManifest = await readNodeManifest(depPath);
100+
} catch (err) {
101+
newPrefix = path.join(newPrefix, '..');
102+
if (newPrefix.length < root.length) {
103+
throw err;
104+
}
105+
}
106+
}
103107

104-
if (dep) {
105-
children.push(dep);
106-
}
107-
}
108+
if (!depPath || !depManifest) {
109+
throw new Error(`Error finding dependencies`);
110+
}
108111

109-
return { name, path: dependencyPath, children };
112+
const result: YarnDependency = {
113+
name,
114+
path: {
115+
src: depPath,
116+
dest: path.relative(root, depPath),
117+
},
118+
children: [],
119+
};
120+
const shouldResolveChildren = !collected.has(depPath);
121+
collected.set(depPath, result);
122+
if (shouldResolveChildren) {
123+
result.children = await resolve(depPath, Object.keys(depManifest.dependencies || {}), collected);
124+
}
125+
return result;
126+
}));
127+
return resolve(root, rootDependencies);
110128
}
111129

112130
function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: string[]): YarnDependency[] {
@@ -154,26 +172,10 @@ function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: st
154172
return reached.values;
155173
}
156174

157-
async function getYarnProductionDependencies(cwd: string, packagedDependencies?: string[]): Promise<YarnDependency[]> {
158-
const raw = await new Promise<string>((c, e) =>
159-
cp.exec(
160-
'yarn list --prod --json',
161-
{ cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 },
162-
(err, stdout) => (err ? e(err) : c(stdout))
163-
)
164-
);
165-
const match = /^{"type":"tree".*$/m.exec(raw);
166-
167-
if (!match || match.length !== 1) {
168-
throw new Error('Could not parse result of `yarn list --json`');
169-
}
170-
175+
async function getYarnProductionDependencies(root: string, manifest: Manifest, packagedDependencies?: string[]): Promise<YarnDependency[]> {
171176
const usingPackagedDependencies = Array.isArray(packagedDependencies);
172-
const trees = JSON.parse(match[0]).data.trees as YarnTreeNode[];
173177

174-
let result = trees
175-
.map(tree => asYarnDependency(path.join(cwd, 'node_modules'), tree, !usingPackagedDependencies))
176-
.filter(nonnull);
178+
let result = await asYarnDependencies(root, Object.keys(manifest.dependencies || {}));
177179

178180
if (usingPackagedDependencies) {
179181
result = selectYarnDependencies(result, packagedDependencies!);
@@ -182,22 +184,35 @@ async function getYarnProductionDependencies(cwd: string, packagedDependencies?:
182184
return result;
183185
}
184186

185-
async function getYarnDependencies(cwd: string, packagedDependencies?: string[]): Promise<string[]> {
186-
const result = new Set([cwd]);
187+
async function getYarnDependencies(cwd: string, root: string, manifest: Manifest, packagedDependencies?: string[]): Promise<SourceAndDestination[]> {
188+
const result: SourceAndDestination[] = [{
189+
src: cwd,
190+
dest: ''
191+
}];
192+
193+
if (await exists(path.join(root, 'yarn.lock'))) {
194+
const deps = await getYarnProductionDependencies(root, manifest, packagedDependencies);
195+
const flatten = (dep: YarnDependency) => {
196+
result.push(dep.path);
197+
dep.children.forEach(flatten);
198+
};
199+
deps.forEach(flatten);
200+
}
187201

188-
const deps = await getYarnProductionDependencies(cwd, packagedDependencies);
189-
const flatten = (dep: YarnDependency) => {
190-
result.add(dep.path);
191-
dep.children.forEach(flatten);
192-
};
193-
deps.forEach(flatten);
202+
const dedup = new Map();
194203

195-
return [...result];
204+
for (const item of result) {
205+
if (!dedup.has(item.src)) {
206+
dedup.set(item.src, item);
207+
}
208+
}
209+
210+
return [...dedup.values()];
196211
}
197212

198-
export async function detectYarn(cwd: string): Promise<boolean> {
213+
export async function detectYarn(root: string) {
199214
for (const name of ['yarn.lock', '.yarnrc', '.yarnrc.yaml', '.pnp.cjs', '.yarn']) {
200-
if (await exists(path.join(cwd, name))) {
215+
if (await exists(path.join(root, name))) {
201216
if (!process.env['VSCE_TESTS']) {
202217
log.info(
203218
`Detected presence of ${name}. Using 'yarn' instead of 'npm' (to override this pass '--no-yarn' on the command line).`
@@ -211,13 +226,16 @@ export async function detectYarn(cwd: string): Promise<boolean> {
211226

212227
export async function getDependencies(
213228
cwd: string,
229+
manifest: Manifest,
214230
dependencies: 'npm' | 'yarn' | 'none' | undefined,
215231
packagedDependencies?: string[]
216-
): Promise<string[]> {
232+
): Promise<SourceAndDestination[]> {
233+
const root = findWorkspaceRoot(cwd) || cwd;
234+
217235
if (dependencies === 'none') {
218-
return [cwd];
219-
} else if (dependencies === 'yarn' || (dependencies === undefined && (await detectYarn(cwd)))) {
220-
return await getYarnDependencies(cwd, packagedDependencies);
236+
return [{ src: root, dest: '' }];
237+
} else if (dependencies === 'yarn' || (dependencies === undefined && (await detectYarn(root)))) {
238+
return await getYarnDependencies(cwd, root, manifest, packagedDependencies);
221239
} else {
222240
return await getNpmDependencies(cwd);
223241
}

0 commit comments

Comments
 (0)