Skip to content

Instantly share code, notes, and snippets.

@chrisui
Created December 10, 2021 13:32
Show Gist options
  • Save chrisui/a3cdf050bf18eccd499fba29b609b90a to your computer and use it in GitHub Desktop.
Save chrisui/a3cdf050bf18eccd499fba29b609b90a to your computer and use it in GitHub Desktop.
Nx Dev Processes
{
"root": "project",
"sourceRoot": "project/src",
"projectType": "application",
"targets": {
"serve": {
"executor": "..."
},
"dev": {
"executor": "my-executor-pkg:serve"
}
},
"tags": [],
"implicitDependencies": []
}
{
"executors": {
"serve": {
"implementation": "./dist/cjs/serve-executor",
"schema": "./schema.json",
"description": "Runs persistent processes tasks of dependencies and self"
}
}
}
import fs from 'fs'
import path from 'path'
export const tmpFilePath = (namespace: string, name: string) => {
const tmpDir = path.join(os.tmpdir(), namespace)
// ensure we have made the tmp directory
fs.mkdirSync(tmpDir, { recursive: true })
return path.join(`${tmpDir}/${name}`)
}
/**
* Interleave an array of async iterators to produce one iterator
* See: https://stackoverflow.com/questions/50585456/how-can-i-interleave-merge-async-iterables
*/
export async function* combineAsyncIterators(
iterable: AsyncIterableIterator<any>[]
) {
const asyncIterators = Array.from(iterable, (o) => o[Symbol.asyncIterator]())
const results = []
let count = asyncIterators.length
const never = new Promise<any>(() => {})
function getNext(asyncIterator: AsyncIterableIterator<any>, index: number) {
return asyncIterator.next().then((result) => ({
index,
result,
}))
}
const nextPromises = asyncIterators.map(getNext)
try {
while (count) {
const { index, result } = await Promise.race(nextPromises)
if (result.done) {
nextPromises[index] = never
results[index] = result.value
count--
} else {
nextPromises[index] = getNext(asyncIterators[index], index)
yield result.value
}
}
} finally {
for (const [index, iterator] of asyncIterators.entries()) {
if (nextPromises[index] !== never && iterator.return) {
iterator.return()
}
}
// no await here - see https://github.com/tc39/proposal-async-iteration/issues/126
}
return results
}
import { ExecutorContext, runExecutor } from '@nrwl/devkit'
import { command } from 'execa'
import kebabCase from 'lodash/kebabCase'
import { DepGraph } from 'dependency-graph'
import { combineAsyncIterators, tmpFilePath } from './lib'
export type DevExecutorOptions = {}
type Node = {
type: string
name: string
data: { targets: Record<string, any> }
}
type NxDependencyGraph = {
nodes: Record<string, Node>
dependencies: Record<string, any>
}
type NxDependencyGraphJson = {
graph: NxDependencyGraph
}
/** The target to run */
const DEV_TARGET = 'serve'
/**
* Get the dependency graph of a single project.
*/
async function getDependencyGraph(project: string) {
const depGraphFileName = `${kebabCase(project)}-dep-graph.json`
const depGraphFilePath = tmpFilePath('nx', depGraphFileName)
// use nx to get the project dependency graph
// Note we use a temp file due to lack of api available from nx/devkit
// ideally it would just have --json stdout which we could read
await command(
`npx nx dep-graph --focus="${project}" --file=${depGraphFilePath}`
)
const json: NxDependencyGraphJson = require(depGraphFilePath)
// build a dependency-graph from our nx metadata which allows for convenient
// graph queries such as dependenciesOf
const graph = new DepGraph<Node>()
Object.entries(json.graph.nodes).forEach(([name, node]) => {
graph.addNode(name, node)
})
Object.entries(json.graph.dependencies).forEach(([name, deps]) => {
deps.forEach((dep: any) => {
if (graph.hasNode(dep.source) && graph.hasNode(dep.target)) {
graph.addDependency(dep.source, dep.target)
}
})
})
return graph
}
export default async function executor(
options: DevExecutorOptions,
context: ExecutorContext
) {
const graph = await getDependencyGraph(context.projectName!)
// skip projects which haven't defined a dev target
const dependencies = graph
.dependenciesOf(context.projectName!)
.filter((name) => !!graph.getNodeData(name).data.targets[DEV_TARGET])
// execute the dev target on dependency projects
const depExecutors = dependencies.map(
async (name) =>
await runExecutor({ project: name!, target: DEV_TARGET }, {}, context)
)
// execute the dev target on the focused project
const selfExecutor = await runExecutor(
{ project: context.projectName!, target: DEV_TARGET },
{},
context
)
// once all executors are loaded, watch out for any failures
// we combine (interleave) all executor iterators here
const result = await Promise.all([selfExecutor, ...depExecutors])
for await (const res of combineAsyncIterators(result)) {
if (!res.success) return res
}
return { success: true }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment