Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nx-spawn): Improve dependency execution #60

Merged
merged 4 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Interested in development? Join the discussion on the Eterna Discord!
- [`@eternagame/eslint-plugin`](./packages/eslint-plugin) - Semi-opinionated ESLint configuration
- [`@eternagame/jest`](./packages/jest) - Opinionated Jest utilities
- [`@eternagame/vite`](./packages/vite) - Opinionated Vite configurations
- [`@eternagame/nx-spawn`](./packages/nx-spawn) - Run a command for all dependencies of a given package, without waiting for their completion
- [`@eternagame/nx-spawn`](./packages/nx-spawn) - Run an npm command with nx dependencies without waiting for them to finish
- [`@eternagame/distify`](./packages/distify) - Lightweight build step for static assets

## Setup
Expand Down
5,368 changes: 1,133 additions & 4,235 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions packages/bootstrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
"postpublish": "shx rm LICENSE",
"prebuild": "shx rm -rf dist",
"build": "vite build",
"postbuild": "shx chmod +x dist/index.js",
"lint": "eslint src/",
"serve": "nx build && nx-spawn npm:build-watch --extraRootCommand \"node-dev dist/index.js\""
"postbuild": "shx chmod +x dist/index.js && npm install --save=false",
"lint": "eslint src/"
},
"type": "module",
"main": "./dist/index.js",
Expand Down
6 changes: 2 additions & 4 deletions packages/distify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@
"postpublish": "shx rm LICENSE",
"prebuild": "shx rm -rf dist",
"build": "tsc -p tsconfig.build.json",
"postbuild": "shx chmod +x dist/index.js && npm install",
"postbuild": "shx chmod +x dist/index.js && npm install --save=false",
"lint": "eslint src/"
},
"bin": {
"distify": "./dist/index.js"
},
"bin": "./dist/index.js",
"dependencies": {
"fast-glob": "^3.2.11"
}
Expand Down
16 changes: 6 additions & 10 deletions packages/nx-spawn/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
# @eternagame/nx-spawn

Run a command for all dependencies of a given package, without waiting for their completion
Run an npm command with nx dependencies without waiting for them to finish

This package is heavily based on `nx/src/task-runner`, though significantly simplified for this use case
(eg, no caching, no match mode, assumes long running dependencies, etc)
luxaritas marked this conversation as resolved.
Show resolved Hide resolved

## Usage

```
nx-spawn <command> [--extraRootCommand <command>] [--noRoot]
nx-spawn <command>
```

The first parameter is the command that will be run in the folder of each package in the dependency
tree for the current package (ie, the one corresponding to the current working directory).
If extraRootCommand is present, that additional command will be run concurrently in the context
of the current package. If noRoot is true, don't run the command for the current package, only its dependencies.

