├── .gitattributes ├── sample ├── packages │ ├── .gitignore │ ├── ignored │ │ └── package.json │ └── child-1 │ │ └── package.json ├── dependencies │ ├── hello-org │ │ ├── hello-org │ │ └── package.json │ ├── hello-dependency │ │ ├── hello-dependency │ │ └── package.json │ ├── fail-dependency │ │ ├── fail-now │ │ └── package.json │ ├── hello-dev-dependency │ │ ├── hello-dev-dependency │ │ └── package.json │ └── hello-single-binary-dependency │ │ ├── hello-single-binary-dependency │ │ └── package.json ├── package.json └── package-lock.json ├── .prettierrc ├── .mocharc.jsonc ├── .npmignore ├── bin └── link-parent-bin ├── src ├── tsconfig.json ├── cli.ts ├── FSUtils.ts ├── program.ts ├── link.ts └── ParentBinLinker.ts ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── test ├── tsconfig.json ├── setup.ts ├── helpers │ └── createLogStub.ts ├── unit │ ├── program.spec.ts │ ├── link.spec.ts │ └── ParentBinLinker.spec.ts └── integration │ └── sample.spec.ts ├── .eslintrc ├── stryker.conf.json ├── tsconfig.settings.json ├── .github └── workflows │ └── ci.yml ├── package.json ├── readme.md └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /sample/packages/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.mocharc.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["source-map-support/register", "dist/test/setup.js"] 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !bin/** 3 | !src/** 4 | !dist/src/** 5 | dist/src/**/*.tsbuildinfo 6 | !readme.md -------------------------------------------------------------------------------- /sample/dependencies/hello-org/hello-org: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | console.log('hello org'); -------------------------------------------------------------------------------- /sample/dependencies/hello-dependency/hello-dependency: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | console.log('hello dependency'); -------------------------------------------------------------------------------- /bin/link-parent-bin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.title = 'parent-bin'; 6 | 7 | require('../dist/src/cli'); -------------------------------------------------------------------------------- /sample/dependencies/fail-dependency/fail-now: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | console.log('Fail now!'); 5 | process.exit(3); 6 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "cSpell.words": [ 4 | "execa" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /sample/dependencies/hello-dev-dependency/hello-dev-dependency: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | console.log('hello dev dependency'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.stryker-tmp 3 | *.js 4 | !tasks/*.js 5 | /dist 6 | /reports 7 | 8 | # IntelliJ project files 9 | .idea 10 | *.iml 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [{*.ts,*.js,*jsx,*tsx,*.json,,*.jsonc,*.code-workspace}] 2 | insert_final_newline = true 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf -------------------------------------------------------------------------------- /sample/dependencies/hello-org/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hello-org/core", 3 | "version": "0.0.0", 4 | "bin": { 5 | "hello-org": "hello-org" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample/dependencies/fail-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fail-dependency", 3 | "version": "0.0.0", 4 | "bin": { 5 | "fail-now": "fail-now" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample/dependencies/hello-single-binary-dependency/hello-single-binary-dependency: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | console.log('hello single-binary-dependency'); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "src" 6 | }, 7 | { 8 | "path": "test" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /sample/dependencies/hello-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-dependency", 3 | "version": "0.0.0", 4 | "bin": { 5 | "helloDependency": "hello-dependency" 6 | } 7 | } -------------------------------------------------------------------------------- /sample/dependencies/hello-single-binary-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-single-binary-dependency", 3 | "version": "0.0.0", 4 | "bin": "hello-single-binary-dependency" 5 | } -------------------------------------------------------------------------------- /sample/packages/ignored/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-1", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "hello-dependency": "helloDependency" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sample/dependencies/hello-dev-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-dev-dependency", 3 | "version": "0.0.0", 4 | "bin": { 5 | "helloDevDependency": "hello-dev-dependency" 6 | } 7 | } -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/test", 5 | "types": ["node", "mocha"] 6 | }, 7 | "references": [ 8 | { 9 | "path": "../src" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module" 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier", 10 | "plugin:prettier/recommended" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /sample/packages/child-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-1", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "hello-org": "hello-org", 7 | "hello-dependency": "helloDependency", 8 | "hello-dev-dependency": "helloDevDependency", 9 | "hello-single-binary-dependency": "hello-single-binary-dependency", 10 | "link-parent-bin-help": "link-parent-bin --help", 11 | "fail-now": "fail-now" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /stryker.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", 3 | "packageManager": "npm", 4 | "reporters": [ 5 | "html", 6 | "clear-text", 7 | "progress", 8 | "dashboard" 9 | ], 10 | "buildCommand": "tsc -b", 11 | "checkers": ["typescript"], 12 | "testRunner": "mocha", 13 | "coverageAnalysis": "perTest", 14 | "mochaOptions": { 15 | "spec": ["dist/test/unit/**/*.js"] 16 | }, 17 | "thresholds": { 18 | "break": 60 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "tsc-watch", 8 | "type": "shell", 9 | "command": "npm start", 10 | "isBackground": true, 11 | "problemMatcher": "$tsc-watch", 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import sinonChai from 'sinon-chai'; 4 | import log4js from 'log4js'; 5 | import sinon from 'sinon'; 6 | 7 | log4js.configure({ 8 | appenders: { 9 | console: { type: 'stdout' }, 10 | }, 11 | categories: { 12 | default: { appenders: ['console'], level: 'fatal' }, 13 | }, 14 | }); 15 | 16 | chai.use(chaiAsPromised); 17 | chai.use(sinonChai); 18 | 19 | export const mochaHooks = { 20 | afterEach(): void { 21 | sinon.restore(); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { ParentBinLinker } from './ParentBinLinker'; 2 | import { program } from './program'; 3 | import * as log4js from 'log4js'; 4 | 5 | const options = program.parse(process.argv); 6 | log4js.configure({ 7 | appenders: { 8 | console: { 9 | type: 'stdout', 10 | layout: { type: 'pattern', pattern: '%[%r (%z) %p %c%] %m' }, 11 | }, 12 | }, 13 | categories: { 14 | default: { appenders: ['console'], level: options.logLevel }, 15 | }, 16 | }); 17 | 18 | new ParentBinLinker(options).linkBinsToChildren().catch((err) => { 19 | console.error('Error Linking packages', err); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /src/FSUtils.ts: -------------------------------------------------------------------------------- 1 | import mkdirp from 'mkdirp'; 2 | import { promises as fs } from 'fs'; 3 | import path from 'path'; 4 | 5 | export class FSUtils { 6 | static mkdirp = mkdirp; 7 | 8 | /** 9 | * Reads dirs only 10 | */ 11 | static readDirs = async (location: string): Promise => { 12 | const files = await fs.readdir(location); 13 | const filesWithStats = await Promise.all( 14 | files.map((name) => 15 | fs.stat(path.resolve(location, name)).then((stat) => ({ name, stat })), 16 | ), 17 | ); 18 | return filesWithStats 19 | .filter((f) => f.stat.isDirectory()) 20 | .map(({ name: file }) => file); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /test/helpers/createLogStub.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import { Logger } from 'log4js'; 3 | 4 | export function createLoggerMock(): sinon.SinonStubbedInstance { 5 | return { 6 | debug: sinon.stub(), 7 | error: sinon.stub(), 8 | fatal: sinon.stub(), 9 | info: sinon.stub(), 10 | isDebugEnabled: sinon.stub(), 11 | isErrorEnabled: sinon.stub(), 12 | isFatalEnabled: sinon.stub(), 13 | isInfoEnabled: sinon.stub(), 14 | clearContext: sinon.stub(), 15 | isLevelEnabled: sinon.stub(), 16 | isTraceEnabled: sinon.stub(), 17 | isWarnEnabled: sinon.stub(), 18 | log: sinon.stub(), 19 | } as sinon.SinonStubbedInstance; 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "composite": true, 6 | "outDir": "dist", 7 | "resolveJsonModule": true, 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "declaration": true, 20 | "types": ["node"], 21 | "lib": [ 22 | "es2017" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "install-local" 7 | }, 8 | "devDependencies": { 9 | "hello-dev-dependency": "file:./dependencies/hello-dev-dependency", 10 | "install-local": "^3.0.1" 11 | }, 12 | "dependencies": { 13 | "fail-dependency": "file:./dependencies/fail-dependency", 14 | "hello-dependency": "file:./dependencies/hello-dependency", 15 | "hello-single-binary-dependency": "file:./dependencies/hello-single-binary-dependency", 16 | "@hello-org/core": "file:./dependencies/hello-org" 17 | }, 18 | "localDependencies": { 19 | "link-parent-bin": ".." 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: ~ 5 | pull_request: ~ 6 | schedule: 7 | - cron: '0 12 * * *' 8 | 9 | jobs: 10 | build_and_test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node-version: [14.x, 16.x] 15 | os: ['ubuntu-latest', 'windows-latest'] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Build & lint & test 28 | run: npm run all 29 | mutation_test: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Install dependencies 34 | run: npm ci 35 | - name: test-mutation 36 | run: npm run test:mutation -- --concurrency 3 37 | env: 38 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach", 9 | "port": 9229, 10 | "request": "attach", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "type": "pwa-node" 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "Unit Tests", 20 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 21 | "args": [ 22 | "--no-timeouts", 23 | "--colors", 24 | "${workspaceRoot}/dist/test/unit/*.js" 25 | ], 26 | "skipFiles": [ 27 | "/**" 28 | ], 29 | "internalConsoleOptions": "openOnSessionStart", 30 | "sourceMaps": true, 31 | "outFiles": [ 32 | "${workspaceRoot}/dist/**/*.js" 33 | ] 34 | }, 35 | { 36 | "type": "node", 37 | "request": "launch", 38 | "name": "Integration Tests", 39 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 40 | "args": [ 41 | "--no-timeouts", 42 | "--colors", 43 | "${workspaceRoot}/dist/test/integration/*.js" 44 | ], 45 | "skipFiles": [ 46 | "/**" 47 | ], 48 | "internalConsoleOptions": "openOnSessionStart", 49 | "sourceMaps": true, 50 | "outFiles": [ 51 | "${workspaceRoot}/dist/**/*.js" 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /test/unit/program.spec.ts: -------------------------------------------------------------------------------- 1 | import { Options, program } from '../../src/program'; 2 | import { expect } from 'chai'; 3 | 4 | const expectToInclude = (actual: T, expected: T) => { 5 | for (const key in expected) { 6 | expect(actual[key], `Matching key ${key}`).to.eq(expected[key]); 7 | } 8 | }; 9 | 10 | describe('program', () => { 11 | describe('parse', () => { 12 | it('should parse [] to default values', () => { 13 | const defaultOptions: Options = { 14 | logLevel: 'info', 15 | linkDependencies: false, 16 | linkDevDependencies: true, 17 | linkLocalDependencies: false, 18 | childDirectoryRoot: 'packages', 19 | filter: '*', 20 | }; 21 | const actualOptions = program.parse(['', '']); 22 | expect(actualOptions).to.contain.all.keys(defaultOptions); 23 | expectToInclude(actualOptions, defaultOptions); 24 | }); 25 | it('should parse overridden option', () => { 26 | const expected: Options = { 27 | logLevel: 'error', 28 | linkDependencies: true, 29 | linkDevDependencies: false, 30 | linkLocalDependencies: false, 31 | childDirectoryRoot: 'some-other-folder', 32 | filter: 'override', 33 | }; 34 | const actualOptions = program.parse([ 35 | '', 36 | '', 37 | '--link-dev-dependencies', 38 | 'false', 39 | '--link-dependencies', 40 | 'true', 41 | '-l', 42 | 'error', 43 | '--child-directory-root', 44 | 'some-other-folder', 45 | '--filter', 46 | 'override', 47 | ]); 48 | expect(actualOptions).to.contain.all.keys(expected); 49 | expectToInclude(actualOptions, expected); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/program.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | 3 | export interface Options { 4 | linkDevDependencies: boolean; 5 | linkDependencies: boolean; 6 | linkLocalDependencies: boolean; 7 | filter: string; 8 | logLevel: 'debug' | 'info' | 'error'; 9 | childDirectoryRoot: string; 10 | } 11 | 12 | const parseBoolean = (val: string) => val.toLowerCase() === 'true'; 13 | const describeLinking = (name: string, defaultValue: boolean) => 14 | `Enables linking of parents \`${name}\`. Defaults to: ${defaultValue}`; 15 | 16 | export const program = { 17 | parse(argv: string[]): Options { 18 | const program = new Command(); 19 | return ( 20 | program 21 | .storeOptionsAsProperties(false) 22 | .usage('[options]') 23 | // eslint-disable-next-line @typescript-eslint/no-var-requires 24 | .version(require('../../package.json').version) 25 | .option( 26 | '-c, --child-directory-root ', 27 | 'The directory that hosts the child packages relative to the parent root.', 28 | 'packages', 29 | ) 30 | .option( 31 | '-d, --link-dev-dependencies ', 32 | describeLinking('devDependencies', true), 33 | parseBoolean, 34 | true, 35 | ) 36 | .option( 37 | '-s, --link-dependencies ', 38 | describeLinking('dependencies', false), 39 | parseBoolean, 40 | false, 41 | ) 42 | .option( 43 | '-o, --link-local-dependencies ', 44 | describeLinking('localDependencies', false), 45 | parseBoolean, 46 | false, 47 | ) 48 | .option( 49 | '-l, --log-level ', 50 | 'Set the log level', 51 | /debug|info|error|off/, 52 | 'info', 53 | ) 54 | .option( 55 | '--filter ', 56 | 'Specify a [minimatch](https://www.npmjs.com/package/minimatch) glob pattern to specify which child packages under the child packages directory should receive symlinks.', 57 | '*', 58 | ) 59 | .parse(argv) 60 | .opts() as Options 61 | ); 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/link.ts: -------------------------------------------------------------------------------- 1 | import { platform } from 'os'; 2 | import path from 'path'; 3 | import { getLogger } from 'log4js'; 4 | import { promises as fs } from 'fs'; 5 | import { FSUtils } from './FSUtils'; 6 | import cmdShim from 'cmd-shim'; 7 | 8 | export type LinkStatus = 9 | | 'success' 10 | | 'alreadyExists' 11 | | 'differentLinkAlreadyExists' 12 | | 'error'; 13 | 14 | export interface LinkResult { 15 | status: LinkStatus; 16 | } 17 | 18 | async function symlink(from: string, to: string): Promise { 19 | to = path.resolve(to); 20 | const toDir = path.dirname(to); 21 | const target = path.relative(toDir, from); 22 | await FSUtils.mkdirp(path.dirname(to)); 23 | await fs.symlink(target, to, 'junction'); 24 | return { 25 | status: 'success', 26 | }; 27 | } 28 | 29 | export async function link(from: string, to: string): Promise { 30 | if (platform() === 'win32') { 31 | return cmdShimIfExists(from, to); 32 | } else { 33 | return linkIfExists(from, to); 34 | } 35 | } 36 | 37 | async function cmdShimIfExists(from: string, to: string): Promise { 38 | try { 39 | await fs.stat(to); 40 | debug(`Link at '${to}' already exists. Leaving it alone.`); 41 | return { status: 'alreadyExists' }; 42 | } catch (_) { 43 | /* link doesn't exist */ 44 | await cmdShim.ifExists(from, to); 45 | return { status: 'success' }; 46 | } 47 | } 48 | 49 | async function linkIfExists(from: string, to: string): Promise { 50 | try { 51 | await fs.stat(from); 52 | const fromOnDisk = await fs.readlink(to); 53 | const toDir = path.dirname(to); 54 | const absoluteFrom = path.resolve(toDir, from); 55 | const absoluteFromOnDisk = path.resolve(toDir, fromOnDisk); 56 | if (absoluteFrom !== absoluteFromOnDisk) { 57 | debug( 58 | `Different link at '${to}' to '${absoluteFromOnDisk}' already exists. Leaving it alone, the package is probably already installed in the child package.`, 59 | ); 60 | return { 61 | status: 'differentLinkAlreadyExists', 62 | }; 63 | } else { 64 | debug(`Link at '${to}' already exists.`); 65 | return { 66 | status: 'alreadyExists', 67 | }; 68 | } 69 | } catch { 70 | return symlink(from, to); 71 | } 72 | } 73 | 74 | function debug(message: string, ...args: unknown[]) { 75 | const log = getLogger('link'); 76 | log.debug(message, ...args); 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link-parent-bin", 3 | "version": "3.0.0", 4 | "description": "", 5 | "main": "dist/src/ParentBinLinker.js", 6 | "bin": { 7 | "link-parent-bin": "bin/link-parent-bin" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nicojs/node-link-parent-bin.git" 12 | }, 13 | "engines": { 14 | "node": ">=14" 15 | }, 16 | "scripts": { 17 | "all": "npm run clean && npm run lint && npm run build && npm run test", 18 | "clean": "rimraf dist reports", 19 | "lint": "eslint --ignore-path .gitignore --ext .ts . && prettier --check .github/**/*.yml", 20 | "build": "tsc -b", 21 | "test": "npm run test:unit && npm run test:integration", 22 | "test:unit": "mocha dist/test/unit/**/*.js", 23 | "test:integration": "mocha dist/test/integration/**/*.js", 24 | "test:mutation": "stryker run", 25 | "start": "tsc -b -w", 26 | "preversion": "npm run all", 27 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", 28 | "postversion": "npm publish && git push && git push --tags", 29 | "release:patch": "npm version patch -m \"chore(release): %s\"", 30 | "release:minor": "npm version minor -m \"chore(release): %s\"", 31 | "release:major": "npm version major -m \"chore(release): %s\"" 32 | }, 33 | "keywords": [], 34 | "author": "", 35 | "license": "ISC", 36 | "devDependencies": { 37 | "@stryker-mutator/core": "^6.0.2", 38 | "@stryker-mutator/mocha-runner": "^6.0.2", 39 | "@stryker-mutator/typescript-checker": "^6.0.2", 40 | "@types/chai": "^4.3.1", 41 | "@types/chai-as-promised": "7.1.5", 42 | "@types/cmd-shim": "^5.0.0", 43 | "@types/minimatch": "^3.0.5", 44 | "@types/mkdirp": "^1.0.2", 45 | "@types/mocha": "^9.1.1", 46 | "@types/mz": "2.7.4", 47 | "@types/rimraf": "3.0.2", 48 | "@types/semver": "^7.3.9", 49 | "@types/sinon": "^10.0.11", 50 | "@types/sinon-chai": "^3.2.8", 51 | "@typescript-eslint/eslint-plugin": "^5.26.0", 52 | "@typescript-eslint/parser": "^5.26.0", 53 | "chai": "^4.3.6", 54 | "chai-as-promised": "^7.1.1", 55 | "conventional-changelog-cli": "^2.2.2", 56 | "eslint": "^8.16.0", 57 | "eslint-config-prettier": "^8.5.0", 58 | "eslint-plugin-prettier": "^4.0.0", 59 | "install-local": "^3.0.1", 60 | "mocha": "^10.0.0", 61 | "prettier": "^2.6.2", 62 | "rimraf": "^3.0.2", 63 | "semver": "^7.3.7", 64 | "sinon": "^14.0.0", 65 | "sinon-chai": "^3.7.0", 66 | "source-map-support": "^0.5.21", 67 | "typescript": "^4.7.2" 68 | }, 69 | "dependencies": { 70 | "cmd-shim": "^5.0.0", 71 | "commander": "^9.2.0", 72 | "log4js": "^6.5.2", 73 | "minimatch": "^5.1.0", 74 | "mkdirp": "^1.0.4", 75 | "mz": "^2.7.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/integration/sample.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import fs from 'fs'; 3 | import execa = require('execa'); 4 | import path from 'path'; 5 | import rimraf from 'rimraf'; 6 | import semver from 'semver'; 7 | 8 | const rm = (location: string) => 9 | new Promise((res, rej) => 10 | rimraf(location, (err) => { 11 | if (err) { 12 | rej(err); 13 | } else { 14 | res(); 15 | } 16 | }), 17 | ); 18 | 19 | const resolve = (relativePath: string) => 20 | path.resolve(__dirname, '../../../sample', relativePath); 21 | 22 | const execInSample = async ( 23 | cmd: string, 24 | cwd = '', 25 | ): Promise> => { 26 | console.log(`exec: ${cmd}`); 27 | const output = await execa.command(cmd, { cwd: resolve(cwd) }); 28 | if (output.stdout) { 29 | console.log(`stdout: ${output.stdout}`); 30 | } 31 | if (output.stderr) { 32 | console.error(`stderr: ${output.stderr}`); 33 | } 34 | 35 | return output; 36 | }; 37 | 38 | const MOCHA_TIMEOUT = 60000; 39 | 40 | describe('Sample project after installing and linking with `link-parent-bin`', function () { 41 | this.timeout(MOCHA_TIMEOUT); 42 | 43 | before(() => 44 | rm(resolve('node_modules')) 45 | .then(() => execInSample('npm i')) 46 | .then(() => 47 | execInSample( 48 | 'npx link-parent-bin --log-level debug -s true -o true --filter child-1', 49 | ), 50 | ), 51 | ); 52 | 53 | it('should be able to run linked dependency commands from child packages', () => { 54 | return expect(execInSample('npm run hello-dependency', 'packages/child-1')) 55 | .to.eventually.have.property('stdout') 56 | .and.match(/hello dependency/g); 57 | }); 58 | 59 | it('should be able to run a linked dev dependency', () => { 60 | return expect( 61 | execInSample('npm run hello-dev-dependency', 'packages/child-1'), 62 | ) 63 | .to.eventually.have.property('stdout') 64 | .and.match(/hello dev dependency/g); 65 | }); 66 | 67 | it('should be able to run a linked local dependency', () => { 68 | return expect( 69 | execInSample('npm run link-parent-bin-help', 'packages/child-1'), 70 | ) 71 | .to.eventually.have.property('stdout') 72 | .and.match(/Usage: link-parent-bin/g); 73 | }); 74 | 75 | it('should be able to run a dependency with a single binary', () => { 76 | return expect( 77 | execInSample( 78 | 'npm run hello-single-binary-dependency', 79 | 'packages/child-1', 80 | ), 81 | ) 82 | .to.eventually.have.property('stdout') 83 | .and.match(/hello single-binary-dependency/g); 84 | }); 85 | 86 | it('should proxy the exit code when the process fails', async () => { 87 | const result = await expect( 88 | execInSample('npm run fail-now', 'packages/child-1'), 89 | ).rejected; 90 | expect(result.exitCode).eq(3); 91 | expect(result.stdout).contains('Fail now!'); 92 | }); 93 | 94 | it('should support scoped packages', async () => { 95 | const result = await execInSample('npm run hello-org', 'packages/child-1'); 96 | expect(result.stdout).contains('hello org'); 97 | }); 98 | 99 | it('should not link in ignored patterns', async () => { 100 | const version = (await execa('npm', ['-v'])).stdout; 101 | if (semver.gte(version, '8.0.0')) { 102 | await execInSample('npm run hello-dependency', 'packages/ignored'); 103 | expect( 104 | fs.existsSync( 105 | resolve('packages/ignored/node_modules/.bin/helloDependency'), 106 | ), 107 | ).false; 108 | } else { 109 | await expect(execInSample('npm run hello-dependency', 'packages/ignored')) 110 | .rejected; 111 | } 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fnicojs%2Fnode-link-parent-bin%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/nicojs/node-link-parent-bin/master) 2 | ![CI](https://github.com/nicojs/node-link-parent-bin/workflows/CI/badge.svg) 3 | 4 | # Link parent bin 5 | 6 | Link the bins of parent (dev) dependencies to the child packages in a multi-package [lerna](https://lernajs.io/)-like project. Stuff just works as expected. 7 | 8 | **NOTE: This package is no longer needed and thus deprecated. Since recent NPM releases, any parent directory's "node_modules/.bin" is automatically added to the PATH when using `npm run`.** 9 | 10 | ## About 11 | 12 | Let's say your repo looks like this: 13 | 14 | ``` 15 | root/ 16 | package.json 17 | packages/ 18 | package-1/ 19 | package.json 20 | package-2/ 21 | package.json 22 | ``` 23 | 24 | Well... you're probably managing your `devDependencies` at root level only. For example: you have one `mocha` installed at root with `npm i -D mocha`. 25 | 26 | Now if you add an npm script in `package-1/package.json`: 27 | 28 | ```json 29 | "scripts": { 30 | "test": "mocha" 31 | } 32 | ``` 33 | 34 | And run: 35 | 36 | ```bash 37 | $ npm run test 38 | 39 | > package-1@0.0.1 test /package-1 40 | > mocha 41 | 42 | 'mocha' is not recognized as an internal or external command 43 | 44 | npm ERR! 45 | ``` 46 | 47 | ...thats not so nice. You're basically forced to run all npm scripts from the root level. *But* after running `link-parent-bin`: 48 | 49 | ```bash 50 | $ npm run test 51 | 52 | > package-1@0.0.1 test /package-1 53 | > mocha 54 | 55 | linked from parent... 56 | √ and it worked! 57 | ``` 58 | 59 | ## Getting started 60 | 61 | Install the package in the **root** of your multiple packages repository. 62 | 63 | ```bash 64 | npm i -D link-parent-bin 65 | ``` 66 | 67 | Add the following npm script in your root `package.json`: 68 | 69 | ```json 70 | "scripts": { 71 | "link-parent-bin": "link-parent-bin" 72 | } 73 | ``` 74 | 75 | Run it with `npm run link-parent-bin`. 76 | 77 | ```bash 78 | npm run link-parent-bin 79 | 80 | [INFO] ParentBinLinker - Linking dependencies ["mocha"] under children ["package-1", "package-2"] 81 | ``` 82 | 83 | And your done. 84 | 85 | ## Improve your workflow 86 | 87 | Since you're probably not releasing your parent module anyway, it might be better to add the linking to the post-install step: 88 | 89 | ```json 90 | "scripts": { 91 | "postinstall": "link-parent-bin" 92 | } 93 | ``` 94 | 95 | *-or if you're using lerna* 96 | 97 | ```json 98 | "scripts": { 99 | "postinstall": "lerna bootstrap && link-parent-bin" 100 | } 101 | ``` 102 | 103 | This way, other developers don't have to run this script manually. 104 | 105 | ## Command line options 106 | 107 | ```bash 108 | $ node_modules/.bin/link-parent-bin --help 109 | 110 | Usage: link-parent-bin [options] 111 | 112 | Options: 113 | -V, --version output the version number 114 | -c, --child-directory-root The directory that hosts the child packages relative to the parent root. (default: "packages") 115 | -d, --link-dev-dependencies Enables linking of parents `devDependencies`. Defaults to: true (default: true) 116 | -s, --link-dependencies Enables linking of parents `dependencies`. Defaults to: false (default: false) 117 | -o, --link-local-dependencies Enables linking of parents `localDependencies`. Defaults to: false (default: false) 118 | -l, --log-level Set the log level (default: "info") 119 | --filter Specify a [minimatch](https://www.npmjs.com/package/minimatch) glob pattern to specify which child packages under the child packages directory should receive symlinks. (default: "*") 120 | -h, --help display help for command 121 | ``` 122 | 123 | ## Use programmatically 124 | 125 | ```js 126 | const { ParentBinLinker } = require('link-parent-bin'); 127 | const linker = new ParentBinLinker({ childDirectoryRoot: 'packages', linkDevDependencies: true, linkDependencies: false, linkLocalDependencies: false }); 128 | linker.linkBinsToChildren() 129 | .then(() => console.log('done')) 130 | .catch(err => console.error('Error Linking packages', err)); 131 | ``` 132 | 133 | Type declaration files are included for the TypeScript developers out there. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.0.0](https://github.com/nicojs/node-link-parent-bin/compare/v2.0.0...v3.0.0) (2022-05-27) 2 | 3 | 4 | ### Features 5 | 6 | * **deprecate:** Add note about deprecation ([f3921c5](https://github.com/nicojs/node-link-parent-bin/commit/f3921c5e40d61bedac5f9cb3ae19cae8167d0646)) 7 | * **node version:** Update min version to 14 ([6a52cd0](https://github.com/nicojs/node-link-parent-bin/commit/6a52cd099e137cc2d1b19e43830e6f79123e18ea)) 8 | 9 | 10 | ### BREAKING CHANGES 11 | 12 | * **deprecate:** This package is deprecated and should no longer be used. Recent NPM versions automatically add any parent's `node_modules/.bin` to the `PATH` when executing an `npm run` command, rendering this package no longer needed 13 | * **node version:** Node 10 or 12 are no longer supported officially. 14 | 15 | 16 | 17 | # [2.0.0](https://github.com/nicojs/node-link-parent-bin/compare/v1.0.2...v2.0.0) (2020-10-28) 18 | 19 | 20 | ### chore 21 | 22 | * general quality of life improvements ([#25](https://github.com/nicojs/node-link-parent-bin/issues/25)) ([f78820c](https://github.com/nicojs/node-link-parent-bin/commit/f78820cf9bf08821488044d66b3baf91e13db102)) 23 | * **deps:** update dependencies ([6ae5ec8](https://github.com/nicojs/node-link-parent-bin/commit/6ae5ec813c6e4a078bde1d9cfb6709b93de63394)) 24 | 25 | 26 | ### Features 27 | 28 | * **filter:** support child packages filter ([#29](https://github.com/nicojs/node-link-parent-bin/issues/29)) ([915d99e](https://github.com/nicojs/node-link-parent-bin/commit/915d99e44576e58b827ace965cf86075e8038d89)) 29 | * **log:** log a summary ([#32](https://github.com/nicojs/node-link-parent-bin/issues/32)) ([332aa13](https://github.com/nicojs/node-link-parent-bin/commit/332aa13a9f6e72e2bd33227aaa58fd0a3cc159b1)) 30 | 31 | 32 | ### BREAKING CHANGES 33 | 34 | * JS output files are now located in the `dist` directory. This only has an effect if you're requiring directly from it. 35 | * **deps:** Node 8 is no longer actively supported 36 | 37 | 38 | 39 | ## [1.0.2](https://github.com/nicojs/node-link-parent-bin/compare/v1.0.1...v1.0.2) (2019-07-09) 40 | 41 | 42 | ### Features 43 | 44 | * **log level:** support log level off ([#11](https://github.com/nicojs/node-link-parent-bin/issues/11)) ([f0cdec3](https://github.com/nicojs/node-link-parent-bin/commit/f0cdec3)) 45 | 46 | 47 | 48 | ## [1.0.1](https://github.com/nicojs/node-link-parent-bin/compare/v1.0.0...v1.0.1) (2019-07-02) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **deps:** update log4js and commander versions ([4d6982b](https://github.com/nicojs/node-link-parent-bin/commit/4d6982b)) 54 | 55 | 56 | 57 | # [1.0.0](https://github.com/nicojs/node-link-parent-bin/compare/v0.3.0...v1.0.0) (2019-02-12) 58 | 59 | 60 | 61 | 62 | # [0.3.0](https://github.com/nicojs/node-link-parent-bin/compare/v0.2.3...v0.3.0) (2019-01-27) 63 | 64 | 65 | ### Features 66 | 67 | * **windows shim link:** Preserve shimlink if it already exists ([a15e13e](https://github.com/nicojs/node-link-parent-bin/commit/a15e13e)) 68 | 69 | 70 | 71 | 72 | ## [0.2.3](https://github.com/nicojs/node-link-parent-bin/compare/v0.2.2...v0.2.3) (2018-08-30) 73 | 74 | 75 | 76 | 77 | ## [0.2.2](https://github.com/nicojs/node-link-parent-bin/compare/v0.2.1...v0.2.2) (2018-08-30) 78 | 79 | 80 | 81 | 82 | ## [0.2.1](https://github.com/nicojs/node-link-parent-bin/compare/v0.2.0...v0.2.1) (2018-08-30) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * **deps:** Update log4js and commander dependencies ([#9](https://github.com/nicojs/node-link-parent-bin/issues/9)) ([44d3439](https://github.com/nicojs/node-link-parent-bin/commit/44d3439)) 88 | 89 | 90 | 91 | 92 | # [0.2.0](https://github.com/nicojs/node-link-parent-bin/compare/v0.1.3...v0.2.0) (2018-04-18) 93 | 94 | 95 | ### Features 96 | 97 | * **pkg.bin:** support for binaries defined as string rather than object ([#6](https://github.com/nicojs/node-link-parent-bin/issues/6)) ([123c817](https://github.com/nicojs/node-link-parent-bin/commit/123c817)) 98 | 99 | 100 | 101 | 102 | ## [0.1.3](https://github.com/nicojs/node-link-parent-bin/compare/v0.1.2...v0.1.3) (2017-08-11) 103 | 104 | 105 | ### Features 106 | 107 | * **link:** Ignore EEXIST error ([020a7e9](https://github.com/nicojs/node-link-parent-bin/commit/020a7e9)) 108 | 109 | 110 | 111 | 112 | ## [0.1.2](https://github.com/nicojs/node-link-parent-bin/compare/v0.1.1...v0.1.2) (2017-06-15) 113 | 114 | 115 | ### Features 116 | 117 | * **local-dependencies:** Support local dependencies ([44731e8](https://github.com/nicojs/node-link-parent-bin/commit/44731e8)) 118 | 119 | 120 | 121 | 122 | ## [0.1.1](https://github.com/nicojs/node-link-parent-bin/compare/v0.1.0...v0.1.1) (2017-04-14) 123 | 124 | 125 | 126 | 127 | # 0.1.0 (2017-04-13) 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /test/unit/link.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import os from 'os'; 3 | import sinon from 'sinon'; 4 | import { promises as fs } from 'fs'; 5 | import { expect } from 'chai'; 6 | import { FSUtils } from './../../src/FSUtils'; 7 | import * as link from '../../src/link'; 8 | import log4js from 'log4js'; 9 | import cmdShim from 'cmd-shim'; 10 | import { createLoggerMock } from '../helpers/createLogStub'; 11 | 12 | describe('link', () => { 13 | let platform: sinon.SinonStub; 14 | let symlink: sinon.SinonStub; 15 | let stat: sinon.SinonStub; 16 | let cmdShimIfExist: sinon.SinonStub; 17 | let logStub: sinon.SinonStubbedInstance; 18 | 19 | beforeEach(() => { 20 | symlink = sinon.stub(fs, 'symlink'); 21 | stat = sinon.stub(fs, 'stat'); 22 | cmdShimIfExist = sinon.stub(cmdShim, 'ifExists'); 23 | sinon.stub(FSUtils, 'mkdirp').resolves(undefined); 24 | platform = sinon.stub(os, 'platform'); 25 | logStub = createLoggerMock(); 26 | sinon.stub(log4js, 'getLogger').returns(logStub); 27 | }); 28 | 29 | describe('when platform !== win32', () => { 30 | beforeEach(() => platform.returns('not win32')); 31 | 32 | it('should symlink', async () => { 33 | stat.rejects(); 34 | const to = 'some/path'; 35 | const expectedResult: link.LinkResult = { status: 'success' }; 36 | const actualResult = await link.link(path.resolve('package.json'), to); 37 | expect(actualResult).deep.eq(expectedResult); 38 | expect(fs.symlink).to.have.been.calledWith( 39 | path.normalize('../package.json'), 40 | path.resolve(to), 41 | 'junction', 42 | ); 43 | expect(cmdShimIfExist).not.to.have.been.called; 44 | }); 45 | 46 | it('should reject when `symlink` rejects', () => { 47 | stat.rejects(); 48 | const err = new Error('some error'); 49 | symlink.rejects(err); 50 | return expect( 51 | link.link(path.resolve('package.json'), 'some/link'), 52 | ).to.be.rejectedWith(err); 53 | }); 54 | 55 | it('should not symlink when a different link `to` already exists', async () => { 56 | stat.resolves(); 57 | const to = path.resolve('package.json'); 58 | const from = to; 59 | sinon.stub(fs, 'readlink').resolves('something else'); 60 | const expectedResult: link.LinkResult = { 61 | status: 'differentLinkAlreadyExists', 62 | }; 63 | const actualResult = await link.link(from, to); 64 | expect(actualResult).deep.eq(expectedResult); 65 | expect(fs.symlink).not.called; 66 | expect(logStub.debug).calledWith( 67 | `Different link at '${from}' to '${path.resolve( 68 | 'something else', 69 | )}' already exists. Leaving it alone, the package is probably already installed in the child package.`, 70 | ); 71 | }); 72 | 73 | it('should not symlink when a same link `to` already exists', async () => { 74 | stat.resolves(); 75 | const to = path.resolve('package.json'); 76 | const from = to; 77 | sinon.stub(fs, 'readlink').resolves(to); 78 | const expectedResult: link.LinkResult = { 79 | status: 'alreadyExists', 80 | }; 81 | const actualResult = await link.link(from, to); 82 | expect(actualResult).deep.eq(expectedResult); 83 | expect(fs.symlink).not.called; 84 | expect(logStub.debug).calledWith( 85 | `Link at '${path.resolve(to)}' already exists.`, 86 | ); 87 | }); 88 | }); 89 | 90 | describe('when platform === win32', () => { 91 | beforeEach(() => platform.returns('win32')); 92 | 93 | it('should `cmdShim`', async () => { 94 | // Arrange 95 | stat.rejects(); 96 | const to = 'some/path'; 97 | const from = path.resolve('package.json'); 98 | cmdShimIfExist.resolves(); 99 | const expectedResult: link.LinkResult = { status: 'success' }; 100 | 101 | // Act 102 | const actualResult = await link.link(from, to); 103 | expect(actualResult).deep.eq(expectedResult); 104 | 105 | // Assert 106 | expect(fs.symlink).not.to.have.been.called; 107 | expect(cmdShimIfExist).calledWith(from, to); 108 | }); 109 | 110 | it('should reject when `cmdShim` errors', () => { 111 | // Arrange 112 | stat.rejects(); 113 | const to = 'some/path'; 114 | const from = path.resolve('package.json'); 115 | const err = new Error('some error'); 116 | cmdShimIfExist.rejects(err); 117 | 118 | // Act 119 | const linkingPromise = link.link(from, to); 120 | 121 | // Assert 122 | return expect(linkingPromise).rejectedWith(err); 123 | }); 124 | 125 | it('should not create a cmdShim if it already exists', async () => { 126 | // Arrange 127 | stat.resolves(); // 'to' exists 128 | const to = 'some/path'; 129 | const from = path.resolve('package.json'); 130 | const expectedResult: link.LinkResult = { status: 'alreadyExists' }; 131 | 132 | // Act 133 | const actualResult = await link.link(from, to); 134 | expect(actualResult).deep.eq(expectedResult); 135 | 136 | // Assert 137 | expect(fs.symlink).not.called; 138 | expect(cmdShimIfExist).not.called; 139 | expect(logStub.debug).calledWith( 140 | `Link at '${to}' already exists. Leaving it alone.`, 141 | ); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/ParentBinLinker.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path from 'path'; 3 | import * as log4js from 'log4js'; 4 | import * as link from './link'; 5 | import * as minimatch from 'minimatch'; 6 | import { Options } from './program'; 7 | import { FSUtils } from './FSUtils'; 8 | 9 | export interface Dictionary { 10 | [name: string]: string; 11 | } 12 | 13 | export interface PackageJson { 14 | name?: string; 15 | bin?: Dictionary | string; 16 | devDependencies?: Dictionary; 17 | dependencies?: Dictionary; 18 | localDependencies?: Dictionary; 19 | } 20 | 21 | export class ParentBinLinker { 22 | public log: log4js.Logger; 23 | 24 | constructor(private options: Options) { 25 | this.log = log4js.getLogger('ParentBinLinker'); 26 | } 27 | 28 | private async linkBin( 29 | binName: string, 30 | from: string, 31 | childPackage: string, 32 | ): Promise { 33 | const to = path.join( 34 | this.options.childDirectoryRoot, 35 | childPackage, 36 | 'node_modules', 37 | '.bin', 38 | binName, 39 | ); 40 | this.log.debug('Creating link at %s for command at %s', to, from); 41 | return link.link(from, to); 42 | } 43 | 44 | private async linkBinsOfDependencies( 45 | childPackages: string[], 46 | dependenciesToLink: string[], 47 | ): Promise { 48 | if (this.log.isInfoEnabled()) { 49 | this.log.info( 50 | `Linking dependencies ${JSON.stringify( 51 | dependenciesToLink, 52 | )} under children ${JSON.stringify(childPackages)}`, 53 | ); 54 | } 55 | 56 | const results = await Promise.all( 57 | dependenciesToLink.map(async (dependency) => { 58 | const moduleDir = path.join('node_modules', dependency); 59 | const packageFile = path.join( 60 | 'node_modules', 61 | dependency, 62 | 'package.json', 63 | ); 64 | try { 65 | const content = await fs.readFile(packageFile); 66 | const pkg: PackageJson = JSON.parse(content.toString()); 67 | if (pkg.bin) { 68 | const binaries = this.binariesFrom(pkg, pkg.bin); 69 | const linkResultArrays = await Promise.all( 70 | Object.keys(binaries).map((bin) => 71 | Promise.all( 72 | childPackages.map((childPackage) => 73 | this.linkBin( 74 | bin, 75 | path.resolve(moduleDir, binaries[bin]), 76 | childPackage, 77 | ).catch((err) => { 78 | this.log.error( 79 | `Could not link bin ${bin} for child ${childPackage}.`, 80 | err, 81 | ); 82 | const result: link.LinkResult = { status: 'error' }; 83 | return result; 84 | }), 85 | ), 86 | ), 87 | ), 88 | ); 89 | return flatten(linkResultArrays); 90 | } else { 91 | this.log.debug( 92 | 'Did not find a bin in dependency %s, skipping.', 93 | dependency, 94 | ); 95 | return []; 96 | } 97 | } catch (err) { 98 | this.log.error(`Could not read ${packageFile}`, err); 99 | return []; 100 | } 101 | }), 102 | ); 103 | return flatten(results); 104 | } 105 | 106 | public async linkBinsToChildren(): Promise { 107 | const [contents, childPackages] = await Promise.all([ 108 | fs.readFile('package.json'), 109 | FSUtils.readDirs(this.options.childDirectoryRoot).then((dirs) => 110 | dirs.filter(minimatch.filter(this.options.filter)), 111 | ), 112 | ]); 113 | const pkg: PackageJson = JSON.parse(contents.toString()); 114 | const allPromises: Promise[] = []; 115 | if (pkg.devDependencies && this.options.linkDevDependencies) { 116 | allPromises.push( 117 | this.linkBinsOfDependencies( 118 | childPackages, 119 | Object.keys(pkg.devDependencies), 120 | ), 121 | ); 122 | } 123 | if (pkg.dependencies && this.options.linkDependencies) { 124 | allPromises.push( 125 | this.linkBinsOfDependencies( 126 | childPackages, 127 | Object.keys(pkg.dependencies), 128 | ), 129 | ); 130 | } 131 | if (pkg.localDependencies && this.options.linkLocalDependencies) { 132 | allPromises.push( 133 | this.linkBinsOfDependencies( 134 | childPackages, 135 | Object.keys(pkg.localDependencies), 136 | ), 137 | ); 138 | } 139 | const resultArrays = await Promise.all(allPromises); 140 | const results = flatten(resultArrays); 141 | const { 142 | successCount, 143 | differentLinkAlreadyExistsCount, 144 | alreadyExistsCount, 145 | errorCount, 146 | } = summary(results); 147 | this.log.info( 148 | `Symlinked ${successCount} bin(s) (${alreadyExistsCount} link(s) already exists, ${differentLinkAlreadyExistsCount} different link(s) already exists, ${errorCount} error(s)). Run with debug log level for more info.`, 149 | ); 150 | return results; 151 | } 152 | 153 | private binariesFrom( 154 | pkg: { name?: string }, 155 | bin: Dictionary | string, 156 | ): Dictionary { 157 | return typeof bin === 'string' ? { [pkg.name ?? '']: bin } : bin; 158 | } 159 | } 160 | 161 | function summary(linkResults: link.LinkResult[]) { 162 | let successCount = 0; 163 | let errorCount = 0; 164 | let alreadyExistsCount = 0; 165 | let differentLinkAlreadyExistsCount = 0; 166 | for (const { status } of linkResults) { 167 | switch (status) { 168 | case 'success': 169 | successCount++; 170 | break; 171 | case 'alreadyExists': 172 | alreadyExistsCount++; 173 | break; 174 | case 'differentLinkAlreadyExists': 175 | differentLinkAlreadyExistsCount++; 176 | break; 177 | case 'error': 178 | errorCount++; 179 | break; 180 | } 181 | } 182 | return { 183 | successCount, 184 | errorCount, 185 | alreadyExistsCount, 186 | differentLinkAlreadyExistsCount, 187 | }; 188 | } 189 | 190 | function flatten(arrayOfArrays: T[][]): T[] { 191 | return arrayOfArrays.reduce((result, arr) => [...result, ...arr], []); 192 | } 193 | -------------------------------------------------------------------------------- /test/unit/ParentBinLinker.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { promises as fs } from 'fs'; 3 | import { expect } from 'chai'; 4 | import log4js, { Logger } from 'log4js'; 5 | import sinon from 'sinon'; 6 | import { Options } from './../../src/program'; 7 | import { FSUtils } from './../../src/FSUtils'; 8 | import * as link from './../../src/link'; 9 | import { ParentBinLinker, PackageJson } from './../../src/ParentBinLinker'; 10 | import { createLoggerMock } from '../helpers/createLogStub'; 11 | 12 | describe('ParentBinLinker', () => { 13 | let readFileStub: sinon.SinonStub; 14 | let linkStub: sinon.SinonStub; 15 | let readDirsStub: sinon.SinonStub; 16 | let sut: ParentBinLinker; 17 | let options: Options; 18 | let loggerMock: sinon.SinonStubbedInstance; 19 | 20 | beforeEach(() => { 21 | loggerMock = createLoggerMock(); 22 | sinon.stub(log4js, 'getLogger').returns(loggerMock); 23 | options = { 24 | linkDependencies: false, 25 | linkDevDependencies: true, 26 | linkLocalDependencies: false, 27 | logLevel: 'info', 28 | childDirectoryRoot: 'packages', 29 | filter: 'child-*', 30 | }; 31 | sut = new ParentBinLinker(options); 32 | readDirsStub = sinon.stub(FSUtils, 'readDirs'); 33 | readFileStub = sinon.stub(fs, 'readFile'); 34 | linkStub = sinon.stub(link, 'link'); 35 | linkStub.resolves({ status: 'success' } as link.LinkResult); 36 | }); 37 | 38 | describe('linkBinsToChildren', () => { 39 | it('should reject when `readFile` rejects', () => { 40 | const error = new Error('some error'); 41 | readDirsStub.resolves([]); 42 | readFileStub.rejects(error); 43 | return expect(sut.linkBinsToChildren()).to.rejectedWith(error); 44 | }); 45 | 46 | it('should reject when `readDir` rejects', () => { 47 | const error = new Error('some error'); 48 | readDirsStub.rejects(error); 49 | readFileStub.resolves(undefined); 50 | return expect(sut.linkBinsToChildren()).to.rejectedWith(error); 51 | }); 52 | 53 | describe('with 2 children and multiple dependencies with and without bins', () => { 54 | const parentPkg: PackageJson = { 55 | devDependencies: { 'devDep-1': 'x', 'devDep-2': 'x' }, 56 | dependencies: { 'dep-1': 'x', 'dep-2': 'x' }, 57 | }; 58 | const devDepPkg: PackageJson = { 59 | bin: { devDep: 'devDep.sh', devDepAwesome: 'devDepAwesome.sh' }, 60 | }; 61 | const depPkg: PackageJson = { 62 | bin: { dep: 'dep.sh' }, 63 | }; 64 | 65 | beforeEach(() => { 66 | readFileStub 67 | .resolves('{}') 68 | .withArgs('package.json') 69 | .resolves(JSON.stringify(parentPkg)) 70 | .withArgs(path.join('node_modules', 'devDep-1', 'package.json')) 71 | .resolves(JSON.stringify(devDepPkg)) 72 | .withArgs(path.join('node_modules', 'dep-1', 'package.json')) 73 | .resolves(JSON.stringify(depPkg)); 74 | readDirsStub.withArgs('packages').resolves(['child-1', 'child-2']); 75 | readDirsStub 76 | .withArgs('alternativeChildHostingDir') 77 | .resolves(['child-3']); 78 | }); 79 | 80 | it('should link only devDependencies', async () => { 81 | const devDepSH = path.resolve('node_modules', 'devDep-1', 'devDep.sh'); 82 | const devDepAwesomeSH = path.resolve( 83 | 'node_modules', 84 | 'devDep-1', 85 | 'devDepAwesome.sh', 86 | ); 87 | await sut.linkBinsToChildren(); 88 | expect(linkStub).callCount(4); 89 | expect(linkStub).calledWith( 90 | devDepSH, 91 | path.join('packages', 'child-1', 'node_modules', '.bin', 'devDep'), 92 | ); 93 | expect(linkStub).calledWith( 94 | devDepSH, 95 | path.join('packages', 'child-2', 'node_modules', '.bin', 'devDep'), 96 | ); 97 | expect(linkStub).calledWith( 98 | devDepAwesomeSH, 99 | path.join( 100 | 'packages', 101 | 'child-1', 102 | 'node_modules', 103 | '.bin', 104 | 'devDepAwesome', 105 | ), 106 | ); 107 | expect(linkStub).calledWith( 108 | devDepAwesomeSH, 109 | path.join( 110 | 'packages', 111 | 'child-2', 112 | 'node_modules', 113 | '.bin', 114 | 'devDepAwesome', 115 | ), 116 | ); 117 | }); 118 | 119 | it('should link dependencies if that is configured', async () => { 120 | // Arrange 121 | options.linkDependencies = true; 122 | options.linkDevDependencies = false; 123 | const depSH = path.resolve('node_modules', 'dep-1', 'dep.sh'); 124 | 125 | // Act 126 | await sut.linkBinsToChildren(); 127 | 128 | // Assert 129 | expect(linkStub).callCount(2); 130 | expect(linkStub).calledWith( 131 | depSH, 132 | path.join('packages', 'child-1', 'node_modules', '.bin', 'dep'), 133 | ); 134 | expect(linkStub).calledWith( 135 | depSH, 136 | path.join('packages', 'child-2', 'node_modules', '.bin', 'dep'), 137 | ); 138 | }); 139 | 140 | it('should log an error if linking is rejected', async () => { 141 | const err = new Error('some error'); 142 | linkStub.rejects(err); 143 | await sut.linkBinsToChildren(); 144 | expect(loggerMock.error).calledWith( 145 | 'Could not link bin devDep for child child-1.', 146 | err, 147 | ); 148 | expect(loggerMock.error).callCount(4); 149 | }); 150 | 151 | it('should use a different child dir if configures', async () => { 152 | options.childDirectoryRoot = 'alternativeChildHostingDir'; 153 | const depSH = path.resolve('node_modules', 'devDep-1', 'devDep.sh'); 154 | await sut.linkBinsToChildren(); 155 | expect(linkStub).calledWith( 156 | depSH, 157 | path.join( 158 | 'alternativeChildHostingDir', 159 | 'child-3', 160 | 'node_modules', 161 | '.bin', 162 | 'devDep', 163 | ), 164 | ); 165 | }); 166 | 167 | it('should log a summary on info', async () => { 168 | options.linkDependencies = true; 169 | options.linkDevDependencies = true; 170 | const results: link.LinkResult[] = [ 171 | { status: 'alreadyExists' }, 172 | { status: 'differentLinkAlreadyExists' }, 173 | { status: 'success' }, 174 | ]; 175 | 176 | linkStub.onFirstCall().returns(results[0]); 177 | linkStub.onSecondCall().returns(results[1]); 178 | linkStub.onThirdCall().returns(results[2]); 179 | 180 | await sut.linkBinsToChildren(); 181 | 182 | expect(loggerMock.info).calledWith( 183 | 'Symlinked 4 bin(s) (1 link(s) already exists, 1 different link(s) already exists, 0 error(s)). Run with debug log level for more info.', 184 | ); 185 | }); 186 | 187 | it('should only symlink in filtered packages', async () => { 188 | options.filter = 'child-1'; 189 | const devDepSH = path.resolve('node_modules', 'devDep-1', 'devDep.sh'); 190 | const devDepAwesomeSH = path.resolve( 191 | 'node_modules', 192 | 'devDep-1', 193 | 'devDepAwesome.sh', 194 | ); 195 | 196 | await sut.linkBinsToChildren(); 197 | expect(linkStub).calledTwice; 198 | expect(linkStub).calledWith( 199 | devDepSH, 200 | path.join('packages', 'child-1', 'node_modules', '.bin', 'devDep'), 201 | ); 202 | 203 | expect(linkStub).calledWith( 204 | devDepAwesomeSH, 205 | path.join( 206 | 'packages', 207 | 'child-1', 208 | 'node_modules', 209 | '.bin', 210 | 'devDepAwesome', 211 | ), 212 | ); 213 | }); 214 | }); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /sample/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sample", 9 | "version": "0.0.0", 10 | "hasInstallScript": true, 11 | "dependencies": { 12 | "@hello-org/core": "file:./dependencies/hello-org", 13 | "fail-dependency": "file:./dependencies/fail-dependency", 14 | "hello-dependency": "file:./dependencies/hello-dependency", 15 | "hello-single-binary-dependency": "file:./dependencies/hello-single-binary-dependency" 16 | }, 17 | "devDependencies": { 18 | "hello-dev-dependency": "file:./dependencies/hello-dev-dependency", 19 | "install-local": "^3.0.1" 20 | } 21 | }, 22 | "dependencies/fail-dependency": { 23 | "version": "0.0.0", 24 | "bin": { 25 | "fail-now": "fail-now" 26 | } 27 | }, 28 | "dependencies/hello-dependency": { 29 | "version": "0.0.0", 30 | "bin": { 31 | "helloDependency": "hello-dependency" 32 | } 33 | }, 34 | "dependencies/hello-dev-dependency": { 35 | "version": "0.0.0", 36 | "dev": true, 37 | "bin": { 38 | "helloDevDependency": "hello-dev-dependency" 39 | } 40 | }, 41 | "dependencies/hello-org": { 42 | "name": "@hello-org/core", 43 | "version": "0.0.0", 44 | "bin": { 45 | "hello-org": "hello-org" 46 | } 47 | }, 48 | "dependencies/hello-single-binary-dependency": { 49 | "version": "0.0.0", 50 | "bin": { 51 | "hello-single-binary-dependency": "hello-single-binary-dependency" 52 | } 53 | }, 54 | "node_modules/@hello-org/core": { 55 | "resolved": "dependencies/hello-org", 56 | "link": true 57 | }, 58 | "node_modules/balanced-match": { 59 | "version": "1.0.0", 60 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 61 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 62 | "dev": true 63 | }, 64 | "node_modules/brace-expansion": { 65 | "version": "1.1.11", 66 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 67 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 68 | "dev": true, 69 | "dependencies": { 70 | "balanced-match": "^1.0.0", 71 | "concat-map": "0.0.1" 72 | } 73 | }, 74 | "node_modules/concat-map": { 75 | "version": "0.0.1", 76 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 77 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 78 | "dev": true 79 | }, 80 | "node_modules/cross-spawn": { 81 | "version": "7.0.3", 82 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 83 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 84 | "dev": true, 85 | "dependencies": { 86 | "path-key": "^3.1.0", 87 | "shebang-command": "^2.0.0", 88 | "which": "^2.0.1" 89 | }, 90 | "engines": { 91 | "node": ">= 8" 92 | } 93 | }, 94 | "node_modules/end-of-stream": { 95 | "version": "1.4.4", 96 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 97 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 98 | "dev": true, 99 | "dependencies": { 100 | "once": "^1.4.0" 101 | } 102 | }, 103 | "node_modules/execa": { 104 | "version": "4.0.3", 105 | "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", 106 | "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", 107 | "dev": true, 108 | "dependencies": { 109 | "cross-spawn": "^7.0.0", 110 | "get-stream": "^5.0.0", 111 | "human-signals": "^1.1.1", 112 | "is-stream": "^2.0.0", 113 | "merge-stream": "^2.0.0", 114 | "npm-run-path": "^4.0.0", 115 | "onetime": "^5.1.0", 116 | "signal-exit": "^3.0.2", 117 | "strip-final-newline": "^2.0.0" 118 | }, 119 | "engines": { 120 | "node": ">=10" 121 | }, 122 | "funding": { 123 | "url": "https://github.com/sindresorhus/execa?sponsor=1" 124 | } 125 | }, 126 | "node_modules/fail-dependency": { 127 | "resolved": "dependencies/fail-dependency", 128 | "link": true 129 | }, 130 | "node_modules/fs.realpath": { 131 | "version": "1.0.0", 132 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 133 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 134 | "dev": true 135 | }, 136 | "node_modules/get-stream": { 137 | "version": "5.2.0", 138 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 139 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 140 | "dev": true, 141 | "dependencies": { 142 | "pump": "^3.0.0" 143 | }, 144 | "engines": { 145 | "node": ">=8" 146 | }, 147 | "funding": { 148 | "url": "https://github.com/sponsors/sindresorhus" 149 | } 150 | }, 151 | "node_modules/glob": { 152 | "version": "7.1.6", 153 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 154 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 155 | "dev": true, 156 | "dependencies": { 157 | "fs.realpath": "^1.0.0", 158 | "inflight": "^1.0.4", 159 | "inherits": "2", 160 | "minimatch": "^3.0.4", 161 | "once": "^1.3.0", 162 | "path-is-absolute": "^1.0.0" 163 | }, 164 | "engines": { 165 | "node": "*" 166 | }, 167 | "funding": { 168 | "url": "https://github.com/sponsors/isaacs" 169 | } 170 | }, 171 | "node_modules/hello-dependency": { 172 | "resolved": "dependencies/hello-dependency", 173 | "link": true 174 | }, 175 | "node_modules/hello-dev-dependency": { 176 | "resolved": "dependencies/hello-dev-dependency", 177 | "link": true 178 | }, 179 | "node_modules/hello-single-binary-dependency": { 180 | "resolved": "dependencies/hello-single-binary-dependency", 181 | "link": true 182 | }, 183 | "node_modules/human-signals": { 184 | "version": "1.1.1", 185 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", 186 | "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", 187 | "dev": true, 188 | "engines": { 189 | "node": ">=8.12.0" 190 | } 191 | }, 192 | "node_modules/inflight": { 193 | "version": "1.0.6", 194 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 195 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 196 | "dev": true, 197 | "dependencies": { 198 | "once": "^1.3.0", 199 | "wrappy": "1" 200 | } 201 | }, 202 | "node_modules/inherits": { 203 | "version": "2.0.4", 204 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 205 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 206 | "dev": true 207 | }, 208 | "node_modules/install-local": { 209 | "version": "3.0.1", 210 | "resolved": "https://registry.npmjs.org/install-local/-/install-local-3.0.1.tgz", 211 | "integrity": "sha512-f5Njj3ZeD/HxaDkjDBZCLYks5XSd4Ps4I/nA2UFyYhM7fWRsdkVnfIoncxWqtUrbOZOZhvjOrtbrPunw2fWoAg==", 212 | "dev": true, 213 | "dependencies": { 214 | "execa": "^4.0.3", 215 | "lodash.flatmap": "^4.5.0", 216 | "rimraf": "^3.0.2", 217 | "uniqid": "^5.2.0" 218 | }, 219 | "bin": { 220 | "install-local": "bin/install-local" 221 | }, 222 | "engines": { 223 | "node": ">=10" 224 | } 225 | }, 226 | "node_modules/is-stream": { 227 | "version": "2.0.0", 228 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 229 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", 230 | "dev": true, 231 | "engines": { 232 | "node": ">=8" 233 | } 234 | }, 235 | "node_modules/isexe": { 236 | "version": "2.0.0", 237 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 238 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 239 | "dev": true 240 | }, 241 | "node_modules/lodash.flatmap": { 242 | "version": "4.5.0", 243 | "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", 244 | "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=", 245 | "dev": true 246 | }, 247 | "node_modules/merge-stream": { 248 | "version": "2.0.0", 249 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 250 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 251 | "dev": true 252 | }, 253 | "node_modules/mimic-fn": { 254 | "version": "2.1.0", 255 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 256 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 257 | "dev": true, 258 | "engines": { 259 | "node": ">=6" 260 | } 261 | }, 262 | "node_modules/minimatch": { 263 | "version": "3.0.4", 264 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 265 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 266 | "dev": true, 267 | "dependencies": { 268 | "brace-expansion": "^1.1.7" 269 | }, 270 | "engines": { 271 | "node": "*" 272 | } 273 | }, 274 | "node_modules/npm-run-path": { 275 | "version": "4.0.1", 276 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 277 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 278 | "dev": true, 279 | "dependencies": { 280 | "path-key": "^3.0.0" 281 | }, 282 | "engines": { 283 | "node": ">=8" 284 | } 285 | }, 286 | "node_modules/once": { 287 | "version": "1.4.0", 288 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 289 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 290 | "dev": true, 291 | "dependencies": { 292 | "wrappy": "1" 293 | } 294 | }, 295 | "node_modules/onetime": { 296 | "version": "5.1.2", 297 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 298 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 299 | "dev": true, 300 | "dependencies": { 301 | "mimic-fn": "^2.1.0" 302 | }, 303 | "engines": { 304 | "node": ">=6" 305 | }, 306 | "funding": { 307 | "url": "https://github.com/sponsors/sindresorhus" 308 | } 309 | }, 310 | "node_modules/path-is-absolute": { 311 | "version": "1.0.1", 312 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 313 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 314 | "dev": true, 315 | "engines": { 316 | "node": ">=0.10.0" 317 | } 318 | }, 319 | "node_modules/path-key": { 320 | "version": "3.1.1", 321 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 322 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 323 | "dev": true, 324 | "engines": { 325 | "node": ">=8" 326 | } 327 | }, 328 | "node_modules/pump": { 329 | "version": "3.0.0", 330 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 331 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 332 | "dev": true, 333 | "dependencies": { 334 | "end-of-stream": "^1.1.0", 335 | "once": "^1.3.1" 336 | } 337 | }, 338 | "node_modules/rimraf": { 339 | "version": "3.0.2", 340 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 341 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 342 | "dev": true, 343 | "dependencies": { 344 | "glob": "^7.1.3" 345 | }, 346 | "bin": { 347 | "rimraf": "bin.js" 348 | }, 349 | "funding": { 350 | "url": "https://github.com/sponsors/isaacs" 351 | } 352 | }, 353 | "node_modules/shebang-command": { 354 | "version": "2.0.0", 355 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 356 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 357 | "dev": true, 358 | "dependencies": { 359 | "shebang-regex": "^3.0.0" 360 | }, 361 | "engines": { 362 | "node": ">=8" 363 | } 364 | }, 365 | "node_modules/shebang-regex": { 366 | "version": "3.0.0", 367 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 368 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 369 | "dev": true, 370 | "engines": { 371 | "node": ">=8" 372 | } 373 | }, 374 | "node_modules/signal-exit": { 375 | "version": "3.0.3", 376 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 377 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 378 | "dev": true 379 | }, 380 | "node_modules/strip-final-newline": { 381 | "version": "2.0.0", 382 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 383 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", 384 | "dev": true, 385 | "engines": { 386 | "node": ">=6" 387 | } 388 | }, 389 | "node_modules/uniqid": { 390 | "version": "5.2.0", 391 | "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.2.0.tgz", 392 | "integrity": "sha512-LH8zsvwJ/GL6YtNfSOmMCrI9piraAUjBfw2MCvleNE6a4pVKJwXjG2+HWhkVeFcSg+nmaPKbMrMOoxwQluZ1Mg==", 393 | "dev": true 394 | }, 395 | "node_modules/which": { 396 | "version": "2.0.2", 397 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 398 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 399 | "dev": true, 400 | "dependencies": { 401 | "isexe": "^2.0.0" 402 | }, 403 | "bin": { 404 | "node-which": "bin/node-which" 405 | }, 406 | "engines": { 407 | "node": ">= 8" 408 | } 409 | }, 410 | "node_modules/wrappy": { 411 | "version": "1.0.2", 412 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 413 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 414 | "dev": true 415 | } 416 | }, 417 | "dependencies": { 418 | "@hello-org/core": { 419 | "version": "file:dependencies/hello-org" 420 | }, 421 | "balanced-match": { 422 | "version": "1.0.0", 423 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 424 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 425 | "dev": true 426 | }, 427 | "brace-expansion": { 428 | "version": "1.1.11", 429 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 430 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 431 | "dev": true, 432 | "requires": { 433 | "balanced-match": "^1.0.0", 434 | "concat-map": "0.0.1" 435 | } 436 | }, 437 | "concat-map": { 438 | "version": "0.0.1", 439 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 440 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 441 | "dev": true 442 | }, 443 | "cross-spawn": { 444 | "version": "7.0.3", 445 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 446 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 447 | "dev": true, 448 | "requires": { 449 | "path-key": "^3.1.0", 450 | "shebang-command": "^2.0.0", 451 | "which": "^2.0.1" 452 | } 453 | }, 454 | "end-of-stream": { 455 | "version": "1.4.4", 456 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 457 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 458 | "dev": true, 459 | "requires": { 460 | "once": "^1.4.0" 461 | } 462 | }, 463 | "execa": { 464 | "version": "4.0.3", 465 | "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", 466 | "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", 467 | "dev": true, 468 | "requires": { 469 | "cross-spawn": "^7.0.0", 470 | "get-stream": "^5.0.0", 471 | "human-signals": "^1.1.1", 472 | "is-stream": "^2.0.0", 473 | "merge-stream": "^2.0.0", 474 | "npm-run-path": "^4.0.0", 475 | "onetime": "^5.1.0", 476 | "signal-exit": "^3.0.2", 477 | "strip-final-newline": "^2.0.0" 478 | } 479 | }, 480 | "fail-dependency": { 481 | "version": "file:dependencies/fail-dependency" 482 | }, 483 | "fs.realpath": { 484 | "version": "1.0.0", 485 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 486 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 487 | "dev": true 488 | }, 489 | "get-stream": { 490 | "version": "5.2.0", 491 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 492 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 493 | "dev": true, 494 | "requires": { 495 | "pump": "^3.0.0" 496 | } 497 | }, 498 | "glob": { 499 | "version": "7.1.6", 500 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 501 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 502 | "dev": true, 503 | "requires": { 504 | "fs.realpath": "^1.0.0", 505 | "inflight": "^1.0.4", 506 | "inherits": "2", 507 | "minimatch": "^3.0.4", 508 | "once": "^1.3.0", 509 | "path-is-absolute": "^1.0.0" 510 | } 511 | }, 512 | "hello-dependency": { 513 | "version": "file:dependencies/hello-dependency" 514 | }, 515 | "hello-dev-dependency": { 516 | "version": "file:dependencies/hello-dev-dependency" 517 | }, 518 | "hello-single-binary-dependency": { 519 | "version": "file:dependencies/hello-single-binary-dependency" 520 | }, 521 | "human-signals": { 522 | "version": "1.1.1", 523 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", 524 | "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", 525 | "dev": true 526 | }, 527 | "inflight": { 528 | "version": "1.0.6", 529 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 530 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 531 | "dev": true, 532 | "requires": { 533 | "once": "^1.3.0", 534 | "wrappy": "1" 535 | } 536 | }, 537 | "inherits": { 538 | "version": "2.0.4", 539 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 540 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 541 | "dev": true 542 | }, 543 | "install-local": { 544 | "version": "3.0.1", 545 | "resolved": "https://registry.npmjs.org/install-local/-/install-local-3.0.1.tgz", 546 | "integrity": "sha512-f5Njj3ZeD/HxaDkjDBZCLYks5XSd4Ps4I/nA2UFyYhM7fWRsdkVnfIoncxWqtUrbOZOZhvjOrtbrPunw2fWoAg==", 547 | "dev": true, 548 | "requires": { 549 | "execa": "^4.0.3", 550 | "lodash.flatmap": "^4.5.0", 551 | "rimraf": "^3.0.2", 552 | "uniqid": "^5.2.0" 553 | } 554 | }, 555 | "is-stream": { 556 | "version": "2.0.0", 557 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 558 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", 559 | "dev": true 560 | }, 561 | "isexe": { 562 | "version": "2.0.0", 563 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 564 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 565 | "dev": true 566 | }, 567 | "lodash.flatmap": { 568 | "version": "4.5.0", 569 | "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", 570 | "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=", 571 | "dev": true 572 | }, 573 | "merge-stream": { 574 | "version": "2.0.0", 575 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 576 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 577 | "dev": true 578 | }, 579 | "mimic-fn": { 580 | "version": "2.1.0", 581 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 582 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 583 | "dev": true 584 | }, 585 | "minimatch": { 586 | "version": "3.0.4", 587 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 588 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 589 | "dev": true, 590 | "requires": { 591 | "brace-expansion": "^1.1.7" 592 | } 593 | }, 594 | "npm-run-path": { 595 | "version": "4.0.1", 596 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 597 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 598 | "dev": true, 599 | "requires": { 600 | "path-key": "^3.0.0" 601 | } 602 | }, 603 | "once": { 604 | "version": "1.4.0", 605 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 606 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 607 | "dev": true, 608 | "requires": { 609 | "wrappy": "1" 610 | } 611 | }, 612 | "onetime": { 613 | "version": "5.1.2", 614 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 615 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 616 | "dev": true, 617 | "requires": { 618 | "mimic-fn": "^2.1.0" 619 | } 620 | }, 621 | "path-is-absolute": { 622 | "version": "1.0.1", 623 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 624 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 625 | "dev": true 626 | }, 627 | "path-key": { 628 | "version": "3.1.1", 629 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 630 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 631 | "dev": true 632 | }, 633 | "pump": { 634 | "version": "3.0.0", 635 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 636 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 637 | "dev": true, 638 | "requires": { 639 | "end-of-stream": "^1.1.0", 640 | "once": "^1.3.1" 641 | } 642 | }, 643 | "rimraf": { 644 | "version": "3.0.2", 645 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 646 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 647 | "dev": true, 648 | "requires": { 649 | "glob": "^7.1.3" 650 | } 651 | }, 652 | "shebang-command": { 653 | "version": "2.0.0", 654 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 655 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 656 | "dev": true, 657 | "requires": { 658 | "shebang-regex": "^3.0.0" 659 | } 660 | }, 661 | "shebang-regex": { 662 | "version": "3.0.0", 663 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 664 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 665 | "dev": true 666 | }, 667 | "signal-exit": { 668 | "version": "3.0.3", 669 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 670 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 671 | "dev": true 672 | }, 673 | "strip-final-newline": { 674 | "version": "2.0.0", 675 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 676 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", 677 | "dev": true 678 | }, 679 | "uniqid": { 680 | "version": "5.2.0", 681 | "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.2.0.tgz", 682 | "integrity": "sha512-LH8zsvwJ/GL6YtNfSOmMCrI9piraAUjBfw2MCvleNE6a4pVKJwXjG2+HWhkVeFcSg+nmaPKbMrMOoxwQluZ1Mg==", 683 | "dev": true 684 | }, 685 | "which": { 686 | "version": "2.0.2", 687 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 688 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 689 | "dev": true, 690 | "requires": { 691 | "isexe": "^2.0.0" 692 | } 693 | }, 694 | "wrappy": { 695 | "version": "1.0.2", 696 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 697 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 698 | "dev": true 699 | } 700 | } 701 | } 702 | --------------------------------------------------------------------------------