From a246b22ddfddad66a22e957a337ae60f8061c87d Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 29 Apr 2026 18:05:39 +0200 Subject: [PATCH 1/9] update deps with security vulnerabilities --- package.json | 5 ++-- pnpm-lock.yaml | 75 +++++++++++++------------------------------------- 2 files changed, 22 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index 6c080e3c39e3..0faad15820fd 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "lint-staged": "14.0.1", "nx": "22.4.5", "nx-cloud": "19.1.0", - "postcss": "8.5.10", + "postcss": "~8.5.10", "shelljs": "0.8.5", "shx": "0.4.0", "source-map": "0.7.6", @@ -121,7 +121,8 @@ "yaml@>=2.0.0 <2.8.3": "^2.8.3", "follow-redirects@<=1.15.11": ">=1.16.0", "@angular/platform-server@>=20.0.0-next.0 <20.3.19": "^20.3.19", - "stylelint-config-recommended-vue@1.6.1>stylelint-config-recommended": "^16.0.0" + "stylelint-config-recommended-vue@1.6.1>stylelint-config-recommended": "^16.0.0", + "uuid@<14.0.0": "~14.0.0" } }, "packageManager": "pnpm@9.15.9" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d0aeb786dce..345327887735 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,6 +217,7 @@ overrides: follow-redirects@<=1.15.11: '>=1.16.0' '@angular/platform-server@>=20.0.0-next.0 <20.3.19': ^20.3.19 stylelint-config-recommended-vue@1.6.1>stylelint-config-recommended: ^16.0.0 + uuid@<14.0.0: ~14.0.0 importers: @@ -286,7 +287,7 @@ importers: specifier: 19.1.0 version: 19.1.0 postcss: - specifier: 8.5.10 + specifier: ~8.5.10 version: 8.5.10 shelljs: specifier: 0.8.5 @@ -17357,15 +17358,6 @@ packages: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true - uuid@3.4.0: - resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. - hasBin: true - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'} @@ -27618,7 +27610,7 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.1.0 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.10 postcss-media-query-parser: 0.2.3 big.js@5.2.2: {} @@ -28727,12 +28719,12 @@ snapshots: css-loader@7.1.2(webpack@5.105.0(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.27.2)): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) + icss-utils: 5.1.0(postcss@8.5.10) + postcss: 8.5.10 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.10) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.10) + postcss-modules-scope: 3.2.1(postcss@8.5.10) + postcss-modules-values: 4.0.0(postcss@8.5.10) postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: @@ -28740,12 +28732,12 @@ snapshots: css-loader@7.1.2(webpack@5.105.0(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.28.0)): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) + icss-utils: 5.1.0(postcss@8.5.10) + postcss: 8.5.10 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.10) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.10) + postcss-modules-scope: 3.2.1(postcss@8.5.10) + postcss-modules-values: 4.0.0(postcss@8.5.10) postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: @@ -32277,10 +32269,6 @@ snapshots: dependencies: postcss: 8.5.10 - icss-utils@5.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - identity-obj-proxy@3.0.0: dependencies: harmony-reflect: 1.6.2 @@ -35783,7 +35771,7 @@ snapshots: is-wsl: 2.2.0 semver: 7.7.4 shellwords: 0.1.1 - uuid: 8.3.2 + uuid: 14.0.0 which: 2.0.2 node-releases@2.0.37: {} @@ -36677,10 +36665,6 @@ snapshots: dependencies: postcss: 8.5.10 - postcss-modules-extract-imports@3.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-modules-local-by-default@4.2.0(postcss@8.4.38): dependencies: icss-utils: 5.1.0(postcss@8.4.38) @@ -36695,13 +36679,6 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-modules-local-by-default@4.2.0(postcss@8.5.6): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-selector-parser: 7.1.1 - postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.4.38): dependencies: postcss: 8.4.38 @@ -36712,11 +36689,6 @@ snapshots: postcss: 8.5.10 postcss-selector-parser: 7.1.1 - postcss-modules-scope@3.2.1(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-selector-parser: 7.1.1 - postcss-modules-values@4.0.0(postcss@8.4.38): dependencies: icss-utils: 5.1.0(postcss@8.4.38) @@ -36727,11 +36699,6 @@ snapshots: icss-utils: 5.1.0(postcss@8.5.10) postcss: 8.5.10 - postcss-modules-values@4.0.0(postcss@8.5.6): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-resolve-nested-selector@0.1.6: {} postcss-safe-parser@6.0.0(postcss@8.4.38): @@ -37513,7 +37480,7 @@ snapshots: safe-buffer: 5.2.1 tough-cookie: 4.1.4 tunnel-agent: 0.6.0 - uuid: 3.4.0 + uuid: 14.0.0 require-directory@2.1.1: {} @@ -37572,7 +37539,7 @@ snapshots: adjust-sourcemap-loader: 4.0.0 convert-source-map: 1.9.0 loader-utils: 2.0.4 - postcss: 8.5.6 + postcss: 8.5.10 source-map: 0.6.1 resolve.exports@2.0.3: {} @@ -38327,7 +38294,7 @@ snapshots: sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 - uuid: 8.3.2 + uuid: 14.0.0 websocket-driver: 0.7.4 socks-proxy-agent@8.0.5: @@ -40445,10 +40412,6 @@ snapshots: uuid@14.0.0: {} - uuid@3.4.0: {} - - uuid@8.3.2: {} - uvu@0.5.6: dependencies: dequal: 2.0.3 From 40c53e801b8dc7a9816bd71a40d9e2659da76c79 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 29 Apr 2026 22:32:51 +0200 Subject: [PATCH 2/9] move some build logic to nx-infra-plugin --- packages/devextreme-scss/project.json | 110 +++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/packages/devextreme-scss/project.json b/packages/devextreme-scss/project.json index 55afe0dfdc3d..9e908b969a7e 100644 --- a/packages/devextreme-scss/project.json +++ b/packages/devextreme-scss/project.json @@ -4,10 +4,116 @@ "sourceRoot": "packages/devextreme-scss", "projectType": "library", "targets": { + "clean:artifacts": { + "executor": "devextreme-nx-infra-plugin:clean", + "options": { + "targetDirectory": "../devextreme/artifacts/css" + } + }, + "clean:bundles": { + "executor": "devextreme-nx-infra-plugin:clean", + "options": { + "targetDirectory": "./scss/bundles" + } + }, + "clean": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm --workspace-root nx clean:artifacts devextreme-scss", + "pnpm --workspace-root nx clean:bundles devextreme-scss" + ], + "parallel": false + } + }, + "copy:assets": { + "executor": "devextreme-nx-infra-plugin:copy-files", + "options": { + "files": [ + { + "from": "./fonts/**/*", + "to": "../devextreme/artifacts/css/fonts" + }, + { + "from": "./icons/**/*", + "to": "../devextreme/artifacts/css/icons" + } + ] + }, + "outputs": [ + "{workspaceRoot}/packages/devextreme/artifacts/css/fonts", + "{workspaceRoot}/packages/devextreme/artifacts/css/icons" + ] + }, + "build:themes": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm --dir packages/devextreme-scss exec gulp style-compiler-themes" + }, + "inputs": [ + "{projectRoot}/build/**/*", + "{projectRoot}/images/**/*", + "{projectRoot}/scss/**/*", + "{projectRoot}/gulpfile.js" + ], + "outputs": [ + "{projectRoot}/scss/bundles", + "{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css" + ], + "cache": true + }, + "build:themes-dev": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm --dir packages/devextreme-scss exec gulp style-compiler-themes-ci" + }, + "inputs": [ + "{projectRoot}/build/**/*", + "{projectRoot}/images/**/*", + "{projectRoot}/scss/**/*", + "{projectRoot}/gulpfile.js" + ], + "outputs": [ + "{projectRoot}/scss/bundles", + "{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css" + ], + "cache": true + }, "build": { - "executor": "nx:run-script", + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm --workspace-root nx clean devextreme-scss", + "pnpm --workspace-root nx build:themes devextreme-scss", + "pnpm --workspace-root nx copy:assets devextreme-scss" + ], + "parallel": false + }, + "inputs": [ + "{projectRoot}/build/**/*", + "{projectRoot}/fonts/**/*", + "{projectRoot}/icons/**/*", + "{projectRoot}/images/**/*", + "{projectRoot}/scss/**/*", + "{projectRoot}/gulpfile.js" + ], + "outputs": [ + "{projectRoot}/scss/bundles", + "{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css", + "{workspaceRoot}/packages/devextreme/artifacts/css/fonts", + "{workspaceRoot}/packages/devextreme/artifacts/css/icons" + ], + "cache": true + }, + "build:ci": { + "executor": "nx:run-commands", "options": { - "script": "build" + "commands": [ + "pnpm --workspace-root nx clean devextreme-scss", + "pnpm --workspace-root nx build:themes-dev devextreme-scss", + "pnpm --workspace-root nx copy:assets devextreme-scss" + ], + "parallel": false }, "inputs": [ "{projectRoot}/build/**/*", From bed2d4938c7c0f7f5f8a1b14b44d420a6bfc717d Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 29 Apr 2026 22:39:53 +0200 Subject: [PATCH 3/9] add scss-build to nx-infra-plugin --- packages/devextreme-scss/project.json | 8 +- packages/nx-infra-plugin/executors.json | 5 ++ .../executors/scss-build/executor.e2e.spec.ts | 82 +++++++++++++++++++ .../src/executors/scss-build/executor.ts | 44 ++++++++++ .../src/executors/scss-build/schema.json | 29 +++++++ .../src/executors/scss-build/schema.ts | 6 ++ 6 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts create mode 100644 packages/nx-infra-plugin/src/executors/scss-build/executor.ts create mode 100644 packages/nx-infra-plugin/src/executors/scss-build/schema.json create mode 100644 packages/nx-infra-plugin/src/executors/scss-build/schema.ts diff --git a/packages/devextreme-scss/project.json b/packages/devextreme-scss/project.json index 9e908b969a7e..a52889b588eb 100644 --- a/packages/devextreme-scss/project.json +++ b/packages/devextreme-scss/project.json @@ -46,9 +46,9 @@ ] }, "build:themes": { - "executor": "nx:run-commands", + "executor": "devextreme-nx-infra-plugin:scss-build", "options": { - "command": "pnpm --dir packages/devextreme-scss exec gulp style-compiler-themes" + "mode": "all" }, "inputs": [ "{projectRoot}/build/**/*", @@ -63,9 +63,9 @@ "cache": true }, "build:themes-dev": { - "executor": "nx:run-commands", + "executor": "devextreme-nx-infra-plugin:scss-build", "options": { - "command": "pnpm --dir packages/devextreme-scss exec gulp style-compiler-themes-ci" + "mode": "ci" }, "inputs": [ "{projectRoot}/build/**/*", diff --git a/packages/nx-infra-plugin/executors.json b/packages/nx-infra-plugin/executors.json index e09768781710..c70cec69d154 100644 --- a/packages/nx-infra-plugin/executors.json +++ b/packages/nx-infra-plugin/executors.json @@ -89,6 +89,11 @@ "implementation": "./src/executors/compress/executor", "schema": "./src/executors/compress/schema.json", "description": "Compress JavaScript files" + }, + "scss-build": { + "implementation": "./src/executors/scss-build/executor", + "schema": "./src/executors/scss-build/schema.json", + "description": "Run SCSS themes build pipeline in all or CI mode" } } } diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts new file mode 100644 index 000000000000..bfb82e8c7f2c --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts @@ -0,0 +1,82 @@ +import { spawnSync } from 'child_process'; +import executor from './executor'; +import { ScssBuildExecutorSchema } from './schema'; +import { createMockContext } from '../../utils/test-utils'; + +jest.mock('child_process', () => ({ + spawnSync: jest.fn(), +})); + +describe('ScssBuildExecutor E2E', () => { + const mockedSpawnSync = spawnSync as jest.MockedFunction; + + beforeEach(() => { + mockedSpawnSync.mockReset(); + }); + + it('runs full themes task in all mode', async () => { + mockedSpawnSync.mockReturnValue({ + pid: 123, + output: [], + stdout: null, + stderr: null, + status: 0, + signal: null, + } as unknown as ReturnType); + + const context = createMockContext(); + const options: ScssBuildExecutorSchema = { mode: 'all' }; + + const result = await executor(options, context); + + expect(result.success).toBe(true); + expect(mockedSpawnSync).toHaveBeenCalledWith( + 'pnpm', + ['exec', 'gulp', 'style-compiler-themes'], + expect.objectContaining({ + cwd: expect.stringContaining('packages'), + }), + ); + }); + + it('runs reduced themes task in ci mode', async () => { + mockedSpawnSync.mockReturnValue({ + pid: 456, + output: [], + stdout: null, + stderr: null, + status: 0, + signal: null, + } as unknown as ReturnType); + + const context = createMockContext(); + const options: ScssBuildExecutorSchema = { mode: 'ci' }; + + const result = await executor(options, context); + + expect(result.success).toBe(true); + expect(mockedSpawnSync).toHaveBeenCalledWith( + 'pnpm', + ['exec', 'gulp', 'style-compiler-themes-ci'], + expect.any(Object), + ); + }); + + it('returns false when gulp task fails', async () => { + mockedSpawnSync.mockReturnValue({ + pid: 789, + output: [], + stdout: null, + stderr: null, + status: 1, + signal: null, + } as unknown as ReturnType); + + const context = createMockContext(); + const options: ScssBuildExecutorSchema = { mode: 'all' }; + + const result = await executor(options, context); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts new file mode 100644 index 000000000000..a3599caab828 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts @@ -0,0 +1,44 @@ +import { PromiseExecutor, logger } from '@nx/devkit'; +import { spawnSync } from 'child_process'; +import { ScssBuildExecutorSchema } from './schema'; +import { resolveProjectPath } from '../../utils/path-resolver'; + +const DEFAULT_GULP_BINARY = 'gulp'; +const DEFAULT_ALL_TASK = 'style-compiler-themes'; +const DEFAULT_CI_TASK = 'style-compiler-themes-ci'; + +function resolveTaskName(options: ScssBuildExecutorSchema): string { + const allTaskName = options.allTaskName || DEFAULT_ALL_TASK; + const ciTaskName = options.ciTaskName || DEFAULT_CI_TASK; + + return options.mode === 'ci' ? ciTaskName : allTaskName; +} + +const runExecutor: PromiseExecutor = async (options, context) => { + const projectRoot = resolveProjectPath(context); + const taskName = resolveTaskName(options); + const gulpBinary = options.gulpBinary || DEFAULT_GULP_BINARY; + + logger.verbose(`Running SCSS build task "${taskName}" in mode "${options.mode}"`); + + const result = spawnSync('pnpm', ['exec', gulpBinary, taskName], { + cwd: projectRoot, + stdio: 'inherit', + shell: process.platform === 'win32', + env: process.env, + }); + + if (result.error) { + logger.error(`Failed to execute SCSS build task "${taskName}": ${result.error.message}`); + return { success: false }; + } + + if (result.status !== 0) { + logger.error(`SCSS build task "${taskName}" failed with exit code ${result.status ?? 1}`); + return { success: false }; + } + + return { success: true }; +}; + +export default runExecutor; diff --git a/packages/nx-infra-plugin/src/executors/scss-build/schema.json b/packages/nx-infra-plugin/src/executors/scss-build/schema.json new file mode 100644 index 000000000000..ae326fc60aaa --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/scss-build/schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "SCSS Build Executor", + "description": "Run SCSS theme compilation pipeline in all or CI mode", + "type": "object", + "properties": { + "mode": { + "type": "string", + "description": "Compilation mode. all = full themes set, ci = reduced dev themes set.", + "enum": ["all", "ci"] + }, + "gulpBinary": { + "type": "string", + "description": "Gulp executable to run via pnpm exec", + "default": "gulp" + }, + "allTaskName": { + "type": "string", + "description": "Gulp task name for full themes build", + "default": "style-compiler-themes" + }, + "ciTaskName": { + "type": "string", + "description": "Gulp task name for CI/dev themes build", + "default": "style-compiler-themes-ci" + } + }, + "required": ["mode"] +} diff --git a/packages/nx-infra-plugin/src/executors/scss-build/schema.ts b/packages/nx-infra-plugin/src/executors/scss-build/schema.ts new file mode 100644 index 000000000000..e58478f64be0 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/scss-build/schema.ts @@ -0,0 +1,6 @@ +export interface ScssBuildExecutorSchema { + mode: 'all' | 'ci'; + gulpBinary?: string; + allTaskName?: string; + ciTaskName?: string; +} From 3731f49f96935326b556cd908ec36f7d1809f0d3 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 29 Apr 2026 22:57:12 +0200 Subject: [PATCH 4/9] build:themes and build:themes-dev are implemented as nx-infra-plugin executor, --- .../executors/scss-build/executor.e2e.spec.ts | 81 +------ .../src/executors/scss-build/executor.ts | 217 ++++++++++++++++-- .../src/executors/scss-build/schema.json | 22 +- .../src/executors/scss-build/schema.ts | 6 +- 4 files changed, 209 insertions(+), 117 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts index bfb82e8c7f2c..9a21bbb3834e 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts @@ -1,82 +1,5 @@ -import { spawnSync } from 'child_process'; -import executor from './executor'; -import { ScssBuildExecutorSchema } from './schema'; -import { createMockContext } from '../../utils/test-utils'; - -jest.mock('child_process', () => ({ - spawnSync: jest.fn(), -})); - describe('ScssBuildExecutor E2E', () => { - const mockedSpawnSync = spawnSync as jest.MockedFunction; - - beforeEach(() => { - mockedSpawnSync.mockReset(); - }); - - it('runs full themes task in all mode', async () => { - mockedSpawnSync.mockReturnValue({ - pid: 123, - output: [], - stdout: null, - stderr: null, - status: 0, - signal: null, - } as unknown as ReturnType); - - const context = createMockContext(); - const options: ScssBuildExecutorSchema = { mode: 'all' }; - - const result = await executor(options, context); - - expect(result.success).toBe(true); - expect(mockedSpawnSync).toHaveBeenCalledWith( - 'pnpm', - ['exec', 'gulp', 'style-compiler-themes'], - expect.objectContaining({ - cwd: expect.stringContaining('packages'), - }), - ); - }); - - it('runs reduced themes task in ci mode', async () => { - mockedSpawnSync.mockReturnValue({ - pid: 456, - output: [], - stdout: null, - stderr: null, - status: 0, - signal: null, - } as unknown as ReturnType); - - const context = createMockContext(); - const options: ScssBuildExecutorSchema = { mode: 'ci' }; - - const result = await executor(options, context); - - expect(result.success).toBe(true); - expect(mockedSpawnSync).toHaveBeenCalledWith( - 'pnpm', - ['exec', 'gulp', 'style-compiler-themes-ci'], - expect.any(Object), - ); - }); - - it('returns false when gulp task fails', async () => { - mockedSpawnSync.mockReturnValue({ - pid: 789, - output: [], - stdout: null, - stderr: null, - status: 1, - signal: null, - } as unknown as ReturnType); - - const context = createMockContext(); - const options: ScssBuildExecutorSchema = { mode: 'all' }; - - const result = await executor(options, context); - - expect(result.success).toBe(false); + it('has test placeholder for native pipeline', () => { + expect(true).toBe(true); }); }); diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts index a3599caab828..593a550e8196 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts @@ -1,44 +1,211 @@ import { PromiseExecutor, logger } from '@nx/devkit'; -import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { createRequire } from 'module'; +import { glob } from 'glob'; import { ScssBuildExecutorSchema } from './schema'; import { resolveProjectPath } from '../../utils/path-resolver'; +import { ensureDir, readFileText, writeFileText } from '../../utils/file-operations'; -const DEFAULT_GULP_BINARY = 'gulp'; -const DEFAULT_ALL_TASK = 'style-compiler-themes'; -const DEFAULT_CI_TASK = 'style-compiler-themes-ci'; +const DEFAULT_BUNDLES_DIR = './scss/bundles'; +const DEFAULT_CSS_OUTPUT_DIR = '../devextreme/artifacts/css'; +const DEFAULT_DEV_BUNDLE_NAMES = [ + 'light', + 'light.compact', + 'dark', + 'contrast', + 'material.blue.light', + 'material.blue.light.compact', + 'material.blue.dark', + 'fluent.blue.light', + 'fluent.blue.light.compact', + 'fluent.blue.dark', + 'fluent.saas.light', + 'fluent.saas.dark', +]; -function resolveTaskName(options: ScssBuildExecutorSchema): string { - const allTaskName = options.allTaskName || DEFAULT_ALL_TASK; - const ciTaskName = options.ciTaskName || DEFAULT_CI_TASK; +const EULA_URL = 'https://js.devexpress.com/Licensing/'; - return options.mode === 'ci' ? ciTaskName : allTaskName; +interface BuildDependencies { + sass: any; + postcss: any; + autoprefixer: () => any; + CleanCss: new (options: unknown) => { minify: (input: string) => { styles: string } }; + themeOptions: { getThemes: () => Array<[string, string, string, string?]> }; + cleanCssSanitizeOptions: unknown; + cleanCssDevOptions: unknown; + devextremeVersion: string; +} + +function resolveDataUri(filePath: string, svgEncoding?: string): string { + const ext = path.extname(filePath).replace('.', ''); + const data = fs.readFileSync(filePath); + + if (ext === 'svg') { + const encoding = svgEncoding || 'image/svg+xml;charset=UTF-8'; + return `data:${encoding},${encodeURIComponent(data.toString())}`; + } + + return `data:image/${ext};base64,${data.toString('base64')}`; +} + +function createLicenseHeader(fileName: string, version: string): string { + return [ + '/*!', + `* DevExtreme (${fileName.replace(/\\/g, '/')})`, + `* Version: ${version}`, + `* Build date: ${new Date().toDateString()}`, + '*', + `* Copyright (c) 2012 - ${new Date().getFullYear()} Developer Express Inc. ALL RIGHTS RESERVED`, + `* Read about DevExtreme licensing here: ${EULA_URL}`, + '*/', + '', + ].join('\n'); +} + +function moveCharsetToTop(css: string): string { + const match = css.match(/@charset\s+[^;]+;\s*/); + if (!match) { + return css; + } + + const charset = match[0]; + const withoutCharset = css.replace(charset, ''); + return charset + withoutCharset; +} + +function generateBundleName(theme: string, size: string, color: string, mode?: string): string { + return 'dx' + + (theme === 'material' || theme === 'fluent' ? `.${theme}` : '') + + `.${color}` + + (mode ? `.${mode}` : '') + + (size === 'default' ? '' : '.compact') + + '.scss'; +} + +async function generateScssBundles( + projectRoot: string, + bundlesDir: string, + deps: BuildDependencies, +): Promise { + const resolvedBundlesDir = path.resolve(projectRoot, bundlesDir); + const buildDir = path.resolve(projectRoot, 'build'); + const readTemplate = async (theme: string) => + readFileText(path.join(buildDir, `bundle-template.${theme}.scss`)); + + await ensureDir(resolvedBundlesDir); + + const themes = deps.themeOptions.getThemes(); + for (const [theme, size, color, mode] of themes) { + const template = await readTemplate(theme); + const content = template.replace('$COLOR', color).replace('$SIZE', size).replace('$MODE', mode || ''); + const fileName = generateBundleName(theme, size, color, mode); + await writeFileText(path.join(resolvedBundlesDir, fileName), content); + } + + const commonTemplate = await readTemplate('common'); + await writeFileText(path.join(resolvedBundlesDir, 'dx.common.scss'), commonTemplate); +} + +function loadDependencies(projectRoot: string): BuildDependencies { + const projectRequire = createRequire(path.join(projectRoot, 'package.json')); + const workspaceRequire = createRequire(path.join(projectRoot, '..', '..', 'package.json')); + + return { + sass: projectRequire('sass-embedded'), + postcss: workspaceRequire('postcss'), + autoprefixer: workspaceRequire('autoprefixer'), + CleanCss: workspaceRequire('clean-css'), + themeOptions: projectRequire(path.resolve(projectRoot, 'build/theme-options.cjs')) as { + getThemes: () => Array<[string, string, string, string?]>; + }, + cleanCssSanitizeOptions: projectRequire(path.resolve(projectRoot, 'build/clean-css-options.json')), + cleanCssDevOptions: workspaceRequire( + path.resolve(projectRoot, '../devextreme-themebuilder/src/data/clean-css-options.json'), + ), + devextremeVersion: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')).version, + }; +} + +function resolveSourceFiles( + projectRoot: string, + options: ScssBuildExecutorSchema, +): Promise { + const bundlesDir = path.resolve(projectRoot, options.bundlesDir || DEFAULT_BUNDLES_DIR); + + if (options.mode === 'ci') { + const bundleNames = options.devBundles || DEFAULT_DEV_BUNDLE_NAMES; + return Promise.resolve(bundleNames.map((name) => path.join(bundlesDir, `dx.${name}.scss`))); + } + + return glob(path.join(bundlesDir, 'dx.*.scss'), { nodir: true }); +} + +function createDataUriFunction(projectRoot: string, sass: any): (args: any[]) => any { + return (args: any[]) => { + const argList = args[0].asList; + const hasEncoding = argList.size === 2; + const encoding = hasEncoding ? argList.get(0).assertString().text : undefined; + const url = argList.get(hasEncoding ? 1 : 0).assertString().text; + const absolutePath = path.resolve(projectRoot, url); + + const dataUri = resolveDataUri(absolutePath, encoding); + return new sass.SassString(`url("${dataUri}")`, { quotes: false }); + }; +} + +async function compileFile( + sourceFile: string, + outputDir: string, + options: ScssBuildExecutorSchema, + deps: BuildDependencies, + projectRoot: string, +): Promise { + const dataUriFunction = createDataUriFunction(projectRoot, deps.sass); + const compiled = deps.sass.compile(sourceFile, { + functions: { + 'data-uri($args...)': dataUriFunction, + }, + }); + + const postcssFactory = (deps.postcss as unknown as { default?: any }).default || deps.postcss; + const prefixed = await postcssFactory([deps.autoprefixer()]).process(compiled.css, { + from: undefined, + }); + + const minifierOptions = options.mode === 'ci' ? deps.cleanCssDevOptions : deps.cleanCssSanitizeOptions; + const minifier = new deps.CleanCss(minifierOptions); + const minified = minifier.minify(prefixed.css).styles; + + const outFileName = path.basename(sourceFile, '.scss') + '.css'; + const withHeader = createLicenseHeader(outFileName, deps.devextremeVersion) + moveCharsetToTop(minified); + await writeFileText(path.join(outputDir, outFileName), withHeader); } const runExecutor: PromiseExecutor = async (options, context) => { const projectRoot = resolveProjectPath(context); - const taskName = resolveTaskName(options); - const gulpBinary = options.gulpBinary || DEFAULT_GULP_BINARY; + const bundlesDir = options.bundlesDir || DEFAULT_BUNDLES_DIR; + const cssOutputDir = path.resolve(projectRoot, options.cssOutputDir || DEFAULT_CSS_OUTPUT_DIR); - logger.verbose(`Running SCSS build task "${taskName}" in mode "${options.mode}"`); + try { + const deps = loadDependencies(projectRoot); + await generateScssBundles(projectRoot, bundlesDir, deps); + await ensureDir(cssOutputDir); - const result = spawnSync('pnpm', ['exec', gulpBinary, taskName], { - cwd: projectRoot, - stdio: 'inherit', - shell: process.platform === 'win32', - env: process.env, - }); + const sources = await resolveSourceFiles(projectRoot, options); + const existingSources = sources.filter((source) => fs.existsSync(source)); - if (result.error) { - logger.error(`Failed to execute SCSS build task "${taskName}": ${result.error.message}`); - return { success: false }; - } + for (const source of existingSources) { + logger.verbose(`Compiling ${source}`); + await compileFile(source, cssOutputDir, options, deps, projectRoot); + } - if (result.status !== 0) { - logger.error(`SCSS build task "${taskName}" failed with exit code ${result.status ?? 1}`); + return { success: true }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error(`SCSS build failed: ${message}`); return { success: false }; } - - return { success: true }; }; export default runExecutor; diff --git a/packages/nx-infra-plugin/src/executors/scss-build/schema.json b/packages/nx-infra-plugin/src/executors/scss-build/schema.json index ae326fc60aaa..168f3b7da931 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/schema.json +++ b/packages/nx-infra-plugin/src/executors/scss-build/schema.json @@ -9,20 +9,22 @@ "description": "Compilation mode. all = full themes set, ci = reduced dev themes set.", "enum": ["all", "ci"] }, - "gulpBinary": { + "bundlesDir": { "type": "string", - "description": "Gulp executable to run via pnpm exec", - "default": "gulp" + "description": "Generated SCSS bundles directory relative to project root", + "default": "./scss/bundles" }, - "allTaskName": { + "cssOutputDir": { "type": "string", - "description": "Gulp task name for full themes build", - "default": "style-compiler-themes" + "description": "Output CSS artifacts directory relative to project root", + "default": "../devextreme/artifacts/css" }, - "ciTaskName": { - "type": "string", - "description": "Gulp task name for CI/dev themes build", - "default": "style-compiler-themes-ci" + "devBundles": { + "type": "array", + "description": "Bundle names used in CI mode", + "items": { + "type": "string" + } } }, "required": ["mode"] diff --git a/packages/nx-infra-plugin/src/executors/scss-build/schema.ts b/packages/nx-infra-plugin/src/executors/scss-build/schema.ts index e58478f64be0..2df3f2853b66 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/schema.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/schema.ts @@ -1,6 +1,6 @@ export interface ScssBuildExecutorSchema { mode: 'all' | 'ci'; - gulpBinary?: string; - allTaskName?: string; - ciTaskName?: string; + bundlesDir?: string; + cssOutputDir?: string; + devBundles?: string[]; } From 765d936f2ee85cf68926e0e6b76bfedba9f01289 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 29 Apr 2026 23:36:36 +0200 Subject: [PATCH 5/9] move watching mode from gulp to nx-infra-plugin executor, --- packages/devextreme-scss/package.json | 4 +- packages/devextreme-scss/project.json | 21 ++- .../src/executors/scss-build/executor.ts | 174 ++++++++++++++++-- .../src/executors/scss-build/schema.json | 17 ++ .../src/executors/scss-build/schema.ts | 2 + 5 files changed, 201 insertions(+), 17 deletions(-) diff --git a/packages/devextreme-scss/package.json b/packages/devextreme-scss/package.json index 6d80381a2f0b..ec0f925e8d79 100644 --- a/packages/devextreme-scss/package.json +++ b/packages/devextreme-scss/package.json @@ -20,10 +20,10 @@ "ts-jest": "29.1.2" }, "scripts": { - "build": "gulp", + "build": "pnpm --workspace-root nx build devextreme-scss", "lint": "stylelint scss/widgets", "test": "jest --no-coverage --runInBand --config=./tests/jest.config.json", - "watch": "gulp watch" + "watch": "pnpm --workspace-root nx run devextreme-scss:watch" }, "version": "26.1.0" } diff --git a/packages/devextreme-scss/project.json b/packages/devextreme-scss/project.json index a52889b588eb..2fc6380391ea 100644 --- a/packages/devextreme-scss/project.json +++ b/packages/devextreme-scss/project.json @@ -53,8 +53,7 @@ "inputs": [ "{projectRoot}/build/**/*", "{projectRoot}/images/**/*", - "{projectRoot}/scss/**/*", - "{projectRoot}/gulpfile.js" + "{projectRoot}/scss/**/*" ], "outputs": [ "{projectRoot}/scss/bundles", @@ -70,8 +69,7 @@ "inputs": [ "{projectRoot}/build/**/*", "{projectRoot}/images/**/*", - "{projectRoot}/scss/**/*", - "{projectRoot}/gulpfile.js" + "{projectRoot}/scss/**/*" ], "outputs": [ "{projectRoot}/scss/bundles", @@ -131,6 +129,21 @@ ], "cache": true }, + "watch": { + "executor": "devextreme-nx-infra-plugin:scss-build", + "options": { + "mode": "all", + "watch": true + }, + "inputs": [ + "{projectRoot}/build/**/*", + "{projectRoot}/fonts/**/*", + "{projectRoot}/icons/**/*", + "{projectRoot}/images/**/*", + "{projectRoot}/scss/**/*" + ], + "cache": false + }, "lint": { "executor": "nx:run-script", "options": { diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts index 593a550e8196..e55b2db55256 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts @@ -37,6 +37,8 @@ interface BuildDependencies { devextremeVersion: string; } +type MinifyProfile = 'all' | 'ci'; + function resolveDataUri(filePath: string, svgEncoding?: string): string { const ext = path.extname(filePath).replace('.', ''); const data = fs.readFileSync(filePath); @@ -127,6 +129,21 @@ function loadDependencies(projectRoot: string): BuildDependencies { }; } +function normalizeBundlesOption(bundles?: string[] | string): string[] | undefined { + if (!bundles) { + return undefined; + } + + if (Array.isArray(bundles)) { + return bundles; + } + + return bundles + .split(',') + .map((bundle) => bundle.trim()) + .filter(Boolean); +} + function resolveSourceFiles( projectRoot: string, options: ScssBuildExecutorSchema, @@ -157,7 +174,7 @@ function createDataUriFunction(projectRoot: string, sass: any): (args: any[]) => async function compileFile( sourceFile: string, outputDir: string, - options: ScssBuildExecutorSchema, + minifyProfile: MinifyProfile, deps: BuildDependencies, projectRoot: string, ): Promise { @@ -173,7 +190,7 @@ async function compileFile( from: undefined, }); - const minifierOptions = options.mode === 'ci' ? deps.cleanCssDevOptions : deps.cleanCssSanitizeOptions; + const minifierOptions = minifyProfile === 'ci' ? deps.cleanCssDevOptions : deps.cleanCssSanitizeOptions; const minifier = new deps.CleanCss(minifierOptions); const minified = minifier.minify(prefixed.css).styles; @@ -182,24 +199,159 @@ async function compileFile( await writeFileText(path.join(outputDir, outFileName), withHeader); } -const runExecutor: PromiseExecutor = async (options, context) => { - const projectRoot = resolveProjectPath(context); +async function copyAssets(projectRoot: string, cssOutputDir: string): Promise { + const fontsFrom = path.resolve(projectRoot, 'fonts'); + const iconsFrom = path.resolve(projectRoot, 'icons'); + const fontsTo = path.resolve(cssOutputDir, 'fonts'); + const iconsTo = path.resolve(cssOutputDir, 'icons'); + + if (fs.existsSync(fontsFrom)) { + await ensureDir(fontsTo); + fs.cpSync(fontsFrom, fontsTo, { recursive: true }); + } + + if (fs.existsSync(iconsFrom)) { + await ensureDir(iconsTo); + fs.cpSync(iconsFrom, iconsTo, { recursive: true }); + } +} + +function resolveSourcesByBundleNames( + projectRoot: string, + bundlesDir: string, + bundleNames: string[], +): string[] { + const resolvedBundlesDir = path.resolve(projectRoot, bundlesDir); + const sources: string[] = []; + + for (const bundleName of bundleNames) { + const source = path.join(resolvedBundlesDir, `dx.${bundleName}.scss`); + if (fs.existsSync(source)) { + sources.push(source); + } else { + logger.warn(`${source} file does not exist`); + } + } + + return sources; +} + +function getWatchBundleNames(options: ScssBuildExecutorSchema): string[] { + const explicitBundles = normalizeBundlesOption(options.bundles); + if (explicitBundles && explicitBundles.length > 0) { + return explicitBundles; + } + + return options.devBundles || DEFAULT_DEV_BUNDLE_NAMES; +} + +async function runSingleBuild( + projectRoot: string, + options: ScssBuildExecutorSchema, + deps: BuildDependencies, +): Promise { const bundlesDir = options.bundlesDir || DEFAULT_BUNDLES_DIR; const cssOutputDir = path.resolve(projectRoot, options.cssOutputDir || DEFAULT_CSS_OUTPUT_DIR); - try { - const deps = loadDependencies(projectRoot); + await generateScssBundles(projectRoot, bundlesDir, deps); + await ensureDir(cssOutputDir); + + const sources = await resolveSourceFiles(projectRoot, options); + const existingSources = sources.filter((source) => fs.existsSync(source)); + const minifyProfile: MinifyProfile = options.mode === 'ci' ? 'ci' : 'all'; + + for (const source of existingSources) { + logger.verbose(`Compiling ${source}`); + await compileFile(source, cssOutputDir, minifyProfile, deps, projectRoot); + } +} + +async function runWatchBuild( + projectRoot: string, + options: ScssBuildExecutorSchema, + deps: BuildDependencies, +): Promise<{ success: boolean }> { + const bundlesDir = options.bundlesDir || DEFAULT_BUNDLES_DIR; + const cssOutputDir = path.resolve(projectRoot, options.cssOutputDir || DEFAULT_CSS_OUTPUT_DIR); + const watchDir = path.resolve(projectRoot, 'scss'); + const watchBundleNames = getWatchBundleNames(options); + + const rebuild = async (): Promise => { await generateScssBundles(projectRoot, bundlesDir, deps); await ensureDir(cssOutputDir); - const sources = await resolveSourceFiles(projectRoot, options); - const existingSources = sources.filter((source) => fs.existsSync(source)); + const sources = resolveSourcesByBundleNames(projectRoot, bundlesDir, watchBundleNames); + for (const source of sources) { + await compileFile(source, cssOutputDir, 'all', deps, projectRoot); + } + + await copyAssets(projectRoot, cssOutputDir); + }; + + await rebuild(); + logger.info('scss-build watch mode is watching for changes...'); + + return await new Promise<{ success: boolean }>((resolve) => { + let timer: NodeJS.Timeout | undefined; + let busy = false; + + const scheduleRebuild = () => { + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(async () => { + if (busy) { + return; + } + + busy = true; + try { + await rebuild(); + logger.info('scss-build watch: rebuild complete'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error(`scss-build watch rebuild failed: ${message}`); + } finally { + busy = false; + } + }, 200); + }; + + const watcher = fs.watch( + watchDir, + { recursive: true }, + (_eventType, fileName) => { + if (!fileName || !fileName.endsWith('.scss')) { + return; + } + scheduleRebuild(); + }, + ); + + const stopWatcher = () => { + watcher.close(); + if (timer) { + clearTimeout(timer); + } + resolve({ success: true }); + }; - for (const source of existingSources) { - logger.verbose(`Compiling ${source}`); - await compileFile(source, cssOutputDir, options, deps, projectRoot); + process.once('SIGINT', stopWatcher); + process.once('SIGTERM', stopWatcher); + }); +} + +const runExecutor: PromiseExecutor = async (options, context) => { + const projectRoot = resolveProjectPath(context); + + try { + const deps = loadDependencies(projectRoot); + if (options.watch) { + return await runWatchBuild(projectRoot, options, deps); } + await runSingleBuild(projectRoot, options, deps); return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/packages/nx-infra-plugin/src/executors/scss-build/schema.json b/packages/nx-infra-plugin/src/executors/scss-build/schema.json index 168f3b7da931..46150b76b51a 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/schema.json +++ b/packages/nx-infra-plugin/src/executors/scss-build/schema.json @@ -25,6 +25,23 @@ "items": { "type": "string" } + }, + "watch": { + "type": "boolean", + "description": "Watch SCSS sources and rebuild on changes", + "default": false + }, + "bundles": { + "description": "Bundle names for watch mode (array or comma-separated string)", + "oneOf": [ + { + "type": "array", + "items": { "type": "string" } + }, + { + "type": "string" + } + ] } }, "required": ["mode"] diff --git a/packages/nx-infra-plugin/src/executors/scss-build/schema.ts b/packages/nx-infra-plugin/src/executors/scss-build/schema.ts index 2df3f2853b66..cbbcb5dccd3d 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/schema.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/schema.ts @@ -3,4 +3,6 @@ export interface ScssBuildExecutorSchema { bundlesDir?: string; cssOutputDir?: string; devBundles?: string[]; + watch?: boolean; + bundles?: string[] | string; } From 529ae5e9035189928a91bed4f169688b91187406 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 30 Apr 2026 00:06:25 +0200 Subject: [PATCH 6/9] remove gulp from devextreme-scss --- .github/workflows/themebuilder_tests.yml | 2 +- .../devextreme-scss/build/gulp-data-uri.js | 46 ----- .../devextreme-scss/build/style-compiler.js | 164 ------------------ packages/devextreme-scss/gulpfile.js | 40 ----- packages/devextreme-scss/package.json | 10 -- packages/devextreme-scss/project.json | 6 +- packages/devextreme/package.json | 2 +- pnpm-lock.yaml | 31 +--- 8 files changed, 5 insertions(+), 296 deletions(-) delete mode 100644 packages/devextreme-scss/build/gulp-data-uri.js delete mode 100644 packages/devextreme-scss/build/style-compiler.js delete mode 100644 packages/devextreme-scss/gulpfile.js diff --git a/.github/workflows/themebuilder_tests.yml b/.github/workflows/themebuilder_tests.yml index da7ccceed35d..d5180e339cd1 100644 --- a/.github/workflows/themebuilder_tests.yml +++ b/.github/workflows/themebuilder_tests.yml @@ -52,7 +52,7 @@ jobs: - name: Build etalon bundles working-directory: ./packages/devextreme-scss - run: pnpm exec gulp style-compiler-themes-ci + run: pnpm --workspace-root nx run devextreme-scss:build:ci - name: Build working-directory: ./packages/devextreme-themebuilder diff --git a/packages/devextreme-scss/build/gulp-data-uri.js b/packages/devextreme-scss/build/gulp-data-uri.js deleted file mode 100644 index 699d1a4d51c2..000000000000 --- a/packages/devextreme-scss/build/gulp-data-uri.js +++ /dev/null @@ -1,46 +0,0 @@ -import path, { dirname } from 'path'; -import fs from 'fs'; -import sass from 'sass-embedded'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const dataUriRegex = /data-uri\((?:'(image\/svg\+xml;charset=UTF-8)',\s)?['"]?([^)'"]+)['"]?\)/g; - -const svg = (buffer, svgEncoding) => { - const encoding = svgEncoding || 'image/svg+xml;charset=UTF-8'; - const svg = encodeURIComponent(buffer.toString()); - - return `"data:${encoding},${svg}"`; -}; - -const img = (buffer, ext) => { - return `"data:image/${ext};base64,${buffer.toString('base64')}"`; -}; - -const handler = (_, svgEncoding, fileName) => { - const relativePath = path.join(__dirname, '..', fileName); - const filePath = path.resolve(relativePath); - const ext = filePath.split('.').pop(); - const data = fs.readFileSync(filePath); - const buffer = Buffer.from(data); - const escapedString = ext === 'svg' ? svg(buffer, svgEncoding) : img(buffer, ext); - return `url(${escapedString})`; -}; - -const sassFunction = (args) => { - const getTextFromSass = (sassValue) => sassValue.assertString().text; - const argList = args[0].asList; - const hasEncoding = argList.size === 2; - const encoding = hasEncoding ? getTextFromSass(argList.get(0)) : null; - const url = getTextFromSass(argList.get(hasEncoding ? 1 : 0)); - - return new sass.SassString(handler(null, encoding, url), { quotes: false }); -}; - -export const resolveDataUri = (content) => content.replace(dataUriRegex, handler); - -export const sassFunctions = { - 'data-uri($args...)': sassFunction, -}; diff --git a/packages/devextreme-scss/build/style-compiler.js b/packages/devextreme-scss/build/style-compiler.js deleted file mode 100644 index 29f70deab20f..000000000000 --- a/packages/devextreme-scss/build/style-compiler.js +++ /dev/null @@ -1,164 +0,0 @@ -import gulp from 'gulp'; -const { task, src, parallel, series, dest, watch } = gulp; - -import { join } from 'path'; -import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; -import replace from 'gulp-replace'; -import plumber from 'gulp-plumber'; -import gulpSass from 'gulp-sass'; -import sassEmbedded from 'sass-embedded'; -import CleanCss from 'clean-css'; -import through from 'through2'; -import parseArguments from 'minimist'; -import autoprefixer from 'gulp-autoprefixer'; -import { createRequire } from 'module'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const require = createRequire(import.meta.url); -const cleanCssSanitizeOptions = require('./clean-css-options.json'); -const cleanCssOptions = require('../../devextreme-themebuilder/src/data/clean-css-options.json'); -const { starLicense } = require('../../devextreme/build/gulp/header-pipes.js'); - -const { getThemes } = require('./theme-options.cjs'); -import { sassFunctions } from './gulp-data-uri.js'; - -const sass = gulpSass(sassEmbedded); - -const cssArtifactsPath = join(process.cwd(), '..', 'devextreme', 'artifacts', 'css'); - -const DEFAULT_DEV_BUNDLE_NAMES = [ - 'light', - 'light.compact', - 'dark', - 'contrast', - 'material.blue.light', - 'material.blue.light.compact', - 'material.blue.dark', - 'fluent.blue.light', - 'fluent.blue.light.compact', - 'fluent.blue.dark', - 'fluent.saas.light', - 'fluent.saas.dark', -]; - -const getBundleSourcePath = name => `scss/bundles/dx.${name}.scss`; - -const compileBundles = (bundles, isDevBundle) => { - return src(bundles) - .pipe(plumber(e => { - console.log(e); - this.emit('end'); - })) - .on('data', (chunk) => console.log('Build: ', chunk.path)) - .pipe(sass({ - functions: sassFunctions - })) - .pipe(autoprefixer()) - .pipe(through.obj((file, enc, callback) => { - const content = file.contents.toString(); - new CleanCss(isDevBundle ? cleanCssOptions : cleanCssSanitizeOptions).minify(content, (_, css) => { - file.contents = new Buffer.from(css.styles); - callback(null, file); - }); - })) - .pipe(starLicense()) - .pipe(replace(/([\s\S]*)(@charset.*?;\s)/, '$2$1')) - .pipe(dest(cssArtifactsPath)); -}; - -function saveBundleFile(folder, fileName, content) { - const bundlePath = join(folder, fileName); - if(!existsSync(folder)) mkdirSync(folder); - writeFileSync(bundlePath, content); -} - -function generateScssBundleName(theme, size, color, mode) { - return 'dx' + - (theme === 'material' || theme === 'fluent' - ? `.${theme}` - : '') - + `.${color}` + - (mode ? `.${mode}` : '') + - (size === 'default' ? '' : '.compact') + - '.scss'; -} - -function generateScssBundles(bundlesFolder, getBundleContent) { - const saveBundle = (theme, size, color, mode) => { - const bundleName = generateScssBundleName(theme, size, color, mode); - const content = getBundleContent(theme, size, color, mode); - - saveBundleFile(bundlesFolder, bundleName, content); - }; - - getThemes().forEach(([theme, size, color, mode]) => saveBundle(theme, size, color, mode)); -} - -function createBundles(callback) { - const bundlesFolder = join(process.cwd(), 'scss', 'bundles'); - const readTemplate = (theme) => readFileSync(join(__dirname, `bundle-template.${theme}.scss`), 'utf8'); - const getBundleContent = (theme, size, color, mode) => { - const bundleTemplate = readTemplate(theme); - const bundleContent = bundleTemplate - .replace('$COLOR', color) - .replace('$SIZE', size) - .replace('$MODE', mode); - return bundleContent; - }; - - generateScssBundles(bundlesFolder, getBundleContent); - saveBundleFile(bundlesFolder, 'dx.common.scss', readTemplate('common')); - - if(callback) callback(); -} - -task('create-scss-bundles', createBundles); - -task('copy-fonts-and-icons', () => { - return src(['fonts/**/*', 'icons/**/*'], { base: '.' }) - .pipe(dest(cssArtifactsPath)); -}); - -task('compile-themes-all', () => compileBundles(getBundleSourcePath('*'))); -task('compile-themes-dev', () => compileBundles(DEFAULT_DEV_BUNDLE_NAMES.map(getBundleSourcePath), true)); - -task('style-compiler-themes', series( - 'create-scss-bundles', - parallel( - 'compile-themes-all', - 'copy-fonts-and-icons' - ) -)); - -task('style-compiler-themes-ci', series( - 'create-scss-bundles', - parallel( - 'compile-themes-dev', - 'copy-fonts-and-icons' - ) -)); - -task('style-compiler-themes-watch', () => { - const args = parseArguments(process.argv); - const bundlesArg = args['bundles']; - - const bundles = ( - bundlesArg - ? bundlesArg.split(',') - : DEFAULT_DEV_BUNDLE_NAMES) - .map((bundle) => { - const sourcePath = getBundleSourcePath(bundle); - if(existsSync(sourcePath)) { - return sourcePath; - } - console.log(`${sourcePath} file does not exists`); - return null; - }); - - watch('scss/**/*', parallel(() => compileBundles(bundles), 'copy-fonts-and-icons')) - .on('ready', () => console.log('style-compiler-themes task is watching for changes...')); -}); diff --git a/packages/devextreme-scss/gulpfile.js b/packages/devextreme-scss/gulpfile.js deleted file mode 100644 index 0494cad08db7..000000000000 --- a/packages/devextreme-scss/gulpfile.js +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-env node */ -/* eslint-disable no-console */ - -import gulp from 'gulp'; -import cache from 'gulp-cache'; -import { createRequire } from 'module'; - -const require = createRequire(import.meta.url); -const env = require('../devextreme/build/gulp/env-variables.js'); -const del = require('del'); - -gulp.task('clean', function(callback) { - del.sync([ - '../devextreme/artifacts/css/**', - '../devextreme/scss/bundles/**' - ], { force: true }); - cache.clearAll(); - callback(); -}); - -import './build/style-compiler.js'; - -if(env.TEST_CI) { - console.warn('Using test CI mode!'); -} - -function createStyleCompilerBatch() { - return gulp.series( - 'clean', - env.TEST_CI - ? ['style-compiler-themes-ci'] - : ['style-compiler-themes'] - ); -} - -gulp.task('default', createStyleCompilerBatch()); - -gulp.task('watch', gulp.series( - 'style-compiler-themes-watch' -)); diff --git a/packages/devextreme-scss/package.json b/packages/devextreme-scss/package.json index ec0f925e8d79..db8a35022966 100644 --- a/packages/devextreme-scss/package.json +++ b/packages/devextreme-scss/package.json @@ -3,20 +3,10 @@ "type": "module", "devDependencies": { "clean-css": "5.3.3", - "del": "2.2.2", - "gulp": "4.0.2", - "gulp-autoprefixer": "10.0.0", - "gulp-cache": "1.1.3", - "gulp-plumber": "1.2.1", - "gulp-replace": "0.6.1", - "gulp-sass": "6.0.1", - "gulp-shell": "0.8.0", - "minimist": "1.2.8", "sass-embedded": "1.93.3", "stylelint": "15.11.0", "stylelint-config-standard-scss": "9.0.0", "stylelint-scss": "6.10.0", - "through2": "2.0.5", "ts-jest": "29.1.2" }, "scripts": { diff --git a/packages/devextreme-scss/project.json b/packages/devextreme-scss/project.json index 2fc6380391ea..f720e5dfbfdd 100644 --- a/packages/devextreme-scss/project.json +++ b/packages/devextreme-scss/project.json @@ -92,8 +92,7 @@ "{projectRoot}/fonts/**/*", "{projectRoot}/icons/**/*", "{projectRoot}/images/**/*", - "{projectRoot}/scss/**/*", - "{projectRoot}/gulpfile.js" + "{projectRoot}/scss/**/*" ], "outputs": [ "{projectRoot}/scss/bundles", @@ -118,8 +117,7 @@ "{projectRoot}/fonts/**/*", "{projectRoot}/icons/**/*", "{projectRoot}/images/**/*", - "{projectRoot}/scss/**/*", - "{projectRoot}/gulpfile.js" + "{projectRoot}/scss/**/*" ], "outputs": [ "{projectRoot}/scss/bundles", diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 133a45eea79f..b0e1b2825143 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -233,7 +233,7 @@ "build:testcafe": "cross-env DEVEXTREME_TEST_CI=TRUE BUILD_ESM_PACKAGE=true BUILD_TESTCAFE=TRUE gulp default", "build-npm-devextreme": "cross-env BUILD_ESM_PACKAGE=true gulp default", "build-dist": "cross-env BUILD_ESM_PACKAGE=true gulp default --uglify", - "build-themes": "gulp style-compiler-themes", + "build-themes": "pnpm --workspace-root nx run devextreme-scss:build:themes && pnpm --workspace-root nx run devextreme-scss:copy:assets", "build:react": "gulp generate-react", "build:react:watch": "gulp generate-react-watch", "build:react:typescript": "gulp generate-react-typescript", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 345327887735..e0debef85932 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2105,33 +2105,6 @@ importers: clean-css: specifier: 5.3.3 version: 5.3.3 - del: - specifier: 2.2.2 - version: 2.2.2 - gulp: - specifier: 4.0.2 - version: 4.0.2 - gulp-autoprefixer: - specifier: 10.0.0 - version: 10.0.0(gulp@4.0.2) - gulp-cache: - specifier: 1.1.3 - version: 1.1.3 - gulp-plumber: - specifier: 1.2.1 - version: 1.2.1 - gulp-replace: - specifier: 0.6.1 - version: 0.6.1 - gulp-sass: - specifier: 6.0.1 - version: 6.0.1 - gulp-shell: - specifier: 0.8.0 - version: 0.8.0 - minimist: - specifier: 1.2.8 - version: 1.2.8 sass-embedded: specifier: 1.93.3 version: 1.93.3 @@ -2144,9 +2117,6 @@ importers: stylelint-scss: specifier: 6.10.0 version: 6.10.0(stylelint@15.11.0(typescript@5.9.3)) - through2: - specifier: 2.0.5 - version: 2.0.5 ts-jest: specifier: 29.1.2 version: 29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.19.37)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3) @@ -15037,6 +15007,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qified@0.9.1: From 7416e09fe30b08ea5cfe5e4b2d1fe8324a7ed2b9 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 30 Apr 2026 00:26:08 +0200 Subject: [PATCH 7/9] add tests --- .../executors/scss-build/executor.e2e.spec.ts | 181 +++++++++++++++++- .../src/executors/scss-build/executor.ts | 5 +- 2 files changed, 182 insertions(+), 4 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts index 9a21bbb3834e..5c1eed640695 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts @@ -1,5 +1,182 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import executor from './executor'; +import { ScssBuildExecutorSchema } from './schema'; +import { createMockContext, createTempDir, cleanupTempDir } from '../../utils/test-utils'; +import { writeFileText, writeJson, readFileText } from '../../utils'; + +function createMockModules(workspaceRoot: string, projectRoot: string): void { + const projectNodeModules = path.join(projectRoot, 'node_modules', 'sass-embedded'); + fs.mkdirSync(projectNodeModules, { recursive: true }); + fs.writeFileSync( + path.join(projectNodeModules, 'index.js'), + [ + 'class SassString {', + ' constructor(value) { this.value = value; }', + '}', + 'module.exports = {', + ' SassString,', + " compile: () => ({ css: '@charset \"UTF-8\"; .a{display:flex}' })", + '};', + '', + ].join('\n'), + 'utf8', + ); + + const workspaceNodeModules = path.join(workspaceRoot, 'node_modules'); + fs.mkdirSync(workspaceNodeModules, { recursive: true }); + + const postcssDir = path.join(workspaceNodeModules, 'postcss'); + fs.mkdirSync(postcssDir, { recursive: true }); + fs.writeFileSync( + path.join(postcssDir, 'index.js'), + [ + 'module.exports = function postcss() {', + ' return {', + ' process: async (css) => ({ css: css + "/*prefixed*/" })', + ' };', + '};', + '', + ].join('\n'), + 'utf8', + ); + + const autoprefixerDir = path.join(workspaceNodeModules, 'autoprefixer'); + fs.mkdirSync(autoprefixerDir, { recursive: true }); + fs.writeFileSync( + path.join(autoprefixerDir, 'index.js'), + 'module.exports = function autoprefixer() { return { postcssPlugin: "autoprefixer" }; };', + 'utf8', + ); + + const cleanCssDir = path.join(workspaceNodeModules, 'clean-css'); + fs.mkdirSync(cleanCssDir, { recursive: true }); + fs.writeFileSync( + path.join(cleanCssDir, 'index.js'), + [ + 'module.exports = class CleanCss {', + ' constructor(options) { this.options = options || {}; }', + ' minify(css) {', + ' return { styles: css + "/*min:" + (this.options.profile || "none") + "*/" };', + ' }', + '};', + '', + ].join('\n'), + 'utf8', + ); +} + +async function setupProjectStructure(workspaceRoot: string): Promise { + const projectRoot = path.join(workspaceRoot, 'packages', 'devextreme-scss'); + const buildDir = path.join(projectRoot, 'build'); + fs.mkdirSync(buildDir, { recursive: true }); + + await writeJson(path.join(workspaceRoot, 'package.json'), { name: 'workspace' }); + await writeJson(path.join(projectRoot, 'package.json'), { name: 'devextreme-scss' }); + + await writeJson(path.join(projectRoot, 'build', 'clean-css-options.json'), { profile: 'all' }); + + const themebuilderDataDir = path.join( + workspaceRoot, + 'packages', + 'devextreme-themebuilder', + 'src', + 'data', + ); + fs.mkdirSync(themebuilderDataDir, { recursive: true }); + await writeJson(path.join(themebuilderDataDir, 'clean-css-options.json'), { profile: 'ci' }); + + const devextremeDir = path.join(workspaceRoot, 'packages', 'devextreme'); + fs.mkdirSync(devextremeDir, { recursive: true }); + await writeJson(path.join(devextremeDir, 'package.json'), { version: '26.1.0-test' }); + + await writeFileText( + path.join(buildDir, 'theme-options.cjs'), + [ + 'module.exports = {', + ' getThemes: () => [', + " ['generic', 'default', 'light'],", + ' ],', + '};', + '', + ].join('\n'), + ); + + await writeFileText(path.join(buildDir, 'bundle-template.common.scss'), '.common { color: red; }'); + await writeFileText(path.join(buildDir, 'bundle-template.generic.scss'), '.generic-$COLOR { color: red; }'); + + createMockModules(workspaceRoot, projectRoot); + return projectRoot; +} + describe('ScssBuildExecutor E2E', () => { - it('has test placeholder for native pipeline', () => { - expect(true).toBe(true); + let tempDir: string; + + beforeEach(() => { + tempDir = createTempDir('nx-scss-build-e2e-'); + }); + + afterEach(() => { + cleanupTempDir(tempDir); + }); + + it('builds all mode bundles and applies license/minification profile', async () => { + const projectRoot = await setupProjectStructure(tempDir); + const context = createMockContext({ + root: tempDir, + projectName: 'devextreme-scss', + projectRoot: 'packages/devextreme-scss', + }); + + const options: ScssBuildExecutorSchema = { mode: 'all', cssOutputDir: './artifacts/css' }; + const result = await executor(options, context); + + expect(result.success).toBe(true); + expect(fs.existsSync(path.join(projectRoot, 'scss', 'bundles', 'dx.light.scss'))).toBe(true); + expect(fs.existsSync(path.join(projectRoot, 'scss', 'bundles', 'dx.common.scss'))).toBe(true); + + const cssDir = path.join(projectRoot, 'artifacts', 'css'); + const generatedCssFiles = fs + .readdirSync(cssDir) + .filter((name) => name.endsWith('.css')) + .sort(); + expect(generatedCssFiles.length).toBeGreaterThan(0); + expect(generatedCssFiles).toContain('dx.common.css'); + + const commonCss = await readFileText(path.join(cssDir, 'dx.common.css')); + + expect(commonCss).toContain('Version: 26.1.0-test'); + expect(commonCss).toContain('/*min:all*/'); + expect(commonCss).toContain('DevExtreme (dx.common.css)'); + }); + + it('builds ci mode only for selected dev bundles and uses ci profile', async () => { + const projectRoot = await setupProjectStructure(tempDir); + const context = createMockContext({ + root: tempDir, + projectName: 'devextreme-scss', + projectRoot: 'packages/devextreme-scss', + }); + + const options: ScssBuildExecutorSchema = { + mode: 'ci', + devBundles: ['light'], + cssOutputDir: './artifacts/css', + }; + const result = await executor(options, context); + + expect(result.success).toBe(true); + + const cssDir = path.join(projectRoot, 'artifacts', 'css'); + const generatedCssFiles = fs + .readdirSync(cssDir) + .filter((name) => name.endsWith('.css')) + .sort(); + + expect(generatedCssFiles).toEqual(['dx.light.css']); + const lightCss = await readFileText(path.join(cssDir, 'dx.light.css')); + expect(lightCss).toContain('/*min:ci*/'); + + expect(fs.existsSync(path.join(projectRoot, 'scss', 'bundles', 'dx.common.scss'))).toBe(true); }); }); diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts index e55b2db55256..f1be4ee994c7 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { createRequire } from 'module'; import { glob } from 'glob'; import { ScssBuildExecutorSchema } from './schema'; -import { resolveProjectPath } from '../../utils/path-resolver'; +import { normalizeGlobPathForWindows, resolveProjectPath } from '../../utils/path-resolver'; import { ensureDir, readFileText, writeFileText } from '../../utils/file-operations'; const DEFAULT_BUNDLES_DIR = './scss/bundles'; @@ -155,7 +155,8 @@ function resolveSourceFiles( return Promise.resolve(bundleNames.map((name) => path.join(bundlesDir, `dx.${name}.scss`))); } - return glob(path.join(bundlesDir, 'dx.*.scss'), { nodir: true }); + const pattern = normalizeGlobPathForWindows(path.join(bundlesDir, 'dx.*.scss')); + return glob(pattern, { nodir: true }); } function createDataUriFunction(projectRoot: string, sass: any): (args: any[]) => any { From 27adba3e9b934fc7f64445b6a470c79c877505ff Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 30 Apr 2026 13:35:23 +0200 Subject: [PATCH 8/9] fix executor for dex-scss --- .../src/executors/scss-build/executor.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts index f1be4ee994c7..fbae39c61b01 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts @@ -29,12 +29,13 @@ const EULA_URL = 'https://js.devexpress.com/Licensing/'; interface BuildDependencies { sass: any; postcss: any; - autoprefixer: () => any; + autoprefixer: (options?: { overrideBrowserslist?: string[] }) => any; CleanCss: new (options: unknown) => { minify: (input: string) => { styles: string } }; themeOptions: { getThemes: () => Array<[string, string, string, string?]> }; cleanCssSanitizeOptions: unknown; cleanCssDevOptions: unknown; devextremeVersion: string; + browsersList: string[]; } type MinifyProfile = 'all' | 'ci'; @@ -71,9 +72,10 @@ function moveCharsetToTop(css: string): string { return css; } - const charset = match[0]; - const withoutCharset = css.replace(charset, ''); - return charset + withoutCharset; + const charset = match[0].trim(); + const withoutCharset = css.replace(match[0], '').replace(/^\s+/, ''); + + return `${charset}${withoutCharset}`; } function generateBundleName(theme: string, size: string, color: string, mode?: string): string { @@ -126,6 +128,7 @@ function loadDependencies(projectRoot: string): BuildDependencies { path.resolve(projectRoot, '../devextreme-themebuilder/src/data/clean-css-options.json'), ), devextremeVersion: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')).version, + browsersList: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')).browserslist, }; } @@ -187,7 +190,11 @@ async function compileFile( }); const postcssFactory = (deps.postcss as unknown as { default?: any }).default || deps.postcss; - const prefixed = await postcssFactory([deps.autoprefixer()]).process(compiled.css, { + const prefixed = await postcssFactory([ + deps.autoprefixer({ + overrideBrowserslist: deps.browsersList, + }), + ]).process(compiled.css, { from: undefined, }); From e2eae0997fd95e4569c5399274cce6b11e495c5c Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 30 Apr 2026 22:37:06 +0200 Subject: [PATCH 9/9] fix lint errors --- .../executors/scss-build/executor.e2e.spec.ts | 12 ++++-- .../src/executors/scss-build/executor.ts | 43 +++++++++++-------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts index 5c1eed640695..c7c8c46bd971 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts @@ -16,7 +16,7 @@ function createMockModules(workspaceRoot: string, projectRoot: string): void { '}', 'module.exports = {', ' SassString,', - " compile: () => ({ css: '@charset \"UTF-8\"; .a{display:flex}' })", + ' compile: () => ({ css: \'@charset "UTF-8"; .a{display:flex}\' })', '};', '', ].join('\n'), @@ -102,8 +102,14 @@ async function setupProjectStructure(workspaceRoot: string): Promise { ].join('\n'), ); - await writeFileText(path.join(buildDir, 'bundle-template.common.scss'), '.common { color: red; }'); - await writeFileText(path.join(buildDir, 'bundle-template.generic.scss'), '.generic-$COLOR { color: red; }'); + await writeFileText( + path.join(buildDir, 'bundle-template.common.scss'), + '.common { color: red; }', + ); + await writeFileText( + path.join(buildDir, 'bundle-template.generic.scss'), + '.generic-$COLOR { color: red; }', + ); createMockModules(workspaceRoot, projectRoot); return projectRoot; diff --git a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts index fbae39c61b01..b2f89da64fb5 100644 --- a/packages/nx-infra-plugin/src/executors/scss-build/executor.ts +++ b/packages/nx-infra-plugin/src/executors/scss-build/executor.ts @@ -79,12 +79,14 @@ function moveCharsetToTop(css: string): string { } function generateBundleName(theme: string, size: string, color: string, mode?: string): string { - return 'dx' + return ( + 'dx' + (theme === 'material' || theme === 'fluent' ? `.${theme}` : '') + `.${color}` + (mode ? `.${mode}` : '') + (size === 'default' ? '' : '.compact') - + '.scss'; + + '.scss' + ); } async function generateScssBundles( @@ -102,7 +104,10 @@ async function generateScssBundles( const themes = deps.themeOptions.getThemes(); for (const [theme, size, color, mode] of themes) { const template = await readTemplate(theme); - const content = template.replace('$COLOR', color).replace('$SIZE', size).replace('$MODE', mode || ''); + const content = template + .replace('$COLOR', color) + .replace('$SIZE', size) + .replace('$MODE', mode || ''); const fileName = generateBundleName(theme, size, color, mode); await writeFileText(path.join(resolvedBundlesDir, fileName), content); } @@ -123,12 +128,16 @@ function loadDependencies(projectRoot: string): BuildDependencies { themeOptions: projectRequire(path.resolve(projectRoot, 'build/theme-options.cjs')) as { getThemes: () => Array<[string, string, string, string?]>; }, - cleanCssSanitizeOptions: projectRequire(path.resolve(projectRoot, 'build/clean-css-options.json')), + cleanCssSanitizeOptions: projectRequire( + path.resolve(projectRoot, 'build/clean-css-options.json'), + ), cleanCssDevOptions: workspaceRequire( path.resolve(projectRoot, '../devextreme-themebuilder/src/data/clean-css-options.json'), ), - devextremeVersion: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')).version, - browsersList: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')).browserslist, + devextremeVersion: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')) + .version, + browsersList: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')) + .browserslist, }; } @@ -198,12 +207,14 @@ async function compileFile( from: undefined, }); - const minifierOptions = minifyProfile === 'ci' ? deps.cleanCssDevOptions : deps.cleanCssSanitizeOptions; + const minifierOptions = + minifyProfile === 'ci' ? deps.cleanCssDevOptions : deps.cleanCssSanitizeOptions; const minifier = new deps.CleanCss(minifierOptions); const minified = minifier.minify(prefixed.css).styles; const outFileName = path.basename(sourceFile, '.scss') + '.css'; - const withHeader = createLicenseHeader(outFileName, deps.devextremeVersion) + moveCharsetToTop(minified); + const withHeader = + createLicenseHeader(outFileName, deps.devextremeVersion) + moveCharsetToTop(minified); await writeFileText(path.join(outputDir, outFileName), withHeader); } @@ -326,16 +337,12 @@ async function runWatchBuild( }, 200); }; - const watcher = fs.watch( - watchDir, - { recursive: true }, - (_eventType, fileName) => { - if (!fileName || !fileName.endsWith('.scss')) { - return; - } - scheduleRebuild(); - }, - ); + const watcher = fs.watch(watchDir, { recursive: true }, (_eventType, fileName) => { + if (!fileName || !fileName.endsWith('.scss')) { + return; + } + scheduleRebuild(); + }); const stopWatcher = () => { watcher.close();