From 0e92005e1dcac2da80c6c3d2146e68024872bebf Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 21 May 2026 23:09:27 +0200 Subject: [PATCH 1/9] migrate TS from gulp to nx, step 1 --- packages/devextreme/package.json | 2 +- packages/devextreme/project.json | 101 ++++++- packages/nx-infra-plugin/AGENTS.md | 4 +- packages/nx-infra-plugin/executors.json | 5 + .../check-declarations.impl.ts | 272 ++++++++++++++++++ .../declaration-check-content.ts | 90 ++++++ .../check-declarations/executor.e2e.spec.ts | 94 ++++++ .../executors/check-declarations/executor.ts | 11 + .../executors/check-declarations/schema.json | 50 ++++ .../executors/check-declarations/schema.ts | 14 + 10 files changed, 634 insertions(+), 9 deletions(-) create mode 100644 packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts create mode 100644 packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts create mode 100644 packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts create mode 100644 packages/nx-infra-plugin/src/executors/check-declarations/executor.ts create mode 100644 packages/nx-infra-plugin/src/executors/check-declarations/schema.json create mode 100644 packages/nx-infra-plugin/src/executors/check-declarations/schema.ts diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index e5fcedb75b3c..489a5ced1669 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -247,7 +247,7 @@ "update-ts-reexports": "dx-tools generate-reexports --sources ./js --exclude \"((dialog|export|list_light|notify|overlay|palette|set_template_engine|splitter_control|themes|themes_callback|track_bar|utils|validation_engine|validation_message)[.d.ts])\" --compiler-options \"{ \\\"typeRoots\\\": [] }\"", "update-ts-bundle": "dx-tools generate-ts-bundle --sources ./js --output-path ./ts/dx.all.d.ts", "regenerate": "pnpm run update-ts-bundle && pnpm run update-ts-reexports", - "validate-ts": "gulp validate-ts", + "validate-ts": "pnpm nx run devextreme:validate:ts", "validate-declarations": "dx-tools validate-declarations --sources ./js --exclude \"js/(renovation|__internal|.eslintrc.js)\" --compiler-options \"{ \\\"typeRoots\\\": [] }\"", "testcafe-in-docker": "docker build -f ./testing/testcafe/docker/Dockerfile -t testcafe-testing . && docker run -it testcafe-testing", "test-jest": "cross-env NODE_OPTIONS='--expose-gc' jest --no-coverage --runInBand --selectProjects jsdom-tests", diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 57272c8b1b82..a37a65fd7af3 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -786,18 +786,76 @@ }, "outputs": ["{projectRoot}/artifacts/js/dx.aspnet.mvc.js"] }, + "verify:ts-modules": { + "executor": "devextreme-nx-infra-plugin:check-declarations", + "options": { + "mode": "modules", + "typescriptModule": "typescript-min" + }, + "inputs": [ + "{projectRoot}/js/**/*.d.ts", + "{projectRoot}/build/gulp/modules_metadata.json" + ] + }, + "verify:ts-bundle": { + "executor": "devextreme-nx-infra-plugin:check-declarations", + "options": { + "mode": "bundle", + "typescriptModule": "typescript-min" + }, + "dependsOn": ["build:npm:dts-bundle"], + "inputs": [ + "{projectRoot}/artifacts/ts/dx.all.d.ts", + "{projectRoot}/ts/dx.all.d.ts", + "{projectRoot}/ts/aliases.d.ts", + "{projectRoot}/build/gulp/license-header.txt" + ] + }, + "verify:ts-jquery": { + "executor": "devextreme-nx-infra-plugin:check-declarations", + "options": { + "mode": "jquery", + "typescriptModule": "typescript-min" + }, + "inputs": [ + "{projectRoot}/ts/dx.all.d.ts", + "{projectRoot}/ts/aliases.d.ts", + "{projectRoot}/build/gulp/modules_metadata.json" + ], + "outputs": ["{projectRoot}/artifacts/globals.ts"] + }, + "copy:ts-vendor": { + "executor": "devextreme-nx-infra-plugin:copy-files", + "options": { + "files": [ + { "from": "./ts/vendor/*", "to": "./artifacts/ts" } + ] + }, + "inputs": ["{projectRoot}/ts/vendor/**/*"], + "outputs": ["{projectRoot}/artifacts/ts/**/*"] + }, "build:declarations": { "executor": "nx:run-commands", "options": { - "command": "gulp ts", - "cwd": "{projectRoot}" + "commands": [ + "pnpm nx run devextreme:copy:ts-vendor", + "pnpm nx run devextreme:build:npm:dts-bundle", + "pnpm nx run devextreme:verify:ts-jquery", + "pnpm nx run devextreme:verify:ts-bundle" + ], + "parallel": false }, "inputs": [ "{projectRoot}/ts/**/*.d.ts", - "{projectRoot}/build/gulp/ts.js" + "{projectRoot}/ts/vendor/**/*", + "{projectRoot}/build/gulp/modules_metadata.json", + "{projectRoot}/build/gulp/license-header.txt" ], "outputs": [ - "{projectRoot}/artifacts/ts" + "{projectRoot}/artifacts/ts", + "{projectRoot}/artifacts/ts/dx.all.d.ts", + "{projectRoot}/artifacts/npm/devextreme/bundles/dx.all.d.ts", + "{projectRoot}/artifacts/globals.ts" ] }, "compress:ts-modules": { @@ -1235,14 +1293,43 @@ } }, "verify:public-modules": { - "executor": "nx:run-commands", + "executor": "devextreme-nx-infra-plugin:check-declarations", "options": { - "command": "cross-env BUILD_ESM_PACKAGE=true gulp ts-check-public-modules", - "cwd": "{projectRoot}" + "mode": "public-modules", + "typescriptModule": "typescript-min" }, + "dependsOn": ["build:npm:dts-modules"], "inputs": [ "{projectRoot}/artifacts/npm/devextreme/**/*.d.ts", "{projectRoot}/build/gulp/modules_metadata.json" + ], + "outputs": ["{projectRoot}/artifacts/modules.ts"], + "configurations": { + "internal": { + "internalPackage": true, + "npmPackageDir": "devextreme-internal" + } + } + }, + "validate:ts": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm nx run devextreme:verify:ts-modules", + "pnpm nx run devextreme:build:npm:dts-bundle", + "pnpm nx run devextreme:verify:ts-bundle", + "pnpm nx run devextreme:verify:ts-jquery", + "pnpm nx run devextreme:verify:public-modules" + ], + "parallel": false, + "cwd": "{workspaceRoot}" + }, + "inputs": [ + "{projectRoot}/js/**/*.d.ts", + "{projectRoot}/ts/dx.all.d.ts", + "{projectRoot}/ts/aliases.d.ts", + "{projectRoot}/build/gulp/modules_metadata.json", + "{projectRoot}/artifacts/npm/devextreme/**/*.d.ts" ] }, "build:npm": { diff --git a/packages/nx-infra-plugin/AGENTS.md b/packages/nx-infra-plugin/AGENTS.md index bf94cd2d9d9e..086a120cdf5b 100644 --- a/packages/nx-infra-plugin/AGENTS.md +++ b/packages/nx-infra-plugin/AGENTS.md @@ -23,7 +23,9 @@ Each executor lives at `src/executors//`: - `.impl.ts` — business logic via `createExecutor` + named exports for cross-executor reuse - `schema.ts`, `schema.json`, `executor.e2e.spec.ts`, optional `defaults.ts` -Each cross-executor concern (license banner, glob-aware copy, file concatenation, debug-block stripping, etc.) is owned by exactly ONE executor and exposed via named exports from its `*.impl.ts`. Discover what is available by reading the named exports of the relevant executor; do not re-implement. The full executor catalogue is in `executors.json`; generic primitives live in `src/utils/`. +Each cross-executor concern (license banner, glob-aware copy, file concatenation, debug-block stripping, declaration type-check entry generation, etc.) is owned by exactly ONE executor and exposed via named exports from its `*.impl.ts`. Discover what is available by reading the named exports of the relevant executor; do not re-implement. The full executor catalogue is in `executors.json`; generic primitives live in `src/utils/`. + +- **`check-declarations`** — noEmit TypeScript checks for `.d.ts` (modes: `jquery`, `bundle`, `modules`, `public-modules`). Replaces gulp `ts-check-*`. Use **`dts-modules`** / **`dts-bundle`** to produce files; this executor only validates. ## Conventions diff --git a/packages/nx-infra-plugin/executors.json b/packages/nx-infra-plugin/executors.json index 8672e3559e1c..1dd5ce841017 100644 --- a/packages/nx-infra-plugin/executors.json +++ b/packages/nx-infra-plugin/executors.json @@ -100,6 +100,11 @@ "schema": "./src/executors/dts-bundle/schema.json", "description": "Assemble TypeScript declaration bundle files from source" }, + "check-declarations": { + "implementation": "./src/executors/check-declarations/executor", + "schema": "./src/executors/check-declarations/schema.json", + "description": "Type-check DevExtreme .d.ts bundles and modules (gulp validate-ts parity)" + }, "npm-assemble": { "implementation": "./src/executors/npm-assemble/executor", "schema": "./src/executors/npm-assemble/schema.json", diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts new file mode 100644 index 000000000000..ad290bbe2334 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts @@ -0,0 +1,272 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { glob } from 'glob'; +import { logger } from '@nx/devkit'; +import { createExecutor } from '../../utils/create-executor'; +import { toPosixPath } from '../../utils/path-resolver'; +import { ensureDir, readFileText, writeFileText } from '../../utils/file-operations'; +import { + buildJqueryCheckContent, + buildPublicModulesCheckContent, + ModuleMetadata, +} from './declaration-check-content'; +import { CheckDeclarationsExecutorSchema } from './schema'; + +const DEFAULT_MODULES_METADATA = './build/gulp/modules_metadata.json'; +const DEFAULT_TS_BUNDLE = './ts/dx.all.d.ts'; +const DEFAULT_BUNDLE_ARTIFACT = './artifacts/ts/dx.all.d.ts'; +const DEFAULT_MODULES_PATTERN = './js/**/*.d.ts'; +const DEFAULT_ENTRY_DIR = './artifacts'; +const DEFAULT_NPM_PACKAGE = 'devextreme'; +const INTERNAL_NPM_PACKAGE = 'devextreme-internal'; + +const NEWLINE = '\n'; +const COMPILATION_FAILED = 'Declaration type check failed'; + +interface ResolvedCheckDeclarations { + projectRoot: string; + mode: CheckDeclarationsExecutorSchema['mode']; + modulesMetadataPath: string; + tsBundleFile: string; + bundleArtifactPath: string; + modulesPattern: string; + entryOutputDir: string; + npmPackageDir: string; + typescriptModule?: string; + extraCompilerOptions: Record; +} + +function readInternalPackageFlag(option?: boolean): boolean { + if (option !== undefined) { + return option; + } + return String(process.env.BUILD_INTERNAL_PACKAGE).toLowerCase() === 'true'; +} + +function resolveNpmPackageDir(options: CheckDeclarationsExecutorSchema): string { + if (options.npmPackageDir) { + return options.npmPackageDir; + } + return readInternalPackageFlag(options.internalPackage) + ? INTERNAL_NPM_PACKAGE + : DEFAULT_NPM_PACKAGE; +} + +export function resolveTypeScript( + projectRoot: string, + typescriptModule?: string, +): typeof import('typescript') { + const candidates = typescriptModule + ? [ + path.isAbsolute(typescriptModule) + ? typescriptModule + : path.resolve(projectRoot, typescriptModule), + ] + : [ + path.join(projectRoot, 'node_modules', 'typescript-min'), + path.join(projectRoot, 'node_modules', 'typescript'), + ]; + + for (const candidate of candidates) { + try { + const resolved = fs.existsSync(path.join(candidate, 'package.json')) + ? candidate + : require.resolve(candidate, { paths: [projectRoot] }); + return require(resolved); + } catch { + // try next candidate + } + } + + return require('typescript'); +} + +export function buildDefaultCompilerOptions( + projectRoot: string, + ts: typeof import('typescript'), + extra?: Record, +): import('typescript').CompilerOptions { + return { + noEmit: true, + noEmitOnError: true, + allowJs: true, + strict: true, + noImplicitAny: false, + types: ['jquery'], + // gulp-typescript accepted legacy names; TS API expects standard lib entries (4.9). + lib: ['ES2017', 'DOM'], + moduleResolution: ts.ModuleResolutionKind.NodeJs, + typeRoots: [path.join(projectRoot, 'node_modules', '@types')], + ...extra, + }; +} + +function formatDiagnostics( + ts: typeof import('typescript'), + diagnostics: readonly import('typescript').Diagnostic[], +): string[] { + return diagnostics.map((diagnostic) => { + if (diagnostic.file) { + const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, NEWLINE); + return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`; + } + return ts.flattenDiagnosticMessageText(diagnostic.messageText, NEWLINE); + }); +} + +export async function runDeclarationsTypeCheck(params: { + projectRoot: string; + rootNames: string[]; + typescriptModule?: string; + compilerOptions?: Record; +}): Promise { + const ts = resolveTypeScript(params.projectRoot, params.typescriptModule); + const compilerOptions = buildDefaultCompilerOptions( + params.projectRoot, + ts, + params.compilerOptions, + ); + + const parsedConfig = ts.parseJsonConfigFileContent( + { compilerOptions: compilerOptions as Record }, + ts.sys, + params.projectRoot, + ); + + const program = ts.createProgram(params.rootNames, parsedConfig.options); + const diagnostics = ts.getPreEmitDiagnostics(program); + const errors = diagnostics.filter( + (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error, + ); + + if (errors.length > 0) { + logger.error(COMPILATION_FAILED); + formatDiagnostics(ts, errors).forEach((message) => logger.error(message)); + throw new Error(COMPILATION_FAILED); + } + + logger.verbose(`Type check passed for ${params.rootNames.length} root file(s)`); +} + +async function loadModulesMetadata(metadataPath: string): Promise { + const raw = await readFileText(metadataPath); + return JSON.parse(raw) as ModuleMetadata[]; +} + +async function resolveModuleDeclarationFiles( + projectRoot: string, + pattern: string, +): Promise { + const globPattern = toPosixPath(path.join(projectRoot, pattern)); + const files = await glob(globPattern, { absolute: true, nodir: true }); + + if (files.length === 0) { + throw new Error(`No declaration files matched pattern: ${pattern}`); + } + + return files; +} + +async function writeCheckEntryFile( + entryDir: string, + fileName: string, + content: string, +): Promise { + await ensureDir(entryDir); + const entryPath = path.join(entryDir, fileName); + await writeFileText(entryPath, content); + return entryPath; +} + +async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise { + const { projectRoot, mode } = resolved; + + switch (mode) { + case 'jquery': { + const modules = await loadModulesMetadata(resolved.modulesMetadataPath); + const content = buildJqueryCheckContent(resolved.tsBundleFile, modules); + const entryPath = await writeCheckEntryFile( + path.join(projectRoot, resolved.entryOutputDir), + 'globals.ts', + content, + ); + await runDeclarationsTypeCheck({ + projectRoot, + rootNames: [entryPath], + typescriptModule: resolved.typescriptModule, + compilerOptions: resolved.extraCompilerOptions, + }); + break; + } + case 'bundle': { + const bundlePath = path.resolve(projectRoot, resolved.bundleArtifactPath); + if (!fs.existsSync(bundlePath)) { + throw new Error(`Bundle artifact not found: ${resolved.bundleArtifactPath}`); + } + await runDeclarationsTypeCheck({ + projectRoot, + rootNames: [bundlePath], + typescriptModule: resolved.typescriptModule, + compilerOptions: resolved.extraCompilerOptions, + }); + break; + } + case 'modules': { + const rootNames = await resolveModuleDeclarationFiles(projectRoot, resolved.modulesPattern); + await runDeclarationsTypeCheck({ + projectRoot, + rootNames, + typescriptModule: resolved.typescriptModule, + compilerOptions: resolved.extraCompilerOptions, + }); + break; + } + case 'public-modules': { + const modules = await loadModulesMetadata(resolved.modulesMetadataPath); + const content = buildPublicModulesCheckContent(modules, resolved.npmPackageDir); + const entryPath = await writeCheckEntryFile( + path.join(projectRoot, resolved.entryOutputDir), + 'modules.ts', + content, + ); + await runDeclarationsTypeCheck({ + projectRoot, + rootNames: [entryPath], + typescriptModule: resolved.typescriptModule, + compilerOptions: { + allowSyntheticDefaultImports: true, + ...resolved.extraCompilerOptions, + }, + }); + break; + } + default: { + const exhaustive: never = mode; + throw new Error(`Unsupported check mode: ${exhaustive}`); + } + } +} + +export default createExecutor({ + name: 'CheckDeclarations', + resolve: (options, { projectRoot }) => ({ + projectRoot, + mode: options.mode, + modulesMetadataPath: path.resolve( + projectRoot, + options.modulesMetadataFile ?? DEFAULT_MODULES_METADATA, + ), + tsBundleFile: options.tsBundleFile ?? DEFAULT_TS_BUNDLE, + bundleArtifactPath: options.bundleArtifactPath ?? DEFAULT_BUNDLE_ARTIFACT, + modulesPattern: options.modulesPattern ?? DEFAULT_MODULES_PATTERN, + entryOutputDir: options.entryOutputDir ?? DEFAULT_ENTRY_DIR, + npmPackageDir: resolveNpmPackageDir(options), + typescriptModule: options.typescriptModule ?? 'typescript-min', + extraCompilerOptions: options.compilerOptions ?? {}, + }), + run: async (resolved) => { + logger.verbose(`Running declaration check: ${resolved.mode}`); + await runModeCheck(resolved); + }, +}); diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts b/packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts new file mode 100644 index 000000000000..c2089cbd5a29 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts @@ -0,0 +1,90 @@ +export interface ModuleExportEntry { + path: string; + isWidget?: boolean; + exportAs?: string; +} + +export interface ModuleMetadata { + name: string; + isInternal?: boolean; + exports?: Record; +} + +export function widgetNameByPath(widgetPath: string): string { + if (widgetPath.startsWith('ui.dx') || widgetPath.startsWith('viz.dx')) { + const parts = widgetPath.split('.'); + return parts.length === 2 ? parts[1] : ''; + } + return ''; +} + +export function buildJqueryCheckContent(tsBundleFile: string, modules: ModuleMetadata[]): string { + let content = `/// \n`; + content += "import * as $ from 'jquery';"; + + content += modules + .map((moduleMeta) => + Object.keys(moduleMeta.exports || {}) + .map((name) => { + if (moduleMeta.isInternal) { + return ''; + } + + const exportEntry = moduleMeta.exports![name]; + if (!exportEntry.isWidget) { + return ''; + } + + const globalPath = exportEntry.path; + const widgetName = widgetNameByPath(globalPath); + if (!widgetName) { + return ''; + } + + return `$().${widgetName}();\n` + `$().${widgetName}('instance');\n`; + }) + .join(''), + ) + .join('\n'); + + return content; +} + +export function buildPublicModulesCheckContent( + modules: ModuleMetadata[], + packageDir: string, +): string { + let content = "import $ from 'jquery';\n"; + + content += modules + .map((moduleMeta) => { + const modulePath = `'./npm/${packageDir}/${moduleMeta.name}'`; + if (!moduleMeta.exports) { + return `import ${modulePath};`; + } + + return Object.keys(moduleMeta.exports) + .map((name) => { + const exportEntry = moduleMeta.exports![name]; + const uniqueIdentifier = moduleMeta.name + .replace(/\./g, '_') + .split('/') + .concat([name]) + .join('__'); + const importIdentifier = + name === 'default' ? uniqueIdentifier : `{ ${name} as ${uniqueIdentifier} }`; + const importStatement = `import ${importIdentifier} from ${modulePath};`; + const widgetName = widgetNameByPath(exportEntry.path); + + if (exportEntry.isWidget && widgetName) { + return `$('
').${widgetName}();\n${importStatement}`; + } + + return importStatement; + }) + .join('\n'); + }) + .join('\n'); + + return content; +} diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts new file mode 100644 index 000000000000..dc3433e319dd --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts @@ -0,0 +1,94 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { logger } from '@nx/devkit'; +import executor from './executor'; +import { buildJqueryCheckContent, widgetNameByPath } from './declaration-check-content'; +import { CheckDeclarationsExecutorSchema } from './schema'; +import { createTempDir, cleanupTempDir, createMockContext } from '../../utils/test-utils'; +import { writeFileText } from '../../utils'; + +describe('declaration-check-content', () => { + it('should resolve widget name from global path', () => { + expect(widgetNameByPath('ui.dxButton')).toBe('dxButton'); + expect(widgetNameByPath('ui.dxDataGrid.extra')).toBe(''); + }); + + it('should emit jquery widget usage for public widget exports', () => { + const content = buildJqueryCheckContent('./ts/dx.all.d.ts', [ + { + name: 'ui/button', + exports: { default: { path: 'ui.dxButton', isWidget: true } }, + }, + { + name: 'internal', + isInternal: true, + exports: { default: { path: 'ui.dxInternal', isWidget: true } }, + }, + ]); + + expect(content).toContain("/// "); + expect(content).toContain('$().dxButton();'); + expect(content).not.toContain('dxInternal'); + }); +}); + +const PLUGIN_TYPESCRIPT = path.join(__dirname, '..', '..', '..', 'node_modules', 'typescript'); + +describe('CheckDeclarationsExecutor E2E', () => { + let tempDir: string; + let context = createMockContext(); + let projectDir: string; + let errorSpy: jest.SpyInstance; + + beforeEach(() => { + tempDir = createTempDir('nx-check-declarations-e2e-'); + context = createMockContext({ root: tempDir }); + projectDir = path.join(tempDir, 'packages', 'test-lib'); + fs.mkdirSync(path.join(projectDir, 'js'), { recursive: true }); + fs.mkdirSync(path.join(projectDir, 'node_modules', '@types', 'jquery'), { recursive: true }); + fs.writeFileSync( + path.join(projectDir, 'node_modules', '@types', 'jquery', 'index.d.ts'), + 'interface JQuery { empty(): JQuery; }\ninterface JQueryStatic { (selector: string): JQuery; }\ndeclare const $: JQueryStatic;\nexport = $;\n', + ); + errorSpy = jest.spyOn(logger, 'error').mockImplementation(() => undefined); + }); + + afterEach(() => { + errorSpy.mockRestore(); + cleanupTempDir(tempDir); + }); + + it('should pass modules mode for valid declaration files', async () => { + await writeFileText( + path.join(projectDir, 'js', 'sample.d.ts'), + 'declare namespace DevExpress { export interface Sample { value: number; } }\n', + ); + + const options: CheckDeclarationsExecutorSchema = { + mode: 'modules', + modulesPattern: './js/**/*.d.ts', + typescriptModule: PLUGIN_TYPESCRIPT, + compilerOptions: { types: [] }, + }; + + const result = await executor(options, context); + expect(result.success).toBe(true); + }); + + it('should fail modules mode when declarations contain type errors', async () => { + await writeFileText( + path.join(projectDir, 'js', 'broken.d.ts'), + 'declare const broken: UnknownType;\n', + ); + + const options: CheckDeclarationsExecutorSchema = { + mode: 'modules', + modulesPattern: './js/**/*.d.ts', + typescriptModule: PLUGIN_TYPESCRIPT, + }; + + const result = await executor(options, context); + expect(result.success).toBe(false); + expect(errorSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/executor.ts b/packages/nx-infra-plugin/src/executors/check-declarations/executor.ts new file mode 100644 index 000000000000..1cb6a6eaecf7 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/check-declarations/executor.ts @@ -0,0 +1,11 @@ +export { default } from './check-declarations.impl'; +export { + buildDefaultCompilerOptions, + resolveTypeScript, + runDeclarationsTypeCheck, +} from './check-declarations.impl'; +export { + buildJqueryCheckContent, + buildPublicModulesCheckContent, + widgetNameByPath, +} from './declaration-check-content'; diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/schema.json b/packages/nx-infra-plugin/src/executors/check-declarations/schema.json new file mode 100644 index 000000000000..7c8772f45f3d --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/check-declarations/schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/schema", + "title": "Check Declarations Executor Schema", + "description": "Type-check DevExtreme declaration files (gulp validate-ts parity)", + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["jquery", "bundle", "modules", "public-modules"], + "description": "Which declaration check to run." + }, + "modulesMetadataFile": { + "type": "string", + "description": "Path to modules_metadata.json (relative to project root)." + }, + "tsBundleFile": { + "type": "string", + "description": "Source bundle .d.ts for jquery check (relative to project root)." + }, + "bundleArtifactPath": { + "type": "string", + "description": "Built bundle .d.ts for bundle check (relative to project root)." + }, + "modulesPattern": { + "type": "string", + "description": "Glob for module .d.ts files (relative to project root)." + }, + "entryOutputDir": { + "type": "string", + "description": "Directory for generated check entry files (relative to project root)." + }, + "npmPackageDir": { + "type": "string", + "description": "Npm package folder name under artifacts/npm (e.g. devextreme)." + }, + "internalPackage": { + "type": "boolean", + "description": "Use devextreme-internal package dir for public-modules imports." + }, + "typescriptModule": { + "type": "string", + "description": "TypeScript package to load (e.g. typescript-min), resolved from project root." + }, + "compilerOptions": { + "type": "object", + "description": "Extra compiler options merged into the default check profile." + } + }, + "required": ["mode"] +} diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/schema.ts b/packages/nx-infra-plugin/src/executors/check-declarations/schema.ts new file mode 100644 index 000000000000..4eb8d22681de --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/check-declarations/schema.ts @@ -0,0 +1,14 @@ +export type CheckDeclarationsMode = 'jquery' | 'bundle' | 'modules' | 'public-modules'; + +export interface CheckDeclarationsExecutorSchema { + mode: CheckDeclarationsMode; + modulesMetadataFile?: string; + tsBundleFile?: string; + bundleArtifactPath?: string; + modulesPattern?: string; + entryOutputDir?: string; + npmPackageDir?: string; + internalPackage?: boolean; + typescriptModule?: string; + compilerOptions?: Record; +} From b1a242961c0808802bdeb4633ee57703b5326358 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 26 May 2026 00:16:05 +0200 Subject: [PATCH 2/9] migrate TS from gulp to nx --- packages/devextreme/build/gulp/npm.js | 7 +- packages/devextreme/build/gulp/ts.js | 186 ------------------ packages/devextreme/gulpfile.js | 3 +- packages/devextreme/project.json | 7 +- .../check-declarations.impl.ts | 56 ++++-- .../executors/dts-modules/dts-modules.impl.ts | 14 +- 6 files changed, 61 insertions(+), 212 deletions(-) delete mode 100644 packages/devextreme/build/gulp/ts.js diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index fad07d2b4b24..96ab6ff7731b 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -1,7 +1,5 @@ 'use strict'; -require('./ts'); - const eol = require('gulp-eol'); const gulp = require('gulp'); const gulpIf = require('gulp-if'); @@ -156,7 +154,8 @@ const packagePath = `${resultPath}/${packageDir}`; const distPath = `${resultPath}/${packageDistDir}`; gulp.task('npm-sources', gulp.series( - 'ts-sources', + shell.task('pnpm nx run devextreme:build:npm:dts-modules'), + shell.task('pnpm nx run devextreme:build:npm:dts-bundle'), () => gulp .src(devextremeDistWorkspacePackageJsonPath) .pipe( @@ -212,4 +211,4 @@ gulp.task('npm-sass', gulp.series( ) )); -gulp.task('npm', gulp.series('npm-sources', 'npm-dist', 'ts-check-public-modules', 'npm-sass')); +gulp.task('npm', gulp.series('npm-sources', 'npm-dist', 'npm-sass')); diff --git a/packages/devextreme/build/gulp/ts.js b/packages/devextreme/build/gulp/ts.js deleted file mode 100644 index 255c48c0b25e..000000000000 --- a/packages/devextreme/build/gulp/ts.js +++ /dev/null @@ -1,186 +0,0 @@ -'use strict'; - -const gulp = require('gulp'); -const file = require('gulp-file'); -const footer = require('gulp-footer'); -const concat = require('gulp-concat'); -const path = require('path'); -const replace = require('gulp-replace'); -const shell = require('gulp-shell'); -const ts = require('gulp-typescript'); -const context = require('./context.js'); -const headerPipes = require('./header-pipes.js'); -const MODULES = require('./modules_metadata.json'); -const { packageDir } = require('./utils'); - -const OUTPUT_ARTIFACTS_DIR = 'artifacts/ts'; - -const TS_BUNDLE_FILE = './ts/dx.all.d.ts'; -const TS_BUNDLE_SOURCES = [TS_BUNDLE_FILE, './ts/aliases.d.ts']; -const src = ['./js/**/*.d.ts']; - -function compileTS(settings) { - return ts.createProject({ - typescript: require('typescript-min'), - types: ['jquery'], - noEmitOnError: true, - allowJs: true, - lib: [ 'es6', 'es7', 'es2017.object', 'dom' ], - strict: true, - noImplicitAny: false, - ...settings - })(ts.reporter.fullReporter()); -} - -const packagePath = `${context.RESULT_NPM_PATH}/${packageDir}`; -const packageBundlesPath = path.join(packagePath, 'bundles'); - -gulp.task('ts-copy-vendor', function() { - return gulp.src('./ts/vendor/*') - .pipe(gulp.dest(OUTPUT_ARTIFACTS_DIR)); -}); - -function bundleTS() { - return gulp.src(TS_BUNDLE_SOURCES) - .pipe(concat('dx.all.d.ts')) - .pipe(headerPipes.bangLicense()); -} - -gulp.task('ts-copy-bundle', gulp.series( - function writeTsBundle() { - return bundleTS() - .pipe(replace(/^declare global\s*{([\s\S]*?)^}/gm, '$1')) - .pipe(gulp.dest(OUTPUT_ARTIFACTS_DIR)); // will be copied to the npm's /dist folder by another task - }, - - function writeTsBundleForNPM() { - return bundleTS() - .pipe(footer('\nexport default DevExpress;')) - .pipe(replace('/*!', '/**')) - .pipe(replace(/(interface JQuery\b[\s\S]*?{)[\s\S]+?(})/gm, '$1$2')) - .pipe(gulp.dest(packageBundlesPath)); - }, - - function writeAngularHack() { - return file('dx.all.js', '// This file is required to compile devextreme-angular', { src: true }) - .pipe(headerPipes.starLicense()) - .pipe(gulp.dest(packageBundlesPath)); - } -)); - -gulp.task('ts-check-jquery', function() { - let content = `/// \n`; - content += 'import * as $ from \'jquery\';'; - - content += MODULES - .map(function(moduleMeta) { - return Object.keys(moduleMeta.exports || []).map(function(name) { - - if(moduleMeta.isInternal) { return ''; } - - const exportEntry = moduleMeta.exports[name]; - if(!exportEntry.isWidget) { return ''; } - - const globalPath = exportEntry.path; - const widgetName = widgetNameByPath(globalPath); - if(!widgetName) { return ''; } - - return `$().${widgetName}();\n` + - `$().${widgetName}('instance');\n`; - }).join(''); - }).join('\n'); - - return file('artifacts/globals.ts', content, { src: true }) - .pipe(compileTS()); -}); - -gulp.task('ts-check-bundle', function() { - - return gulp.src(path.join(OUTPUT_ARTIFACTS_DIR, 'dx.all.d.ts')) - .pipe(compileTS()); -}); - -gulp.task('ts-check-modules', function() { - return gulp.src(src) - .pipe(compileTS()); -}); - -gulp.task('ts-copy-modules', gulp.series( - function tsCopyModulesCopy() { - const BUNDLE_IMPORT = 'import DevExpress from \'../bundles/dx.all\';'; - - return gulp.src(src) - /* legacy modules */ - .pipe(file('events/click.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/contextmenu.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/dblclick.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/drag.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/hold.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/hover.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/pointer.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/swipe.d.ts', BUNDLE_IMPORT)) - .pipe(file('events/transform.d.ts', BUNDLE_IMPORT)) - .pipe(file('integration/jquery.d.ts', 'import \'jquery\';')) - .pipe(headerPipes.starLicense()) - .pipe(gulp.dest(packagePath)); - }, - shell.task('pnpm nx run devextreme:compress:ts-modules') -)); - -gulp.task('ts-sources', gulp.series('ts-copy-modules', 'ts-copy-bundle')); - -gulp.task('ts-check-public-modules', gulp.series('ts-copy-modules', function() { - let content = 'import $ from \'jquery\';\n'; - - content += MODULES.map(function(moduleMeta) { - const modulePath = `'./npm/${packageDir}/${moduleMeta.name}'`; - if(!moduleMeta.exports) { - return `import ${modulePath};`; - } - - return Object.keys(moduleMeta.exports).map(function(name) { - const exportEntry = moduleMeta.exports[name]; - - const uniqueIdentifier = moduleMeta.name - .replace(/\./g, '_') - .split('/') - .concat([name]) - .join('__'); - - const importIdentifier = name === 'default' ? uniqueIdentifier : `{ ${name} as ${uniqueIdentifier} }`; - - const importStatement = `import ${importIdentifier} from ${modulePath};`; - const widgetName = widgetNameByPath(exportEntry.path); - if(exportEntry.isWidget && widgetName) { - return `$('
').${widgetName}();\n${importStatement}`; - } - - return importStatement; - }).join('\n'); - }).join('\n'); - - return file('artifacts/modules.ts', content, { src: true }) - .pipe(compileTS({ allowSyntheticDefaultImports: true })); -})); - -gulp.task('validate-ts', gulp.series( - 'ts-check-modules', - 'ts-copy-bundle', - 'ts-check-bundle', - 'ts-check-jquery', - 'ts-check-public-modules' -)); - -gulp.task('ts', gulp.series( - 'ts-copy-vendor', - 'ts-copy-bundle', - 'ts-check-jquery', - 'ts-check-bundle' -)); - -function widgetNameByPath(widgetPath) { - if(widgetPath.startsWith('ui.dx') || widgetPath.startsWith('viz.dx')) { - const parts = widgetPath.split('.'); - return parts.length === 2 ? parts[1] : ''; - } -} diff --git a/packages/devextreme/gulpfile.js b/packages/devextreme/gulpfile.js index 3e3bebe1f7f3..d430098c98e6 100644 --- a/packages/devextreme/gulpfile.js +++ b/packages/devextreme/gulpfile.js @@ -31,7 +31,6 @@ gulp.task('clean', function(callback) { require('./build/gulp/bundler-config'); require('./build/gulp/transpile'); require('./build/gulp/js-bundles'); -require('./build/gulp/ts'); require('./build/gulp/localization'); require('./build/gulp/systemjs'); @@ -69,6 +68,8 @@ gulp.task('aspnet', shell.task( gulp.task('vendor', shell.task('pnpm nx run devextreme:copy:vendor')); +gulp.task('ts', shell.task('pnpm nx run devextreme:build:declarations')); + gulp.task('check-license-notices', shell.task('pnpm nx run devextreme:verify:licenses')); gulp.task('state-manager-optimize', shell.task('pnpm nx run devextreme:state-manager:optimize')); diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index a37a65fd7af3..977bcae02fc1 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -821,8 +821,7 @@ "{projectRoot}/ts/dx.all.d.ts", "{projectRoot}/ts/aliases.d.ts", "{projectRoot}/build/gulp/modules_metadata.json" - ], - "outputs": ["{projectRoot}/artifacts/globals.ts"] + ] }, "copy:ts-vendor": { "executor": "devextreme-nx-infra-plugin:copy-files", @@ -854,8 +853,7 @@ "outputs": [ "{projectRoot}/artifacts/ts", "{projectRoot}/artifacts/ts/dx.all.d.ts", - "{projectRoot}/artifacts/npm/devextreme/bundles/dx.all.d.ts", - "{projectRoot}/artifacts/globals.ts" + "{projectRoot}/artifacts/npm/devextreme/bundles/dx.all.d.ts" ] }, "compress:ts-modules": { @@ -1303,7 +1301,6 @@ "{projectRoot}/artifacts/npm/devextreme/**/*.d.ts", "{projectRoot}/build/gulp/modules_metadata.json" ], - "outputs": ["{projectRoot}/artifacts/modules.ts"], "configurations": { "internal": { "internalPackage": true, diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts index ad290bbe2334..7d53d234b259 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts +++ b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts @@ -179,6 +179,28 @@ async function writeCheckEntryFile( return entryPath; } +async function removeCheckEntryFile(entryPath: string): Promise { + try { + await fs.promises.unlink(entryPath); + logger.verbose(`Removed check entry file: ${entryPath}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } +} + +async function runTypeCheckWithTemporaryEntry( + entryPath: string, + typeCheck: () => Promise, +): Promise { + try { + await typeCheck(); + } finally { + await removeCheckEntryFile(entryPath); + } +} + async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise { const { projectRoot, mode } = resolved; @@ -191,12 +213,14 @@ async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise 'globals.ts', content, ); - await runDeclarationsTypeCheck({ - projectRoot, - rootNames: [entryPath], - typescriptModule: resolved.typescriptModule, - compilerOptions: resolved.extraCompilerOptions, - }); + await runTypeCheckWithTemporaryEntry(entryPath, () => + runDeclarationsTypeCheck({ + projectRoot, + rootNames: [entryPath], + typescriptModule: resolved.typescriptModule, + compilerOptions: resolved.extraCompilerOptions, + }), + ); break; } case 'bundle': { @@ -230,15 +254,17 @@ async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise 'modules.ts', content, ); - await runDeclarationsTypeCheck({ - projectRoot, - rootNames: [entryPath], - typescriptModule: resolved.typescriptModule, - compilerOptions: { - allowSyntheticDefaultImports: true, - ...resolved.extraCompilerOptions, - }, - }); + await runTypeCheckWithTemporaryEntry(entryPath, () => + runDeclarationsTypeCheck({ + projectRoot, + rootNames: [entryPath], + typescriptModule: resolved.typescriptModule, + compilerOptions: { + allowSyntheticDefaultImports: true, + ...resolved.extraCompilerOptions, + }, + }), + ); break; } default: { diff --git a/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts b/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts index 0773815325e3..bbc0414575d5 100644 --- a/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts +++ b/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts @@ -12,6 +12,9 @@ import type { PackageJson } from '../../utils/types'; import { DtsModulesExecutorSchema } from './schema'; const BUNDLES_PREFIX = 'bundles/'; +const DIST_PREFIX = 'dist/'; +const LICENSE_PREFIX = 'license/'; +const LICENSE_EXCLUDED_PREFIXES = [BUNDLES_PREFIX, DIST_PREFIX, LICENSE_PREFIX] as const; const BACKSLASH_REGEX = /\\/g; const FORWARD_SLASH = '/'; @@ -31,6 +34,10 @@ function toRelativePosix(baseDir: string, filePath: string): string { return path.relative(baseDir, filePath).replace(BACKSLASH_REGEX, FORWARD_SLASH); } +function isExcludedFromModuleLicense(relativePath: string): boolean { + return LICENSE_EXCLUDED_PREFIXES.some((prefix) => relativePath.startsWith(prefix)); +} + export default createExecutor({ name: 'DtsModules', resolve: async (options, { projectRoot }) => { @@ -70,6 +77,11 @@ export default createExecutor({ path.resolve(resolved.outputDir, relative), ); + const dtsFilesForLicense = dtsFiles.filter( + (filePath) => + !isExcludedFromModuleLicense(toRelativePosix(resolved.outputDir, filePath)), + ); + const bannerInputs = { pkg: resolved.pkg, templatePath: resolved.templatePath, @@ -80,7 +92,7 @@ export default createExecutor({ await Promise.all([ applyLicenseHeadersToFiles({ ...bannerInputs, - files: dtsFiles, + files: dtsFilesForLicense, baseDir: resolved.outputDir, filenameMode: 'relative', }), From ffdc9068887ecc83234211da6d2d9a6a4bdd0246 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 26 May 2026 08:46:41 +0200 Subject: [PATCH 3/9] fix lint --- .../executors/check-declarations/declaration-check-content.ts | 4 +++- .../src/executors/dts-modules/dts-modules.impl.ts | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts b/packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts index c2089cbd5a29..f30634984ea9 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts +++ b/packages/nx-infra-plugin/src/executors/check-declarations/declaration-check-content.ts @@ -41,7 +41,9 @@ export function buildJqueryCheckContent(tsBundleFile: string, modules: ModuleMet return ''; } - return `$().${widgetName}();\n` + `$().${widgetName}('instance');\n`; + return ( + `$().${widgetName}();\n` + `$().${widgetName}('instance');\n` + ); }) .join(''), ) diff --git a/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts b/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts index bbc0414575d5..f401e3ce49d7 100644 --- a/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts +++ b/packages/nx-infra-plugin/src/executors/dts-modules/dts-modules.impl.ts @@ -78,8 +78,7 @@ export default createExecutor({ ); const dtsFilesForLicense = dtsFiles.filter( - (filePath) => - !isExcludedFromModuleLicense(toRelativePosix(resolved.outputDir, filePath)), + (filePath) => !isExcludedFromModuleLicense(toRelativePosix(resolved.outputDir, filePath)), ); const bannerInputs = { From 44a6fa511e382eb512f7c0d9f1288a61efbe21e3 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 26 May 2026 09:20:40 +0200 Subject: [PATCH 4/9] fix text in AGENTS.md --- packages/nx-infra-plugin/AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nx-infra-plugin/AGENTS.md b/packages/nx-infra-plugin/AGENTS.md index 086a120cdf5b..86152471d2fb 100644 --- a/packages/nx-infra-plugin/AGENTS.md +++ b/packages/nx-infra-plugin/AGENTS.md @@ -25,7 +25,7 @@ Each executor lives at `src/executors//`: Each cross-executor concern (license banner, glob-aware copy, file concatenation, debug-block stripping, declaration type-check entry generation, etc.) is owned by exactly ONE executor and exposed via named exports from its `*.impl.ts`. Discover what is available by reading the named exports of the relevant executor; do not re-implement. The full executor catalogue is in `executors.json`; generic primitives live in `src/utils/`. -- **`check-declarations`** — noEmit TypeScript checks for `.d.ts` (modes: `jquery`, `bundle`, `modules`, `public-modules`). Replaces gulp `ts-check-*`. Use **`dts-modules`** / **`dts-bundle`** to produce files; this executor only validates. +- **`check-declarations`** — noEmit TypeScript checks for `.d.ts` (modes: `jquery`, `bundle`, `modules`, `public-modules`). Use **`dts-modules`** / **`dts-bundle`** to produce files; this executor only validates. ## Conventions From 8d7defd5659dbcf669c40fbc4de7d196158fd4bb Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 27 May 2026 16:41:21 +0200 Subject: [PATCH 5/9] fix review notes --- .../check-declarations/check-declarations.impl.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts index 7d53d234b259..888aa6cb52eb 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts +++ b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts @@ -56,12 +56,17 @@ export function resolveTypeScript( projectRoot: string, typescriptModule?: string, ): typeof import('typescript') { - const candidates = typescriptModule + const moduleNameCandidates = typescriptModule ? [ path.isAbsolute(typescriptModule) ? typescriptModule - : path.resolve(projectRoot, typescriptModule), + : typescriptModule.startsWith('.') + ? path.resolve(projectRoot, typescriptModule) + : typescriptModule, ] + : []; + const candidates = typescriptModule + ? moduleNameCandidates : [ path.join(projectRoot, 'node_modules', 'typescript-min'), path.join(projectRoot, 'node_modules', 'typescript'), From a930a5b725ffc60a16795a884eba8e55d09043d6 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 27 May 2026 17:10:12 +0200 Subject: [PATCH 6/9] fix review notes --- packages/devextreme/project.json | 2 ++ .../check-declarations/check-declarations.impl.ts | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 977bcae02fc1..8c0e756a33d7 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -817,7 +817,9 @@ "mode": "jquery", "typescriptModule": "typescript-min" }, + "dependsOn": ["build:npm:dts-bundle"], "inputs": [ + "{projectRoot}/artifacts/ts/dx.all.d.ts", "{projectRoot}/ts/dx.all.d.ts", "{projectRoot}/ts/aliases.d.ts", "{projectRoot}/build/gulp/modules_metadata.json" diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts index 888aa6cb52eb..da8983442b92 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts +++ b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts @@ -175,11 +175,12 @@ async function resolveModuleDeclarationFiles( async function writeCheckEntryFile( entryDir: string, - fileName: string, + fileStem: string, content: string, ): Promise { await ensureDir(entryDir); - const entryPath = path.join(entryDir, fileName); + const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const entryPath = path.join(entryDir, `${fileStem}-${uniqueSuffix}.ts`); await writeFileText(entryPath, content); return entryPath; } @@ -215,7 +216,7 @@ async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise const content = buildJqueryCheckContent(resolved.tsBundleFile, modules); const entryPath = await writeCheckEntryFile( path.join(projectRoot, resolved.entryOutputDir), - 'globals.ts', + 'globals', content, ); await runTypeCheckWithTemporaryEntry(entryPath, () => @@ -256,7 +257,7 @@ async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise const content = buildPublicModulesCheckContent(modules, resolved.npmPackageDir); const entryPath = await writeCheckEntryFile( path.join(projectRoot, resolved.entryOutputDir), - 'modules.ts', + 'modules', content, ); await runTypeCheckWithTemporaryEntry(entryPath, () => From 70506c7a5d469f7351d7b2e034d65ea176692f0c Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 27 May 2026 18:13:30 +0200 Subject: [PATCH 7/9] fix review notes --- packages/devextreme/project.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 8c0e756a33d7..9e8d7f46ab05 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -840,7 +840,6 @@ "options": { "commands": [ "pnpm nx run devextreme:copy:ts-vendor", - "pnpm nx run devextreme:build:npm:dts-bundle", "pnpm nx run devextreme:verify:ts-jquery", "pnpm nx run devextreme:verify:ts-bundle" ], @@ -1315,7 +1314,6 @@ "options": { "commands": [ "pnpm nx run devextreme:verify:ts-modules", - "pnpm nx run devextreme:build:npm:dts-bundle", "pnpm nx run devextreme:verify:ts-bundle", "pnpm nx run devextreme:verify:ts-jquery", "pnpm nx run devextreme:verify:public-modules" From 5cc9ffc6ab05f3f4c11d3fdb5c8a8038ddc91cdc Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 27 May 2026 18:33:33 +0200 Subject: [PATCH 8/9] fix review notes fix path resolve --- packages/devextreme/project.json | 10 ++++- .../check-declarations.impl.ts | 43 ++++++++++++------- .../check-declarations/executor.e2e.spec.ts | 13 +++++- .../executors/check-declarations/executor.ts | 2 + .../executors/check-declarations/schema.json | 2 +- 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 9e8d7f46ab05..ad0365469461 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -1297,10 +1297,16 @@ "mode": "public-modules", "typescriptModule": "typescript-min" }, - "dependsOn": ["build:npm:dts-modules"], + "dependsOn": [ + { "target": "build:npm:dts-modules", "params": "forward" }, + { "target": "build:npm:dts-bundle", "params": "forward" } + ], "inputs": [ "{projectRoot}/artifacts/npm/devextreme/**/*.d.ts", - "{projectRoot}/build/gulp/modules_metadata.json" + "{projectRoot}/artifacts/npm/devextreme/bundles/dx.all.d.ts", + "{projectRoot}/build/gulp/modules_metadata.json", + "{projectRoot}/ts/dx.all.d.ts", + "{projectRoot}/ts/aliases.d.ts" ], "configurations": { "internal": { diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts index da8983442b92..b61282bec334 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts +++ b/packages/nx-infra-plugin/src/executors/check-declarations/check-declarations.impl.ts @@ -13,7 +13,7 @@ import { import { CheckDeclarationsExecutorSchema } from './schema'; const DEFAULT_MODULES_METADATA = './build/gulp/modules_metadata.json'; -const DEFAULT_TS_BUNDLE = './ts/dx.all.d.ts'; +const DEFAULT_TS_BUNDLE = './artifacts/ts/dx.all.d.ts'; const DEFAULT_BUNDLE_ARTIFACT = './artifacts/ts/dx.all.d.ts'; const DEFAULT_MODULES_PATTERN = './js/**/*.d.ts'; const DEFAULT_ENTRY_DIR = './artifacts'; @@ -173,14 +173,25 @@ async function resolveModuleDeclarationFiles( return files; } -async function writeCheckEntryFile( - entryDir: string, - fileStem: string, - content: string, -): Promise { - await ensureDir(entryDir); +export function resolvePathFromProjectRoot(projectRoot: string, relativePath: string): string { + return path.isAbsolute(relativePath) ? relativePath : path.resolve(projectRoot, relativePath); +} + +export function toTripleSlashReferencePath(fromDirectory: string, targetFile: string): string { + let relative = toPosixPath(path.relative(fromDirectory, targetFile)); + if (!relative.startsWith('.')) { + relative = relative ? `./${relative}` : '.'; + } + return relative; +} + +function prepareCheckEntryPath(entryDir: string, fileStem: string): string { const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - const entryPath = path.join(entryDir, `${fileStem}-${uniqueSuffix}.ts`); + return path.join(entryDir, `${fileStem}-${uniqueSuffix}.ts`); +} + +async function writeCheckEntryFile(entryPath: string, content: string): Promise { + await ensureDir(path.dirname(entryPath)); await writeFileText(entryPath, content); return entryPath; } @@ -213,12 +224,14 @@ async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise switch (mode) { case 'jquery': { const modules = await loadModulesMetadata(resolved.modulesMetadataPath); - const content = buildJqueryCheckContent(resolved.tsBundleFile, modules); - const entryPath = await writeCheckEntryFile( - path.join(projectRoot, resolved.entryOutputDir), - 'globals', - content, + const entryDir = path.join(projectRoot, resolved.entryOutputDir); + const entryPath = prepareCheckEntryPath(entryDir, 'globals'); + const bundleReference = toTripleSlashReferencePath( + path.dirname(entryPath), + resolvePathFromProjectRoot(projectRoot, resolved.tsBundleFile), ); + const content = buildJqueryCheckContent(bundleReference, modules); + await writeCheckEntryFile(entryPath, content); await runTypeCheckWithTemporaryEntry(entryPath, () => runDeclarationsTypeCheck({ projectRoot, @@ -255,11 +268,11 @@ async function runModeCheck(resolved: ResolvedCheckDeclarations): Promise case 'public-modules': { const modules = await loadModulesMetadata(resolved.modulesMetadataPath); const content = buildPublicModulesCheckContent(modules, resolved.npmPackageDir); - const entryPath = await writeCheckEntryFile( + const entryPath = prepareCheckEntryPath( path.join(projectRoot, resolved.entryOutputDir), 'modules', - content, ); + await writeCheckEntryFile(entryPath, content); await runTypeCheckWithTemporaryEntry(entryPath, () => runDeclarationsTypeCheck({ projectRoot, diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts index dc3433e319dd..4e8d0de74a47 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/check-declarations/executor.e2e.spec.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { logger } from '@nx/devkit'; import executor from './executor'; import { buildJqueryCheckContent, widgetNameByPath } from './declaration-check-content'; +import { toTripleSlashReferencePath } from './check-declarations.impl'; import { CheckDeclarationsExecutorSchema } from './schema'; import { createTempDir, cleanupTempDir, createMockContext } from '../../utils/test-utils'; import { writeFileText } from '../../utils'; @@ -13,6 +14,14 @@ describe('declaration-check-content', () => { expect(widgetNameByPath('ui.dxDataGrid.extra')).toBe(''); }); + it('should compute triple-slash path relative to entry directory', () => { + const projectRoot = path.join('C:', 'proj'); + const entryDir = path.join(projectRoot, 'artifacts'); + const bundleFile = path.join(projectRoot, 'artifacts', 'ts', 'dx.all.d.ts'); + + expect(toTripleSlashReferencePath(entryDir, bundleFile)).toBe('./ts/dx.all.d.ts'); + }); + it('should emit jquery widget usage for public widget exports', () => { const content = buildJqueryCheckContent('./ts/dx.all.d.ts', [ { @@ -32,7 +41,9 @@ describe('declaration-check-content', () => { }); }); -const PLUGIN_TYPESCRIPT = path.join(__dirname, '..', '..', '..', 'node_modules', 'typescript'); +const PLUGIN_TYPESCRIPT = path.dirname( + require.resolve('typescript/package.json', { paths: [__dirname] }), +); describe('CheckDeclarationsExecutor E2E', () => { let tempDir: string; diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/executor.ts b/packages/nx-infra-plugin/src/executors/check-declarations/executor.ts index 1cb6a6eaecf7..3703d073b5c3 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/executor.ts +++ b/packages/nx-infra-plugin/src/executors/check-declarations/executor.ts @@ -1,8 +1,10 @@ export { default } from './check-declarations.impl'; export { buildDefaultCompilerOptions, + resolvePathFromProjectRoot, resolveTypeScript, runDeclarationsTypeCheck, + toTripleSlashReferencePath, } from './check-declarations.impl'; export { buildJqueryCheckContent, diff --git a/packages/nx-infra-plugin/src/executors/check-declarations/schema.json b/packages/nx-infra-plugin/src/executors/check-declarations/schema.json index 7c8772f45f3d..50186dd10ff8 100644 --- a/packages/nx-infra-plugin/src/executors/check-declarations/schema.json +++ b/packages/nx-infra-plugin/src/executors/check-declarations/schema.json @@ -15,7 +15,7 @@ }, "tsBundleFile": { "type": "string", - "description": "Source bundle .d.ts for jquery check (relative to project root)." + "description": "Bundle .d.ts for jquery check (path relative to project root; embedded in /// relative to the generated entry file)." }, "bundleArtifactPath": { "type": "string", From ba7ae600d9709b9d6951284b5c438d83b1415ebe Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 27 May 2026 22:23:26 +0200 Subject: [PATCH 9/9] fix review notes --- packages/devextreme/gulpfile.js | 12 +++++++++++- packages/devextreme/project.json | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/gulpfile.js b/packages/devextreme/gulpfile.js index d430098c98e6..aaad61a9a326 100644 --- a/packages/devextreme/gulpfile.js +++ b/packages/devextreme/gulpfile.js @@ -48,6 +48,12 @@ function getTranspileConfig() { const transpileConfig = getTranspileConfig(); +function getDeclarationsConfiguration() { + return env.BUILD_INTERNAL_PACKAGE ? 'internal' : ''; +} + +const declarationsConfig = getDeclarationsConfiguration(); + gulp.task('transpile', shell.task( transpileConfig ? `pnpm nx run devextreme:build:transpile -c ${transpileConfig}` @@ -68,7 +74,11 @@ gulp.task('aspnet', shell.task( gulp.task('vendor', shell.task('pnpm nx run devextreme:copy:vendor')); -gulp.task('ts', shell.task('pnpm nx run devextreme:build:declarations')); +gulp.task('ts', shell.task( + declarationsConfig + ? `pnpm nx run devextreme:build:declarations -c ${declarationsConfig}` + : 'pnpm nx run devextreme:build:declarations' +)); gulp.task('check-license-notices', shell.task('pnpm nx run devextreme:verify:licenses')); diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index ad0365469461..61bdca3ffff9 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -845,6 +845,16 @@ ], "parallel": false }, + "configurations": { + "internal": { + "commands": [ + "pnpm nx run devextreme:copy:ts-vendor", + "pnpm nx run devextreme:verify:ts-jquery -c internal", + "pnpm nx run devextreme:verify:ts-bundle -c internal" + ], + "parallel": false + } + }, "inputs": [ "{projectRoot}/ts/**/*.d.ts", "{projectRoot}/ts/vendor/**/*",