Commands are run via [concurrently](https://www.npmjs.com/package/concurrently), so they may
be specified using compatible syntax (eg, a `serve` script that builds all dependencies in watch mode
and spins up a development server might look like `nx-spawn npm:build-watch npm:_serve`)
The package to run the command for will be determined by the current working directory
22 changes: 13 additions & 9 deletions packages/nx-spawn/package.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
{
"name": "@eternagame/nx-spawn",
"description": "Run a command for all dependencies of a given package, without waiting for their completion",
"version": "1.0.1",
"description": "Run an npm command with nx dependencies without waiting for them to finish",
"version": "1.0.0",
"license": "BSD-3-Clause",
"type": "module",
"scripts": {
"prepublishOnly": "shx cp ../../LICENSE . && nx build",
"postpublish": "shx rm LICENSE",
"prebuild": "shx rm -rf dist",
"build": "tsc -p tsconfig.build.json",
"postbuild": "shx chmod +x dist/index.js",
"build": "vite build",
"postbuild": "shx chmod +x dist/index.js && npm install --save=false",
"lint": "eslint src/"
},
"bin": {
"nx-spawn": "./dist/index.js"
},
"type": "module",
"main": "./dist/index.js",
"bin": "./dist/index.js",
"dependencies": {
"@nrwl/devkit": "^14.5.0",
"concurrently": "^7.3.0",
"chokidar": "^3.5.3",
"nx": "^14.5.0",
"yargs": "^17.5.1"
},
"nx": {
"implicitDependencies": [
"vite"
]
}
}
106 changes: 19 additions & 87 deletions packages/nx-spawn/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,39 @@
#!/usr/bin/env node
import { exit, cwd, argv } from 'process';
import { join } from 'path';
import {
createProjectGraphAsync,
workspaceRoot,
type ProjectGraph,
} from '@nrwl/devkit';
/* eslint-disable import/extensions */
import { Workspaces } from 'nx/src/config/workspaces.js';
/* eslint-enable import/extensions */
import concurrently from 'concurrently';
import { workspaceRoot, Workspaces } from '@nrwl/devkit';
import { argv, cwd, exit } from 'process';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

interface CommandInfo {
packageName: string;
cwd: string;
}

function computeDepCommands(
projectGraph: ProjectGraph,
parentPackage: string,
command: string
): CommandInfo[] {
const deps = projectGraph.dependencies[parentPackage];
if (!deps) return [];
return (
deps
// A dependency may or may not be one of our packages - if it is, projectGraph.nodes will include it
.filter((dep) => Object.keys(projectGraph.nodes).includes(dep.target))
.map((dep) => {
const data = projectGraph.nodes[dep.target]?.data as unknown;
if (typeof data !== 'object' || data === null)
throw new Error('Unable to determine dependency directory');
const { root } = data as Record<string, unknown>;
if (!root || typeof root !== 'string')
throw new Error(`Root directory for ${dep.target} not found`);
const resolvedRoot = join(workspaceRoot, root);

return [
...computeDepCommands(projectGraph, dep.target, command),
{
packageName: dep.target,
cwd: resolvedRoot,
},
];
})
.flat()
);
}
import TaskOrchestrator from './task-orchestrator';

async function run() {
tkaragianes marked this conversation as resolved.
Show resolved Hide resolved
// Handle CLI args
const args = await yargs(hideBin(argv))
.command(
'$0 <command>',
'Run a command for all dependencies of a given package',
'Run an npm command, taking into account nx dependencies, allowing long-running tasks in dependencies',
(yargsCommand) =>
yargsCommand
.positional('command', {
describe: 'command to run for all dependencies',
type: 'string',
})
.options({
noRoot: {
default: false,
type: 'boolean',
describe:
"If true, don't run the command for the current package, only its dependencies",
},
extraRootCommand: {
default: '',
type: 'string',
describe:
'If present, run this additional command concurrently in the context of the current package',
},
})
yargsCommand.positional('command', {
describe: 'command to run',
type: 'string',
demandOption: true,
})
)
.parse();
// Yargs does ensure it's not undefined, but for some reason the types don't reflect that
const command = args.command as string;
const { command } = args;

// Figure out the package we're running on based on our cwd
const ws = new Workspaces(workspaceRoot);
const packageToRun = ws.calculateDefaultProjectName(
const packageName = ws.calculateDefaultProjectName(
cwd(),
ws.readWorkspaceConfiguration()
);
const projectGraph = await createProjectGraphAsync();
const depCommands = computeDepCommands(projectGraph, packageToRun, command);

await concurrently(
[
...(args.extraRootCommand
? [{ name: packageToRun, command: args.extraRootCommand }]
: []),
...(!args.noRoot ? [{ name: packageToRun, command }] : []),
...depCommands.map((dep) => ({
name: dep.packageName,
command,
cwd: dep.cwd,
})),
],
{ prefixColors: ['cyan'] }
).result;
// When the nx task runner prints output from a task to the console, prepend the
// package it's coming from to the output so that you can tell what output is coming from where
process.env['NX_PREFIX_OUTPUT'] = 'true';

// Do the thing!
await new TaskOrchestrator(command, packageName).run();
}

run().catch((e) => {
Expand Down
39 changes: 39 additions & 0 deletions packages/nx-spawn/src/promise-walker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Wrap an array of promises so that you can wait for each promise to complete,
* handling them one at a time as they finish
*/
export default class PromiseWalker<T> {
/**
* Register some promises to be included in the set that's being walked through
*
* @param promises
*/
add(...promises: Promise<T>[]) {
this._promises.push(...promises);
}

/**
* Wait for one of the registered promises to complete then de-register it so that
* the next time this function is run, we get the next completed promise
*
* @returns The result of the completed promise
*/
async next(): Promise<T> {
const wrapper = await Promise.race(
this._promises.map(async (promise) => {
const result = await promise;
return { promise, result };
})
);
this._promises.splice(this._promises.indexOf(wrapper.promise));
return wrapper.result;
}

/** The number of promises left to complete */
get length(): number {
return this._promises.length;
}

/** The promises left to complete */
private _promises: Promise<T>[] = [];
}
Loading