From d7734cd10ba382d295949f420b63020affe6686f Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 14 Nov 2017 18:09:37 +0100 Subject: [PATCH] expose yarn in `listFiles` api --- src/api.ts | 15 ++++++++++- src/npm.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++++- src/package.ts | 12 ++++----- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/api.ts b/src/api.ts index aa57350..b9f841b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -54,12 +54,25 @@ export interface IPublishOptions { baseImagesUrl?: string; } +/** + * The supported list of package managers. + */ +export enum PackageManager { + Npm, + Yarn +} + export interface IListFilesOptions { /** * The working directory of the extension. Defaults to `process.cwd()`. */ cwd?: string; + + /** + * The package manager to use. Defaults to `PackageManager.Npm`. + */ + packageManager?: PackageManager; } export interface IPublishVSIXOptions { @@ -100,7 +113,7 @@ export function publish(options: IPublishOptions = {}): Promise { * Lists the files included in the extension's package. */ export function listFiles(options: IListFilesOptions = {}): Promise { - return _listFiles(options.cwd); + return _listFiles(options.cwd, options.packageManager === PackageManager.Yarn); } /** diff --git a/src/npm.ts b/src/npm.ts index ca54b3f..c2410bb 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -1,5 +1,7 @@ import * as path from 'path'; import * as cp from 'child_process'; +import * as semver from 'semver'; +import * as _ from 'lodash'; import { CancellationToken } from './util'; interface IOptions { @@ -49,7 +51,7 @@ function checkNPM(cancellationToken?: CancellationToken): Promise { }); } -export function getDependencies(cwd: string): Promise { +function getNpmDependencies(cwd: string): Promise { return checkNPM() .then(() => exec('npm list --production --parseable --depth=99999', { cwd })) .then(({ stdout }) => stdout @@ -57,6 +59,72 @@ export function getDependencies(cwd: string): Promise { .filter(dir => path.isAbsolute(dir))); } +interface YarnTreeNode { + name: string; + children: YarnTreeNode[]; +} + +export interface YarnDependency { + name: string; + version: string; + path: string; + children: YarnDependency[]; +} + +function asYarnDependency(prefix: string, tree: YarnTreeNode): YarnDependency | null { + if (/@[\^~]/.test(tree.name)) { + return null; + } + + const version = tree.name.replace(/^[^@]+@/, ''); + + if (!semver.valid(version)) { + return null; + } + + const name = tree.name.replace(/@[^@]+$/, ''); + const dependencyPath = path.join(prefix, name); + const children: YarnDependency[] = []; + + for (const child of (tree.children || [])) { + const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), child); + + if (dep) { + children.push(dep); + } + } + + return { name, version, path: dependencyPath, children }; +} + +async function getYarnProductionDependencies(cwd: string): Promise { + const raw = await new Promise((c, e) => cp.exec('yarn list --json', { cwd, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' } }, (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`'); + } + + const trees = JSON.parse(match[0]).data.trees as YarnTreeNode[]; + + return trees + .map(tree => asYarnDependency(path.join(cwd, 'node_modules'), tree)) + .filter(dep => !!dep); +} + +async function getYarnDependencies(cwd: string): Promise { + const deps = await getYarnProductionDependencies(cwd); + const result: string[] = [cwd]; + const flatten = (dep: YarnDependency) => { result.push(dep.path); dep.children.forEach(flatten); }; + deps.forEach(flatten); + + return _.uniq(result); +} + +export function getDependencies(cwd: string, useYarn = false): Promise { + return useYarn ? getYarnDependencies(cwd) : getNpmDependencies(cwd); +} + export function getLatestVersion(name: string, cancellationToken?: CancellationToken): Promise { return checkNPM(cancellationToken) .then(() => exec(`npm show ${name} version`, {}, cancellationToken)) diff --git a/src/package.ts b/src/package.ts index 50e01f9..6964e9a 100644 --- a/src/package.ts +++ b/src/package.ts @@ -612,8 +612,8 @@ const defaultIgnore = [ '**/*.vsixmanifest' ]; -function collectAllFiles(cwd: string): Promise { - return getDependencies(cwd).then(deps => { +function collectAllFiles(cwd: string, useYarn = false): Promise { + return getDependencies(cwd, useYarn).then(deps => { const promises = deps.map(dep => { return glob('**', { cwd: dep, nodir: true, dot: true, ignore: 'node_modules/**' }) .then(files => files @@ -625,8 +625,8 @@ function collectAllFiles(cwd: string): Promise { }); } -function collectFiles(cwd: string): Promise { - return collectAllFiles(cwd).then(files => { +function collectFiles(cwd: string, useYarn = false): Promise { + return collectAllFiles(cwd, useYarn).then(files => { files = files.filter(f => !/\r$/m.test(f)); return readFile(path.join(cwd, '.vscodeignore'), 'utf8') @@ -736,9 +736,9 @@ export function packageCommand(options: IPackageOptions = {}): Promise { /** * Lists the files included in the extension's package. Does not run prepublish. */ -export function listFiles(cwd = process.cwd()): Promise { +export function listFiles(cwd = process.cwd(), useYarn = false): Promise { return readManifest(cwd) - .then(manifest => collectFiles(cwd)); + .then(manifest => collectFiles(cwd, useYarn)); } /**