From 53a3c38735eec488a1ce8bfd0ad8fa0ad3c778e5 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Mon, 22 Dec 2025 15:53:15 +0200 Subject: [PATCH 01/14] chore(devextreme): create the localization gulp task alternative based on nx --- nx.json | 9 + packages/devextreme/project.json | 81 ++++ packages/nx-infra-plugin/executors.json | 5 + packages/nx-infra-plugin/package.json | 4 +- .../add-license-headers/executor.e2e.spec.ts | 103 ++-- .../executors/add-license-headers/executor.ts | 335 ++++++++----- .../executors/add-license-headers/schema.json | 19 + .../executors/add-license-headers/schema.ts | 4 + .../localization/executor.e2e.spec.ts | 219 +++++++++ .../src/executors/localization/executor.ts | 443 ++++++++++++++++++ .../src/executors/localization/schema.json | 54 +++ .../src/executors/localization/schema.ts | 11 + pnpm-lock.yaml | 56 ++- 13 files changed, 1151 insertions(+), 192 deletions(-) create mode 100644 packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts create mode 100644 packages/nx-infra-plugin/src/executors/localization/executor.ts create mode 100644 packages/nx-infra-plugin/src/executors/localization/schema.json create mode 100644 packages/nx-infra-plugin/src/executors/localization/schema.ts diff --git a/nx.json b/nx.json index f32039a35ff6..7113236a0bd7 100644 --- a/nx.json +++ b/nx.json @@ -8,6 +8,15 @@ "{projectRoot}/**/*.ts", "{projectRoot}/tsconfig.json", { "externalDependencies": [ "devextreme-internal-tools", "ts-node", "typescript"] } + ], + "devextreme-sources": [ + "{projectRoot}/js/**/*", + "{projectRoot}/ts/**/*" + ], + "devextreme-build-config": [ + "{projectRoot}/build/**/*", + "{projectRoot}/webpack.config.js", + "{projectRoot}/gulpfile.js" ] }, "targetDefaults": { diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 1f3cc3cabdae..1f8c425fe69b 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -7,6 +7,87 @@ "devextreme-scss" ], "targets": { + "clean:artifacts": { + "executor": "devextreme-nx-infra-plugin:clean", + "options": { + "targetDirectory": "./artifacts", + "excludePatterns": [ + "./artifacts/css", + "./artifacts/npm/devextreme/package.json", + "./artifacts/npm/devextreme-dist/package.json" + ] + } + }, + "clean:cldr-data": { + "executor": "devextreme-nx-infra-plugin:clean", + "options": { + "targetDirectory": "./js/__internal/core/localization/cldr-data" + } + }, + "build:localization:generate": { + "executor": "devextreme-nx-infra-plugin:localization", + "options": { + "messagesDir": "./js/localization/messages", + "messageTemplate": "./build/gulp/localization-template.jst", + "messageOutputDir": "./artifacts/js/localization", + "generatedTemplate": "./build/gulp/generated_js.jst", + "cldrDataOutputDir": "./js/__internal/core/localization/cldr-data", + "defaultMessagesOutputDir": "./js/__internal/core/localization" + }, + "inputs": [ + "{projectRoot}/js/localization/messages/**/*.json", + "{projectRoot}/build/gulp/localization-template.jst", + "{projectRoot}/build/gulp/generated_js.jst" + ], + "outputs": [ + "{projectRoot}/artifacts/js/localization", + "{projectRoot}/js/__internal/core/localization/default_messages.ts", + "{projectRoot}/js/__internal/core/localization/cldr-data" + ], + "cache": true + }, + "build:localization:headers": { + "executor": "devextreme-nx-infra-plugin:add-license-headers", + "options": { + "targetDirectory": "./artifacts/js/localization", + "licenseTemplateFile": "./build/gulp/license-header.txt", + "eulaUrl": "https://js.devexpress.com/Licensing/", + "prependAfterLicense": "\"use strict\";\n\n", + "separatorBetweenBannerAndContent": "", + "includePatterns": ["**/*.js"] + }, + "inputs": [ + "{projectRoot}/artifacts/js/localization/**/*.js", + "{projectRoot}/build/gulp/license-header.txt" + ], + "outputs": [ + "{projectRoot}/artifacts/js/localization" + ], + "cache": true + }, + "build:localization": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm nx clean:cldr-data devextreme", + "pnpm nx build:localization:generate devextreme", + "pnpm nx build:localization:headers devextreme" + ], + "parallel": false + }, + "inputs": [ + "{projectRoot}/js/localization/messages/**/*.json", + "{projectRoot}/build/gulp/localization-template.jst", + "{projectRoot}/build/gulp/generated_js.jst", + "{projectRoot}/build/gulp/license-header.txt" + ], + "outputs": [ + "{projectRoot}/artifacts/js/localization", + "{projectRoot}/js/__internal/core/localization/default_messages.ts", + "{projectRoot}/js/__internal/core/localization/cldr-data" + ], + "cache": true + }, "build": { "executor": "nx:run-script", "options": { diff --git a/packages/nx-infra-plugin/executors.json b/packages/nx-infra-plugin/executors.json index 71cc7c710954..30a85f6e44a9 100644 --- a/packages/nx-infra-plugin/executors.json +++ b/packages/nx-infra-plugin/executors.json @@ -54,6 +54,11 @@ "implementation": "./src/executors/karma-multi-env/executor", "schema": "./src/executors/karma-multi-env/schema.json", "description": "Run Karma tests sequentially across multiple Angular environments (client, server, hydration)" + }, + "localization": { + "implementation": "./src/executors/localization/executor", + "schema": "./src/executors/localization/schema.json", + "description": "Generate localization message files and TypeScript CLDR data modules" } } } diff --git a/packages/nx-infra-plugin/package.json b/packages/nx-infra-plugin/package.json index 7d94d61f8257..ec5c04c11b06 100644 --- a/packages/nx-infra-plugin/package.json +++ b/packages/nx-infra-plugin/package.json @@ -12,9 +12,10 @@ "./package.json": "./package.json" }, "dependencies": { - "fs-extra": "^11.2.0", + "fs-extra": "11.2.0", "glob": "11.1.0", "normalize-path": "3.0.0", + "lodash": "4.17.21", "rimraf": "3.0.2" }, "peerDependencies": { @@ -34,6 +35,7 @@ "@types/jest": "29.5.14", "@types/normalize-path": "3.0.2", "@types/node": "18.19.130", + "@types/lodash": "4.17.0", "prettier": "catalog:tools", "ts-jest": "29.1.3", "typescript": "4.9.5" diff --git a/packages/nx-infra-plugin/src/executors/add-license-headers/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/add-license-headers/executor.e2e.spec.ts index d48cc4ec263b..1800cae38b34 100644 --- a/packages/nx-infra-plugin/src/executors/add-license-headers/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/add-license-headers/executor.e2e.spec.ts @@ -65,6 +65,10 @@ describe('AddLicenseHeadersExecutor E2E', () => { expect(indexContent).toContain('test-package'); expect(indexContent).toContain('Version: 1.0.0'); expect(indexContent).toContain('Developer Express Inc.'); + expect(indexContent).toContain('MIT license'); + const currentYear = new Date().getFullYear(); + expect(indexContent).toContain(`2012 - ${currentYear}`); + expect(indexContent).toMatch(/Build date:/); const utilsContent = await readFileText(path.join(npmDir, 'utils.js')); expect(utilsContent).toMatch(/^\/\*!/); @@ -107,6 +111,46 @@ describe('AddLicenseHeadersExecutor E2E', () => { expect(newContent).toContain(originalContent.trim()); }); + + it('should support custom license template', async () => { + const projectDir = path.join(tempDir, 'packages', 'test-lib'); + const buildDir = path.join(projectDir, 'build', 'gulp'); + fs.mkdirSync(buildDir, { recursive: true }); + + await writeFileText( + path.join(buildDir, 'license-header.txt'), + `/*! +* DevExtreme (<%= file.relative %>) +* Version: <%= version %> +* Build date: <%= date %> +* +* Copyright (c) 2012 - <%= year %> Developer Express Inc. ALL RIGHTS RESERVED +* Read about DevExtreme licensing here: <%= eula %> +*/ +`, + ); + + const options: AddLicenseHeadersExecutorSchema = { + targetDirectory: './npm', + packageJsonPath: './package.json', + licenseTemplateFile: './build/gulp/license-header.txt', + eulaUrl: 'https://js.devexpress.com/Licensing/', + prependAfterLicense: '"use strict";\n\n', + includePatterns: ['**/*.js'], + }; + + const result = await executor(options, context); + expect(result.success).toBe(true); + + const npmDir = path.join(projectDir, 'npm'); + const content = await readFileText(path.join(npmDir, 'index.js')); + + expect(content).toMatch(/^\/\*!/); + expect(content).toContain('DevExtreme (index.js)'); + expect(content).toContain('https://js.devexpress.com/Licensing/'); + expect(content).toContain('"use strict";'); + expect(content).toContain("return 'Hello'"); + }); }); describe('Idempotence', () => { @@ -159,65 +203,6 @@ describe('AddLicenseHeadersExecutor E2E', () => { }); }); - describe('Header content validation', () => { - it('should include package name in header', async () => { - const options: AddLicenseHeadersExecutorSchema = { - targetDirectory: './npm', - packageJsonPath: './package.json', - }; - - await executor(options, context); - - const npmDir = path.join(tempDir, 'packages', 'test-lib', 'npm'); - const content = await readFileText(path.join(npmDir, 'index.js')); - - expect(content).toContain('test-package'); - }); - - it('should include version in header', async () => { - const options: AddLicenseHeadersExecutorSchema = { - targetDirectory: './npm', - packageJsonPath: './package.json', - }; - - await executor(options, context); - - const npmDir = path.join(tempDir, 'packages', 'test-lib', 'npm'); - const content = await readFileText(path.join(npmDir, 'index.js')); - - expect(content).toContain('Version: 1.0.0'); - }); - - it('should include current year in header', async () => { - const options: AddLicenseHeadersExecutorSchema = { - targetDirectory: './npm', - packageJsonPath: './package.json', - }; - - await executor(options, context); - - const npmDir = path.join(tempDir, 'packages', 'test-lib', 'npm'); - const content = await readFileText(path.join(npmDir, 'index.js')); - - const currentYear = new Date().getFullYear(); - expect(content).toContain(`2012 - ${currentYear}`); - }); - - it('should include build date in header', async () => { - const options: AddLicenseHeadersExecutorSchema = { - targetDirectory: './npm', - packageJsonPath: './package.json', - }; - - await executor(options, context); - - const npmDir = path.join(tempDir, 'packages', 'test-lib', 'npm'); - const content = await readFileText(path.join(npmDir, 'index.js')); - - expect(content).toMatch(/Build date:/); - }); - }); - describe('Error handling', () => { it('should fail gracefully with missing package.json', async () => { const options: AddLicenseHeadersExecutorSchema = { diff --git a/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts b/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts index ef18959cad62..bffce3e121ab 100644 --- a/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts +++ b/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts @@ -1,56 +1,231 @@ import { PromiseExecutor, logger } from '@nx/devkit'; import * as path from 'path'; import { glob } from 'glob'; +import _ from 'lodash'; import { AddLicenseHeadersExecutorSchema } from './schema'; import { resolveProjectPath, normalizeGlobPathForWindows } from '../../utils/path-resolver'; import { isWindowsOS } from '../../utils/common'; import { logError } from '../../utils/error-handler'; import { readJson, readFileText, writeFileText } from '../../utils/file-operations'; -const DEFAULT_TARGET_DIR = './npm'; -const DEFAULT_PACKAGE_JSON = './package.json'; +interface PackageJson { + name: string; + version: string; + repository?: string | { url?: string }; +} -const DEFAULT_INCLUDE_PATTERNS = ['**/*.{ts,js}']; -const DEFAULT_EXCLUDE_PATTERNS = ['**/*.json', '**/*.map']; +interface BaseTemplateData { + pkg: PackageJson; + date: string; + year: number; + githubUrl: string; + eula: string; + version: string; +} -const LICENSE_MARKER = '/*!'; -const COMMENT_END = ' */'; -const COMMENT_PREFIX = ' *'; -const NEWLINE = '\n'; -const EMPTY_LINE = ''; +interface FileTemplateData extends BaseTemplateData { + file: { + relative: string; + }; + commentType: string; +} -const COPYRIGHT_START = - ' * Copyright (c) 2012 - <%= year %> Developer Express Inc. ALL RIGHTS RESERVED'; +const DEFAULTS = { + TARGET_DIR: './npm', + PACKAGE_JSON: './package.json', + INCLUDE_PATTERNS: ['**/*.{ts,js}'], + EXCLUDE_PATTERNS: ['**/*.json', '**/*.map'], +} as const; -const BANNER_PKG_NAME = COMMENT_PREFIX + ' ' + '<%= pkg.name %>'; -const BANNER_VERSION = COMMENT_PREFIX + ' ' + 'Version: <%= pkg.version %>'; -const BANNER_BUILD_DATE = COMMENT_PREFIX + ' ' + 'Build date: <%= date %>'; -const BANNER_LICENSE_LINE1 = - COMMENT_PREFIX + ' ' + 'This software may be modified and distributed under the terms'; -const BANNER_LICENSE_LINE2 = - COMMENT_PREFIX - + ' ' - + 'of the MIT license. See the LICENSE file in the root of the project for details.'; -const BANNER_GITHUB = COMMENT_PREFIX + ' ' + '<%= githubUrl %>'; +const COMMENT = { + MARKER: '/*!', + END: ' */', + PREFIX: ' *', +} as const; + +const CHARS = { + NEWLINE: '\n', + EMPTY_LINE: '', +} as const; + +const BANNER = { + PKG_NAME: `${COMMENT.PREFIX} <%= pkg.name %>`, + VERSION: `${COMMENT.PREFIX} Version: <%= pkg.version %>`, + BUILD_DATE: `${COMMENT.PREFIX} Build date: <%= date %>`, + COPYRIGHT: `${COMMENT.PREFIX} Copyright (c) 2012 - <%= year %> Developer Express Inc. ALL RIGHTS RESERVED`, + LICENSE_LINE1: `${COMMENT.PREFIX} This software may be modified and distributed under the terms`, + LICENSE_LINE2: `${COMMENT.PREFIX} of the MIT license. See the LICENSE file in the root of the project for details.`, + GITHUB: `${COMMENT.PREFIX} <%= githubUrl %>`, +} as const; const TEMPLATE_REGEX = /<%=\s*(\w+(?:\.\w+)*)\s*%>/g; +function extractGitHubUrl( + repository: string | { url?: string } | undefined, + packageJsonPath: string, +): string { + if (!repository) { + throw new Error( + `Missing 'repository' field in ${packageJsonPath}. License headers require a repository URL.`, + ); + } + + const rawUrl = typeof repository === 'string' ? repository : repository.url; + + if (!rawUrl) { + throw new Error( + `Invalid 'repository' format in ${packageJsonPath}. Expected string or object with 'url' property.`, + ); + } + + return rawUrl.replace(/^git\+/, '').replace(/\.git$/, ''); +} + +function buildDefaultBannerTemplate(): string { + return [ + COMMENT.MARKER, + BANNER.PKG_NAME, + BANNER.VERSION, + BANNER.BUILD_DATE, + COMMENT.PREFIX, + BANNER.COPYRIGHT, + COMMENT.PREFIX, + BANNER.LICENSE_LINE1, + BANNER.LICENSE_LINE2, + COMMENT.PREFIX, + BANNER.GITHUB, + COMMENT.END, + CHARS.EMPTY_LINE, + ].join(CHARS.NEWLINE); +} + +function renderTemplate(template: string, data: unknown): string { + return template.replace(TEMPLATE_REGEX, (_match, key) => { + const keys = key.split('.'); + let value = data; + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = (value as Record)[k]; + } else { + return ''; + } + } + + return String(value); + }); +} + +interface DiscoverFilesOptions { + targetDirectory: string; + includePatterns: readonly string[]; + excludePatterns: readonly string[]; +} + +async function discoverFiles(options: DiscoverFilesOptions): Promise { + const { targetDirectory, includePatterns, excludePatterns } = options; + + const patterns = includePatterns.map((pattern) => { + const fullPath = path.join(targetDirectory, pattern); + return isWindowsOS() ? normalizeGlobPathForWindows(fullPath) : fullPath; + }); + + const allFiles: string[] = []; + for (const pattern of patterns) { + const matchedFiles = await glob(pattern, { ignore: [...excludePatterns] }); + allFiles.push(...matchedFiles); + } + + return [...new Set(allFiles)]; +} + +interface ProcessFileOptions { + file: string; + targetDirectory: string; + baseData: BaseTemplateData; + bannerTemplate: string; + compiledTemplate: ReturnType | null; + useCustomTemplate: boolean; + separatorBetweenBannerAndContent: string; + prependAfterLicense: string; +} + +async function processFile(options: ProcessFileOptions): Promise { + const { + file, + targetDirectory, + baseData, + bannerTemplate, + compiledTemplate, + useCustomTemplate, + separatorBetweenBannerAndContent, + prependAfterLicense, + } = options; + + const content = await readFileText(file); + + if (content.startsWith(COMMENT.MARKER)) { + return; + } + + const relativePath = path.relative(targetDirectory, file).replace(/\\/g, '/'); + const fileData: FileTemplateData = { + ...baseData, + file: { relative: relativePath }, + commentType: '!', + }; + + const banner = useCustomTemplate + ? compiledTemplate!(fileData) + : renderTemplate(bannerTemplate, fileData); + + const finalContent = banner + separatorBetweenBannerAndContent + prependAfterLicense + content; + await writeFileText(file, finalContent); +} + +interface LoadTemplateResult { + success: true; + template: string; +} + +interface LoadTemplateError { + success: false; +} + +async function loadBannerTemplate( + absoluteProjectRoot: string, + licenseTemplateFile: string | undefined, +): Promise { + if (!licenseTemplateFile) { + return { success: true, template: buildDefaultBannerTemplate() }; + } + + const templatePath = path.join(absoluteProjectRoot, licenseTemplateFile); + try { + const template = await readFileText(templatePath); + return { success: true, template }; + } catch (error) { + logError(`Failed to read license template: ${templatePath}`, error); + return { success: false }; + } +} + const runExecutor: PromiseExecutor = async (options, context) => { const absoluteProjectRoot = resolveProjectPath(context); const targetDirectory = path.join( absoluteProjectRoot, - options.targetDirectory || DEFAULT_TARGET_DIR, + options.targetDirectory ?? DEFAULTS.TARGET_DIR, ); const packageJsonPath = path.join( absoluteProjectRoot, - options.packageJsonPath || DEFAULT_PACKAGE_JSON, + options.packageJsonPath ?? DEFAULTS.PACKAGE_JSON, ); const separatorBetweenBannerAndContent = - typeof options.separatorBetweenBannerAndContent === 'undefined' - ? NEWLINE - : options.separatorBetweenBannerAndContent; + options.separatorBetweenBannerAndContent ?? CHARS.NEWLINE; + const prependAfterLicense = options.prependAfterLicense ?? ''; + const useCustomTemplate = !!options.licenseTemplateFile; - let pkg; + let pkg: PackageJson; try { pkg = await readJson(packageJsonPath); } catch (error) { @@ -58,83 +233,48 @@ const runExecutor: PromiseExecutor = async (opt return { success: false }; } - const now = new Date(); - - let githubUrl: string; + const githubUrl = useCustomTemplate ? '' : extractGitHubUrl(pkg.repository, packageJsonPath); - if (!pkg.repository) { - throw new Error( - `Missing 'repository' field in ${packageJsonPath}. License headers require a repository URL.`, - ); - } else if (typeof pkg.repository === 'string') { - githubUrl = pkg.repository.replace(/^git\+/, '').replace(/\.git$/, ''); - } else if (pkg.repository.url) { - githubUrl = pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, ''); - } else { - throw new Error( - `Invalid 'repository' format in ${packageJsonPath}. Expected string or object with 'url' property.`, - ); + const templateResult = await loadBannerTemplate(absoluteProjectRoot, options.licenseTemplateFile); + if (!templateResult.success) { + return { success: false }; } + const bannerTemplate = templateResult.template; - const data = { + const now = new Date(); + const baseData: BaseTemplateData = { pkg, date: now.toDateString(), year: now.getFullYear(), githubUrl, + eula: options.eulaUrl ?? '', + version: options.version ?? pkg.version, }; - const bannerTemplate = [ - LICENSE_MARKER, - BANNER_PKG_NAME, - BANNER_VERSION, - BANNER_BUILD_DATE, - COMMENT_PREFIX, - COPYRIGHT_START, - COMMENT_PREFIX, - BANNER_LICENSE_LINE1, - BANNER_LICENSE_LINE2, - COMMENT_PREFIX, - BANNER_GITHUB, - COMMENT_END, - EMPTY_LINE, - ].join(NEWLINE); - - const banner = renderTemplate(bannerTemplate, data); - try { - const includePatterns = options.includePatterns || DEFAULT_INCLUDE_PATTERNS; - const excludePatterns = options.excludePatterns || DEFAULT_EXCLUDE_PATTERNS; - - const patterns = includePatterns.map((pattern) => { - const result = path.join(targetDirectory, pattern); - - if (isWindowsOS()) { - return normalizeGlobPathForWindows(result); - } - - return result; + const files = await discoverFiles({ + targetDirectory, + includePatterns: options.includePatterns ?? DEFAULTS.INCLUDE_PATTERNS, + excludePatterns: options.excludePatterns ?? DEFAULTS.EXCLUDE_PATTERNS, }); - const allFiles: string[] = []; - for (const pattern of patterns) { - const matchedFiles = await glob(pattern, { ignore: excludePatterns }); - allFiles.push(...matchedFiles); - } - - const files = [...new Set(allFiles)]; - logger.info(`Adding license headers to ${files.length} files...`); - await Promise.all( - files.map(async (file) => { - const content = await readFileText(file); - - if (content.startsWith(LICENSE_MARKER)) { - return; - } + const compiledTemplate = useCustomTemplate ? _.template(bannerTemplate) : null; - await writeFileText(file, banner + separatorBetweenBannerAndContent + content); - }), + await Promise.all( + files.map((file) => + processFile({ + file, + targetDirectory, + baseData, + bannerTemplate, + compiledTemplate, + useCustomTemplate, + separatorBetweenBannerAndContent, + prependAfterLicense, + }), + ), ); logger.info('License headers added successfully'); @@ -145,21 +285,4 @@ const runExecutor: PromiseExecutor = async (opt } }; -function renderTemplate(template: string, data: unknown): string { - return template.replace(TEMPLATE_REGEX, (_match, key) => { - const keys = key.split('.'); - let value = data; - - for (const k of keys) { - if (value && typeof value === 'object' && k in value) { - value = (value as Record)[k]; - } else { - return ''; - } - } - - return String(value); - }); -} - export default runExecutor; diff --git a/packages/nx-infra-plugin/src/executors/add-license-headers/schema.json b/packages/nx-infra-plugin/src/executors/add-license-headers/schema.json index c5c6146fb972..3513654e0605 100644 --- a/packages/nx-infra-plugin/src/executors/add-license-headers/schema.json +++ b/packages/nx-infra-plugin/src/executors/add-license-headers/schema.json @@ -1,4 +1,7 @@ { + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Add License Headers Executor", + "description": "Add license headers to compiled files with support for custom templates", "type": "object", "properties": { "targetDirectory": { @@ -31,6 +34,22 @@ "type": "string" }, "default": [] + }, + "licenseTemplateFile": { + "type": "string", + "description": "Path to custom license template file (uses default MIT template if not specified)" + }, + "eulaUrl": { + "type": "string", + "description": "EULA URL for template variable <%= eula %>" + }, + "prependAfterLicense": { + "type": "string", + "description": "Content to prepend after license header (e.g., '\"use strict\";\\n\\n')" + }, + "version": { + "type": "string", + "description": "Version to use in template (defaults to pkg.version)" } }, "required": [] diff --git a/packages/nx-infra-plugin/src/executors/add-license-headers/schema.ts b/packages/nx-infra-plugin/src/executors/add-license-headers/schema.ts index 2df16465548e..706bc02cfb85 100644 --- a/packages/nx-infra-plugin/src/executors/add-license-headers/schema.ts +++ b/packages/nx-infra-plugin/src/executors/add-license-headers/schema.ts @@ -4,4 +4,8 @@ export interface AddLicenseHeadersExecutorSchema { separatorBetweenBannerAndContent?: string; includePatterns?: string[]; excludePatterns?: string[]; + licenseTemplateFile?: string; + eulaUrl?: string; + prependAfterLicense?: string; + version?: string; } diff --git a/packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts new file mode 100644 index 000000000000..0f399aa717ee --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts @@ -0,0 +1,219 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import executor from './executor'; +import { LocalizationExecutorSchema } from './schema'; +import { createTempDir, cleanupTempDir, createMockContext } from '../../utils/test-utils'; +import { writeFileText, writeJson, readFileText } from '../../utils'; + +const PROJECT_SUBPATH = ['packages', 'test-lib'] as const; + +const MESSAGE_FILE = { + EN: 'dx.messages.en.js', + DE: 'dx.messages.de.js', +} as const; + +const TEMPLATE_FILE = { + LOCALIZATION: 'localization-template.jst', + GENERATED_JS: 'generated_js.jst', +} as const; + +const GENERATED_FILE = { + DEFAULT_MESSAGES: 'default_messages.ts', + PARENT_LOCALES: 'parent_locales.ts', + FIRST_DAY_OF_WEEK: 'first_day_of_week_data.ts', + ACCOUNTING_FORMATS: 'accounting_formats.ts', + EN_CLDR: 'en.ts', + SUPPLEMENTAL: 'supplemental.ts', +} as const; + +const EXPECTED_CLDR_FILES = [ + GENERATED_FILE.PARENT_LOCALES, + GENERATED_FILE.FIRST_DAY_OF_WEEK, + GENERATED_FILE.ACCOUNTING_FORMATS, + GENERATED_FILE.EN_CLDR, + GENERATED_FILE.SUPPLEMENTAL, +] as const; + +const LOCALIZATION_TEMPLATE = `(function(root, factory) { + if(typeof define === 'function' && define.amd) { + define(function(require) { + factory(require("devextreme/common/core/localization")); + }); + } else if(typeof module === "object" && module.exports) { + factory(require("devextreme/common/core/localization")); + } else { + factory(DevExpress.localization); + } +}(this, function(localization) { + localization.loadMessages(<%= json %>); +})); +`; + +const GENERATED_JS_TEMPLATE = `/* eslint-disable @stylistic/quotes,@stylistic/indent,@stylistic/quote-props,@stylistic/max-len,@stylistic/comma-dangle,i18n/no-russian-character */ +// !!! AUTO-GENERATED FILE, DO NOT EDIT +export <%= exportName ? 'const ' + exportName + ' =' : 'default' %> <%= json %>; +`; + +interface LocalizationTestFixture { + projectDir: string; + messagesDir: string; + buildDir: string; + artifactsDir: string; + cldrDataDir: string; + localizationDir: string; +} + +async function createLocalizationTestFixture(tempDir: string): Promise { + const projectDir = path.join(tempDir, ...PROJECT_SUBPATH); + const messagesDir = path.join(projectDir, 'js', 'localization', 'messages'); + const buildDir = path.join(projectDir, 'build', 'gulp'); + const artifactsDir = path.join(projectDir, 'artifacts', 'js', 'localization'); + const cldrDataDir = path.join( + projectDir, + 'js', + '__internal', + 'core', + 'localization', + 'cldr-data', + ); + const localizationDir = path.join(projectDir, 'js', '__internal', 'core', 'localization'); + + fs.mkdirSync(messagesDir, { recursive: true }); + fs.mkdirSync(buildDir, { recursive: true }); + fs.mkdirSync(artifactsDir, { recursive: true }); + fs.mkdirSync(cldrDataDir, { recursive: true }); + fs.mkdirSync(localizationDir, { recursive: true }); + + await writeJson(path.join(messagesDir, 'en.json'), { + en: { + Yes: 'Yes', + No: 'No', + Cancel: 'Cancel', + Loading: 'Loading...', + }, + }); + + await writeJson(path.join(messagesDir, 'de.json'), { + de: { + Yes: 'Ja', + No: 'Nein', + Cancel: 'Abbrechen', + Loading: 'Wird geladen...', + }, + }); + + await writeFileText(path.join(buildDir, TEMPLATE_FILE.LOCALIZATION), LOCALIZATION_TEMPLATE); + await writeFileText(path.join(buildDir, TEMPLATE_FILE.GENERATED_JS), GENERATED_JS_TEMPLATE); + + await writeJson(path.join(projectDir, 'package.json'), { + name: 'devextreme', + version: '25.2.0', + }); + + return { projectDir, messagesDir, buildDir, artifactsDir, cldrDataDir, localizationDir }; +} + +describe('LocalizationExecutor E2E', () => { + let tempDir: string; + let context = createMockContext(); + let fixture: LocalizationTestFixture; + + beforeEach(async () => { + tempDir = createTempDir('nx-localization-e2e-'); + context = createMockContext({ root: tempDir }); + fixture = await createLocalizationTestFixture(tempDir); + }); + + afterEach(() => { + cleanupTempDir(tempDir); + }); + + it('should generate message files for all locales', async () => { + const options: LocalizationExecutorSchema = { + messagesDir: './js/localization/messages', + messageTemplate: './build/gulp/localization-template.jst', + messageOutputDir: './artifacts/js/localization', + skipCldrGeneration: true, + }; + + const result = await executor(options, context); + + expect(result.success).toBe(true); + + const enFile = path.join(fixture.artifactsDir, MESSAGE_FILE.EN); + const deFile = path.join(fixture.artifactsDir, MESSAGE_FILE.DE); + + expect(fs.existsSync(enFile)).toBe(true); + expect(fs.existsSync(deFile)).toBe(true); + + const enContent = await readFileText(enFile); + expect(enContent).toContain('localization.loadMessages'); + expect(enContent).toContain('"Yes"'); + expect(enContent).toContain('"No"'); + expect(enContent).toContain('define.amd'); + + const deContent = await readFileText(deFile); + expect(deContent).toContain('"Ja"'); + expect(deContent).toContain('"Nein"'); + }); + + it('should generate CLDR TypeScript modules', async () => { + const options: LocalizationExecutorSchema = { + messagesDir: './js/localization/messages', + messageTemplate: './build/gulp/localization-template.jst', + messageOutputDir: './artifacts/js/localization', + generatedTemplate: './build/gulp/generated_js.jst', + cldrDataOutputDir: './js/__internal/core/localization/cldr-data', + defaultMessagesOutputDir: './js/__internal/core/localization', + }; + + const result = await executor(options, context); + + expect(result.success).toBe(true); + + const defaultMessagesFile = path.join(fixture.localizationDir, GENERATED_FILE.DEFAULT_MESSAGES); + expect(fs.existsSync(defaultMessagesFile)).toBe(true); + + const defaultMessagesContent = await readFileText(defaultMessagesFile); + expect(defaultMessagesContent).toContain('export const defaultMessages'); + expect(defaultMessagesContent).toContain('AUTO-GENERATED FILE'); + + for (const file of EXPECTED_CLDR_FILES) { + const filePath = path.join(fixture.cldrDataDir, file); + expect(fs.existsSync(filePath)).toBe(true); + + const content = await readFileText(filePath); + expect(content).toContain('AUTO-GENERATED FILE'); + } + }); + + it('should have correct output structure', async () => { + const options: LocalizationExecutorSchema = { + messagesDir: './js/localization/messages', + messageTemplate: './build/gulp/localization-template.jst', + messageOutputDir: './artifacts/js/localization', + generatedTemplate: './build/gulp/generated_js.jst', + cldrDataOutputDir: './js/__internal/core/localization/cldr-data', + defaultMessagesOutputDir: './js/__internal/core/localization', + }; + + const result = await executor(options, context); + expect(result.success).toBe(true); + + const expectedStructure = { + [`artifacts/js/localization/${MESSAGE_FILE.EN}`]: true, + [`artifacts/js/localization/${MESSAGE_FILE.DE}`]: true, + [`js/__internal/core/localization/${GENERATED_FILE.DEFAULT_MESSAGES}`]: true, + [`js/__internal/core/localization/cldr-data/${GENERATED_FILE.PARENT_LOCALES}`]: true, + [`js/__internal/core/localization/cldr-data/${GENERATED_FILE.FIRST_DAY_OF_WEEK}`]: true, + [`js/__internal/core/localization/cldr-data/${GENERATED_FILE.ACCOUNTING_FORMATS}`]: true, + [`js/__internal/core/localization/cldr-data/${GENERATED_FILE.EN_CLDR}`]: true, + [`js/__internal/core/localization/cldr-data/${GENERATED_FILE.SUPPLEMENTAL}`]: true, + }; + + for (const [relativePath, shouldExist] of Object.entries(expectedStructure)) { + const absolutePath = path.join(fixture.projectDir, relativePath); + expect(fs.existsSync(absolutePath)).toBe(shouldExist); + } + }); +}); diff --git a/packages/nx-infra-plugin/src/executors/localization/executor.ts b/packages/nx-infra-plugin/src/executors/localization/executor.ts new file mode 100644 index 000000000000..bb5c60969203 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/localization/executor.ts @@ -0,0 +1,443 @@ +import { PromiseExecutor, logger } from '@nx/devkit'; +import * as path from 'path'; +import * as fs from 'fs'; +import { createRequire } from 'module'; +import _ from 'lodash'; +import { LocalizationExecutorSchema } from './schema'; +import { resolveProjectPath } from '../../utils/path-resolver'; +import { logError } from '../../utils/error-handler'; +import { readFileText, writeFileText, readJson } from '../../utils/file-operations'; + +interface CldrInstance { + supplemental: { + weekData: { + firstDay: () => string; + }; + }; +} + +interface CldrConstructor { + load: (...data: unknown[]) => void; + new (locale: string): CldrInstance; +} + +interface CldrModuleDefinition { + data: unknown; + filename: string; + exportName?: string; + destination: string; +} + +interface CldrDependencies { + Cldr: CldrConstructor; + locales: string[]; + weekData: unknown; + likelySubtags: unknown; + parentLocales: Record; + globalizeEnCldr: unknown; + globalizeSupplementalCldr: unknown; +} + +const DEFAULT_MESSAGES_DIR = './js/localization/messages'; +const DEFAULT_MESSAGE_TEMPLATE = './build/gulp/localization-template.jst'; +const DEFAULT_MESSAGE_OUTPUT_DIR = './artifacts/js/localization'; +const DEFAULT_GENERATED_TEMPLATE = './build/gulp/generated_js.jst'; +const DEFAULT_CLDR_DATA_OUTPUT_DIR = './js/__internal/core/localization/cldr-data'; +const DEFAULT_DEFAULT_MESSAGES_OUTPUT_DIR = './js/__internal/core/localization'; + +const PARENT_LOCALE_SEPARATOR = '-'; + +const DAY_INDEXES = { + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6, +} as const; + +const DEFAULT_DAY_OF_WEEK_INDEX = DAY_INDEXES.sun; + +const ERROR_MESSAGES = { + MESSAGES_DIR_NOT_FOUND: (dir: string) => `Messages directory not found: ${dir}`, + MESSAGE_TEMPLATE_NOT_FOUND: (path: string) => `Message template not found: ${path}`, + GENERATED_TEMPLATE_NOT_FOUND: (path: string) => `Generated template not found: ${path}`, + CLDR_DEPENDENCIES_LOAD_FAILED: (error: string) => + `Failed to load CLDR dependencies. Ensure cldr-core, cldrjs, and devextreme-cldr-data ` + + `are installed in the project: ${error}`, +} as const; + +const CLDR_MODULE_CONFIGS = { + DEFAULT_MESSAGES: { + filename: 'default_messages.ts', + exportName: 'defaultMessages', + }, + PARENT_LOCALES: { + filename: 'parent_locales.ts', + }, + FIRST_DAY_OF_WEEK: { + filename: 'first_day_of_week_data.ts', + }, + ACCOUNTING_FORMATS: { + filename: 'accounting_formats.ts', + }, + EN_CLDR: { + filename: 'en.ts', + exportName: 'enCldr', + }, + SUPPLEMENTAL: { + filename: 'supplemental.ts', + exportName: 'supplementalCldr', + }, +} as const; + +function loadCldrDependencies(projectRequire: NodeRequire): CldrDependencies { + try { + return { + Cldr: projectRequire('cldrjs') as CldrConstructor, + locales: projectRequire('cldr-core/availableLocales.json').availableLocales.full, + weekData: projectRequire('cldr-core/supplemental/weekData.json'), + likelySubtags: projectRequire('cldr-core/supplemental/likelySubtags.json'), + parentLocales: projectRequire('cldr-core/supplemental/parentLocales.json').supplemental + .parentLocales.parentLocale, + globalizeEnCldr: projectRequire('devextreme-cldr-data/en.json'), + globalizeSupplementalCldr: projectRequire('devextreme-cldr-data/supplemental.json'), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(ERROR_MESSAGES.CLDR_DEPENDENCIES_LOAD_FAILED(message)); + } +} + +function validateInputPaths( + messagesDir: string, + messageTemplate: string, + generatedTemplate: string, + skipMessageGeneration: boolean, + skipCldrGeneration: boolean, +): void { + if (!fs.existsSync(messagesDir)) { + throw new Error(ERROR_MESSAGES.MESSAGES_DIR_NOT_FOUND(messagesDir)); + } + if (!skipMessageGeneration && !fs.existsSync(messageTemplate)) { + throw new Error(ERROR_MESSAGES.MESSAGE_TEMPLATE_NOT_FOUND(messageTemplate)); + } + if (!skipCldrGeneration && !fs.existsSync(generatedTemplate)) { + throw new Error(ERROR_MESSAGES.GENERATED_TEMPLATE_NOT_FOUND(generatedTemplate)); + } +} + +function shouldIncludeLocaleInFirstDayData( + firstDayIndex: number, + parentLocale: string | false, + getFirstIndex: (locale: string) => number, +): boolean { + if (firstDayIndex === DEFAULT_DAY_OF_WEEK_INDEX) { + return false; + } + if (!parentLocale) { + return true; + } + return firstDayIndex !== getFirstIndex(parentLocale); +} + +function createCldrModuleDefinitions( + enMessages: unknown, + deps: CldrDependencies, + firstDayData: Record, + accountingFormats: Record, + cldrDataOutputDir: string, + defaultMessagesOutputDir: string, +): CldrModuleDefinition[] { + return [ + { + data: enMessages, + ...CLDR_MODULE_CONFIGS.DEFAULT_MESSAGES, + destination: defaultMessagesOutputDir, + }, + { + data: deps.parentLocales, + ...CLDR_MODULE_CONFIGS.PARENT_LOCALES, + destination: cldrDataOutputDir, + }, + { + data: firstDayData, + ...CLDR_MODULE_CONFIGS.FIRST_DAY_OF_WEEK, + destination: cldrDataOutputDir, + }, + { + data: accountingFormats, + ...CLDR_MODULE_CONFIGS.ACCOUNTING_FORMATS, + destination: cldrDataOutputDir, + }, + { + data: deps.globalizeEnCldr, + ...CLDR_MODULE_CONFIGS.EN_CLDR, + destination: cldrDataOutputDir, + }, + { + data: deps.globalizeSupplementalCldr, + ...CLDR_MODULE_CONFIGS.SUPPLEMENTAL, + destination: cldrDataOutputDir, + }, + ]; +} + +function getLocales(directory: string): string[] { + return fs + .readdirSync(directory) + .filter((file) => file.endsWith('.json')) + .map((file) => file.replace('.json', '')); +} + +function serializeObject(obj: unknown, shift = false): string { + const tab = ' '; + let result = JSON.stringify(obj, null, tab); + + if (shift) { + result = result.replace(/(\n)/g, '$1' + tab); + } + + return result; +} + +function getParentLocale(parentLocales: Record, locale: string): string | false { + const parentLocale = parentLocales[locale]; + + if (parentLocale) { + return parentLocale !== 'root' && parentLocale; + } + + const lastSeparatorIndex = locale.lastIndexOf(PARENT_LOCALE_SEPARATOR); + return lastSeparatorIndex > 0 ? locale.substring(0, lastSeparatorIndex) : false; +} + +async function generateMessageFiles( + messagesDir: string, + templatePath: string, + outputDir: string, +): Promise { + const templateContent = await readFileText(templatePath); + const compiled = _.template(templateContent); + + const locales = getLocales(messagesDir); + + logger.info(`Processing ${locales.length} locales...`); + + await Promise.all( + locales.map(async (locale) => { + const messagesPath = path.join(messagesDir, `${locale}.json`); + const messages = await readJson(messagesPath); + const json = serializeObject(messages, true); + + const content = compiled({ json }); + + const outputPath = path.join(outputDir, `dx.messages.${locale}.js`); + await writeFileText(outputPath, content); + }), + ); +} + +async function generateCldrModules( + projectRoot: string, + messagesDir: string, + templatePath: string, + cldrDataOutputDir: string, + defaultMessagesOutputDir: string, + lintGeneratedFiles: boolean, +): Promise { + const templateContent = await readFileText(templatePath); + const compiled = _.template(templateContent); + + const projectRequire = createRequire(path.join(projectRoot, 'package.json')); + const deps = loadCldrDependencies(projectRequire); + const enMessages = await readJson(path.join(messagesDir, 'en.json')); + + const firstDayData = computeFirstDayOfWeekData(deps); + const accountingFormats = computeAccountingFormats(deps.locales, projectRequire); + + const modules = createCldrModuleDefinitions( + enMessages, + deps, + firstDayData, + accountingFormats, + cldrDataOutputDir, + defaultMessagesOutputDir, + ); + + await Promise.all( + modules.map(async (module) => { + const json = serializeObject(module.data); + const content = compiled({ + exportName: module.exportName, + json, + }); + const outputPath = path.join(module.destination, module.filename); + await writeFileText(outputPath, content); + }), + ); + + if (lintGeneratedFiles) { + await lintFiles(cldrDataOutputDir, defaultMessagesOutputDir, projectRoot, projectRequire); + } +} + +function computeFirstDayOfWeekData(deps: CldrDependencies): Record { + const { Cldr, locales, weekData, likelySubtags, parentLocales } = deps; + const result: Record = {}; + + Cldr.load(weekData, likelySubtags); + + const getFirstIndex = (locale: string): number => { + const firstDay = new Cldr(locale).supplemental.weekData.firstDay(); + return DAY_INDEXES[firstDay as keyof typeof DAY_INDEXES]; + }; + + for (const locale of locales) { + const firstDayIndex = getFirstIndex(locale); + const parentLocale = getParentLocale(parentLocales, locale); + + if (shouldIncludeLocaleInFirstDayData(firstDayIndex, parentLocale, getFirstIndex)) { + result[locale] = firstDayIndex; + } + } + + return result; +} + +function computeAccountingFormats( + locales: string[], + projectRequire: NodeRequire, +): Record { + const result: Record = {}; + + for (const locale of locales) { + try { + const numbersData = projectRequire(`cldr-numbers-full/main/${locale}/numbers.json`); + const accounting = + numbersData.main[locale].numbers['currencyFormats-numberSystem-latn'].accounting; + result[locale] = accounting; + } catch { + // Skip locales without numbers data + } + } + + return result; +} + +async function lintFiles( + cldrDataOutputDir: string, + defaultMessagesOutputDir: string, + projectRoot: string, + projectRequire: NodeRequire, +): Promise { + try { + const { ESLint } = projectRequire('eslint'); + + const eslint = new ESLint({ + fix: true, + cwd: projectRoot, + overrideConfig: { + linterOptions: { + reportUnusedDisableDirectives: 'off', + }, + }, + }); + + const filesToLint = [ + path.join(cldrDataOutputDir, '*.ts'), + path.join(defaultMessagesOutputDir, 'default_messages.ts'), + ]; + + const results = await eslint.lintFiles(filesToLint); + + await ESLint.outputFixes(results); + + const errorCount = results.reduce( + (sum: number, result: { errorCount: number }) => sum + result.errorCount, + 0, + ); + if (errorCount > 0) { + logger.warn(`ESLint found ${errorCount} errors in generated files`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn(`ESLint not available, skipping linting of generated files: ${errorMessage}`); + } +} + +const runExecutor: PromiseExecutor = async (options, context) => { + const absoluteProjectRoot = resolveProjectPath(context); + + const messagesDir = path.join(absoluteProjectRoot, options.messagesDir || DEFAULT_MESSAGES_DIR); + const messageTemplate = path.join( + absoluteProjectRoot, + options.messageTemplate || DEFAULT_MESSAGE_TEMPLATE, + ); + const messageOutputDir = path.join( + absoluteProjectRoot, + options.messageOutputDir || DEFAULT_MESSAGE_OUTPUT_DIR, + ); + const generatedTemplate = path.join( + absoluteProjectRoot, + options.generatedTemplate || DEFAULT_GENERATED_TEMPLATE, + ); + const cldrDataOutputDir = path.join( + absoluteProjectRoot, + options.cldrDataOutputDir || DEFAULT_CLDR_DATA_OUTPUT_DIR, + ); + const defaultMessagesOutputDir = path.join( + absoluteProjectRoot, + options.defaultMessagesOutputDir || DEFAULT_DEFAULT_MESSAGES_OUTPUT_DIR, + ); + + const skipCldrGeneration = options.skipCldrGeneration ?? false; + const skipMessageGeneration = options.skipMessageGeneration ?? false; + const lintGeneratedFiles = options.lintGeneratedFiles ?? true; + + try { + validateInputPaths( + messagesDir, + messageTemplate, + generatedTemplate, + skipMessageGeneration, + skipCldrGeneration, + ); + + if (!skipMessageGeneration) { + fs.mkdirSync(messageOutputDir, { recursive: true }); + } + if (!skipCldrGeneration) { + fs.mkdirSync(cldrDataOutputDir, { recursive: true }); + fs.mkdirSync(defaultMessagesOutputDir, { recursive: true }); + } + + if (!skipMessageGeneration) { + logger.info('Generating localization message files...'); + await generateMessageFiles(messagesDir, messageTemplate, messageOutputDir); + logger.info(`Message files generated in ${messageOutputDir}`); + } + + if (!skipCldrGeneration) { + logger.info('Generating CLDR TypeScript modules...'); + await generateCldrModules( + absoluteProjectRoot, + messagesDir, + generatedTemplate, + cldrDataOutputDir, + defaultMessagesOutputDir, + lintGeneratedFiles, + ); + logger.info(`CLDR modules generated in ${cldrDataOutputDir}`); + } + + logger.info('Localization generation completed successfully'); + return { success: true }; + } catch (error) { + logError('Localization executor failed', error); + return { success: false }; + } +}; + +export default runExecutor; diff --git a/packages/nx-infra-plugin/src/executors/localization/schema.json b/packages/nx-infra-plugin/src/executors/localization/schema.json new file mode 100644 index 000000000000..b8579581a2dc --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/localization/schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Localization Executor", + "description": "Generates localization message files and TypeScript CLDR data modules", + "type": "object", + "properties": { + "messagesDir": { + "type": "string", + "description": "Directory containing locale message JSON files (e.g., en.json, de.json)", + "default": "./js/localization/messages" + }, + "messageTemplate": { + "type": "string", + "description": "Path to the Lodash template file for UMD message generation", + "default": "./build/gulp/localization-template.jst" + }, + "messageOutputDir": { + "type": "string", + "description": "Output directory for generated dx.messages.{locale}.js files", + "default": "./artifacts/js/localization" + }, + "generatedTemplate": { + "type": "string", + "description": "Path to the Lodash template file for TypeScript exports", + "default": "./build/gulp/generated_js.jst" + }, + "cldrDataOutputDir": { + "type": "string", + "description": "Output directory for CLDR data TypeScript modules", + "default": "./js/__internal/core/localization/cldr-data" + }, + "defaultMessagesOutputDir": { + "type": "string", + "description": "Output directory for default_messages.ts", + "default": "./js/__internal/core/localization" + }, + "lintGeneratedFiles": { + "type": "boolean", + "description": "Run ESLint with auto-fix on generated TypeScript files", + "default": true + }, + "skipCldrGeneration": { + "type": "boolean", + "description": "Skip CLDR TypeScript generation (only generate message files)", + "default": false + }, + "skipMessageGeneration": { + "type": "boolean", + "description": "Skip message file generation (only generate CLDR TypeScript files)", + "default": false + } + }, + "required": [] +} diff --git a/packages/nx-infra-plugin/src/executors/localization/schema.ts b/packages/nx-infra-plugin/src/executors/localization/schema.ts new file mode 100644 index 000000000000..e2ff8efe1db5 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/localization/schema.ts @@ -0,0 +1,11 @@ +export interface LocalizationExecutorSchema { + messagesDir?: string; + messageTemplate?: string; + messageOutputDir?: string; + generatedTemplate?: string; + cldrDataOutputDir?: string; + defaultMessagesOutputDir?: string; + lintGeneratedFiles?: boolean; + skipCldrGeneration?: boolean; + skipMessageGeneration?: boolean; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9312131e810..02d55a52dedd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2238,7 +2238,7 @@ importers: packages/nx-infra-plugin: dependencies: fs-extra: - specifier: ^11.2.0 + specifier: 11.2.0 version: 11.2.0 glob: specifier: 11.1.0 @@ -2246,6 +2246,9 @@ importers: karma: specifier: '>=6.0.0' version: 6.4.4 + lodash: + specifier: 4.17.21 + version: 4.17.21 ng-packagr: specifier: '>=19.0.0' version: 19.2.2(@angular/compiler-cli@21.0.8(@angular/compiler@21.0.8)(typescript@4.9.5))(tslib@2.8.1)(typescript@4.9.5) @@ -2262,6 +2265,9 @@ importers: '@types/jest': specifier: 29.5.14 version: 29.5.14 + '@types/lodash': + specifier: 4.17.0 + version: 4.17.0 '@types/node': specifier: 18.19.130 version: 18.19.130 @@ -6696,8 +6702,8 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/lodash@4.17.13': - resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} + '@types/lodash@4.17.0': + resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -15122,7 +15128,6 @@ 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.5.3: @@ -21793,7 +21798,7 @@ snapshots: '@devexpress/callsite-record@4.1.7': dependencies: - '@types/lodash': 4.17.13 + '@types/lodash': 4.17.0 callsite: 1.0.0 chalk: 2.4.2 error-stack-parser: 2.1.4 @@ -22351,8 +22356,8 @@ snapshots: espree: 7.3.1 globals: 13.24.0 ignore: 4.0.6 - import-fresh: 3.3.0 - js-yaml: 3.14.1 + import-fresh: 3.3.1 + js-yaml: 3.14.2 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -22364,8 +22369,8 @@ snapshots: debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 - ignore: 5.3.1 - import-fresh: 3.3.0 + ignore: 5.3.2 + import-fresh: 3.3.1 js-yaml: 4.1.0 minimatch: 3.1.2 strip-json-comments: 3.1.1 @@ -22379,7 +22384,7 @@ snapshots: espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 js-yaml: 4.1.0 minimatch: 3.1.2 strip-json-comments: 3.1.1 @@ -25282,7 +25287,7 @@ snapshots: '@types/clean-css@4.2.11': dependencies: - '@types/node': 18.19.64 + '@types/node': 20.12.8 source-map: 0.6.1 '@types/connect-history-api-fallback@1.5.4': @@ -25380,7 +25385,7 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 18.19.64 + '@types/node': 20.12.8 '@types/glob@7.2.0': dependencies: @@ -25440,7 +25445,7 @@ snapshots: dependencies: '@types/node': 20.12.8 - '@types/lodash@4.17.13': {} + '@types/lodash@4.17.0': {} '@types/mdast@3.0.15': dependencies: @@ -26550,7 +26555,7 @@ snapshots: '@yarnpkg/parsers@3.0.0-rc.46': dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 tslib: 2.6.3 '@zkochan/js-yaml@0.0.7': @@ -27005,7 +27010,7 @@ snapshots: call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.24.0 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: @@ -28653,7 +28658,7 @@ snapshots: cosmiconfig@6.0.0: dependencies: '@types/parse-json': 4.0.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 parse-json: 5.2.0 path-type: 4.0.0 yaml: 1.10.2 @@ -30492,7 +30497,7 @@ snapshots: eslint-plugin-es-x@7.8.0(eslint@9.18.0(jiti@2.6.1)): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.18.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 eslint: 9.18.0(jiti@2.6.1) eslint-compat-utils: 0.5.1(eslint@9.18.0(jiti@2.6.1)) @@ -30904,10 +30909,10 @@ snapshots: glob-parent: 5.1.2 globals: 13.24.0 ignore: 4.0.6 - import-fresh: 3.3.0 + import-fresh: 3.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 - js-yaml: 3.14.1 + js-yaml: 3.14.2 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 @@ -30927,8 +30932,8 @@ snapshots: eslint@9.18.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.18.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.18.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.19.2 '@eslint/core': 0.10.0 '@eslint/eslintrc': 3.3.1 @@ -30953,7 +30958,7 @@ snapshots: file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 @@ -31697,7 +31702,7 @@ snapshots: front-matter@4.0.2: dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 fs-constants@1.0.0: {} @@ -32207,7 +32212,7 @@ snapshots: vinyl-fs: 4.0.0 optionalDependencies: '@types/eslint': 9.6.1 - '@types/node': 18.19.64 + '@types/node': 20.12.8 transitivePeerDependencies: - jiti - supports-color @@ -33015,7 +33020,6 @@ snapshots: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - optional: true import-lazy@3.1.0: {} @@ -34141,7 +34145,7 @@ snapshots: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 20.14.5 + '@types/node': 20.12.8 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 From 726adc51bb6abfaaec000d74303f843944438c38 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Wed, 7 Jan 2026 16:29:11 +0200 Subject: [PATCH 02/14] fix(nx-infra-plugin): fix CLDR deps resolution in localization e2e tests --- .../localization/executor.e2e.spec.ts | 23 +++++++++++++++++-- .../nx-infra-plugin/src/utils/test-utils.ts | 14 +++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts index 0f399aa717ee..72eeda0d567a 100644 --- a/packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/localization/executor.e2e.spec.ts @@ -2,8 +2,17 @@ import * as fs from 'fs'; import * as path from 'path'; import executor from './executor'; import { LocalizationExecutorSchema } from './schema'; -import { createTempDir, cleanupTempDir, createMockContext } from '../../utils/test-utils'; -import { writeFileText, writeJson, readFileText } from '../../utils'; +import { + writeFileText, + writeJson, + cleanupTempDir, + readFileText, + createTempDir, + createMockContext, + findWorkspaceRoot, +} from '../../utils'; + +const WORKSPACE_ROOT = findWorkspaceRoot(); const PROJECT_SUBPATH = ['packages', 'test-lib'] as const; @@ -122,6 +131,16 @@ describe('LocalizationExecutor E2E', () => { tempDir = createTempDir('nx-localization-e2e-'); context = createMockContext({ root: tempDir }); fixture = await createLocalizationTestFixture(tempDir); + + const devextremeNodeModules = path.join( + WORKSPACE_ROOT, + 'packages', + 'devextreme', + 'node_modules', + ); + + const tempNodeModules = path.join(fixture.projectDir, 'node_modules'); + fs.symlinkSync(devextremeNodeModules, tempNodeModules, 'junction'); }); afterEach(() => { diff --git a/packages/nx-infra-plugin/src/utils/test-utils.ts b/packages/nx-infra-plugin/src/utils/test-utils.ts index 98694687b197..65bdb31bbcf7 100644 --- a/packages/nx-infra-plugin/src/utils/test-utils.ts +++ b/packages/nx-infra-plugin/src/utils/test-utils.ts @@ -41,3 +41,17 @@ export function createMockContext(options: MockContextOptions = {}): ExecutorCon }, }; } + +export function findWorkspaceRoot(): string { + let dir = process.cwd(); + while (dir !== path.dirname(dir)) { + if ( + fs.existsSync(path.join(dir, 'nx.json')) + || fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')) + ) { + return dir; + } + dir = path.dirname(dir); + } + throw new Error('Could not find workspace root'); +} From f071ceaae86d26cbaf878b67dfe1de942703789b Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Wed, 7 Jan 2026 16:38:15 +0200 Subject: [PATCH 03/14] chore: log only failed tests during CI workflow execution --- .github/workflows/default_workflow.yml | 1 + packages/nx-infra-plugin/project.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/default_workflow.yml b/.github/workflows/default_workflow.yml index bf0fd1091955..8d6635151055 100644 --- a/.github/workflows/default_workflow.yml +++ b/.github/workflows/default_workflow.yml @@ -56,6 +56,7 @@ jobs: run: > pnpx nx run-many -t lint,test + --configuration ci --exclude devextreme devextreme-themebuilder diff --git a/packages/nx-infra-plugin/project.json b/packages/nx-infra-plugin/project.json index 1c0103ee6907..daa144c51ac4 100644 --- a/packages/nx-infra-plugin/project.json +++ b/packages/nx-infra-plugin/project.json @@ -23,6 +23,11 @@ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { "jestConfig": "{projectRoot}/jest.config.ts" + }, + "configurations": { + "ci": { + "silent": true + } } } } From 6fcb05482db21569b8b757a4b214d7be6418b3fe Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Wed, 14 Jan 2026 17:06:10 +0200 Subject: [PATCH 04/14] chore: cleanup migrated localization gulp tasks --- .../devextreme/build/gulp/localization.js | 181 +----------------- 1 file changed, 2 insertions(+), 179 deletions(-) diff --git a/packages/devextreme/build/gulp/localization.js b/packages/devextreme/build/gulp/localization.js index 217bfa0f8458..ecf67b940fc5 100644 --- a/packages/devextreme/build/gulp/localization.js +++ b/packages/devextreme/build/gulp/localization.js @@ -2,116 +2,14 @@ const gulp = require('gulp'); const path = require('path'); -const rename = require('gulp-rename'); -const del = require('del'); -const template = require('gulp-template'); -const lint = require('gulp-eslint-new'); +const shell = require('gulp-shell'); const through = require('through2'); const fs = require('fs'); -const headerPipes = require('./header-pipes.js'); -const compressionPipes = require('./compression-pipes.js'); -const context = require('./context.js'); - -const Cldr = require('cldrjs'); -const locales = require('cldr-core/availableLocales.json').availableLocales.full; -const weekData = require('cldr-core/supplemental/weekData.json'); -const likelySubtags = require('cldr-core/supplemental/likelySubtags.json'); -const parentLocales = require('cldr-core/supplemental/parentLocales.json').supplemental.parentLocales.parentLocale; - -const globalizeEnCldr = require('devextreme-cldr-data/en.json'); -const globalizeSupplementalCldr = require('devextreme-cldr-data/supplemental.json'); - -const PARENT_LOCALE_SEPARATOR = '-'; const DEFAULT_LOCALE = 'en'; - -const getParentLocale = (parentLocales, locale) => { - const parentLocale = parentLocales[locale]; - - if(parentLocale) { - return parentLocale !== 'root' && parentLocale; - } - - return locale.substr(0, locale.lastIndexOf(PARENT_LOCALE_SEPARATOR)); -}; - -const firstDayOfWeekData = function() { - const DAY_INDEXES = { - 'sun': 0, - 'mon': 1, - 'tue': 2, - 'wed': 3, - 'thu': 4, - 'fri': 5, - 'sat': 6 - }; - const DEFAULT_DAY_OF_WEEK_INDEX = 0; - - const result = {}; - - Cldr.load(weekData, likelySubtags); - - const getFirstIndex = (locale) => { - const firstDay = new Cldr(locale).supplemental.weekData.firstDay(); - return DAY_INDEXES[firstDay]; - }; - - locales.forEach(function(locale) { - const firstDayIndex = getFirstIndex(locale); - - const parentLocale = getParentLocale(parentLocales, locale); - if(firstDayIndex !== DEFAULT_DAY_OF_WEEK_INDEX && (!parentLocale || firstDayIndex !== getFirstIndex(parentLocale))) { - result[locale] = firstDayIndex; - } - }); - - return result; -}; - -const accountingFormats = function() { - const result = {}; - - locales.forEach(function(locale) { - const dataFilePath = `../../node_modules/cldr-numbers-full/main/${locale}/numbers.json`; - - if(fs.existsSync(path.join(__dirname, dataFilePath))) { - const numbersData = require(dataFilePath); - result[locale] = numbersData.main[locale].numbers['currencyFormats-numberSystem-latn'].accounting; - } - }); - - return result; -}; - -const RESULT_PATH = path.join(context.RESULT_JS_PATH, 'localization'); const DICTIONARY_SOURCE_FOLDER = 'js/localization/messages'; -const getLocales = function(directory) { - return fs.readdirSync(directory).map(file => { - return file.split('.')[0]; - }); -}; - -const serializeObject = function(obj, shift) { - const tab = ' '; - let result = JSON.stringify(obj, null, tab); - - if(shift) { - result = result.replace(/(\n)/g, '$1' + tab); - } - - return result; -}; - -const getMessages = function(directory, locale) { - const json = require(path.join('../../', directory, locale + '.json')); - - return serializeObject(json, true); -}; - -gulp.task('clean-cldr-data', function() { - return del('js/__internal/core/localization/cldr-data/**', { force: true }); -}); +gulp.task('localization', shell.task('pnpm nx build:localization devextreme')); gulp.task('generate-community-locales', () => { const defaultFile = fs.readFileSync(path.join(DICTIONARY_SOURCE_FOLDER, DEFAULT_LOCALE + '.json')).toString(); @@ -150,78 +48,3 @@ gulp.task('generate-community-locales', () => { })) .pipe(gulp.dest(DICTIONARY_SOURCE_FOLDER)); }); - -gulp.task('localization-messages', gulp.parallel(getLocales(DICTIONARY_SOURCE_FOLDER).map(locale => Object.assign( - function() { - return gulp - .src('build/gulp/localization-template.jst') - .pipe(template({ - json: getMessages(DICTIONARY_SOURCE_FOLDER, locale) - })) - .pipe(rename(['dx', 'messages', locale, 'js'].join('.'))) - .pipe(compressionPipes.beautify()) - .pipe(headerPipes.useStrict()) - .pipe(headerPipes.bangLicense()) - .pipe(gulp.dest(RESULT_PATH)); - }, - { displayName: 'dx.messages.' + locale } -)))); - -gulp.task('localization-generated-sources', gulp.parallel([ - { - data: require('../../js/localization/messages/en.json'), - filename: 'default_messages.ts', - exportName: 'defaultMessages', - destination: 'js/__internal/core/localization' - }, - { - data: parentLocales, - filename: 'parent_locales.ts', - destination: 'js/__internal/core/localization/cldr-data' - }, - { - data: firstDayOfWeekData(), - filename: 'first_day_of_week_data.ts', - destination: 'js/__internal/core/localization/cldr-data' - }, - { - data: accountingFormats(), - filename: 'accounting_formats.ts', - destination: 'js/__internal/core/localization/cldr-data' - - }, - { - data: globalizeEnCldr, - exportName: 'enCldr', - filename: 'en.ts', - destination: 'js/__internal/core/localization/cldr-data' - }, - { - data: globalizeSupplementalCldr, - exportName: 'supplementalCldr', - filename: 'supplemental.ts', - destination: 'js/__internal/core/localization/cldr-data' - } -].map((source) => Object.assign( - function() { - return gulp - .src('build/gulp/generated_js.jst') - .pipe(template({ - exportName: source.exportName, - json: serializeObject(source.data) - })) - .pipe(lint({ fix: true })) - .pipe(lint.format()) - .pipe(rename(source.filename)) - .pipe(gulp.dest(source.destination)); - }, - { displayName: source.filename } -)))); - -gulp.task('localization', - gulp.series( - 'clean-cldr-data', - 'localization-messages', - 'localization-generated-sources' - ) -); From 7eb3e120fa3893fc3dfef02d1d36d49cf5a36a64 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Wed, 14 Jan 2026 17:08:18 +0200 Subject: [PATCH 05/14] feat(nx-infra-plugin): copy-files, support glob patterns --- packages/nx-infra-plugin/package.json | 2 +- .../executors/copy-files/executor.e2e.spec.ts | 25 ++++++ .../src/executors/copy-files/executor.ts | 84 ++++++++++++++----- .../src/executors/copy-files/schema.json | 10 ++- pnpm-lock.yaml | 12 +-- 5 files changed, 104 insertions(+), 29 deletions(-) diff --git a/packages/nx-infra-plugin/package.json b/packages/nx-infra-plugin/package.json index ec5c04c11b06..6a998082b561 100644 --- a/packages/nx-infra-plugin/package.json +++ b/packages/nx-infra-plugin/package.json @@ -35,7 +35,7 @@ "@types/jest": "29.5.14", "@types/normalize-path": "3.0.2", "@types/node": "18.19.130", - "@types/lodash": "4.17.0", + "@types/lodash": "4.17.13", "prettier": "catalog:tools", "ts-jest": "29.1.3", "typescript": "4.9.5" diff --git a/packages/nx-infra-plugin/src/executors/copy-files/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/copy-files/executor.e2e.spec.ts index 72a266c51719..ac36e37a8999 100644 --- a/packages/nx-infra-plugin/src/executors/copy-files/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/copy-files/executor.e2e.spec.ts @@ -105,4 +105,29 @@ describe('CopyFilesExecutor E2E', () => { expect(content).not.toBe('Old content'); }); }); + + describe('Glob patterns', () => { + it('should copy files using glob pattern', async () => { + const projectDir = path.join(tempDir, 'packages', 'test-lib'); + const srcDir = path.join(projectDir, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + + await writeFileText(path.join(srcDir, 'file1.ts'), 'export const a = 1;'); + await writeFileText(path.join(srcDir, 'file2.ts'), 'export const b = 2;'); + await writeFileText(path.join(srcDir, 'other.js'), 'module.exports = {};'); + + const options: CopyFilesExecutorSchema = { + files: [{ from: './src/*.ts', to: './dist' }], + }; + + const result = await executor(options, context); + + expect(result.success).toBe(true); + + const distDir = path.join(projectDir, 'dist'); + expect(fs.existsSync(path.join(distDir, 'file1.ts'))).toBe(true); + expect(fs.existsSync(path.join(distDir, 'file2.ts'))).toBe(true); + expect(fs.existsSync(path.join(distDir, 'other.js'))).toBe(false); + }); + }); }); diff --git a/packages/nx-infra-plugin/src/executors/copy-files/executor.ts b/packages/nx-infra-plugin/src/executors/copy-files/executor.ts index fedaad6ed28c..977bb0700066 100644 --- a/packages/nx-infra-plugin/src/executors/copy-files/executor.ts +++ b/packages/nx-infra-plugin/src/executors/copy-files/executor.ts @@ -1,19 +1,72 @@ import { PromiseExecutor, logger } from '@nx/devkit'; import * as path from 'path'; -import * as fs from 'fs/promises'; +import { stat } from 'fs/promises'; +import { glob } from 'glob'; import { CopyFilesExecutorSchema } from './schema'; -import { resolveProjectPath } from '../../utils/path-resolver'; +import { resolveProjectPath, normalizeGlobPathForWindows } from '../../utils/path-resolver'; +import { isWindowsOS } from '../../utils/common'; import { logError } from '../../utils/error-handler'; -import { copyFile, copyRecursive, exists } from '../../utils/file-operations'; +import { copyFile, copyRecursive, exists, ensureDir } from '../../utils/file-operations'; -const ERROR_FILES_MUST_BE_ARRAY = 'Files option must be an array'; -const ERROR_FAILED_TO_COPY = 'Failed to copy files'; +const ERROR_MESSAGES = { + FILES_MUST_BE_ARRAY: 'Files option must be an array', + FAILED_TO_COPY: 'Failed to copy files', + NO_FILES_MATCH_PATTERN: (pattern: string) => `No files found matching pattern: ${pattern}`, + SOURCE_NOT_FOUND: (source: string) => `Source file not found: ${source}`, +} as const; + +function containsGlobPattern(pattern: string): boolean { + return /[*?[\]{}]/.test(pattern); +} + +async function copyGlobPatternFiles( + sourcePath: string, + destPath: string, +): Promise<{ success: boolean }> { + const globPattern = isWindowsOS() ? normalizeGlobPathForWindows(sourcePath) : sourcePath; + const files = await glob(globPattern, { nodir: true }); + + if (files.length === 0) { + logger.error(ERROR_MESSAGES.NO_FILES_MATCH_PATTERN(sourcePath)); + return { success: false }; + } + + await ensureDir(destPath); + + for (const file of files) { + const fileName = path.basename(file); + const destFile = path.join(destPath, fileName); + await copyFile(file, destFile); + logger.info(`Copied file ${file} -> ${destFile}`); + } + + return { success: true }; +} + +async function copyDirectPath(sourcePath: string, destPath: string): Promise<{ success: boolean }> { + if (!(await exists(sourcePath))) { + logger.error(ERROR_MESSAGES.SOURCE_NOT_FOUND(sourcePath)); + return { success: false }; + } + + const sourceStat = await stat(sourcePath); + + if (sourceStat.isDirectory()) { + await copyRecursive(sourcePath, destPath); + logger.info(`Copied directory ${sourcePath} -> ${destPath}`); + return { success: true }; + } + + await copyFile(sourcePath, destPath); + logger.info(`Copied file ${sourcePath} -> ${destPath}`); + return { success: true }; +} const runExecutor: PromiseExecutor = async (options, context) => { const projectRoot = resolveProjectPath(context); if (!options.files || !Array.isArray(options.files)) { - logger.error(ERROR_FILES_MUST_BE_ARRAY); + logger.error(ERROR_MESSAGES.FILES_MUST_BE_ARRAY); return { success: false }; } @@ -22,25 +75,18 @@ const runExecutor: PromiseExecutor = async (options, co const sourcePath = path.resolve(projectRoot, from); const destPath = path.resolve(projectRoot, to); - if (!(await exists(sourcePath))) { - logger.error(`Source file not found: ${sourcePath}`); - return { success: false }; - } + const result = containsGlobPattern(from) + ? await copyGlobPatternFiles(sourcePath, destPath) + : await copyDirectPath(sourcePath, destPath); - const stat = await fs.stat(sourcePath); - - if (stat.isDirectory()) { - await copyRecursive(sourcePath, destPath); - logger.info(`Copied directory ${sourcePath} -> ${destPath}`); - } else { - await copyFile(sourcePath, destPath); - logger.info(`Copied file ${sourcePath} -> ${destPath}`); + if (!result.success) { + return { success: false }; } } return { success: true }; } catch (error) { - logError(ERROR_FAILED_TO_COPY, error); + logError(ERROR_MESSAGES.FAILED_TO_COPY, error); return { success: false }; } }; diff --git a/packages/nx-infra-plugin/src/executors/copy-files/schema.json b/packages/nx-infra-plugin/src/executors/copy-files/schema.json index 7c777b705386..10e9cc609c07 100644 --- a/packages/nx-infra-plugin/src/executors/copy-files/schema.json +++ b/packages/nx-infra-plugin/src/executors/copy-files/schema.json @@ -1,17 +1,21 @@ { + "$schema": "https://json-schema.org/schema", "type": "object", + "description": "Copy files or directories to a destination. Supports glob patterns and recursive directory copying.", "properties": { "files": { "type": "array", - "description": "Files to copy (array of {from, to})", + "description": "Array of file copy operations to perform.", "items": { "type": "object", "properties": { "from": { - "type": "string" + "type": "string", + "description": "Source path relative to project root. Supports glob patterns (e.g., './src/*.ts', './assets/**/*')." }, "to": { - "type": "string" + "type": "string", + "description": "Destination path relative to project root. For glob patterns, this should be a directory." } }, "required": ["from", "to"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02d55a52dedd..fc621ae1b7f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2266,8 +2266,8 @@ importers: specifier: 29.5.14 version: 29.5.14 '@types/lodash': - specifier: 4.17.0 - version: 4.17.0 + specifier: 4.17.13 + version: 4.17.13 '@types/node': specifier: 18.19.130 version: 18.19.130 @@ -6702,8 +6702,8 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/lodash@4.17.0': - resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -21798,7 +21798,7 @@ snapshots: '@devexpress/callsite-record@4.1.7': dependencies: - '@types/lodash': 4.17.0 + '@types/lodash': 4.17.13 callsite: 1.0.0 chalk: 2.4.2 error-stack-parser: 2.1.4 @@ -25445,7 +25445,7 @@ snapshots: dependencies: '@types/node': 20.12.8 - '@types/lodash@4.17.0': {} + '@types/lodash@4.17.13': {} '@types/mdast@3.0.15': dependencies: From 8833e56b05606c685bf6b64f0f6cdeddc07244df Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Thu, 15 Jan 2026 13:24:42 +0200 Subject: [PATCH 06/14] chore(devextreme): migrate all:build-dev to nx with wrapped as nx targets gulp tasks --- package.json | 2 +- packages/devextreme/package.json | 1 - packages/devextreme/project.json | 165 ++++++++++++++++++++++++++++++- packages/workflows/project.json | 72 ++++++++++++++ tools/scripts/build-all.ts | 39 +++----- 5 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 packages/workflows/project.json diff --git a/package.json b/package.json index 31896e8a7376..8e6278fffb6c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "prepare": "husky install", "all:update-version": "ts-node tools/scripts/update-version.ts", "all:build": "ts-node tools/scripts/build-all.ts", - "all:build-dev": "pnpm run all:build --dev", + "all:build-dev": "nx all:build-dev workflows", "all:pack-and-copy": "nx run-many -t pack-and-copy", "demos:prepare": "nx run devextreme-demos:prepare-js", "demos:start": "http-server ./apps/demos --port 8080 -c-1" diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 0cdb1564d281..adf976c15440 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -221,7 +221,6 @@ "lint-dts": "eslint './js/**/*.d.ts'", "lint-staged": "lint-staged", "lint-texts": "node build/linters/validate-non-latin-symbols.js", - "build": "cross-env BUILD_ESM_PACKAGE=true gulp default", "build:dev": "cross-env DEVEXTREME_TEST_CI=TRUE BUILD_ESM_PACKAGE=true gulp default", "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", diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 1f8c425fe69b..5a7b390d0530 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -88,10 +88,171 @@ ], "cache": true }, + "build:transpile": { + "executor": "nx:run-commands", + "options": { + "command": "cross-env BUILD_ESM_PACKAGE=true gulp transpile", + "cwd": "{projectRoot}" + }, + "inputs": [ + "{projectRoot}/js/**/*.js", + "{projectRoot}/build/gulp/transpile.js", + "{projectRoot}/babel.config.cjs" + ], + "outputs": [ + "{projectRoot}/artifacts/transpiled", + "{projectRoot}/artifacts/transpiled-esm-npm", + "{projectRoot}/artifacts/transpiled-renovation-npm" + ], + "cache": true + }, + "bundle:debug": { + "executor": "nx:run-commands", + "options": { + "command": "gulp js-bundles-debug", + "cwd": "{projectRoot}" + }, + "inputs": [ + { "env": "BUILD_TEST_INTERNAL_PACKAGE" }, + "{projectRoot}/artifacts/transpiled/**/*", + "{projectRoot}/artifacts/transpiled-esm/**/*", + "{projectRoot}/build/gulp/js-bundles.js", + "{projectRoot}/webpack.config.js" + ], + "outputs": [ + "{projectRoot}/artifacts/js/dx.all.debug.js", + "{projectRoot}/artifacts/js/dx.all.debug.js.map" + ], + "cache": true + }, + "bundle:prod": { + "executor": "nx:run-commands", + "options": { + "command": "gulp js-bundles-prod", + "cwd": "{projectRoot}" + }, + "inputs": [ + { "env": "BUILD_TEST_INTERNAL_PACKAGE" }, + "{projectRoot}/artifacts/transpiled/**/*", + "{projectRoot}/artifacts/transpiled-esm/**/*", + "{projectRoot}/build/gulp/js-bundles.js", + "{projectRoot}/webpack.config.js" + ], + "outputs": [ + "{projectRoot}/artifacts/js/dx.all.js", + "{projectRoot}/artifacts/js/dx.all.js.map" + ], + "cache": true + }, + "build:vectormap": { + "executor": "nx:run-commands", + "options": { + "command": "gulp vectormap", + "cwd": "{projectRoot}" + }, + "inputs": [ + "{projectRoot}/build/gulp/vectormap.js" + ], + "outputs": [ + "{projectRoot}/artifacts/js/vectormap-data" + ], + "cache": true + }, + "build:aspnet": { + "executor": "nx:run-commands", + "options": { + "command": "gulp aspnet", + "cwd": "{projectRoot}" + }, + "inputs": [ + "{projectRoot}/build/gulp/aspnet.js" + ], + "outputs": [ + "{projectRoot}/artifacts/js/aspnet" + ], + "cache": true + }, + "build:declarations": { + "executor": "nx:run-commands", + "options": { + "command": "gulp ts", + "cwd": "{projectRoot}" + }, + "inputs": [ + "{projectRoot}/ts/**/*.d.ts", + "{projectRoot}/build/gulp/ts.js" + ], + "outputs": [ + "{projectRoot}/artifacts/ts" + ], + "cache": true + }, + "verify:licenses": { + "executor": "nx:run-commands", + "options": { + "command": "gulp check-license-notices", + "cwd": "{projectRoot}" + }, + "cache": false + }, + "copy:vendor": { + "executor": "nx:run-commands", + "options": { + "command": "gulp vendor", + "cwd": "{projectRoot}" + }, + "inputs": [ + "{projectRoot}/build/gulp/vendor.js" + ], + "outputs": [ + "{projectRoot}/artifacts/js/vectormap-utils", + "{projectRoot}/artifacts/js/cldr" + ], + "cache": true + }, + "pack:devextreme-npm": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm pack", + "cwd": "{projectRoot}/artifacts/npm/devextreme" + } + }, + "pack:devextreme-dist-npm": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm pack", + "cwd": "{projectRoot}/artifacts/npm/devextreme-dist" + } + }, + "build:npm": { + "executor": "nx:run-commands", + "options": { + "command": "cross-env BUILD_ESM_PACKAGE=true gulp npm", + "cwd": "{projectRoot}" + }, + "inputs": [ + { "env": "BUILD_TEST_INTERNAL_PACKAGE" }, + "{projectRoot}/artifacts/transpiled/**/*", + "{projectRoot}/artifacts/transpiled-esm-npm/**/*" + ], + "outputs": [ + "{projectRoot}/artifacts/npm/devextreme", + "{projectRoot}/artifacts/npm/devextreme-dist" + ], + "cache": true + }, "build": { - "executor": "nx:run-script", + "executor": "nx:run-commands", "options": { - "script": "build" + "commands": [ + "pnpm nx clean:artifacts devextreme", + "pnpm nx build:localization devextreme", + "pnpm nx build:transpile devextreme", + "pnpm nx run-many --targets=bundle:debug,bundle:prod,build:vectormap,copy:vendor,build:aspnet,build:declarations --projects=devextreme --parallel", + "pnpm nx build:npm devextreme", + "pnpm nx verify:licenses devextreme" + ], + "parallel": false }, "inputs": [ { "env": "BUILD_TEST_INTERNAL_PACKAGE" }, diff --git a/packages/workflows/project.json b/packages/workflows/project.json new file mode 100644 index 000000000000..f2f401df6ba3 --- /dev/null +++ b/packages/workflows/project.json @@ -0,0 +1,72 @@ +{ + "name": "workflows", + "metadata": { + "description": "Build workflow that orchestrates targets across multiple packages for specific use cases like local development, CI/CD, etc." + }, + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "targets": { + "copy:artifacts-to-root": { + "executor": "devextreme-nx-infra-plugin:copy-files", + "options": { + "files": [ + { "from": "../devextreme/artifacts/js", "to": "../../artifacts/js" }, + { "from": "../devextreme/artifacts/css", "to": "../../artifacts/css" } + ] + } + }, + "copy:bootstrap": { + "executor": "devextreme-nx-infra-plugin:copy-files", + "options": { + "files": [ + { "from": "../devextreme-themebuilder/node_modules/bootstrap/dist/js/bootstrap.js", "to": "../../artifacts/js/bootstrap.js" }, + { "from": "../devextreme-themebuilder/node_modules/bootstrap/dist/js/bootstrap.min.js", "to": "../../artifacts/js/bootstrap.min.js" }, + { "from": "../devextreme-themebuilder/node_modules/bootstrap/dist/css/bootstrap.css", "to": "../../artifacts/css/bootstrap.css" }, + { "from": "../devextreme-themebuilder/node_modules/bootstrap/dist/css/bootstrap.min.css", "to": "../../artifacts/css/bootstrap.min.css" } + ] + } + }, + "copy:npm-tgz": { + "executor": "devextreme-nx-infra-plugin:copy-files", + "options": { + "files": [ + { "from": "../devextreme/artifacts/npm/devextreme/*.tgz", "to": "../../artifacts/npm/" }, + { "from": "../devextreme/artifacts/npm/devextreme-dist/*.tgz", "to": "../../artifacts/npm/" } + ] + } + }, + "copy:wrappers-tgz": { + "executor": "devextreme-nx-infra-plugin:copy-files", + "options": { + "files": [ + { "from": "../devextreme-angular/npm/dist/*.tgz", "to": "../../artifacts/npm/" }, + { "from": "../devextreme-react/npm/*.tgz", "to": "../../artifacts/npm/" }, + { "from": "../devextreme-vue/npm/*.tgz", "to": "../../artifacts/npm/" } + ] + } + }, + "all:build-dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm nx build devextreme", + "pnpm nx build devextreme-themebuilder", + "pnpm nx copy:artifacts-to-root workflows", + "pnpm nx copy:bootstrap workflows", + "pnpm run all:pack-and-copy", + "pnpm nx pack devextreme-react", + "pnpm nx pack devextreme-vue", + "pnpm nx pack devextreme-angular", + "pnpm nx pack:devextreme-npm devextreme", + "pnpm nx pack:devextreme-dist-npm devextreme", + "pnpm nx copy:npm-tgz workflows", + "pnpm nx copy:wrappers-tgz workflows" + ], + "parallel": false + }, + "metadata": { + "description": "Build all packages for the local development environment." + } + } + } +} diff --git a/tools/scripts/build-all.ts b/tools/scripts/build-all.ts index 9f7e8aaf3343..d231a22fde9a 100644 --- a/tools/scripts/build-all.ts +++ b/tools/scripts/build-all.ts @@ -1,17 +1,8 @@ import sh from 'shelljs'; import path from 'node:path'; -import yargs from 'yargs'; import { ARTIFACTS_DIR, INTERNAL_TOOLS_ARTIFACTS, ROOT_DIR, NPM_DIR, JS_ARTIFACTS, CSS_ARTIFACTS } from './common/paths'; import { version as devextremeNpmVersion } from '../../packages/devextreme/package.json'; -const argv = yargs - .option('dev', { type: 'boolean', default: false }) - .parseSync(); - -const devMode = argv.dev; - -console.log(`Dev mode: ${devMode}`); - const DEVEXTREME_NPM_DIR = path.join(ROOT_DIR, 'packages/devextreme/artifacts/npm'); const injectDescriptions = () => { @@ -45,26 +36,20 @@ const MAJOR_VERSION = monorepoVersion.split('.').slice(0, 2).join('_'); sh.cd(ROOT_DIR); -if (!devMode) { - // aspnet metadata will be used in Build custom-tasks to inject aspnet descriptions - sh.exec(`pnpx nx run devextreme-metadata:make-aspnet-metadata`); +// aspnet metadata will be used in Build custom-tasks to inject aspnet descriptions +sh.exec(`pnpx nx run devextreme-metadata:make-aspnet-metadata`); - injectDescriptions(); -} +injectDescriptions(); -if (devMode) { - sh.exec('pnpx nx build devextreme'); -} else { - sh.exec('pnpx nx build devextreme-scss'); - sh.exec('pnpx nx build-dist devextreme --skipNxCache', { - env: { - ...sh.env, - BUILD_INTERNAL_PACKAGE: 'false' - } - }); -} +sh.exec('pnpx nx build devextreme-scss'); +sh.exec('pnpx nx build-dist devextreme --skipNxCache', { + env: { + ...sh.env, + BUILD_INTERNAL_PACKAGE: 'false' + } +}); -sh.exec(`pnpx nx build devextreme-themebuilder${devMode ? '' : ' --skipNxCache'}`); +sh.exec('pnpx nx build devextreme-themebuilder --skipNxCache'); // Copy artifacts for DXBuild (Installation) sh.pushd(path.join(ROOT_DIR, 'packages/devextreme/artifacts')); @@ -80,7 +65,7 @@ sh.exec('pnpm run all:pack-and-copy'); sh.exec('pnpx nx pack devextreme-react', { silent: true }); sh.exec('pnpx nx pack devextreme-vue', { silent: true }); -sh.exec(`pnpx nx pack${devMode ? '' : '-with-descriptions'} devextreme-angular`, { silent: true }); +sh.exec('pnpx nx pack-with-descriptions devextreme-angular', { silent: true }); sh.pushd(path.join(DEVEXTREME_NPM_DIR, 'devextreme')); packAndCopy(NPM_DIR); From 47be5a45772495bb6722df6990afb7d1ab021800 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Thu, 15 Jan 2026 17:46:33 +0200 Subject: [PATCH 07/14] chore: create all:build-testing workflow --- .github/workflows/demos_visual_tests.yml | 5 +---- .github/workflows/publish-demos.yml | 4 +--- .github/workflows/testcafe_tests.yml | 3 +-- .github/workflows/wrapper_tests.yml | 6 +----- .github/workflows/wrapper_tests_e2e.yml | 5 +---- packages/devextreme/project.json | 10 +++++++++- packages/workflows/project.json | 23 +++++++++++++++++++++++ 7 files changed, 37 insertions(+), 19 deletions(-) diff --git a/.github/workflows/demos_visual_tests.yml b/.github/workflows/demos_visual_tests.yml index 8c3cea1d6108..819c33fda764 100644 --- a/.github/workflows/demos_visual_tests.yml +++ b/.github/workflows/demos_visual_tests.yml @@ -15,7 +15,6 @@ on: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} - BUILD_TEST_INTERNAL_PACKAGE: true RUN_TESTS: true jobs: @@ -149,9 +148,7 @@ jobs: - name: DevExtreme - Build-all if: needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' - env: - BUILD_TEST_INTERNAL_PACKAGE: true - run: pnpm run all:build-dev + run: pnpm nx all:build-testing workflows - name: Zip artifacts (for jQuery tests) working-directory: ./packages/devextreme diff --git a/.github/workflows/publish-demos.yml b/.github/workflows/publish-demos.yml index bfb90c636b9c..ee81704afa95 100644 --- a/.github/workflows/publish-demos.yml +++ b/.github/workflows/publish-demos.yml @@ -38,9 +38,7 @@ jobs: pnpm install --frozen-lockfile - name: DevExtreme - Build-all - env: - BUILD_TEST_INTERNAL_PACKAGE: true - run: pnpm run all:build-dev + run: pnpm nx all:build-testing workflows - name: Move packages run: | diff --git a/.github/workflows/testcafe_tests.yml b/.github/workflows/testcafe_tests.yml index 9a96b37eded1..5eb20ca67bb2 100644 --- a/.github/workflows/testcafe_tests.yml +++ b/.github/workflows/testcafe_tests.yml @@ -15,7 +15,6 @@ on: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} - BUILD_TEST_INTERNAL_PACKAGE: true RUN_TESTS: true jobs: @@ -72,7 +71,7 @@ jobs: NODE_OPTIONS: --max-old-space-size=8192 run: | pnpx nx build devextreme-scss - pnpx nx build devextreme + pnpx nx build devextreme -c testing - name: Zip artifacts working-directory: ./packages/devextreme diff --git a/.github/workflows/wrapper_tests.yml b/.github/workflows/wrapper_tests.yml index 45a67fe5d3c1..50e6025fe0b4 100644 --- a/.github/workflows/wrapper_tests.yml +++ b/.github/workflows/wrapper_tests.yml @@ -10,7 +10,6 @@ on: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} - BUILD_TEST_INTERNAL_PACKAGE: true jobs: build: @@ -49,10 +48,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build devextreme package - env: - BUILD_TEST_INTERNAL_PACKAGE: true - working-directory: ./packages/devextreme - run: pnpx nx build + run: pnpx nx build devextreme -c testing check-regenerate: runs-on: devextreme-shr2 diff --git a/.github/workflows/wrapper_tests_e2e.yml b/.github/workflows/wrapper_tests_e2e.yml index 49cc1b435d62..022816c61ff6 100644 --- a/.github/workflows/wrapper_tests_e2e.yml +++ b/.github/workflows/wrapper_tests_e2e.yml @@ -15,7 +15,6 @@ on: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_TOKEN }} NX_SKIP_NX_CACHE: ${{ (github.event_name != 'pull_request' || contains( github.event.pull_request.labels.*.name, 'skip-cache')) && 'true' || 'false' }} - BUILD_TEST_INTERNAL_PACKAGE: true jobs: build-packages: @@ -55,9 +54,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build all DevExtreme packages - env: - BUILD_TEST_INTERNAL_PACKAGE: true - run: pnpm run all:build-dev + run: pnpm nx all:build-testing workflows - name: Build wrappers apps working-directory: e2e/wrappers diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 5a7b390d0530..d304f133e299 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -264,7 +264,15 @@ "{projectRoot}/js/bundles/dx.custom.js", "{projectRoot}/js/common/core/localization/cldr-data", "{projectRoot}/js/common/core/localization/default_messages.js" - ] + ], + "cache": true, + "configurations": { + "testing": { + "env": { + "BUILD_TEST_INTERNAL_PACKAGE": "true" + } + } + } }, "build-dist": { "executor": "nx:run-script", diff --git a/packages/workflows/project.json b/packages/workflows/project.json index f2f401df6ba3..cee8e09854ff 100644 --- a/packages/workflows/project.json +++ b/packages/workflows/project.json @@ -67,6 +67,29 @@ "metadata": { "description": "Build all packages for the local development environment." } + }, + "all:build-testing": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm nx build devextreme -c testing", + "pnpm nx build devextreme-themebuilder", + "pnpm nx copy:artifacts-to-root workflows", + "pnpm nx copy:bootstrap workflows", + "pnpm run all:pack-and-copy", + "pnpm nx pack devextreme-react", + "pnpm nx pack devextreme-vue", + "pnpm nx pack devextreme-angular", + "pnpm nx pack:devextreme-npm devextreme", + "pnpm nx pack:devextreme-dist-npm devextreme", + "pnpm nx copy:npm-tgz workflows", + "pnpm nx copy:wrappers-tgz workflows" + ], + "parallel": false + }, + "metadata": { + "description": "Build all packages for the testing environment." + } } } } From 8700a81a8ac0884da313dbebf15aa30c5ed5367f Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Wed, 14 Jan 2026 17:26:39 +0200 Subject: [PATCH 08/14] chore(devextreme): align output paths in nx project config --- packages/devextreme/project.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index d304f133e299..6867a8cb1894 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -261,9 +261,9 @@ ], "outputs": [ "{projectRoot}/artifacts", - "{projectRoot}/js/bundles/dx.custom.js", - "{projectRoot}/js/common/core/localization/cldr-data", - "{projectRoot}/js/common/core/localization/default_messages.js" + "{projectRoot}/build/bundle-templates/dx.custom.js", + "{projectRoot}/js/__internal/core/localization/cldr-data", + "{projectRoot}/js/__internal/core/localization/default_messages.ts" ], "cache": true, "configurations": { @@ -308,9 +308,9 @@ ], "outputs": [ "{projectRoot}/artifacts", - "{projectRoot}/js/bundles/dx.custom.js", - "{projectRoot}/js/common/core/localization/cldr-data", - "{projectRoot}/js/common/core/localization/default_messages.js" + "{projectRoot}/build/bundle-templates/dx.custom.js", + "{projectRoot}/js/__internal/core/localization/cldr-data", + "{projectRoot}/js/__internal/core/localization/default_messages.ts" ], "cache": true }, From e146c04419c8cdf81708f7156ee3bac5dcbf129f Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Thu, 15 Jan 2026 12:32:50 +0200 Subject: [PATCH 09/14] fix: devextreme-angular, correct config is forwarded to target deps --- packages/devextreme-angular/project.json | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/devextreme-angular/project.json b/packages/devextreme-angular/project.json index 00c4391e592b..b96349108499 100644 --- a/packages/devextreme-angular/project.json +++ b/packages/devextreme-angular/project.json @@ -167,7 +167,13 @@ }, "build": { "executor": "nx:run-commands", - "dependsOn": ["^build"], + "dependsOn": [ + { + "dependencies": true, + "target": "build", + "params": "forward" + } + ], "options": { "commands": [ "pnpm --workspace-root nx clean:dist devextreme-angular", @@ -182,7 +188,14 @@ "{projectRoot}/npm/dist" ], "cache": true, - "inputs": ["default"] + "inputs": ["default"], + "configurations": { + "testing": { + "env": { + "BUILD_TEST_INTERNAL_PACKAGE": "true" + } + } + } }, "pack": { "executor": "nx:run-commands", @@ -289,7 +302,7 @@ "executor": "nx:run-commands", "options": { "commands": [ - "pnpm --workspace-root nx build devextreme-angular", + "pnpm --workspace-root nx build devextreme-angular -c testing", "pnpm --workspace-root nx build:tests devextreme-angular", "pnpm --workspace-root nx test:all devextreme-angular" ], From a38158cd6bb664e99fabe144b941d98cc7b18276 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Thu, 15 Jan 2026 13:14:24 +0200 Subject: [PATCH 10/14] chore: remove unnecessary steps from all:build-dev workflow --- packages/workflows/project.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/workflows/project.json b/packages/workflows/project.json index cee8e09854ff..4e9f9cc10063 100644 --- a/packages/workflows/project.json +++ b/packages/workflows/project.json @@ -51,16 +51,11 @@ "commands": [ "pnpm nx build devextreme", "pnpm nx build devextreme-themebuilder", - "pnpm nx copy:artifacts-to-root workflows", - "pnpm nx copy:bootstrap workflows", - "pnpm run all:pack-and-copy", "pnpm nx pack devextreme-react", "pnpm nx pack devextreme-vue", "pnpm nx pack devextreme-angular", "pnpm nx pack:devextreme-npm devextreme", - "pnpm nx pack:devextreme-dist-npm devextreme", - "pnpm nx copy:npm-tgz workflows", - "pnpm nx copy:wrappers-tgz workflows" + "pnpm nx pack:devextreme-dist-npm devextreme" ], "parallel": false }, From 5346a287b79a04ddb4570379ac0824d0620c6fd1 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Thu, 15 Jan 2026 13:14:35 +0200 Subject: [PATCH 11/14] chore: remove unnecessary steps from all:build-testing workflow --- packages/workflows/project.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/workflows/project.json b/packages/workflows/project.json index 4e9f9cc10063..62e5a97dad7a 100644 --- a/packages/workflows/project.json +++ b/packages/workflows/project.json @@ -69,16 +69,11 @@ "commands": [ "pnpm nx build devextreme -c testing", "pnpm nx build devextreme-themebuilder", - "pnpm nx copy:artifacts-to-root workflows", - "pnpm nx copy:bootstrap workflows", - "pnpm run all:pack-and-copy", "pnpm nx pack devextreme-react", "pnpm nx pack devextreme-vue", "pnpm nx pack devextreme-angular", "pnpm nx pack:devextreme-npm devextreme", - "pnpm nx pack:devextreme-dist-npm devextreme", - "pnpm nx copy:npm-tgz workflows", - "pnpm nx copy:wrappers-tgz workflows" + "pnpm nx pack:devextreme-dist-npm devextreme" ], "parallel": false }, From b1794df289c57aace298f8fb5ea239c7386057d7 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Thu, 15 Jan 2026 15:10:05 +0200 Subject: [PATCH 12/14] feat(nx-infra-plugin): make executor output suppressible for cleaner CI logs --- .../executors/add-license-headers/executor.ts | 4 +- .../build-angular-library/executor.ts | 16 ++--- .../executors/build-typescript/executor.ts | 4 +- .../src/executors/clean/executor.ts | 10 ++-- .../src/executors/copy-files/executor.ts | 6 +- .../generate-component-names/executor.ts | 4 +- .../generate-components/angular-generator.ts | 20 +++---- .../executors/generate-components/executor.ts | 26 ++++----- .../src/executors/karma-multi-env/executor.ts | 58 +++++++++---------- .../src/executors/localization/executor.ts | 12 ++-- .../src/executors/pack-npm/executor.ts | 4 +- .../prepare-package-json/executor.ts | 2 +- .../executors/prepare-submodules/executor.ts | 8 +-- 13 files changed, 87 insertions(+), 87 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts b/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts index bffce3e121ab..342bc40a22d5 100644 --- a/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts +++ b/packages/nx-infra-plugin/src/executors/add-license-headers/executor.ts @@ -258,7 +258,7 @@ const runExecutor: PromiseExecutor = async (opt excludePatterns: options.excludePatterns ?? DEFAULTS.EXCLUDE_PATTERNS, }); - logger.info(`Adding license headers to ${files.length} files...`); + logger.verbose(`Adding license headers to ${files.length} files...`); const compiledTemplate = useCustomTemplate ? _.template(bannerTemplate) : null; @@ -277,7 +277,7 @@ const runExecutor: PromiseExecutor = async (opt ), ); - logger.info('License headers added successfully'); + logger.verbose('License headers added successfully'); return { success: true }; } catch (error) { logError('Failed to add license headers', error); diff --git a/packages/nx-infra-plugin/src/executors/build-angular-library/executor.ts b/packages/nx-infra-plugin/src/executors/build-angular-library/executor.ts index 4e22991a7585..31dcab78e85d 100644 --- a/packages/nx-infra-plugin/src/executors/build-angular-library/executor.ts +++ b/packages/nx-infra-plugin/src/executors/build-angular-library/executor.ts @@ -84,14 +84,14 @@ async function executeNgPackagrBuild(config: BuildConfiguration): Promise { return new Promise((resolve, reject) => { - logger.info(`Spawning process: ${options.command} ${options.args.join(' ')}`); + logger.verbose(`Spawning process: ${options.command} ${options.args.join(' ')}`); const child = spawn(options.command, options.args, options.options); child.on('close', (code, signal) => { - logger.info(`Process closed with code: ${code}, signal: ${signal || 'none'}`); + logger.verbose(`Process closed with code: ${code}, signal: ${signal || 'none'}`); const actualExitCode = code === null ? -1 : code; resolve({ exitCode: actualExitCode, signal: signal || undefined }); }); @@ -183,7 +183,7 @@ const runExecutor: PromiseExecutor = async ( context, ) => { try { - logger.info('Building Angular library with ng-packagr...'); + logger.verbose('Building Angular library with ng-packagr...'); const config = resolveBuildConfiguration(options, context); await validateBuildConfiguration(config); diff --git a/packages/nx-infra-plugin/src/executors/build-typescript/executor.ts b/packages/nx-infra-plugin/src/executors/build-typescript/executor.ts index 0e0fe22bc8c9..0cf58cd70a33 100644 --- a/packages/nx-infra-plugin/src/executors/build-typescript/executor.ts +++ b/packages/nx-infra-plugin/src/executors/build-typescript/executor.ts @@ -96,7 +96,7 @@ const runExecutor: PromiseExecutor = async (optio throw new Error(`No source files matched pattern: ${srcPattern}`); } - logger.info(`Building ${module.toUpperCase()} for ${sourceFiles.length} source files...`); + logger.verbose(`Building ${module.toUpperCase()} for ${sourceFiles.length} source files...`); const parsedConfig = ts.parseJsonConfigFileContent( tsconfigContent, @@ -121,7 +121,7 @@ const runExecutor: PromiseExecutor = async (optio return { success: false }; } - logger.info(`✓ ${module.toUpperCase()} build completed successfully`); + logger.verbose(`✓ ${module.toUpperCase()} build completed successfully`); return { success: true }; } catch (error) { logError(`Failed to build ${module.toUpperCase()}`, error); diff --git a/packages/nx-infra-plugin/src/executors/clean/executor.ts b/packages/nx-infra-plugin/src/executors/clean/executor.ts index 5b370238254f..f207c6a3d66f 100644 --- a/packages/nx-infra-plugin/src/executors/clean/executor.ts +++ b/packages/nx-infra-plugin/src/executors/clean/executor.ts @@ -98,12 +98,12 @@ const runExecutor: PromiseExecutor = async (options, contex ); const excludePatterns = options.excludePatterns || []; - logger.info( + logger.verbose( `Cleaning ${targetDirectory}${excludePatterns.length > 0 ? ` with ${excludePatterns.length} exclusions` : ' completely'}...`, ); if (excludePatterns.length > 0) { - logger.info(`Excluding patterns: ${excludePatterns.join(', ')}`); + logger.verbose(`Excluding patterns: ${excludePatterns.join(', ')}`); } try { @@ -111,16 +111,16 @@ const runExecutor: PromiseExecutor = async (options, contex if (excludePatterns.length === 0) { await removeDirectoryCompletely(targetDirectory); - logger.info(`Removed directory: ${targetDirectory}`); + logger.verbose(`Removed directory: ${targetDirectory}`); } else { if (!fs.existsSync(targetDirectory)) { - logger.info(`Directory does not exist: ${targetDirectory}`); + logger.verbose(`Directory does not exist: ${targetDirectory}`); return { success: true }; } await removeDirectoryWithExclusions(targetDirectory, absoluteExcludePaths); - logger.info( + logger.verbose( `Cleaned directory: ${targetDirectory} with ${absoluteExcludePaths.length} exclusions preserved`, ); } diff --git a/packages/nx-infra-plugin/src/executors/copy-files/executor.ts b/packages/nx-infra-plugin/src/executors/copy-files/executor.ts index 977bb0700066..0a0e714ae54e 100644 --- a/packages/nx-infra-plugin/src/executors/copy-files/executor.ts +++ b/packages/nx-infra-plugin/src/executors/copy-files/executor.ts @@ -37,7 +37,7 @@ async function copyGlobPatternFiles( const fileName = path.basename(file); const destFile = path.join(destPath, fileName); await copyFile(file, destFile); - logger.info(`Copied file ${file} -> ${destFile}`); + logger.verbose(`Copied file ${file} -> ${destFile}`); } return { success: true }; @@ -53,12 +53,12 @@ async function copyDirectPath(sourcePath: string, destPath: string): Promise<{ s if (sourceStat.isDirectory()) { await copyRecursive(sourcePath, destPath); - logger.info(`Copied directory ${sourcePath} -> ${destPath}`); + logger.verbose(`Copied directory ${sourcePath} -> ${destPath}`); return { success: true }; } await copyFile(sourcePath, destPath); - logger.info(`Copied file ${sourcePath} -> ${destPath}`); + logger.verbose(`Copied file ${sourcePath} -> ${destPath}`); return { success: true }; } diff --git a/packages/nx-infra-plugin/src/executors/generate-component-names/executor.ts b/packages/nx-infra-plugin/src/executors/generate-component-names/executor.ts index 267c0bb8766c..91ad72e971d0 100644 --- a/packages/nx-infra-plugin/src/executors/generate-component-names/executor.ts +++ b/packages/nx-infra-plugin/src/executors/generate-component-names/executor.ts @@ -45,7 +45,7 @@ const runExecutor: PromiseExecutor = async const projectRoot = resolveProjectPath(context); try { - logger.info(MSG_GENERATING); + logger.verbose(MSG_GENERATING); validateDependencies(); @@ -59,7 +59,7 @@ const runExecutor: PromiseExecutor = async const generator = new AngularComponentNamesGenerator(config); generator.generate(); - logger.info(MSG_GENERATED); + logger.verbose(MSG_GENERATED); return { success: true }; } catch (error) { logError(ERROR_GENERATION_FAILED, error); diff --git a/packages/nx-infra-plugin/src/executors/generate-components/angular-generator.ts b/packages/nx-infra-plugin/src/executors/generate-components/angular-generator.ts index 470053ed3ef8..d1ccf036ccb9 100644 --- a/packages/nx-infra-plugin/src/executors/generate-components/angular-generator.ts +++ b/packages/nx-infra-plugin/src/executors/generate-components/angular-generator.ts @@ -32,7 +32,7 @@ export async function generateAngularComponents( path.join(path.dirname(componentsDir), 'metadata', 'generated'), ); - logger.info('📝 Generating Angular-specific metadata...'); + logger.verbose('📝 Generating Angular-specific metadata...'); const metadataGenerator = new AngularMetadataGenerator(); metadataGenerator.generate({ outputFolderPath: metadataDir, @@ -45,9 +45,9 @@ export async function generateAngularComponents( sourceMetadataFilePath: require.resolve('devextreme-metadata/NGMetaData.json'), imdMetadataFilePath: require.resolve('devextreme-metadata/integration-data.json'), }); - logger.info('✓ Metadata generation completed'); + logger.verbose('✓ Metadata generation completed'); - logger.info('🔨 Generating component TypeScript files...'); + logger.verbose('🔨 Generating component TypeScript files...'); const componentGenerator = new AngularDotGenerator(); componentGenerator.generate({ metadataFolderPath: metadataDir, @@ -58,9 +58,9 @@ export async function generateAngularComponents( nestedPathPart: 'nested', basePathPart: 'base', }); - logger.info('✓ Component files generated'); + logger.verbose('✓ Component files generated'); - logger.info('📦 Generating module facades...'); + logger.verbose('📦 Generating module facades...'); const moduleFacadeGenerator = new AngularModuleFacadeGenerator(); moduleFacadeGenerator.generate({ moduleFacades: { @@ -72,9 +72,9 @@ export async function generateAngularComponents( }, }, }); - logger.info('✓ Module facades generated'); + logger.verbose('✓ Module facades generated'); - logger.info('📋 Generating index facades...'); + logger.verbose('📋 Generating index facades...'); const facadeGenerator = new AngularFacadeGenerator(); facadeGenerator.generate({ facades: { @@ -85,13 +85,13 @@ export async function generateAngularComponents( commonImports: ['./common', './common/grids', './common/charts'], templatingOptions: config.templatingOptions, }); - logger.info('✓ Index facades generated'); + logger.verbose('✓ Index facades generated'); - logger.info('🔗 Generating common reexports...'); + logger.verbose('🔗 Generating common reexports...'); AngularCommonReexportsGenerator.generate({ outputPath: path.dirname(componentsDir), metadata: metaData, templatingOptions: config.templatingOptions, }); - logger.info('✓ Common reexports generated'); + logger.verbose('✓ Common reexports generated'); } diff --git a/packages/nx-infra-plugin/src/executors/generate-components/executor.ts b/packages/nx-infra-plugin/src/executors/generate-components/executor.ts index 0472c98dd704..2cc927b1fae9 100644 --- a/packages/nx-infra-plugin/src/executors/generate-components/executor.ts +++ b/packages/nx-infra-plugin/src/executors/generate-components/executor.ts @@ -91,8 +91,8 @@ function resolveDefaultMetadataPath(): string { } function loadMetadata(metadataPath: string): any { - logger.info(MSG_LOADING_METADATA); - logger.info(` Path: ${metadataPath}`); + logger.verbose(MSG_LOADING_METADATA); + logger.verbose(` Path: ${metadataPath}`); if (!fs.existsSync(metadataPath)) { throw new Error(`Metadata file not found: ${metadataPath}`); @@ -102,7 +102,7 @@ function loadMetadata(metadataPath: string): any { const metaData = JSON.parse(metadataContent); const widgetCount = Object.keys(metaData.Widgets || {}).length; - logger.info(`✓ Loaded ${widgetCount} widget definitions`); + logger.verbose(`✓ Loaded ${widgetCount} widget definitions`); return metaData; } @@ -136,7 +136,7 @@ function loadConfigFromFile(projectRoot: string, configPath: string, framework: const config = require(absoluteConfigPath); const frameworkName = framework.charAt(0).toUpperCase() + framework.slice(1); - logger.info(`✓ Loaded ${frameworkName} configuration from ${configPath}`); + logger.verbose(`✓ Loaded ${frameworkName} configuration from ${configPath}`); return config; } catch (error) { logger.warn(`⚠️ Could not load configuration from ${configPath}: ${getErrorMessage(error)}`); @@ -166,7 +166,7 @@ function loadConfigFromGeneratorsFile( } const messages = createMessages(framework); - logger.info(messages.loadedConfig); + logger.verbose(messages.loadedConfig); return config; } catch (error) { logger.warn(`⚠️ Could not load generators-config.js: ${getErrorMessage(error)}`); @@ -244,26 +244,26 @@ async function executeGeneration( const messages = createMessages(framework); const handler = getFrameworkHandler(framework); - logger.info(messages.generating); + logger.verbose(messages.generating); await handler.executeGeneration(generateComponents, config, metaData); - logger.info(MSG_GENERATION_COMPLETED); + logger.verbose(MSG_GENERATION_COMPLETED); if (fs.existsSync(indexFileName)) { const indexContent = fs.readFileSync(indexFileName, ENCODING_UTF8); const exportCount = (indexContent.match(EXPORT_PATTERN) || []).length; - logger.info(` Exports: ${exportCount}`); + logger.verbose(` Exports: ${exportCount}`); } if (fs.existsSync(componentsDir)) { const dirCount = fs .readdirSync(componentsDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name !== CORE_DIR).length; - logger.info(` Component Directories: ${dirCount}`); + logger.verbose(` Component Directories: ${dirCount}`); } - logger.info(messages.generationSuccess); + logger.verbose(messages.generationSuccess); } const runExecutor: PromiseExecutor = async ( @@ -276,10 +276,10 @@ const runExecutor: PromiseExecutor = asyn const framework: Framework = options.framework || 'react'; const messages = createMessages(framework); - logger.info(messages.starting); + logger.verbose(messages.starting); const projectRelativePath = path.relative(workspaceRoot, absoluteProjectRoot) || DOT_SLASH_PREFIX; - logger.info(` Project root: ${projectRelativePath}`); - logger.info(` Framework: ${framework}`); + logger.verbose(` Project root: ${projectRelativePath}`); + logger.verbose(` Framework: ${framework}`); try { const componentsDir = path.resolve( diff --git a/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts b/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts index 56182a6da8a7..f8c4a6727fcf 100644 --- a/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts +++ b/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts @@ -203,7 +203,7 @@ async function executeSingleRun( return (exitCode: number) => { const duration = Date.now() - startTime; - logger.info( + logger.verbose( `[${environment.toUpperCase()}] Karma callback called with exit code: ${exitCode}`, ); @@ -231,7 +231,7 @@ async function executeSingleRun( }; if (testResult.success) { - logger.info(`\n[${environment.toUpperCase()}] Tests completed successfully`); + logger.verbose(`\n[${environment.toUpperCase()}] Tests completed successfully`); } else { errorHandler.logError(testResult.error!); } @@ -319,7 +319,7 @@ const createWatchModeCallback = ( try { if (server && server.stop) { - logger.info(`[${environment.toUpperCase()}] Stopping Karma server...`); + logger.verbose(`[${environment.toUpperCase()}] Stopping Karma server...`); server.stop(); } } catch (cleanupError) { @@ -343,7 +343,7 @@ const createWatchModeCallback = ( const setupSignalHandlers = (server: any): void => { const handleExit = (signal: string) => { - logger.info(`\n${STATUS_ICONS.STOP} Received ${signal} - stopping watch mode...`); + logger.verbose(`\n${STATUS_ICONS.STOP} Received ${signal} - stopping watch mode...`); if (server && server.stop) { server.stop(); } @@ -403,18 +403,18 @@ const createExecutionResult = ( const logExecutionStart = (plan: ExecutionPlan, options: KarmaMultiEnvExecutorSchema): void => { if (options.watch) return; - logger.info(`Running tests in environments: ${plan.executionOrder.join(', ')}`); + logger.verbose(`Running tests in environments: ${plan.executionOrder.join(', ')}`); if (options.verbose) { - logger.info(`Karma config: ${options.karmaConfig}`); - logger.info(`Timeout: ${plan.timeout}ms`); + logger.verbose(`Karma config: ${options.karmaConfig}`); + logger.verbose(`Timeout: ${plan.timeout}ms`); } }; const logEnvironmentStart = (environment: KarmaEnvironment): void => - logger.info(`\n[${environment.toUpperCase()}] Starting tests...`); + logger.verbose(`\n[${environment.toUpperCase()}] Starting tests...`); const logWatchModeStart = (environment: KarmaEnvironment): void => - logger.info(`[${environment.toUpperCase()}] Watch mode enabled - starting Karma server...`); + logger.verbose(`[${environment.toUpperCase()}] Watch mode enabled - starting Karma server...`); const logTestResults = ( summary: TestSummary, @@ -422,27 +422,27 @@ const logTestResults = ( options: KarmaMultiEnvExecutorSchema, ): void => { if (options.watch) { - logger.info(`\n${STATUS_ICONS.WATCH} Watch mode active for: ${plan.executionOrder.join(', ')}`); + logger.verbose(`\n${STATUS_ICONS.WATCH} Watch mode active for: ${plan.executionOrder.join(', ')}`); if (options.verbose) { - logger.info(`Karma config: ${options.karmaConfig}`); - logger.info('Watching file changes...'); + logger.verbose(`Karma config: ${options.karmaConfig}`); + logger.verbose('Watching file changes...'); } - logger.info('Press CTRL+C to stop watching...'); + logger.verbose('Press CTRL+C to stop watching...'); return; } - logger.info('\n' + '='.repeat(50)); - logger.info(`${STATUS_ICONS.DOCUMENTATION} TEST RESULTS SUMMARY`); - logger.info('='.repeat(50)); - logger.info(`\n${STATUS_ICONS.SUCCESS} Environments tested: ${plan.executionOrder.join(', ')}`); - logger.info(`${STATUS_ICONS.CLOCK} Total duration: ${summary.totalDuration}ms`); + logger.verbose('\n' + '='.repeat(50)); + logger.verbose(`${STATUS_ICONS.DOCUMENTATION} TEST RESULTS SUMMARY`); + logger.verbose('='.repeat(50)); + logger.verbose(`\n${STATUS_ICONS.SUCCESS} Environments tested: ${plan.executionOrder.join(', ')}`); + logger.verbose(`${STATUS_ICONS.CLOCK} Total duration: ${summary.totalDuration}ms`); summary.results.forEach((result) => { const statusIcon = result.success ? STATUS_ICONS.SUCCESS : STATUS_ICONS.FAILURE; const durationText = `${result.duration}ms`; const statusText = result.success ? 'PASS' : 'FAIL'; - logger.info( + logger.verbose( `\n${statusIcon} ${result.environment.toUpperCase()}: ${statusText} (${durationText})`, ); if (!result.success && result.error) { @@ -451,7 +451,7 @@ const logTestResults = ( }); if (summary.summary.failed === 0) { - logger.info(`\n${STATUS_ICONS.CELEBRATION} SUCCESS: All tests passed`); + logger.verbose(`\n${STATUS_ICONS.CELEBRATION} SUCCESS: All tests passed`); } else { logger.error(`\n${STATUS_ICONS.ERROR} FAILURE: Some tests failed`); } @@ -461,7 +461,7 @@ const setupWatchModeEvents = (environment: KarmaEnvironment, server: any): void if (!server.on || typeof server.on !== 'function') return; server.on('browsers_ready', () => { - logger.info( + logger.verbose( `\n${STATUS_ICONS.WATCH} Watch mode active - browsers ready and watching for file changes...`, ); }); @@ -470,13 +470,13 @@ const setupWatchModeEvents = (environment: KarmaEnvironment, server: any): void const statusIcon = results.success ? STATUS_ICONS.SUCCESS : STATUS_ICONS.FAILURE; const statusText = results.success ? 'All tests passed' : 'Some tests failed'; - logger.info(`\n[${environment.toUpperCase()}] Test run completed. Success: ${results.success}`); - logger.info(`${statusIcon} ${statusText} in watch mode - continuing to watch...`); - logger.info('Press CTRL+C to stop watching...'); + logger.verbose(`\n[${environment.toUpperCase()}] Test run completed. Success: ${results.success}`); + logger.verbose(`${statusIcon} ${statusText} in watch mode - continuing to watch...`); + logger.verbose('Press CTRL+C to stop watching...'); }); server.on('file_list_modified', () => { - logger.info(`\n${STATUS_ICONS.REFRESH} File changes detected, re-running tests...`); + logger.verbose(`\n${STATUS_ICONS.REFRESH} File changes detected, re-running tests...`); }); }; @@ -496,7 +496,7 @@ async function executeWatchMode( setupWatchModeEvents(environment, server); setupSignalHandlers(server); - logger.info(`\n${STATUS_ICONS.START} Starting Karma server in watch mode...`); + logger.verbose(`\n${STATUS_ICONS.START} Starting Karma server in watch mode...`); server.start(); }); } @@ -529,10 +529,10 @@ const setupDebugModeEvents = (environment: KarmaEnvironment, server: any): void if (!server.on || typeof server.on !== 'function') return; server.on('browsers_ready', () => { - logger.info( + logger.verbose( `\n${STATUS_ICONS.DEBUG} Debug mode for the ${environment} environment is active. Click the "DEBUG" button in the opened browser window to start debugging.`, ); - logger.info('Press CTRL+C to stop debugging...'); + logger.verbose('Press CTRL+C to stop debugging...'); }); }; @@ -549,7 +549,7 @@ async function launchDebugMode( setupDebugModeEvents(environment, server); setupSignalHandlers(server); - logger.info(`\n${STATUS_ICONS.START} Starting Karma server in debug mode...`); + logger.verbose(`\n${STATUS_ICONS.START} Starting Karma server in debug mode...`); server.start(); }); } diff --git a/packages/nx-infra-plugin/src/executors/localization/executor.ts b/packages/nx-infra-plugin/src/executors/localization/executor.ts index bb5c60969203..787aea32c342 100644 --- a/packages/nx-infra-plugin/src/executors/localization/executor.ts +++ b/packages/nx-infra-plugin/src/executors/localization/executor.ts @@ -223,7 +223,7 @@ async function generateMessageFiles( const locales = getLocales(messagesDir); - logger.info(`Processing ${locales.length} locales...`); + logger.verbose(`Processing ${locales.length} locales...`); await Promise.all( locales.map(async (locale) => { @@ -414,13 +414,13 @@ const runExecutor: PromiseExecutor = async (options, } if (!skipMessageGeneration) { - logger.info('Generating localization message files...'); + logger.verbose('Generating localization message files...'); await generateMessageFiles(messagesDir, messageTemplate, messageOutputDir); - logger.info(`Message files generated in ${messageOutputDir}`); + logger.verbose(`Message files generated in ${messageOutputDir}`); } if (!skipCldrGeneration) { - logger.info('Generating CLDR TypeScript modules...'); + logger.verbose('Generating CLDR TypeScript modules...'); await generateCldrModules( absoluteProjectRoot, messagesDir, @@ -429,10 +429,10 @@ const runExecutor: PromiseExecutor = async (options, defaultMessagesOutputDir, lintGeneratedFiles, ); - logger.info(`CLDR modules generated in ${cldrDataOutputDir}`); + logger.verbose(`CLDR modules generated in ${cldrDataOutputDir}`); } - logger.info('Localization generation completed successfully'); + logger.verbose('Localization generation completed successfully'); return { success: true }; } catch (error) { logError('Localization executor failed', error); diff --git a/packages/nx-infra-plugin/src/executors/pack-npm/executor.ts b/packages/nx-infra-plugin/src/executors/pack-npm/executor.ts index 25b5edc84ec3..9deaacde7b4c 100644 --- a/packages/nx-infra-plugin/src/executors/pack-npm/executor.ts +++ b/packages/nx-infra-plugin/src/executors/pack-npm/executor.ts @@ -21,7 +21,7 @@ const runExecutor: PromiseExecutor = async (options, cont } try { - logger.info(`Running pnpm pack from ${absoluteProjectRoot} (packaging ${distDirectory})...`); + logger.verbose(`Running pnpm pack from ${absoluteProjectRoot} (packaging ${distDirectory})...`); const projectPath = path.join(workspaceRoot, 'packages', context.projectName); @@ -30,7 +30,7 @@ const runExecutor: PromiseExecutor = async (options, cont stdio: 'inherit', }); - logger.info(MSG_PACK_SUCCESS); + logger.verbose(MSG_PACK_SUCCESS); return { success: true }; } catch (error) { logError(MSG_PACK_FAILED, error); diff --git a/packages/nx-infra-plugin/src/executors/prepare-package-json/executor.ts b/packages/nx-infra-plugin/src/executors/prepare-package-json/executor.ts index 8315a808e821..afd664eac3b9 100644 --- a/packages/nx-infra-plugin/src/executors/prepare-package-json/executor.ts +++ b/packages/nx-infra-plugin/src/executors/prepare-package-json/executor.ts @@ -32,7 +32,7 @@ const runExecutor: PromiseExecutor = async (options, c const distPackageJson = path.join(distDirectory, PACKAGE_JSON_FILE); await writeJson(distPackageJson, pkg, JSON_INDENT); - logger.info(`Created ${distPackageJson}`); + logger.verbose(`Created ${distPackageJson}`); return { success: true }; } catch (error) { diff --git a/packages/nx-infra-plugin/src/executors/prepare-submodules/executor.ts b/packages/nx-infra-plugin/src/executors/prepare-submodules/executor.ts index cd955d3a5b42..299d10e21133 100644 --- a/packages/nx-infra-plugin/src/executors/prepare-submodules/executor.ts +++ b/packages/nx-infra-plugin/src/executors/prepare-submodules/executor.ts @@ -44,10 +44,10 @@ const runExecutor: PromiseExecutor = async (opt const distDirectory = path.join(absoluteProjectRoot, options.distDirectory || DEFAULT_DIST_DIR); try { - logger.info(MSG_PREPARING); + logger.verbose(MSG_PREPARING); if (options.submoduleFolders) { - logger.info( + logger.verbose( `Using custom submoduleFolders: ${JSON.stringify(options.submoduleFolders, null, 2)}`, ); } @@ -72,7 +72,7 @@ const runExecutor: PromiseExecutor = async (opt const allModuleParams: PackParam[] = [...packParamsForModules, ...packParamsForFolders]; - logger.info(`Processing ${allModuleParams.length} submodules...`); + logger.verbose(`Processing ${allModuleParams.length} submodules...`); await Promise.all( allModuleParams.map(([folder, moduleFileNames, moduleFilePath]) => @@ -80,7 +80,7 @@ const runExecutor: PromiseExecutor = async (opt ), ); - logger.info(MSG_SUCCESS); + logger.verbose(MSG_SUCCESS); return { success: true }; } catch (error) { logError(ERROR_PREPARE_SUBMODULES, error); From df7e43a23e623d87d1a274be0a657a2a735b97ec Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Thu, 15 Jan 2026 15:42:38 +0200 Subject: [PATCH 13/14] fix: eslint errors --- .../src/executors/karma-multi-env/executor.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts b/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts index f8c4a6727fcf..37b4969ced3f 100644 --- a/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts +++ b/packages/nx-infra-plugin/src/executors/karma-multi-env/executor.ts @@ -422,7 +422,9 @@ const logTestResults = ( options: KarmaMultiEnvExecutorSchema, ): void => { if (options.watch) { - logger.verbose(`\n${STATUS_ICONS.WATCH} Watch mode active for: ${plan.executionOrder.join(', ')}`); + logger.verbose( + `\n${STATUS_ICONS.WATCH} Watch mode active for: ${plan.executionOrder.join(', ')}`, + ); if (options.verbose) { logger.verbose(`Karma config: ${options.karmaConfig}`); logger.verbose('Watching file changes...'); @@ -434,7 +436,9 @@ const logTestResults = ( logger.verbose('\n' + '='.repeat(50)); logger.verbose(`${STATUS_ICONS.DOCUMENTATION} TEST RESULTS SUMMARY`); logger.verbose('='.repeat(50)); - logger.verbose(`\n${STATUS_ICONS.SUCCESS} Environments tested: ${plan.executionOrder.join(', ')}`); + logger.verbose( + `\n${STATUS_ICONS.SUCCESS} Environments tested: ${plan.executionOrder.join(', ')}`, + ); logger.verbose(`${STATUS_ICONS.CLOCK} Total duration: ${summary.totalDuration}ms`); summary.results.forEach((result) => { @@ -470,7 +474,9 @@ const setupWatchModeEvents = (environment: KarmaEnvironment, server: any): void const statusIcon = results.success ? STATUS_ICONS.SUCCESS : STATUS_ICONS.FAILURE; const statusText = results.success ? 'All tests passed' : 'Some tests failed'; - logger.verbose(`\n[${environment.toUpperCase()}] Test run completed. Success: ${results.success}`); + logger.verbose( + `\n[${environment.toUpperCase()}] Test run completed. Success: ${results.success}`, + ); logger.verbose(`${statusIcon} ${statusText} in watch mode - continuing to watch...`); logger.verbose('Press CTRL+C to stop watching...'); }); From a346a1c3bf0c1abb68cc7ba61cedfa9b8e8b2002 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Fri, 16 Jan 2026 16:36:21 +0200 Subject: [PATCH 14/14] fix: correctly lauch devextreme build in demos_visual_tests --- .github/workflows/demos_visual_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/demos_visual_tests.yml b/.github/workflows/demos_visual_tests.yml index 819c33fda764..4f0f2a1770df 100644 --- a/.github/workflows/demos_visual_tests.yml +++ b/.github/workflows/demos_visual_tests.yml @@ -144,7 +144,7 @@ jobs: shell: bash run: | pnpx nx build devextreme-scss - pnpx nx build devextreme + pnpx nx build devextreme -c testing - name: DevExtreme - Build-all if: needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none'