From cbbb0aa68d9f94457aeacdb27f19be1c4ab9f816 Mon Sep 17 00:00:00 2001 From: Adel Khamatov Date: Mon, 22 Dec 2025 15:53:15 +0200 Subject: [PATCH] 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 | 2 + .../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 | 7 +- 13 files changed, 1126 insertions(+), 166 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 226d866b3dfe..6cae931767a5 100644 --- a/packages/nx-infra-plugin/package.json +++ b/packages/nx-infra-plugin/package.json @@ -14,6 +14,7 @@ "dependencies": { "fs-extra": "^11.2.0", "glob": "11.1.0", + "lodash": "^4.17.21", "rimraf": "3.0.2" }, "peerDependencies": { @@ -31,6 +32,7 @@ "devDependencies": { "@types/fs-extra": "^11.0.4", "@types/jest": "29.5.12", + "@types/lodash": "^4.17.0", "@types/node": "^18.0.0", "prettier": "catalog:tools", "ts-jest": "29.1.3", 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 5ae184939e4a..a73b6d0bb5ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2184,6 +2184,9 @@ importers: karma: specifier: '>=6.0.0' version: 6.4.4 + lodash: + specifier: ^4.17.21 + version: 4.17.21 ng-packagr: specifier: '>=17.0.0' version: 17.3.0(@angular/compiler-cli@21.0.5(@angular/compiler@21.0.5)(typescript@4.9.5))(tslib@2.8.1)(typescript@4.9.5) @@ -2197,6 +2200,9 @@ importers: '@types/jest': specifier: 29.5.12 version: 29.5.12 + '@types/lodash': + specifier: ^4.17.0 + version: 4.17.13 '@types/node': specifier: ^18.0.0 version: 18.19.64 @@ -16611,7 +16617,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) qjobs@1.2.0: