Skip to content

Commit

Permalink
feat: Support for yarn workspaces (#493)
Browse files Browse the repository at this point in the history
* 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 <joao.moreno@microsoft.com>
  • Loading branch information
sebas2day and joaomoreno committed Dec 13, 2023
1 parent 7be15a9 commit 3e8410b
Show file tree
Hide file tree
Showing 39 changed files with 1,543 additions and 96 deletions.
52 changes: 40 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"chalk": "^2.4.2",
"cheerio": "^1.0.0-rc.9",
"commander": "^6.2.1",
"find-yarn-workspace-root": "^2.0.0",
"glob": "^7.0.6",
"hosted-git-info": "^4.0.2",
"jsonc-parser": "^3.2.0",
Expand Down
150 changes: 84 additions & 66 deletions src/npm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as path from 'path';
import * as fs from 'fs';
import * as cp from 'child_process';
import parseSemver from 'parse-semver';
import { CancellationToken, log, nonnull } from './util';
import findWorkspaceRoot from 'find-yarn-workspace-root';
import { Manifest } from './manifest';
import { readNodeManifest } from './package';
import { CancellationToken, log } from './util';

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

function getNpmDependencies(cwd: string): Promise<string[]> {
function getNpmDependencies(cwd: string): Promise<SourceAndDestination[]> {
return checkNPM()
.then(() =>
exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 })
)
.then(({ stdout }) => stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir)));
}

interface YarnTreeNode {
name: string;
children: YarnTreeNode[];
.then(({ stdout }) => stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir))
.map(dir => {
return {
src: dir,
dest: path.relative(cwd, dir)
}
}));
}

export interface YarnDependency {
name: string;
path: string;
path: SourceAndDestination;
children: YarnDependency[];
}

function asYarnDependency(prefix: string, tree: YarnTreeNode, prune: boolean): YarnDependency | null {
if (prune && /@[\^~]/.test(tree.name)) {
return null;
}

let name: string;

try {
const parseResult = parseSemver(tree.name);
name = parseResult.name;
} catch (err) {
name = tree.name.replace(/^([^@+])@.*$/, '$1');
}

const dependencyPath = path.join(prefix, name);
const children: YarnDependency[] = [];
export interface SourceAndDestination {
src: string;
dest: string;
}

for (const child of tree.children || []) {
const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), child, prune);
async function asYarnDependencies(root: string, rootDependencies: string[]): Promise<YarnDependency[]> {
const resolve = async (prefix: string, dependencies: string[], collected: Map<string, YarnDependency> = new Map()): Promise<YarnDependency[]> => await Promise.all(dependencies
.map(async (name: string) => {
let newPrefix = prefix, depPath = null, depManifest = null;
while (!depManifest && root.length <= newPrefix.length) {
depPath = path.join(newPrefix, 'node_modules', name);
try {
depManifest = await readNodeManifest(depPath);
} catch (err) {
newPrefix = path.join(newPrefix, '..');
if (newPrefix.length < root.length) {
throw err;
}
}
}

if (dep) {
children.push(dep);
}
}
if (!depPath || !depManifest) {
throw new Error(`Error finding dependencies`);
}

return { name, path: dependencyPath, children };
const result: YarnDependency = {
name,
path: {
src: depPath,
dest: path.relative(root, depPath),
},
children: [],
};
const shouldResolveChildren = !collected.has(depPath);
collected.set(depPath, result);
if (shouldResolveChildren) {
result.children = await resolve(depPath, Object.keys(depManifest.dependencies || {}), collected);
}
return result;
}));
return resolve(root, rootDependencies);
}

function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: string[]): YarnDependency[] {
Expand Down Expand Up @@ -154,26 +172,10 @@ function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: st
return reached.values;
}

async function getYarnProductionDependencies(cwd: string, packagedDependencies?: string[]): Promise<YarnDependency[]> {
const raw = await new Promise<string>((c, e) =>
cp.exec(
'yarn list --prod --json',
{ cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 },
(err, stdout) => (err ? e(err) : c(stdout))
)
);
const match = /^{"type":"tree".*$/m.exec(raw);

if (!match || match.length !== 1) {
throw new Error('Could not parse result of `yarn list --json`');
}

async function getYarnProductionDependencies(root: string, manifest: Manifest, packagedDependencies?: string[]): Promise<YarnDependency[]> {
const usingPackagedDependencies = Array.isArray(packagedDependencies);
const trees = JSON.parse(match[0]).data.trees as YarnTreeNode[];

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

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

async function getYarnDependencies(cwd: string, packagedDependencies?: string[]): Promise<string[]> {
const result = new Set([cwd]);
async function getYarnDependencies(cwd: string, root: string, manifest: Manifest, packagedDependencies?: string[]): Promise<SourceAndDestination[]> {
const result: SourceAndDestination[] = [{
src: cwd,
dest: ''
}];

if (await exists(path.join(root, 'yarn.lock'))) {
const deps = await getYarnProductionDependencies(root, manifest, packagedDependencies);
const flatten = (dep: YarnDependency) => {
result.push(dep.path);
dep.children.forEach(flatten);
};
deps.forEach(flatten);
}

const deps = await getYarnProductionDependencies(cwd, packagedDependencies);
const flatten = (dep: YarnDependency) => {
result.add(dep.path);
dep.children.forEach(flatten);
};
deps.forEach(flatten);
const dedup = new Map();

return [...result];
for (const item of result) {
if (!dedup.has(item.src)) {
dedup.set(item.src, item);
}
}

return [...dedup.values()];
}

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

export async function getDependencies(
cwd: string,
manifest: Manifest,
dependencies: 'npm' | 'yarn' | 'none' | undefined,
packagedDependencies?: string[]
): Promise<string[]> {
): Promise<SourceAndDestination[]> {
const root = findWorkspaceRoot(cwd) || cwd;

if (dependencies === 'none') {
return [cwd];
} else if (dependencies === 'yarn' || (dependencies === undefined && (await detectYarn(cwd)))) {
return await getYarnDependencies(cwd, packagedDependencies);
return [{ src: root, dest: '' }];
} else if (dependencies === 'yarn' || (dependencies === undefined && (await detectYarn(root)))) {
return await getYarnDependencies(cwd, root, manifest, packagedDependencies);
} else {
return await getNpmDependencies(cwd);
}
Expand Down

0 comments on commit 3e8410b

Please sign in to comment.