Created
December 10, 2021 13:32
-
-
Save chrisui/a3cdf050bf18eccd499fba29b609b90a to your computer and use it in GitHub Desktop.
Nx Dev Processes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"root": "project", | |
"sourceRoot": "project/src", | |
"projectType": "application", | |
"targets": { | |
"serve": { | |
"executor": "..." | |
}, | |
"dev": { | |
"executor": "my-executor-pkg:serve" | |
} | |
}, | |
"tags": [], | |
"implicitDependencies": [] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"executors": { | |
"serve": { | |
"implementation": "./dist/cjs/serve-executor", | |
"schema": "./schema.json", | |
"description": "Runs persistent processes tasks of dependencies and self" | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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