diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 8095106910..140969b332 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -14,6 +14,8 @@ jobs: env: CI: true + BACKSTAGE_CACHE_DIR: /.backstage-build-cache + BACKSTAGE_CACHE_MAX_ENTRIES: 2 steps: - uses: actions/checkout@v2 @@ -34,6 +36,13 @@ jobs: with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - name: cache build cache + uses: actions/cache@v1 + with: + path: .backstage-build-cache + key: build-cache-${{ github.sha }} + restore-keys: | + build-cache- - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: @@ -43,5 +52,7 @@ jobs: - run: yarn lint - run: yarn build - run: yarn test + - name: yarn bundle, if app was changed + run: git diff --quiet origin/master HEAD -- packages/app packages/core || yarn bundle - name: verify storybook run: yarn workspace storybook build-storybook diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index e82c6c3a4b..f36154aa48 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -14,6 +14,8 @@ jobs: env: CI: true + BACKSTAGE_CACHE_DIR: /.backstage-build-cache + BACKSTAGE_CACHE_MAX_ENTRIES: 2 steps: - uses: actions/checkout@v2 @@ -32,6 +34,13 @@ jobs: with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - name: cache build cache + uses: actions/cache@v1 + with: + path: .backstage-build-cache + key: build-cache-${{ github.sha }} + restore-keys: | + build-cache- - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: diff --git a/package.json b/package.json index eaf85f7567..137c4de0bb 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,12 @@ }, "scripts": { "start": "yarn build && yarn workspace example-app start", + "bundle": "yarn build && yarn workspace example-app bundle", "build": "lerna run build", "test": "cross-env CI=true lerna run test --since origin/master -- --coverage", "create-plugin": "backstage-cli create-plugin", "release": "if [ \"$(git symbolic-ref --short HEAD)\" = master ]; then echo \"don't try to release master\"; exit 1; else lerna version --no-push; fi", - "lint": "lerna run lint", + "lint": "cross-env CI=true lerna run lint --since origin/master --", "storybook": "yarn workspace storybook start" }, "workspaces": { diff --git a/packages/app/package.json b/packages/app/package.json index c332d2f0da..681593a369 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -26,7 +26,7 @@ }, "scripts": { "start": "backstage-cli app:serve", - "build": "backstage-cli app:build", + "bundle": "backstage-cli app:build", "test": "backstage-cli test", "test:e2e": "start-server-and-test start http://localhost:3000 cy:dev", "test:e2e:ci": "start-server-and-test start http://localhost:3000 cy:run", diff --git a/packages/cli/package.json b/packages/cli/package.json index f26b13a421..005fcf01ad 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -19,7 +19,7 @@ "main": "dist", "scripts": { "exec": "npx ts-node ./src", - "build": "tsc --outDir dist --noEmit false --module CommonJS", + "build": "backstage-cli build-cache -- tsc --outDir dist --noEmit false --module CommonJS", "lint": "backstage-cli lint", "test": "backstage-cli test", "start": "nodemon ." @@ -32,10 +32,12 @@ "@types/ora": "^3.2.0", "@types/react-dev-utils": "^9.0.4", "@types/recursive-readdir": "^2.2.0", + "@types/tar": "^4.0.3", "@types/webpack": "^4.41.7", "@types/webpack-dev-server": "^3.10.0", "del": "^5.1.0", "nodemon": "^2.0.2", + "tar": "^6.0.1", "ts-node": "^8.6.2" }, "bin": { diff --git a/packages/cli/src/commands/build-cache/archive.ts b/packages/cli/src/commands/build-cache/archive.ts new file mode 100644 index 0000000000..b4a736240d --- /dev/null +++ b/packages/cli/src/commands/build-cache/archive.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import tar from 'tar'; +import { dirname } from 'path'; + +// packages all files in inputDir into an archive at archivePath, deleting any existing archive +export async function createArchive( + archivePath: string, + inputDir: string, +): Promise { + await fs.remove(archivePath); + await fs.ensureDir(dirname(archivePath)); + await tar.create({ gzip: true, file: archivePath, cwd: inputDir }, ['.']); +} + +// extracts archive at archive path into outputDir, deleting any existing files at outputDir +export async function extractArchive( + archivePath: string, + outputDir: string, +): Promise { + await fs.remove(outputDir); + await fs.ensureDir(outputDir); + await tar.extract({ file: archivePath, cwd: outputDir }); +} diff --git a/packages/cli/src/commands/build-cache/cache.ts b/packages/cli/src/commands/build-cache/cache.ts new file mode 100644 index 0000000000..f52e081a3f --- /dev/null +++ b/packages/cli/src/commands/build-cache/cache.ts @@ -0,0 +1,195 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { resolve as resolvePath, relative as relativePath } from 'path'; +import { runPlain, runCheck } from '../../helpers/run'; +import { Options } from './options'; +import { extractArchive, createArchive } from './archive'; + +const INFO_FILE = '.backstage-build-cache'; + +// Result from a cache query +export type CacheQueryResult = { + // True if there was a cache hit + hit: boolean; + // If there is a cache hit and this method is defined, it needs to be called to restore the + // output contents before continuing. + copy?: (outputDir: string) => Promise; + // Call after a successful build to archive the output content. + // The content will be archived using the same key as was used to query the cache. + archive: (outputDir: string, maxEntries: number) => Promise; +}; + +// Key that determines whether cached output can be reused +export type CacheKey = string[]; + +type CacheEntry = { + // Key for the input of this cache entry + key: CacheKey; + // Path to the archive of this cache entry + path: string; +}; + +// Struct containing information about cache entries on the filesystem. +// Stored inside the INFO_FILE as JSON. +type CacheInfo = { + // Optional key entry for the contents of the current directory. Used to key the + // output present in the outputs folder, where the info file resides inside the output folder. + key?: CacheKey; + // Optional list of cache archives present in the same directory. Resides in the external + // cache location inside one info file for each package. + entries?: CacheEntry[]; +}; + +export class Cache { + // Read the current cache state form the filesystem. + static async read(options: Options) { + const repoPath = relativePath(options.repoRoot, process.cwd()); + const location = resolvePath(options.cacheDir, repoPath); + + const outputInfo = await readCacheInfo(options.output); + const localKey = outputInfo?.key; + + const { entries = [] } = (await readCacheInfo(location)) ?? {}; + return new Cache(location, entries, localKey); + } + + // Generates a key based on the contents of the input paths. + // Returns undefined if it's not possible to generate a stable key. + static async readInputKey( + inputPaths: string[], + ): Promise { + const quotedInputPaths = inputPaths.map(input => `'${input}'`); + + // Make sure we don't have any uncommitted changes to the input, in that case we skip caching. + const noChanges = await runCheck( + `git diff --quiet HEAD -- ${quotedInputPaths.join(' ')}`, + ); + if (!noChanges) { + return undefined; + } + + const trees = []; + for (const quotedInputPath of quotedInputPaths) { + const output = await runPlain(`git ls-tree HEAD ${quotedInputPath}`); + const [, , sha] = output.split(/\s+/, 3); + trees.push(sha); + } + return trees; + } + + constructor( + private readonly location: string, + private readonly entries: CacheEntry[] = [], + private readonly localKey?: CacheKey, + ) {} + + // Query for the presense of cached output for a given key + query(key: CacheKey): CacheQueryResult { + const { location } = this; + + const archive = async (outputDir: string, maxEntries: number) => { + await writeCacheInfo(outputDir, { key }); + + const timestamp = new Date().toISOString().replace(/-|:|\..*/g, ''); + const rand = Math.random() + .toString(36) + .slice(2, 6); + const archiveName = `cache-${timestamp}-${rand}.tgz`; + const archivePath = resolvePath(location, archiveName); + + // Read existing entries and prepend the new one + const { entries = [] } = (await readCacheInfo(location)) ?? {}; + + // Check if there's already aan entry for this key, in that case we just wanna bump it + const entryIndex = entries.findIndex(e => compareKeys(e.key, key)); + if (entryIndex !== -1) { + const [existingEntry] = entries.splice(entryIndex, 1); + entries.unshift(existingEntry); + + await writeCacheInfo(location, { entries }); + return; + } + + // Create and add new archive to entries + await createArchive(archivePath, outputDir); + entries.unshift({ key, path: archiveName }); + + // Remove old cache entries + const removedEntries = entries.splice(maxEntries); + for (const entry of removedEntries) { + try { + await fs.remove(resolvePath(location, entry.path)); + } catch (error) { + process.stderr.write(`failed to remove old cache entry, ${error}\n`); + } + } + + await writeCacheInfo(location, { entries }); + }; + + if (compareKeys(this.localKey, key)) { + return { hit: true, archive }; + } + + const matchingEntry = this.entries.find(e => compareKeys(e.key, key)); + if (!matchingEntry) { + return { hit: false, archive }; + } + + return { + hit: true, + archive, + copy: async (outputDir: string) => { + const archivePath = resolvePath(location, matchingEntry.path); + await extractArchive(archivePath, outputDir); + }, + }; + } +} + +// Compares to cache keys, returning true if they are both defined and equal +function compareKeys(a?: CacheKey, b?: CacheKey): boolean { + if (!a || !b) { + return false; + } + return a.join(',') === b.join(','); +} + +// Read and parse a cache info file in the given directory +async function readCacheInfo( + parentDir: string, +): Promise { + const infoFile = resolvePath(parentDir, INFO_FILE); + const exists = await fs.pathExists(infoFile); + if (!exists) { + return undefined; + } + + const infoData = await fs.readFile(infoFile); + const cacheInfo = JSON.parse(infoData.toString('utf8')) as CacheInfo; + return cacheInfo; +} + +// Write a cache info file to the given directory +async function writeCacheInfo( + parentDir: string, + cacheInfo: CacheInfo, +): Promise { + const infoData = Buffer.from(JSON.stringify(cacheInfo, null, 2), 'utf8'); + await fs.writeFile(resolvePath(parentDir, INFO_FILE), infoData); +} diff --git a/packages/cli/src/commands/build-cache/index.ts b/packages/cli/src/commands/build-cache/index.ts new file mode 100644 index 0000000000..ab8984ecc3 --- /dev/null +++ b/packages/cli/src/commands/build-cache/index.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command } from 'commander'; +import { run } from '../../helpers/run'; +import { Cache } from './cache'; +import { parseOptions } from './options'; + +function print(msg: string) { + process.stdout.write(`[build-cache] ${msg}\n`); +} + +/* + * The build-cache command is used to make builds a no-op if there are no changes to the package. + * It supports both local development where the output directory remains intact, as well as CI + * where the output directory is stored in a separate cache dir. + */ +export default async (cmd: Command, args: string[]) => { + const options = await parseOptions(cmd); + + const key = await Cache.readInputKey(options.inputs); + if (!key) { + print('input directory is dirty, skipping cache'); + await run(args[0], args.slice(1)); + return; + } + + const cache = await Cache.read(options); + + const cacheResult = cache.query(key); + if (cacheResult.hit) { + if (cacheResult.copy) { + print('external cache hit, copying archive to output folder'); + await cacheResult.copy(options.output); + } else { + print('cache hit, nothing to be done'); + } + return; + } + + print('cache miss, need to build'); + + await run(args[0], args.slice(1)); + + print('caching build output'); + await cacheResult.archive(options.output, options.maxCacheEntries); +}; diff --git a/packages/cli/src/commands/build-cache/options.ts b/packages/cli/src/commands/build-cache/options.ts new file mode 100644 index 0000000000..bad7d6c27e --- /dev/null +++ b/packages/cli/src/commands/build-cache/options.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolve as resolvePath } from 'path'; +import { Command } from 'commander'; +import { runPlain } from '../../helpers/run'; + +const DEFAULT_MAX_ENTRIES = 10; + +export type Options = { + inputs: string[]; + output: string; + cacheDir: string; + maxCacheEntries: number; + repoRoot: string; +}; + +export async function parseOptions(cmd: Command): Promise { + const repoRoot = await runPlain('git rev-parse --show-toplevel'); + const argTransformer = (arg: string) => + resolvePath(arg.replace(//g, repoRoot).replace(/'/g, '')); + + const inputs = cmd.input.map(argTransformer) as string[]; + if (inputs.length === 0) { + inputs.push(argTransformer('.')); + } + const output = argTransformer(cmd.output); + const cacheDir = argTransformer( + process.env.BACKSTAGE_CACHE_DIR || cmd.cacheDir, + ); + const maxCacheEntries = + Number(process.env.BACKSTAGE_CACHE_MAX_ENTRIES) || DEFAULT_MAX_ENTRIES; + return { inputs, output, cacheDir, repoRoot, maxCacheEntries }; +} diff --git a/packages/cli/src/helpers/run.ts b/packages/cli/src/helpers/run.ts index 3eec19a148..cbeb2c080c 100644 --- a/packages/cli/src/helpers/run.ts +++ b/packages/cli/src/helpers/run.ts @@ -14,8 +14,15 @@ * limitations under the License. */ -import { SpawnOptions, spawn, ChildProcess } from 'child_process'; +import { + SpawnOptions, + spawn, + ChildProcess, + exec as execCb, +} from 'child_process'; import { ExitCodeError } from './errors'; +import { promisify } from 'util'; +const exec = promisify(execCb); type SpawnOptionsPartialEnv = Omit & { env?: Partial; @@ -43,6 +50,27 @@ export async function run( await waitForExit(child, name); } +export async function runPlain(cmd: string) { + try { + const { stdout } = await exec(cmd); + return stdout.trim(); + } catch (error) { + if (error.stderr) { + process.stderr.write(error.stderr); + } + throw new ExitCodeError(error.code, cmd); + } +} + +export async function runCheck(cmd: string): Promise { + try { + await exec(cmd); + return true; + } catch (error) { + return false; + } +} + export async function waitForExit( child: ChildProcess & { exitCode?: number }, name?: string, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5f24190237..997c05c18c 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -19,6 +19,7 @@ import chalk from 'chalk'; import fs from 'fs'; import createPluginCommand from './commands/create-plugin/createPlugin'; import watch from './commands/watch-deps'; +import buildCache from './commands/build-cache'; import lintCommand from './commands/lint'; import testCommand from './commands/testCommand'; import appBuild from './commands/app/build'; @@ -76,6 +77,23 @@ const main = (argv: string[]) => { .description('Watch all dependencies while running another command') .action(actionHandler(watch)); + program + .command('build-cache') + .description('Wrap build command with a cache') + .option( + '--input ', + 'List of input directories that invalidate the cache [.]', + (value, acc) => acc.concat(value), + [], + ) + .option('--output ', 'Output directory to cache', 'dist') + .option( + '--cache-dir ', + 'Cache dir', + '/node_modules/.cache/backstage-builds', + ) + .action(actionHandler(buildCache)); + program.on('command:*', () => { console.log(); console.log( diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index b2bd6f2c82..c7220a8fd3 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "build": "backstage-cli plugin:build", + "build": "backstage-cli build-cache -- backstage-cli plugin:build", "lint": "backstage-cli lint", "test": "backstage-cli test" }, diff --git a/packages/core/package.json b/packages/core/package.json index 8adb1307fe..1105d79c46 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,7 +19,7 @@ "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", "scripts": { - "build": "tsc --outDir dist/cjs --noEmit false --module CommonJS", + "build": "backstage-cli build-cache -- tsc --outDir dist/cjs --noEmit false --module CommonJS", "lint": "backstage-cli lint", "test": "backstage-cli test" }, diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json index 33be457cba..91e3da417f 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -19,7 +19,7 @@ "react-dom": "^16.12.0" }, "scripts": { - "build": "backstage-cli plugin:build", + "build": "backstage-cli build-cache -- backstage-cli plugin:build", "lint": "backstage-cli lint", "test": "backstage-cli test" }, diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index b38977afab..e1672b4c40 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "build": "backstage-cli plugin:build", + "build": "backstage-cli build-cache -- backstage-cli plugin:build", "lint": "backstage-cli lint", "test": "backstage-cli test" }, diff --git a/yarn.lock b/yarn.lock index dcde1de95a..db975122b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3612,6 +3612,13 @@ resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/minipass@*": + version "2.2.0" + resolved "https://registry.npmjs.org/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651" + integrity sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@>= 8", "@types/node@^13.7.2": version "13.9.2" resolved "https://registry.npmjs.org/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349" @@ -3776,6 +3783,14 @@ resolved "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== +"@types/tar@^4.0.3": + version "4.0.3" + resolved "https://registry.npmjs.org/@types/tar/-/tar-4.0.3.tgz#e2cce0b8ff4f285293243f5971bd7199176ac489" + integrity sha512-Z7AVMMlkI8NTWF0qGhC4QIX0zkV/+y0J8x7b/RsHrN0310+YNjoJd8UrApCiGBCWtKjxS9QhNqLi2UJNToh5hA== + dependencies: + "@types/minipass" "*" + "@types/node" "*" + "@types/testing-library__cypress@^5.0.3": version "5.0.3" resolved "https://registry.npmjs.org/@types/testing-library__cypress/-/testing-library__cypress-5.0.3.tgz#94969b7c1eea96e09d8e023a1d225590fa75a1fe" @@ -5795,7 +5810,7 @@ chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.3.1: optionalDependencies: fsevents "~2.1.2" -chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.4: +chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.3, chownr@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -13039,6 +13054,14 @@ minizlib@^1.2.1: dependencies: minipass "^2.9.0" +minizlib@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/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.npmjs.org/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -13078,7 +13101,7 @@ mkdirp-promise@^5.0.1: dependencies: mkdirp "*" -mkdirp@*: +mkdirp@*, mkdirp@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== @@ -18083,6 +18106,18 @@ tar@^4.4.10, tar@^4.4.12, tar@^4.4.13, tar@^4.4.8: safe-buffer "^5.1.2" yallist "^3.0.3" +tar@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/tar/-/tar-6.0.1.tgz#7b3bd6c313cb6e0153770108f8d70ac298607efa" + integrity sha512-bKhKrrz2FJJj5s7wynxy/fyxpE0CmCjmOQ1KV4KkgXFWOgoIT/NbTMnB1n+LFNrNk0SSBVGGxcK5AGsyC+pW5Q== + dependencies: + chownr "^1.1.3" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.0" + mkdirp "^1.0.3" + yallist "^4.0.0" + telejson@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/telejson/-/telejson-3.3.0.tgz#6d814f3c0d254d5c4770085aad063e266b56ad03"