Unverified Commit 16fd8a21 authored by Yuge Zhang's avatar Yuge Zhang Committed by GitHub
Browse files

Add support for .nniignore (#2454)

parent 10e56560
......@@ -274,7 +274,7 @@ function countFilesRecursively(directory: string): Promise<number> {
});
}
function validateFileName(fileName: string): boolean {
export function validateFileName(fileName: string): boolean {
const pattern: string = '^[a-z0-9A-Z._-]+$';
const validateResult = fileName.match(pattern);
if (validateResult) {
......
......@@ -16,6 +16,7 @@
"child-process-promise": "^2.2.1",
"express": "^4.16.3",
"express-joi-validator": "^2.0.0",
"ignore": "^5.1.4",
"js-base64": "^2.4.9",
"kubernetes-client": "^6.5.0",
"rx": "^4.1.0",
......@@ -23,6 +24,7 @@
"ssh2": "^0.6.1",
"stream-buffers": "^3.0.2",
"tail-stream": "^0.3.4",
"tar": "^6.0.2",
"tree-kill": "^1.2.0",
"ts-deferred": "^1.0.4",
"typescript-ioc": "^1.2.4",
......@@ -42,6 +44,7 @@
"@types/sqlite3": "^3.1.3",
"@types/ssh2": "^0.5.35",
"@types/stream-buffers": "^3.0.2",
"@types/tar": "^4.0.3",
"@types/tmp": "^0.0.33",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
......
......@@ -6,12 +6,44 @@
import * as cpp from 'child-process-promise';
import * as cp from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import ignore from 'ignore';
import * as path from 'path';
import * as tar from 'tar';
import { String } from 'typescript-string-operations';
import { countFilesRecursively, getNewLine, validateFileNameRecursively } from '../../common/utils';
import { validateFileName } from '../../common/utils';
import { GPU_INFO_COLLECTOR_FORMAT_WINDOWS } from './gpuData';
/**
* List all files in directory except those ignored by .nniignore.
* @param source
* @param destination
*/
export function* listDirWithIgnoredFiles(root: string, relDir: string, ignoreFiles: string[]): Iterable<string> {
let ignoreFile = undefined;
const source = path.join(root, relDir);
if (fs.existsSync(path.join(source, '.nniignore'))) {
ignoreFile = path.join(source, '.nniignore');
ignoreFiles.push(ignoreFile);
}
const ig = ignore();
ignoreFiles.forEach((i) => ig.add(fs.readFileSync(i).toString()));
for (const d of fs.readdirSync(source)) {
const entry = path.join(relDir, d);
if (ig.ignores(entry))
continue;
const entryStat = fs.statSync(path.join(root, entry));
if (entryStat.isDirectory()) {
yield entry;
yield* listDirWithIgnoredFiles(root, entry, ignoreFiles);
}
else if (entryStat.isFile())
yield entry;
}
if (ignoreFile !== undefined) {
ignoreFiles.pop();
}
}
/**
* Validate codeDir, calculate file count recursively under codeDir, and throw error if any rule is broken
*
......@@ -19,28 +51,28 @@ import { GPU_INFO_COLLECTOR_FORMAT_WINDOWS } from './gpuData';
* @returns file number under codeDir
*/
export async function validateCodeDir(codeDir: string): Promise<number> {
let fileCount: number | undefined;
let fileCount: number = 0;
let fileTotalSize: number = 0;
let fileNameValid: boolean = true;
try {
fileCount = await countFilesRecursively(codeDir);
} catch (error) {
throw new Error(`Call count file error: ${error}`);
}
try {
fileNameValid = await validateFileNameRecursively(codeDir);
} catch (error) {
throw new Error(`Validate file name error: ${error}`);
}
if (fileCount !== undefined && fileCount > 1000) {
const errMessage: string = `Too many files(${fileCount} found}) in ${codeDir},`
+ ` please check if it's a valid code dir`;
throw new Error(errMessage);
}
if (!fileNameValid) {
const errMessage: string = `File name in ${codeDir} is not valid, please check file names, only support digit number、alphabet and (.-_) in file name.`;
throw new Error(errMessage);
for (const relPath of listDirWithIgnoredFiles(codeDir, '', [])) {
const d = path.join(codeDir, relPath);
fileCount += 1;
fileTotalSize += fs.statSync(d).size;
if (fileCount > 2000) {
throw new Error(`Too many files and directories (${fileCount} already scanned) in ${codeDir},`
+ ` please check if it's a valid code dir`);
}
if (fileTotalSize > 300 * 1024 * 1024) {
throw new Error(`File total size too large in code dir (${fileTotalSize} bytes already scanned, exceeds 300MB).`);
}
fileNameValid = true;
relPath.split(path.sep).forEach(fpart => {
if (fpart !== '' && !validateFileName(fpart))
fileNameValid = false;
});
if (!fileNameValid) {
throw new Error(`Validate file name error: '${d}' is an invalid file name.`);
}
}
return fileCount;
......@@ -68,10 +100,16 @@ export async function execMkdir(directory: string, share: boolean = false): Prom
* @param destination
*/
export async function execCopydir(source: string, destination: string): Promise<void> {
if (process.platform === 'win32') {
await cpp.exec(`powershell.exe Copy-Item "${source}\\*" -Destination "${destination}" -Recurse`);
} else {
await cpp.exec(`cp -r '${source}/.' '${destination}'`);
if (!fs.existsSync(destination))
await fs.promises.mkdir(destination);
for (const relPath of listDirWithIgnoredFiles(source, '', [])) {
const sourcePath = path.join(source, relPath);
const destPath = path.join(destination, relPath);
if (fs.statSync(sourcePath).isDirectory()) {
await fs.promises.mkdir(destPath);
} else {
await fs.promises.copyFile(sourcePath, destPath);
}
}
return Promise.resolve();
......@@ -165,28 +203,19 @@ export function setEnvironmentVariable(variable: { key: string; value: string })
* @param tarPath
*/
export async function tarAdd(tarPath: string, sourcePath: string): Promise<void> {
if (process.platform === 'win32') {
const tarFilePath: string = tarPath.split('\\')
.join('\\\\');
const sourceFilePath: string = sourcePath.split('\\')
.join('\\\\');
const script: string[] = [];
script.push(
`import os`,
`import tarfile`,
String.Format(`tar = tarfile.open("{0}","w:gz")\r\nroot="{1}"\r\nfor file_path,dir,files in os.walk(root):`, tarFilePath, sourceFilePath),
` for file in files:`,
` full_path = os.path.join(file_path, file)`,
` file = os.path.relpath(full_path, root)`,
` tar.add(full_path, arcname=file)`,
`tar.close()`);
await fs.promises.writeFile(path.join(os.tmpdir(), 'tar.py'), script.join(getNewLine()), { encoding: 'utf8', mode: 0o777 });
const tarScript: string = path.join(os.tmpdir(), 'tar.py');
await cpp.exec(`python ${tarScript}`);
} else {
await cpp.exec(`tar -czf ${tarPath} -C ${sourcePath} .`);
const fileList = [];
for (const d of listDirWithIgnoredFiles(sourcePath, '', [])) {
fileList.push(d);
}
tar.create(
{
gzip: true,
file: tarPath,
sync: true,
cwd: sourcePath,
},
fileList
);
return Promise.resolve();
}
......
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
'use strict';
import * as assert from 'assert';
import * as chai from 'chai';
import * as fs from 'fs';
import * as path from 'path';
import * as tar from 'tar';
import { execCopydir, tarAdd, validateCodeDir } from '../common/util';
const deleteFolderRecursive = (filePath: string) => {
if (fs.existsSync(filePath)) {
fs.readdirSync(filePath).forEach((file, index) => {
const curPath = path.join(filePath, file);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(filePath);
}
};
describe('fileUtility', () => {
/*
Test file utilities, includes:
- Copy directory
- Ignore with ignore file
- Add to tar
*/
const sourceDir = 'test-fileUtilityTestSource';
const destDir = 'test-fileUtilityTestDest';
beforeEach(() => {
fs.mkdirSync(sourceDir);
fs.writeFileSync(path.join(sourceDir, '.nniignore'), 'abc\nxyz');
fs.writeFileSync(path.join(sourceDir, 'abc'), '123');
fs.writeFileSync(path.join(sourceDir, 'abcd'), '1234');
fs.mkdirSync(path.join(sourceDir, 'xyz'));
fs.mkdirSync(path.join(sourceDir, 'xyy'));
fs.mkdirSync(path.join(sourceDir, 'www'));
fs.mkdirSync(path.join(sourceDir, 'xx')); // empty dir
fs.writeFileSync(path.join(sourceDir, 'xyy', '.nniignore'), 'qq'); // nested nniignore
fs.writeFileSync(path.join(sourceDir, 'xyy', 'abc'), '123');
fs.writeFileSync(path.join(sourceDir, 'xyy', 'qq'), '1234');
fs.writeFileSync(path.join(sourceDir, 'xyy', 'pp'), '1234');
fs.writeFileSync(path.join(sourceDir, 'www', '.nniignore'), 'pp'); // pop nniignore
fs.writeFileSync(path.join(sourceDir, 'www', 'abc'), '123');
fs.writeFileSync(path.join(sourceDir, 'www', 'qq'), '1234');
fs.writeFileSync(path.join(sourceDir, 'www', 'pp'), '1234');
});
afterEach(() => {
deleteFolderRecursive(sourceDir);
deleteFolderRecursive(destDir);
if (fs.existsSync(`${destDir}.tar`)) {
fs.unlinkSync(`${destDir}.tar`);
}
});
it('Test file copy', async () => {
await execCopydir(sourceDir, destDir);
const existFiles = [
'abcd',
'xyy',
'xx',
path.join('xyy', '.nniignore'),
path.join('xyy', 'pp'),
path.join('www', '.nniignore'),
path.join('www', 'qq'),
]
const notExistFiles = [
'abc',
'xyz',
path.join('xyy', 'abc'),
path.join('xyy', 'qq'),
path.join('www', 'pp'),
path.join('www', 'abc'),
]
existFiles.forEach(d => assert.ok(fs.existsSync(path.join(destDir, d))));
notExistFiles.forEach(d => assert.ok(!fs.existsSync(path.join(destDir, d))));
});
it('Test file copy without ignore', async () => {
fs.unlinkSync(path.join(sourceDir, '.nniignore'));
await execCopydir(sourceDir, destDir);
assert.ok(fs.existsSync(path.join(destDir, 'abcd')));
assert.ok(fs.existsSync(path.join(destDir, 'abc')));
assert.ok(fs.existsSync(path.join(destDir, 'xyz')));
assert.ok(fs.existsSync(path.join(destDir, 'xyy')));
assert.ok(fs.existsSync(path.join(destDir, 'xx')));
});
it('Test tar file', async () => {
const tarPath = `${destDir}.tar`;
await tarAdd(tarPath, sourceDir);
assert.ok(fs.existsSync(tarPath));
fs.mkdirSync(destDir);
tar.extract({
file: tarPath,
cwd: destDir,
sync: true
})
assert.ok(fs.existsSync(path.join(destDir, 'abcd')));
assert.ok(!fs.existsSync(path.join(destDir, 'abc')));
});
it('Validate code ok', async () => {
assert.doesNotThrow(async () => validateCodeDir(sourceDir));
});
it('Validate code too many files', async () => {
for (let i = 0; i < 2000; ++i)
fs.writeFileSync(path.join(sourceDir, `${i}.txt`), 'a');
try {
await validateCodeDir(sourceDir);
} catch (error) {
chai.expect(error.message).to.contains('many files');
return;
}
chai.expect.fail(null, null, 'Did not fail.');
});
it('Validate code too many files ok', async() => {
for (let i = 0; i < 2000; ++i)
fs.writeFileSync(path.join(sourceDir, `${i}.txt`), 'a');
fs.writeFileSync(path.join(sourceDir, '.nniignore'), '*.txt');
assert.doesNotThrow(async () => validateCodeDir(sourceDir));
});
});
......@@ -296,6 +296,13 @@
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
"@types/minipass@*":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651"
integrity sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg==
dependencies:
"@types/node" "*"
"@types/mocha@^5.2.5":
version "5.2.5"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073"
......@@ -442,6 +449,14 @@
dependencies:
"@types/node" "*"
"@types/tar@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.3.tgz#e2cce0b8ff4f285293243f5971bd7199176ac489"
integrity sha512-Z7AVMMlkI8NTWF0qGhC4QIX0zkV/+y0J8x7b/RsHrN0310+YNjoJd8UrApCiGBCWtKjxS9QhNqLi2UJNToh5hA==
dependencies:
"@types/minipass" "*"
"@types/node" "*"
"@types/tmp@^0.0.33":
version "0.0.33"
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.0.33.tgz#1073c4bc824754ae3d10cfab88ab0237ba964e4d"
......@@ -992,6 +1007,11 @@ chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
ci-info@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
......@@ -1880,6 +1900,13 @@ fs-minipass@^1.2.5:
dependencies:
minipass "^2.2.1"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs-vacuum@^1.2.10, fs-vacuum@~1.2.10:
version "1.2.10"
resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36"
......@@ -2282,6 +2309,11 @@ ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
ignore@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
import-fresh@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
......@@ -2605,10 +2637,10 @@ istanbul-lib-source-maps@^4.0.0:
istanbul-lib-coverage "^3.0.0"
source-map "^0.6.1"
istanbul-reports@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.1.tgz#1343217244ad637e0c3b18e7f6b746941a9b5e9a"
integrity sha512-Vm9xwCiQ8t2cNNnckyeAV0UdxKpcQUz4nMxsBvIu8n2kmPSiyb5uaF/8LpmKr+yqL/MdOXaX2Nmdo4Qyxium9Q==
istanbul-reports@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
dependencies:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
......@@ -3154,6 +3186,13 @@ minipass@^2.3.5, minipass@^2.8.6, minipass@^2.9.0:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minipass@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
dependencies:
yallist "^4.0.0"
minizlib@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
......@@ -3166,6 +3205,14 @@ minizlib@^1.2.1:
dependencies:
minipass "^2.9.0"
minizlib@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3"
integrity sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mississippi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
......@@ -3188,6 +3235,11 @@ mkdirp@0.5.3, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
dependencies:
minimist "^1.2.5"
mkdirp@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mocha@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.1.tgz#89fbb30d09429845b1bb893a830bf5771049a441"
......@@ -3344,7 +3396,7 @@ node-pre-gyp@^0.10.3:
semver "^5.3.0"
tar "^4"
node-preload@^0.2.0:
node-preload@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301"
integrity sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==
......@@ -3641,9 +3693,9 @@ number-is-nan@^1.0.0:
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
nyc@^15.0.0:
version "15.0.0"
resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.0.tgz#eb32db2c0f29242c2414fe46357f230121cfc162"
integrity sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==
version "15.0.1"
resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.1.tgz#bd4d5c2b17f2ec04370365a5ca1fc0ed26f9f93d"
integrity sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg==
dependencies:
"@istanbuljs/load-nyc-config" "^1.0.0"
"@istanbuljs/schema" "^0.1.2"
......@@ -3660,10 +3712,9 @@ nyc@^15.0.0:
istanbul-lib-processinfo "^2.0.2"
istanbul-lib-report "^3.0.0"
istanbul-lib-source-maps "^4.0.0"
istanbul-reports "^3.0.0"
js-yaml "^3.13.1"
istanbul-reports "^3.0.2"
make-dir "^3.0.0"
node-preload "^0.2.0"
node-preload "^0.2.1"
p-map "^3.0.0"
process-on-spawn "^1.0.0"
resolve-from "^5.0.0"
......@@ -3671,7 +3722,6 @@ nyc@^15.0.0:
signal-exit "^3.0.2"
spawn-wrap "^2.0.0"
test-exclude "^6.0.0"
uuid "^3.3.3"
yargs "^15.0.2"
oauth-sign@~0.8.2:
......@@ -4980,6 +5030,18 @@ tar@^4.4.10, tar@^4.4.12, tar@^4.4.13:
safe-buffer "^5.1.2"
yallist "^3.0.3"
tar@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39"
integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.0"
mkdirp "^1.0.3"
yallist "^4.0.0"
term-size@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
......@@ -5438,6 +5500,11 @@ yallist@^3.0.3:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yargs-parser@13.1.2, yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment