Skip to content

Commit

Permalink
feat(core): add initial implementation for running sync generators
Browse files Browse the repository at this point in the history
  • Loading branch information
leosvelperez committed Jul 5, 2024
1 parent 995898e commit 225865f
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 9 deletions.
18 changes: 18 additions & 0 deletions packages/nx/src/daemon/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ import {
HandleGetTaskHistoryForHashesMessage,
HandleWriteTaskRunsToHistoryMessage,
} from '../message-types/task-history';
import {
GET_SYNC_GENERATOR_CHANGES,
type HandleGetSyncGeneratorChangesMessage,
} from '../message-types/get-sync-generator-changes';
import type { FileChange } from '../../generators/tree';

const DAEMON_ENV_SETTINGS = {
NX_PROJECT_GLOB_CACHE: 'false',
Expand Down Expand Up @@ -336,6 +341,19 @@ export class DaemonClient {
return this.sendMessageToDaemon(message);
}

async getSyncGeneratorChanges(generators: string[]): Promise<FileChange[]> {
const message: HandleGetSyncGeneratorChangesMessage = {
type: GET_SYNC_GENERATOR_CHANGES,
generators,
};
const changes = await this.sendToDaemonViaQueue(message);
for (const change of changes) {
// We need to convert the content back to a buffer
change.content = Buffer.from(change.content);
}
return changes;
}

async isServerAvailable(): Promise<boolean> {
return new Promise((resolve) => {
try {
Expand Down
17 changes: 17 additions & 0 deletions packages/nx/src/daemon/message-types/get-sync-generator-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const GET_SYNC_GENERATOR_CHANGES = 'GET_SYNC_GENERATOR_CHANGES' as const;

export type HandleGetSyncGeneratorChangesMessage = {
type: typeof GET_SYNC_GENERATOR_CHANGES;
generators: string[];
};

export function isHandleGetSyncGeneratorChangesMessage(
message: unknown
): message is HandleGetSyncGeneratorChangesMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === GET_SYNC_GENERATOR_CHANGES
);
}
13 changes: 13 additions & 0 deletions packages/nx/src/daemon/server/handle-get-sync-generator-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getSyncGeneratorChanges } from '../../utils/sync-generators';
import type { HandlerResult } from './server';

export async function handleGetSyncGeneratorChanges(
generators: string[]
): Promise<HandlerResult> {
const changes = await getSyncGeneratorChanges(generators);

return {
response: JSON.stringify(changes),
description: 'handleGetSyncGeneratorChanges',
};
}
9 changes: 9 additions & 0 deletions packages/nx/src/daemon/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ import {
} from '../message-types/task-history';
import { handleGetTaskHistoryForHashes } from './handle-get-task-history';
import { handleWriteTaskRunsToHistory } from './handle-write-task-runs-to-history';
import {
GET_SYNC_GENERATOR_CHANGES,
isHandleGetSyncGeneratorChangesMessage,
} from '../message-types/get-sync-generator-changes';
import { handleGetSyncGeneratorChanges } from './handle-get-sync-generator-changes';

let performanceObserver: PerformanceObserver | undefined;
let workspaceWatcherError: Error | undefined;
Expand Down Expand Up @@ -216,6 +221,10 @@ async function handleMessage(socket, data: string) {
await handleResult(socket, 'WRITE_TASK_RUNS_TO_HISTORY', () =>
handleWriteTaskRunsToHistory(payload.taskRuns)
);
} else if (isHandleGetSyncGeneratorChangesMessage(payload)) {
await handleResult(socket, GET_SYNC_GENERATOR_CHANGES, () =>
handleGetSyncGeneratorChanges(payload.generators)
);
} else {
await respondWithErrorAndExit(
socket,
Expand Down
106 changes: 97 additions & 9 deletions packages/nx/src/tasks-runner/run-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import { StoreRunInformationLifeCycle } from './life-cycles/store-run-informatio
import { createTaskHasher } from '../hasher/create-task-hasher';
import { TaskHistoryLifeCycle } from './life-cycles/task-history-life-cycle';
import { isNxCloudUsed } from '../utils/nx-cloud-utils';
import { getSyncGeneratorChanges } from '../utils/sync-generators';
import { flushChanges } from '../generators/tree';
import { createProjectGraphAsync } from '../project-graph/project-graph';
import { prompt } from 'enquirer';

async function getTerminalOutputLifeCycle(
initiatingProject: string,
Expand Down Expand Up @@ -134,7 +138,7 @@ function createTaskGraphAndValidateCycles(

export async function runCommand(
projectsToRun: ProjectGraphProjectNode[],
projectGraph: ProjectGraph,
currentProjectGraph: ProjectGraph,
{ nxJson }: { nxJson: NxJsonConfiguration },
nxArgs: NxArgs,
overrides: any,
Expand All @@ -147,14 +151,15 @@ export async function runCommand(
async () => {
const projectNames = projectsToRun.map((t) => t.name);

const taskGraph = createTaskGraphAndValidateCycles(
projectGraph,
extraTargetDependencies ?? {},
projectNames,
nxArgs,
overrides,
extraOptions
);
const { projectGraph, taskGraph } =
await ensureWorkspaceIsInSyncAndGetGraphs(
currentProjectGraph,
projectNames,
nxArgs,
overrides,
extraTargetDependencies,
extraOptions
);
const tasks = Object.values(taskGraph.tasks);

const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle(
Expand Down Expand Up @@ -186,6 +191,89 @@ export async function runCommand(
return status;
}

async function ensureWorkspaceIsInSyncAndGetGraphs(
projectGraph: ProjectGraph,
projectNames: string[],
nxArgs: NxArgs,
overrides: any,
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>,
extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
): Promise<{
projectGraph: ProjectGraph;
taskGraph: TaskGraph;
}> {
let taskGraph = createTaskGraphAndValidateCycles(
projectGraph,
extraTargetDependencies ?? {},
projectNames,
nxArgs,
overrides,
extraOptions
);

// collect unique syncGenerators from the tasks
const uniqueSyncGenerators = new Set<string>();
for (const { target } of Object.values(taskGraph.tasks)) {
const { syncGenerators } =
projectGraph.nodes[target.project].data.targets[target.target];
if (!syncGenerators) {
continue;
}

for (const generator of syncGenerators) {
uniqueSyncGenerators.add(generator);
}
}

if (!uniqueSyncGenerators.size) {
// There are no sync generators registered in the tasks to run
return { projectGraph, taskGraph };
}

const changes = await getSyncGeneratorChanges(
Array.from(uniqueSyncGenerators)
);
if (!changes.length) {
// There are no changes to sync, workspace is up to date
return { projectGraph, taskGraph };
}

// TODO(leo): check wording and potentially change prompt to allow customizing options and provide footer
const applySyncChanges = await prompt<{ applySyncChanges: boolean }>([
{
name: 'applySyncChanges',
type: 'confirm',
message: 'Your workspace is out of sync. Would you like to sync it?',
initial: true,
},
]).then((a) => a.applySyncChanges);

if (applySyncChanges) {
// Write changes to disk
flushChanges(workspaceRoot, changes);
// Re-create project graph and task graph
projectGraph = await createProjectGraphAsync();
taskGraph = createTaskGraphAndValidateCycles(
projectGraph,
extraTargetDependencies ?? {},
projectNames,
nxArgs,
overrides,
extraOptions
);
} else {
output.warn({
title: 'Workspace is out of sync',
bodyLines: [
'This could lead to unexpected results or errors when running tasks.',
'You can fix this by running `nx sync`.',
],
});
}

return { projectGraph, taskGraph };
}

function setEnvVarsBasedOnArgs(nxArgs: NxArgs, loadDotEnvFiles: boolean) {
if (
nxArgs.outputStyle == 'stream' ||
Expand Down
52 changes: 52 additions & 0 deletions packages/nx/src/utils/sync-generators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { parseGeneratorString } from '../command-line/generate/generate';
import { getGeneratorInformation } from '../command-line/generate/generator-utils';
import type { ProjectConfiguration } from '../config/workspace-json-project-json';
import { daemonClient } from '../daemon/client/client';
import { isOnDaemon } from '../daemon/is-on-daemon';
import { FsTree, type FileChange } from '../generators/tree';
import {
createProjectGraphAsync,
readProjectsConfigurationFromProjectGraph,
} from '../project-graph/project-graph';
import { workspaceRoot } from './workspace-root';

export async function getSyncGeneratorChanges(
generators: string[]
): Promise<FileChange[]> {
if (isOnDaemon() || !daemonClient.enabled()) {
return await runSyncGenerators(generators);
}

// TODO(leo): look into registering the generators with the daemon and let it run them in the background when changes are detected
return await daemonClient.getSyncGeneratorChanges(generators);
}

async function runSyncGenerators(generators: string[]): Promise<FileChange[]> {
const tree = new FsTree(workspaceRoot, false, 'running sync generators');
const projectGraph = isOnDaemon()
? (await daemonClient.getProjectGraphAndSourceMaps()).projectGraph
: await createProjectGraphAsync();
const { projects } = readProjectsConfigurationFromProjectGraph(projectGraph);

for (const generator of generators) {
await runGenerator(tree, generator, projects);
}

return tree.listChanges();
}

async function runGenerator(
tree: FsTree,
generatorSpecifier: string,
projects: Record<string, ProjectConfiguration>
): Promise<void> {
const { collection, generator } = parseGeneratorString(generatorSpecifier);
const { implementationFactory } = getGeneratorInformation(
collection,
generator,
workspaceRoot,
projects
);
const implementation = implementationFactory();
await implementation(tree, {});
}

0 comments on commit 225865f

Please sign in to comment.