From a494e485ff14140d2e5f0ae22b289edd9204190a Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Wed, 28 Aug 2024 18:31:37 +0200 Subject: [PATCH] feat(workspace-plugin): implement clean executor (#32401) --- tools/workspace-plugin/executors.json | 2 +- .../src/executors/clean/executor.spec.ts | 91 ++++++++++++++++++- .../src/executors/clean/executor.ts | 57 ++++++++++-- .../src/executors/clean/schema.d.ts | 7 +- .../src/executors/clean/schema.json | 12 ++- 5 files changed, 155 insertions(+), 14 deletions(-) diff --git a/tools/workspace-plugin/executors.json b/tools/workspace-plugin/executors.json index 33b41e0b97e728..0945d1dd927ec8 100644 --- a/tools/workspace-plugin/executors.json +++ b/tools/workspace-plugin/executors.json @@ -23,7 +23,7 @@ "clean": { "implementation": "./src/executors/clean/executor", "schema": "./src/executors/clean/schema.json", - "description": "clean executor" + "description": "clean executor - remove build artifacts." } } } diff --git a/tools/workspace-plugin/src/executors/clean/executor.spec.ts b/tools/workspace-plugin/src/executors/clean/executor.spec.ts index 7a6666c49c9e28..2b59c074b691a0 100644 --- a/tools/workspace-plugin/src/executors/clean/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/clean/executor.spec.ts @@ -1,18 +1,103 @@ -import { ExecutorContext } from '@nx/devkit'; +import { ExecutorContext, logger } from '@nx/devkit'; import { CleanExecutorSchema } from './schema'; import executor from './executor'; +import { join } from 'node:path'; +import { rm } from 'node:fs/promises'; +import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'; + +jest.mock('node:fs/promises', () => { + return { + ...jest.requireActual('node:fs/promises'), + rm: jest.fn(() => Promise.resolve()), + }; +}); const options: CleanExecutorSchema = {}; const context: ExecutorContext = { - root: '', + root: join(__dirname, '__fixtures__'), + projectName: 'proj', + projectsConfigurations: { + projects: { proj: { root: 'proj' } }, + version: 2, + }, cwd: process.cwd(), - isVerbose: false, + isVerbose: true, }; +const noop = () => { + return; +}; +const fixtureRoot = context.root; +const projRoot = join(fixtureRoot, 'proj'); + +function prepareFixture() { + if (!existsSync(fixtureRoot)) { + mkdirSync(fixtureRoot); + mkdirSync(projRoot); + mkdirSync(join(projRoot, 'dist')); + writeFileSync(join(projRoot, 'dist', 'file.txt'), 'file', 'utf-8'); + mkdirSync(join(projRoot, 'lib')); + writeFileSync(join(projRoot, 'lib', 'file.txt'), 'file', 'utf-8'); + } + return () => { + rmSync(fixtureRoot, { recursive: true, force: true }); + }; +} + describe('Clean Executor', () => { + let cleanup = noop; + + beforeAll(() => { + cleanup = prepareFixture(); + }); + afterAll(() => { + cleanup(); + }); + + beforeEach(() => { + jest.spyOn(logger, 'info').mockImplementation(noop); + jest.spyOn(logger, 'error').mockImplementation(noop); + }); + it('can run', async () => { + const rmMock = rm as jest.Mock; const output = await executor(options, context); expect(output.success).toBe(true); + + expect(rmMock.mock.calls.flat()).toEqual([ + expect.stringContaining('tools/workspace-plugin/src/executors/clean/__fixtures__/proj/dist'), + { + force: true, + recursive: true, + }, + expect.stringContaining('tools/workspace-plugin/src/executors/clean/__fixtures__/proj/lib'), + { + force: true, + recursive: true, + }, + ]); + }); + + it('accepts custom paths that override default ones', async () => { + const rmMock = rm as jest.Mock; + mkdirSync(join(projRoot, 'foo-bar')); + mkdirSync(join(projRoot, 'mr-wick')); + + const output = await executor({ ...options, paths: ['foo-bar', 'mr-wick'] }, context); + expect(output.success).toBe(true); + + expect(rmMock.mock.calls.flat()).toEqual([ + expect.stringContaining('tools/workspace-plugin/src/executors/clean/__fixtures__/proj/foo-bar'), + { + force: true, + recursive: true, + }, + expect.stringContaining('tools/workspace-plugin/src/executors/clean/__fixtures__/proj/mr-wick'), + { + force: true, + recursive: true, + }, + ]); }); }); diff --git a/tools/workspace-plugin/src/executors/clean/executor.ts b/tools/workspace-plugin/src/executors/clean/executor.ts index 35d5015a5331c4..6e167912150f20 100644 --- a/tools/workspace-plugin/src/executors/clean/executor.ts +++ b/tools/workspace-plugin/src/executors/clean/executor.ts @@ -1,11 +1,54 @@ -import { PromiseExecutor } from '@nx/devkit'; -import { CleanExecutorSchema } from './schema'; +import { type ExecutorContext, type PromiseExecutor, logger } from '@nx/devkit'; +import { type CleanExecutorSchema } from './schema'; +import { join } from 'node:path'; +import { rm } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; -const runExecutor: PromiseExecutor = async options => { - console.log('Executor ran for Clean', options); - return { - success: true, - }; +const runExecutor: PromiseExecutor = async (schema, context) => { + const options = normalizeOptions(schema, context); + + const success = await runClean(options, context); + + return { success }; }; +interface NormalizedOptions extends ReturnType {} + +async function runClean(options: NormalizedOptions, context: ExecutorContext): Promise { + const projectAbsoluteRootPath = join(context.root, options.project.root); + + const results = options.paths.map(dir => { + const dirPath = join(projectAbsoluteRootPath, dir); + if (existsSync(dirPath)) { + verboseLog(`removing "${dirPath}"`); + return rm(dirPath, { force: true, recursive: true }); + } + return Promise.resolve(); + }); + + return Promise.all(results) + .then(() => { + return true; + }) + .catch(err => { + logger.error(err); + return false; + }); +} + export default runExecutor; + +function normalizeOptions(schema: CleanExecutorSchema, context: ExecutorContext) { + const defaults = { + paths: ['temp', 'dist', 'dist-storybook', 'storybook-static', 'lib', 'lib-amd', 'lib-commonjs', 'coverage'], + }; + const project = context.projectsConfigurations!.projects[context.projectName!]; + + return { ...defaults, ...schema, project }; +} + +function verboseLog(message: string, kind: keyof typeof logger = 'info') { + if (process.env.NX_VERBOSE_LOGGING === 'true') { + logger[kind](message); + } +} diff --git a/tools/workspace-plugin/src/executors/clean/schema.d.ts b/tools/workspace-plugin/src/executors/clean/schema.d.ts index ef7eacbd8f066e..803d05e409caa6 100644 --- a/tools/workspace-plugin/src/executors/clean/schema.d.ts +++ b/tools/workspace-plugin/src/executors/clean/schema.d.ts @@ -1 +1,6 @@ -export interface CleanExecutorSchema {} // eslint-disable-line +export interface CleanExecutorSchema { + /** + * Files/Directories to remove (provide relative paths to project root) + */ + paths?: string[]; +} diff --git a/tools/workspace-plugin/src/executors/clean/schema.json b/tools/workspace-plugin/src/executors/clean/schema.json index d941cd5dad3a2a..a72a2a41ffb108 100644 --- a/tools/workspace-plugin/src/executors/clean/schema.json +++ b/tools/workspace-plugin/src/executors/clean/schema.json @@ -2,8 +2,16 @@ "$schema": "https://json-schema.org/schema", "version": 2, "title": "Clean executor", - "description": "", + "description": "Remove build artifacts.", "type": "object", - "properties": {}, + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Files/Directories to remove (provide relative paths to project root)" + } + }, "required": [] }