Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions .emdash.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"preservePatterns": [
".env",
".env.keys",
".env.local",
".env.*.local",
".envrc",
"docker-compose.override.yml"
]
}
1 change: 0 additions & 1 deletion tools/executors/check-mirror-exists/executor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable max-lines -- Executor file with comprehensive error handling and documentation */
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

Expand Down
9 changes: 9 additions & 0 deletions tools/executors/check-mirror-exists/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
"sourceRoot": "tools/executors/check-mirror-exists",
"projectType": "library",
"targets": {
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "biome check --write .",
"cwd": "tools/executors/check-mirror-exists"
},
"cache": true,
"inputs": ["default", "{workspaceRoot}/biome.json"]
},
"type-check": {
"executor": "nx:run-commands",
"options": {
Expand Down
36 changes: 23 additions & 13 deletions tools/executors/dev-proxy/executor.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import type { spawnSync } from 'node:child_process';
import type { ExecutorContext } from '@nx/devkit';

Expand Down Expand Up @@ -41,8 +42,8 @@ const _originalSpawn = childProcess.spawn;
function _fakeSpawn(_cmd: string, _args: string[], _opts: any) {
return {
kill: () => {},
on: (_ev: string, _cb: (data: unknown) => void) => {},
} as { kill(): void; on(event: string, callback: (data: unknown) => void): void };
on: (_ev: string, _cb: (...args: unknown[]) => void) => {},
} as any;
}

let _originalExit: typeof process.exit;
Expand All @@ -55,7 +56,6 @@ beforeEach(() => {
// stub process.exit so tests can simulate SIGINT without killing the test runner
_originalExit = process.exit;
_exitCalled = false;
// @ts-expect-error override for test
process.exit = ((_code?: number) => {
_exitCalled = true;
// do not actually exit during tests
Expand Down Expand Up @@ -87,17 +87,21 @@ describe('dev-proxy executor with mocked runExecutor', () => {
// Snapshot existing SIGINT listeners
const beforeListeners = process.listeners('SIGINT').slice();

const resPromise = runExecutor(
const gen = runExecutor(
{
plugins: ['opencode-warcraft-notifications-plugin'],
__runExecutor: mockRunExecutor,
__spawnSync: mockSpawnSync,
__noExit: true,
},
context,
);

// Start the generator to execute the executor body
const resPromise = gen.next();

// Wait briefly to let executor start and attach iterator
await new Promise((r) => setTimeout(r, 50));
await new Promise((r) => setTimeout(r, 100));

// Simulate SIGINT by sending the signal to the process
process.emit('SIGINT' as any);
Expand All @@ -108,9 +112,8 @@ describe('dev-proxy executor with mocked runExecutor', () => {
expect(iterator._returned()).toBe(true);

const res = await resPromise;
expect(res?.success).toBe(true);
expect(res?.value?.success).toBe(true);

// Restore SIGINT listeners to avoid side effects on other tests
const afterListeners = process.listeners('SIGINT');
for (const l of afterListeners) {
if (!beforeListeners.includes(l)) process.removeListener('SIGINT', l);
Expand Down Expand Up @@ -155,15 +158,19 @@ describe('dev-proxy executor with mocked runExecutor', () => {

const beforeListeners = process.listeners('SIGINT').slice();

const resPromise = runExecutor(
const gen = runExecutor(
{
plugins: ['opencode-warcraft-notifications-plugin'],
__runExecutor: mockRunExecutor,
__spawnSync: mockSpawnSync,
__noExit: true,
},
context,
);

// Start the generator to execute the executor body
const resPromise = gen.next();

await new Promise((r) => setTimeout(r, 100));

process.emit('SIGINT' as any);
Expand All @@ -172,7 +179,7 @@ describe('dev-proxy executor with mocked runExecutor', () => {
expect(childKilled).toBe(true);

const res = await resPromise;
expect(res?.success).toBe(true);
expect(res?.value?.success).toBe(true);

// restore spawn and listeners
childProcess.spawn = originalSpawn;
Expand All @@ -197,12 +204,15 @@ describe('dev-proxy executor with mocked runExecutor', () => {

const beforeListeners = process.listeners('SIGINT').slice();

const resPromise = runExecutor(
{ plugins: ['pA', 'pB'], __runExecutor: mockRunExecutor, __spawnSync: mockSpawnSync },
const gen = runExecutor(
{ plugins: ['pA', 'pB'], __runExecutor: mockRunExecutor, __spawnSync: mockSpawnSync, __noExit: true },
context,
);

await new Promise((r) => setTimeout(r, 50));
// Start the generator to execute the executor body
const resPromise = gen.next();

await new Promise((r) => setTimeout(r, 100));

process.emit('SIGINT' as any);
await new Promise((r) => setTimeout(r, 50));
Expand All @@ -211,7 +221,7 @@ describe('dev-proxy executor with mocked runExecutor', () => {
expect(iterB._returned()).toBe(true);

const res = await resPromise;
expect(res?.success).toBe(true);
expect(res?.value?.success).toBe(true);

const afterListeners = process.listeners('SIGINT');
for (const l of afterListeners) {
Expand Down
49 changes: 32 additions & 17 deletions tools/executors/dev-proxy/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface DevProxyOptions {
apply?: boolean;
__runExecutor?: typeof nxRunExecutor;
__spawnSync?: typeof spawnSync;
__noExit?: boolean;
}

interface ExecutorResult {
Expand All @@ -24,18 +25,19 @@ interface ResolvedProject {
* Nx executor for running dev proxy with build watchers
* @param options - Executor options including plugin names and symlink configuration
* @param context - Nx executor context
* @returns Executor result indicating success or failure
* @yields Executor results during execution
*/
// eslint-disable-next-line max-statements, complexity
const runExecutor = async (options: DevProxyOptions, context: ExecutorContext): Promise<ExecutorResult> => {
async function* runExecutor(options: DevProxyOptions, context: ExecutorContext): AsyncGenerator<ExecutorResult> {
const workspaceRoot = context.root;

const requestedPlugins =
options.plugins && options.plugins.length > 0 ? options.plugins : context.projectName ? [context.projectName] : [];

if (requestedPlugins.length === 0) {
console.error('No project specified for dev-proxy (provide --plugins or run from a project context)');
return { success: false };
yield { success: false };
return;
}

// Resolve projects - simplified to just use the names
Expand All @@ -48,6 +50,8 @@ const runExecutor = async (options: DevProxyOptions, context: ExecutorContext):
const runExecutorImpl = options.__runExecutor ?? nxRunExecutor;
const spawnSyncImpl = options.__spawnSync ?? spawnSync;

console.info('dev-proxy: workspaceRoot=', workspaceRoot);

for (const r of resolved) {
const projName = r.name;
let started = false;
Expand All @@ -60,23 +64,27 @@ const runExecutor = async (options: DevProxyOptions, context: ExecutorContext):
context,
);

if (iterator && Symbol.asyncIterator in iterator) {
(async () => {
try {
for await (const out of iterator) {
if (!out || !out.success) console.error(`Build for ${projName} reported failure`);
if (iterator) {
const it = iterator as AsyncIterable<{ success: boolean }> & { return?: () => Promise<void> };
if (Symbol.asyncIterator in it) {
(async () => {
try {
for await (const out of it) {
if (!out || !out.success) console.error(`Build for ${projName} reported failure`);
}
} catch (err) {
console.error(`runExecutor iterator error for ${projName}:`, err);
}
} catch (err) {
console.error(`runExecutor iterator error for ${projName}:`, err);
}
})();
})();
}
stopFns.push(async () => {
try {
if (typeof iterator.return === 'function') await iterator.return();
if (typeof it.return === 'function') await it.return();
} catch {
// Failed to stop iterator
}
});
console.info(`Started build target for ${projName} via @nx/devkit.runExecutor`);
started = true;
}
} catch (err) {
Expand All @@ -86,6 +94,7 @@ const runExecutor = async (options: DevProxyOptions, context: ExecutorContext):

if (!started) {
try {
console.info(`Falling back to CLI watcher for ${projName}`);
const child = spawn('bunx', ['nx', 'run', `${projName}:build`, '--watch'], {
stdio: 'inherit',
cwd: workspaceRoot,
Expand All @@ -110,19 +119,24 @@ const runExecutor = async (options: DevProxyOptions, context: ExecutorContext):
if (options.apply === false) args.push('--no-apply');
args.push(...requestedPlugins);

console.info('Running dev proxy runtime:', ['bunx', 'tsx', script, ...args].join(' '));

// Ensure cleanup on SIGINT
let exiting = false;
const sigintHandler = async () => {
if (exiting) return;
exiting = true;
console.info('\nInterrupted. Stopping build watchers and exiting...');
for (const fn of stopFns) {
try {
await fn();
} catch {
// Failed to stop watcher
}
}
process.exit(0);
if (!options.__noExit) {
process.exit(0);
}
};
process.on('SIGINT', sigintHandler);

Expand All @@ -140,9 +154,10 @@ const runExecutor = async (options: DevProxyOptions, context: ExecutorContext):

if (res?.error) {
console.error('Failed to run dev proxy runtime', res.error);
return { success: false };
yield { success: false };
return;
}
return { success: res?.status === 0 };
};
yield { success: res?.status === 0 };
}

export default runExecutor;
Loading
Loading