diff --git a/README.md b/README.md index fccc4138..ddfb2d9b 100644 --- a/README.md +++ b/README.md @@ -71,18 +71,29 @@ clark hoist clark hoist --package ``` -### Run a command each package directory +### Run a command in each package directory ```bash clark exec ``` +The following environment variables will be available to your script: + +* `CLARK_ROOT_PATH`: The monorepo's root path. +* `CLARK_PACKAGE_REL_PATH`: The relative path within the monorepo to the package currently being acted upon. +* `CLARK_PACKAGE_ABS_PATH`: The absolute path to the package currently being acted upon. +* `CLARK_PACKAGE_NAME`: The name of the package being acted upon according to its `package.json` + +The script will be invoked from within the package's directory. + ### Run a command in a single package directory ```bash clark exec --package ``` +The script will be invoked from within the package's directory. + ### Magic While Clark, obviously, provides its own commands, there's a set of very project specific commands that we simply can't dictate for you. These are commands like `build`, `lint`, and `test` that you want to run each independently against each package. @@ -99,6 +110,8 @@ clark init --script test='mocha test/*/spec/**/*.js' --script build='babel -d di Package commands are executed sequentially in each package directory. They may be overridden with an entry in the package's package.json. +> Note: magic scripts receive the same environment variables as `clark exec` and are executed within each package directory. + For example, your repository might use [mocha](https://mochajs.org/) to run your integration tests ```json diff --git a/src/lib/packages.ts b/src/lib/packages.ts index f6e84fa6..753fd61d 100644 --- a/src/lib/packages.ts +++ b/src/lib/packages.ts @@ -2,13 +2,25 @@ import debugFactory from 'debug'; import {sync as glob} from 'glob'; import {readFile, writeFile} from 'mz/fs'; import {dirname, resolve} from 'path'; -import {read as readRootPackage, write as writeRootPackage} from './project'; +import { + findProjectRoot, + read as readRootPackage, + write as writeRootPackage, +} from './project'; import {spawn} from './spawn'; const debug = debugFactory('clark:lib:packages'); const cwd = 'packages/node_modules'; +/** + * Finds the relative path to the specified package. + * @param packageName + */ +export async function findPackagePath(packageName: string): Promise { + return `./packages/node_modules/${packageName}`; +} + /** * Lists all packages in the monorepo */ @@ -202,11 +214,22 @@ export async function exec(cmd: string, packageName: string): Promise { const bin = 'bash'; const args = ['-c', cmd]; const {PATH, ...env} = process.env; + const clarkEnv = { + CLARK_PACKAGE_ABS_PATH: resolve( + await findProjectRoot(), + await findPackagePath(packageName), + ), + CLARK_PACKAGE_NAME: packageName, + CLARK_PACKAGE_REL_PATH: await findPackagePath(packageName), + CLARK_ROOT_PATH: await findProjectRoot(), + ...filterEnv(env), + }; + try { const result = await spawn(bin, args, { cwd: resolve(cwd, packageName), env: { - ...env, + ...clarkEnv, PATH: `${PATH}:${resolve(process.cwd(), 'node_modules', '.bin')}`, }, }); @@ -217,3 +240,22 @@ export async function exec(cmd: string, packageName: string): Promise { throw err; } } + +/** + * Removes any `CLARK_` prefixed variables from env before passing them to + * `spawn()`. + * @param env + */ +function filterEnv(env: object): object { + return Object.entries(env).reduce((acc, [key, value]) => { + if (!key.startsWith('CLARK_')) { + acc[key] = value; + } + + return acc; + }, {}); +} + +interface EnvObject { + [key: string]: string; +} diff --git a/test/integration/spec/exec.ts b/test/integration/spec/exec.ts index 7fb30d0b..df490387 100644 --- a/test/integration/spec/exec.ts +++ b/test/integration/spec/exec.ts @@ -1,7 +1,17 @@ import {assert} from 'chai'; import {resolve} from 'path'; - import run from '../lib/run'; + +function stringToObject(str: string) { + return str + .split('\n') + .map(row => row.split('=')) + .reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); +} + describe('exec', () => { it('executes a command in every directory', async () => { const result = await run('exec pwd'); @@ -41,6 +51,29 @@ describe('exec', () => { ); }); + it('injects useful environment variables', async () => { + const result = await run( + 'exec --package @example/scoped-package-the-first "env | grep CLARK"', + ); + // strip out this project's path so we can write consistent assertions + const modified = result.replace( + new RegExp(resolve(__dirname, '..', '..'), 'g'), + 'REPLACED', + ); + + // different versions of node unexpectedly impact the order of the results, + // so we need to do an object comparison instead of a string comparison. + + assert.deepEqual(stringToObject(modified), { + CLARK_ROOT_PATH: 'REPLACED/integration/fixtures/monorepo', + CLARK_PACKAGE_REL_PATH: + './packages/node_modules/@example/scoped-package-the-first', + CLARK_PACKAGE_ABS_PATH: + 'REPLACED/integration/fixtures/monorepo/packages/node_modules/@example/scoped-package-the-first', + CLARK_PACKAGE_NAME: '@example/scoped-package-the-first', + }); + }); + describe('with --package', () => { it('executes a comand in the specified package directory', async () => { const result = await run('exec --package-name not-scoped pwd');