From c194e27df5e5cada178f43f35eaefedd134ca2a0 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Wed, 11 Mar 2026 11:09:07 +0400 Subject: [PATCH 01/10] start version --- .../__docs__/scripts/data_grid/constants.ts | 108 +++ .../__docs__/scripts/data_grid/generate.ts | 186 +++++ .../scripts/data_grid/graph-builder.ts | 211 ++++++ .../scripts/data_grid/html-template.ts | 576 ++++++++++++++ .../__docs__/scripts/data_grid/parser.ts | 706 ++++++++++++++++++ .../__docs__/scripts/data_grid/resolver.ts | 366 +++++++++ .../grids/__docs__/scripts/data_grid/types.ts | 168 +++++ .../__docs__/scripts/{ => grid_core}/cli.ts | 0 .../scripts/{ => grid_core}/constants.ts | 0 .../generate-architecture-doc.ts | 0 .../scripts/{ => grid_core}/graph-builder.ts | 0 .../scripts/{ => grid_core}/html-template.ts | 0 .../scripts/{ => grid_core}/parser.ts | 0 .../scripts/{ => grid_core}/resolver.ts | 0 .../__docs__/scripts/{ => grid_core}/types.ts | 0 15 files changed, 2321 insertions(+) create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/cli.ts (100%) rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/constants.ts (100%) rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/generate-architecture-doc.ts (100%) rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/graph-builder.ts (100%) rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/html-template.ts (100%) rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/parser.ts (100%) rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/resolver.ts (100%) rename packages/devextreme/js/__internal/grids/__docs__/scripts/{ => grid_core}/types.ts (100%) diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts new file mode 100644 index 000000000000..2cc6cd845603 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts @@ -0,0 +1,108 @@ +import * as path from 'path'; + +export const DATA_GRID_ROOT = path.resolve(__dirname, '..', '..', '..', 'data_grid'); +export const GRID_CORE_ROOT = path.resolve(__dirname, '..', '..', '..', 'grid_core'); +export const OUTPUT_DIR = path.resolve(__dirname, '..', '..', 'artifacts'); + +export const EXCLUDED_DIRS = new Set(['__tests__', '__mock__', 'scripts', 'new', '__docs__']); +export const EXCLUDED_FILE_NAMES = new Set(); + +export const REGISTER_MODULE_RECEIVERS = new Set([ + 'gridCore', + 'core', + 'treeListCore', + 'dataGridCore', +]); + +export const DATA_SOURCE_ADAPTER_PROVIDER = 'dataSourceAdapterProvider'; + +export const GRID_CORE_IMPORT_PATTERNS = [ + '@ts/grids/grid_core/', + '../../grid_core/', + '../../../grid_core/', +]; + +export type ModificationCategory = 'passthrough' | 'extended' | 'replaced' | 'new'; + +const DATA_GRID_FEATURE_MAP: Record = { + m_data_controller: 'Data', + m_data_source_adapter: 'Data', + + m_core: 'Core', + m_widget: 'Core', + m_widget_base: 'Core', + m_utils: 'Core', + + m_editing: 'Editing', + + grouping: 'Grouping', + summary: 'Summary', + export: 'Export', + + keyboard_navigation: 'Navigation', + focus: 'Navigation', + + m_columns_controller: 'Columns', + m_aggregate_calculator: 'Data', + + module_not_extended: 'Passthrough', +}; + +const MODULE_NOT_EXTENDED_FEATURE_MAP: Record = { + sorting: 'Sorting', + selection: 'Selection', + search: 'Filtering', + filter_row: 'Filtering', + filter_sync: 'Filtering', + filter_panel: 'Filtering', + filter_builder: 'Filtering', + header_filter: 'Filtering', + column_headers: 'Columns', + column_chooser: 'Column Management', + column_fixing: 'Column Management', + sticky_columns: 'Column Management', + virtual_columns: 'Column Management', + columns_resizing_reordering: 'Column Management', + adaptivity: 'Column Management', + keyboard_navigation: 'Navigation', + editing_row_based: 'Editing', + editing_form_based: 'Editing', + editing_cell_based: 'Editing', + editor_factory: 'Editing', + validating: 'Editing', + virtual_scrolling: 'Scrolling', + state_storing: 'State', + pager: 'Paging', + rows: 'Core', + grid_view: 'Core', + header_panel: 'Columns', + context_menu: 'Core', + error_handling: 'Core', + master_detail: 'Master Detail', + row_dragging: 'Row Dragging', + toast: 'Core', + ai_column: 'AI', +}; + +export function getFeatureAreaFromPath(relPath: string): string { + const segments = relPath.split('/'); + const firstSegment = segments[0]; + + if (firstSegment === 'module_not_extended') { + const fileName = segments[1]?.replace('.ts', '') ?? ''; + return MODULE_NOT_EXTENDED_FEATURE_MAP[fileName] ?? 'Other'; + } + + if (DATA_GRID_FEATURE_MAP[firstSegment]) { + return DATA_GRID_FEATURE_MAP[firstSegment]; + } + + const baseName = firstSegment.replace('.ts', ''); + if (DATA_GRID_FEATURE_MAP[baseName]) { + return DATA_GRID_FEATURE_MAP[baseName]; + } + + return 'Other'; +} + +export const WIDGET_BASE_FILE = path.resolve(DATA_GRID_ROOT, 'm_widget_base.ts'); diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts new file mode 100644 index 000000000000..af7b95969300 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts @@ -0,0 +1,186 @@ +#!/usr/bin/env tsx +/* eslint-disable no-console, spellcheck/spell-checker */ +import * as fs from 'fs'; +import * as path from 'path'; + +import { DATA_GRID_ROOT, GRID_CORE_ROOT, OUTPUT_DIR } from './constants'; +import { generateHtml } from './html-template'; +import { + discoverDataGridFiles, + getRelativePath, + parseDataGridFile, + parseGridCoreModules, + parseModulesOrder, +} from './parser'; +import { + buildExtenderPipelines, + buildInheritanceChains, + classifyModules, + collectDataSourceAdapterChain, +} from './resolver'; +import type { DataGridArchitectureData, DataGridParsedFile } from './types'; + +interface CliArgs { + jsonOnly: boolean; + htmlOnly: boolean; +} + +function parseArgs(): CliArgs { + const args = process.argv.slice(2); + const result: CliArgs = { jsonOnly: false, htmlOnly: false }; + + for (const arg of args) { + switch (arg) { + case '--json': + result.jsonOnly = true; + break; + case '--html': + result.htmlOnly = true; + break; + default: + console.error(`Error: Unknown argument "${arg}"`); + process.exit(1); + } + } + + if (result.jsonOnly && result.htmlOnly) { + console.error('Error: Cannot specify both --json and --html. Use neither to generate both.'); + process.exit(1); + } + + return result; +} + +function appendMissingModuleNames(modulesOrder: string[], parsedFiles: DataGridParsedFile[]): void { + for (const pf of parsedFiles) { + for (const reg of pf.registerModuleCalls) { + if (!modulesOrder.includes(reg.moduleName)) { + modulesOrder.push(reg.moduleName); + } + } + } +} + +function main(): void { + console.log('DataGrid Extensions Architecture Documentation Generator'); + console.log(`DataGrid root: ${DATA_GRID_ROOT}`); + console.log(`Output dir: ${OUTPUT_DIR}`); + + try { + // 1. Parse module order from source + // NOTE: registerModulesOrder defines ascending priority. + // processModules (m_modules.ts) sorts by: orderIndex1 - orderIndex2, + // which means index 0 processes first and the last index processes last. + // Extenders are applied in the same ascending order, so earlier modules + // are extended first, and later ones wrap on top. + const modulesOrder = parseModulesOrder(); + console.log(`Parsed ${modulesOrder.length} modules from registerModulesOrder (ascending order)`); + + // 2. Parse grid_core modules (to show full extension/inheritance chains) + const gridCoreModules = parseGridCoreModules(GRID_CORE_ROOT); + console.log(`Parsed ${gridCoreModules.length} grid_core modules`); + + // 3. Discover data_grid source files + const sourceFiles = discoverDataGridFiles(DATA_GRID_ROOT); + console.log(`Discovered ${sourceFiles.length} data_grid source files`); + + // 4. Parse all files + const allParsedFiles = sourceFiles.flatMap((file) => { + console.log(` Parsing: ${getRelativePath(file)}`); + try { + return [parseDataGridFile(file)]; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.warn(` WARN: Failed to parse ${getRelativePath(file)}: ${msg}`); + return []; + } + }); + + // 5. Discover all registered module names and build full order + appendMissingModuleNames(modulesOrder, allParsedFiles); + + // 6. Classify modules (using full order) + const allModules = classifyModules(allParsedFiles, modulesOrder); + + console.log(`\nClassified ${allModules.length} modules:`); + const counts = { + passthrough: 0, + extended: 0, + replaced: 0, + new: 0, + }; + for (const mod of allModules) { + counts[mod.category] += 1; + console.log(` [${mod.category.toUpperCase().padEnd(11)}] ${mod.moduleName} (${mod.relPath})`); + } + console.log(`\n Passthrough: ${counts.passthrough}`); + console.log(` Replaced: ${counts.replaced}`); + console.log(` Extended: ${counts.extended}`); + console.log(` New: ${counts.new}`); + + // 7. Build extender pipelines + const extenderPipelines = buildExtenderPipelines(allModules); + console.log(`\nBuilt ${extenderPipelines.length} extender pipelines:`); + for (const p of extenderPipelines) { + console.log(` ${p.targetType} '${p.targetName}' — ${p.steps.length} step(s): ${p.steps.map((s) => s.moduleName).join(' → ')}`); + } + + // 8. Collect DataSourceAdapter chain + const dsaChain = collectDataSourceAdapterChain(allParsedFiles); + console.log(`\nDataSourceAdapter chain (${dsaChain.length} extensions):`); + for (const ext of dsaChain) { + console.log(` ${ext.order + 1}. ${ext.extenderName} (${ext.relPath})${ext.isImportedFromGridCore ? ' [from grid_core]' : ''}`); + } + + // 9. Build inheritance chains + const inheritanceChains = buildInheritanceChains(allParsedFiles); + console.log(`\nBuilt ${inheritanceChains.length} inheritance chains`); + + // 10. Build output data + const data: DataGridArchitectureData = { + generatedAt: new Date().toISOString(), + dataGridRoot: 'packages/devextreme/js/__internal/grids/data_grid', + gridCoreRoot: 'packages/devextreme/js/__internal/grids/grid_core', + modulesOrder, + modules: allModules, + gridCoreModules, + extenderPipelines, + dataSourceAdapterChain: dsaChain, + inheritanceChains, + summary: { + total: allModules.length, + ...counts, + }, + }; + + // 11. Write output files + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + } + + const args = parseArgs(); + + if (!args.htmlOnly) { + const jsonPath = path.join(OUTPUT_DIR, 'data_grid_architecture.generated.json'); + fs.writeFileSync(jsonPath, `${JSON.stringify(data, null, 2)}\n`); + console.log(`\nJSON written to: ${jsonPath}`); + } + + if (!args.jsonOnly) { + const htmlPath = path.join(OUTPUT_DIR, 'data_grid_architecture.generated.html'); + fs.writeFileSync(htmlPath, generateHtml(data)); + console.log(`HTML written to: ${htmlPath}`); + } + + console.log('\nDone.'); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error(`ERROR: ${msg}`); + if (e instanceof Error && e.stack) { + console.error(e.stack); + } + process.exit(1); + } +} + +main(); diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts new file mode 100644 index 000000000000..2975b1d49e12 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts @@ -0,0 +1,211 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { DataGridArchitectureData, GridCoreModuleInfo } from './types'; + +interface CytoscapeElement { + group: 'nodes' | 'edges'; + data: Record; + classes?: string; +} + +interface EdgeData extends Record { + edgeType: string; + targetName?: string; +} + +/** + * Match a grid_core module to a data_grid module by comparing + * the module's registeredAs name with the data_grid module name. + */ +function findGridCoreModule( + dgModuleName: string, + gridCoreModules: GridCoreModuleInfo[], +): GridCoreModuleInfo | undefined { + return gridCoreModules.find( + (gc) => gc.registeredAs === dgModuleName, + ); +} + +/** + * Builds a unified graph where: + * - Nodes = registered modules (data_grid) + grid_core source modules + * - Edges show direct extension chains between modules: + * - grid_core → data_grid source edges + * - Controller extender chains (e.g. grouping → editing for 'data' ctrl) + * - View extender chains + * - DataSourceAdapter chain + * - Registration order (subtle dotted) + */ +export function buildCytoscapeElements(data: DataGridArchitectureData): CytoscapeElement[] { + const elements: CytoscapeElement[] = []; + const nodeIds = new Set(); + const edgeIds = new Set(); + + function addNode(id: string, nodeData: Record, classes: string): void { + if (nodeIds.has(id)) return; + nodeIds.add(id); + elements.push({ group: 'nodes', data: { id, ...nodeData }, classes }); + } + + function addEdge( + source: string, + target: string, + edgeData: EdgeData, + classes: string, + ): void { + const targetName = edgeData.targetName ?? ''; + const id = `e-${source}-${target}-${edgeData.edgeType}-${targetName}`; + if (!nodeIds.has(source) || !nodeIds.has(target) || edgeIds.has(id)) return; + edgeIds.add(id); + elements.push({ + group: 'edges', + data: { + id, source, target, ...edgeData, + }, + classes, + }); + } + + // ─── Grid Core module nodes ───────────────────────────────────────────────── + // Add grid_core modules that are referenced by data_grid modules + const usedGcModules = new Set(); + for (const mod of data.modules) { + const gcMod = findGridCoreModule(mod.moduleName, data.gridCoreModules); + if (gcMod) { + usedGcModules.add(gcMod.moduleName); + } + } + + for (const gcMod of data.gridCoreModules) { + if (!usedGcModules.has(gcMod.moduleName)) { + // eslint-disable-next-line no-continue + continue; + } + + const gcId = `gc-${gcMod.moduleName}`; + + const labelParts: string[] = [gcMod.registeredAs ?? gcMod.moduleName]; + const ctrls = Object.keys(gcMod.controllers); + const vws = Object.keys(gcMod.views); + const extCtrls = Object.keys(gcMod.extenders.controllers); + const extVws = Object.keys(gcMod.extenders.views); + if (ctrls.length > 0) labelParts.push(`ctrl: ${ctrls.join(', ')}`); + if (vws.length > 0) labelParts.push(`view: ${vws.join(', ')}`); + if (extCtrls.length > 0) labelParts.push(`ext ctrl: ${extCtrls.join(', ')}`); + if (extVws.length > 0) labelParts.push(`ext view: ${extVws.join(', ')}`); + + addNode(gcId, { + label: labelParts.join('\n'), + nodeType: 'gridCoreModule', + category: 'grid-core', + sourceFile: gcMod.sourceFile, + featureArea: gcMod.featureArea, + registrationOrder: -1, + details: `grid_core module: ${gcMod.sourceFile}`, + gridCoreSource: '', + moduleName: gcMod.registeredAs ?? gcMod.moduleName, + controllers: JSON.stringify(gcMod.controllers), + views: JSON.stringify(gcMod.views), + extenders: JSON.stringify(gcMod.extenders), + }, 'module grid-core'); + } + + // ─── Data Grid module nodes ───────────────────────────────────────────────── + for (const mod of data.modules) { + const moduleId = `mod-${mod.moduleName}`; + const orderNum = mod.registrationOrder + 1; + + const labelParts: string[] = [`#${orderNum} ${mod.moduleName}`]; + if (mod.category !== 'passthrough') { + labelParts.push(`[${mod.category}]`); + } + + const extCtrl = mod.overriddenExtenderControllers; + const extView = mod.overriddenExtenderViews; + if (extCtrl.length > 0) labelParts.push(`ext ctrl: ${extCtrl.join(', ')}`); + if (extView.length > 0) labelParts.push(`ext view: ${extView.join(', ')}`); + if (mod.newControllers.length > 0) labelParts.push(`ctrl: ${mod.newControllers.join(', ')}`); + if (mod.newViews.length > 0) labelParts.push(`view: ${mod.newViews.join(', ')}`); + + addNode(moduleId, { + label: labelParts.join('\n'), + nodeType: 'module', + category: mod.category, + sourceFile: mod.relPath, + featureArea: mod.featureArea, + registrationOrder: mod.registrationOrder, + details: mod.details, + gridCoreSource: mod.gridCoreSourceModule ?? '', + moduleName: mod.moduleName, + }, `module ${mod.category}`); + } + + // ─── Grid Core → Data Grid source edges ───────────────────────────────────── + for (const mod of data.modules) { + const gcMod = findGridCoreModule(mod.moduleName, data.gridCoreModules); + if (gcMod) { + const gcId = `gc-${gcMod.moduleName}`; + const dgId = `mod-${mod.moduleName}`; + addEdge(gcId, dgId, { + edgeType: 'grid-core-source', + label: mod.category === 'passthrough' ? 'passthrough' : mod.category, + }, 'edge-gc-source'); + } + } + + // ─── Registration order spine (subtle) ───────────────────────────────────── + for (let i = 0; i < data.modules.length - 1; i += 1) { + addEdge( + `mod-${data.modules[i].moduleName}`, + `mod-${data.modules[i + 1].moduleName}`, + { edgeType: 'order' }, + 'edge-order', + ); + } + + // ─── Extender chain edges (direct inter-module edges) ────────────────────── + for (const pipeline of data.extenderPipelines) { + const { targetName, targetType, steps } = pipeline; + const edgeClass = targetType === 'controller' ? 'edge-ext-ctrl' : 'edge-ext-view'; + + for (let i = 0; i < steps.length - 1; i += 1) { + const src = `mod-${steps[i].moduleName}`; + const tgt = `mod-${steps[i + 1].moduleName}`; + addEdge(src, tgt, { + edgeType: 'extender-chain', + targetName, + targetType, + label: targetName, + chainIndex: i, + chainLength: steps.length, + }, edgeClass); + } + } + + // ─── DataSourceAdapter chain edges ───────────────────────────────────────── + const dsaModuleOrder: { moduleName: string; relPath: string; isFromGridCore: boolean }[] = []; + for (const ext of data.dataSourceAdapterChain) { + const mod = data.modules.find((m) => m.relPath === ext.relPath); + if (mod) { + dsaModuleOrder.push({ + moduleName: mod.moduleName, + relPath: ext.relPath, + isFromGridCore: ext.isImportedFromGridCore, + }); + } + } + + for (let i = 0; i < dsaModuleOrder.length - 1; i += 1) { + addEdge( + `mod-${dsaModuleOrder[i].moduleName}`, + `mod-${dsaModuleOrder[i + 1].moduleName}`, + { + edgeType: 'dsa-chain', + targetName: 'DataSourceAdapter', + label: 'DSA', + }, + 'edge-dsa', + ); + } + + return elements; +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts new file mode 100644 index 000000000000..b9739cd8df1e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts @@ -0,0 +1,576 @@ +/* eslint-disable spellcheck/spell-checker */ +import { buildCytoscapeElements } from './graph-builder'; +import type { DataGridArchitectureData } from './types'; + +export function generateHtml(data: DataGridArchitectureData): string { + const cytoscapeElements = buildCytoscapeElements(data); + const elementsJson = JSON.stringify(cytoscapeElements, null, 2); + const pipelinesJson = JSON.stringify(data.extenderPipelines); + const dsaJson = JSON.stringify(data.dataSourceAdapterChain); + const gridCoreModulesJson = JSON.stringify(data.gridCoreModules.map((gc) => ({ + moduleName: gc.moduleName, + registeredAs: gc.registeredAs, + sourceFile: gc.sourceFile, + featureArea: gc.featureArea, + controllers: gc.controllers, + views: gc.views, + extenders: gc.extenders, + hasDefaultOptions: gc.hasDefaultOptions, + }))); + const modulesJson = JSON.stringify(data.modules.map((m) => ({ + moduleName: m.moduleName, + category: m.category, + relPath: m.relPath, + featureArea: m.featureArea, + registrationOrder: m.registrationOrder, + details: m.details, + gridCoreSourceModule: m.gridCoreSourceModule, + newControllers: m.newControllers, + newViews: m.newViews, + overriddenControllers: m.overriddenControllers, + overriddenExtenderControllers: m.overriddenExtenderControllers, + overriddenExtenderViews: m.overriddenExtenderViews, + hasDefaultOptionsOverride: m.hasDefaultOptionsOverride, + }))); + const featureAreas = [...new Set(data.modules.map((m) => m.featureArea))].sort(); + + return ` + + + + +DataGrid Extensions Architecture + + + + + + + + +
+
+
+

Click a module to see its extension and inheritance chains.

+
+
+ + + +`; +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts new file mode 100644 index 000000000000..08bbfba4a6a7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts @@ -0,0 +1,706 @@ +/* eslint-disable spellcheck/spell-checker,max-depth */ +import * as fs from 'fs'; +import * as path from 'path'; +// eslint-disable-next-line import/no-extraneous-dependencies +import ts from 'typescript'; + +import { + discoverSourceFiles as discoverGridCoreFiles, + parseFile as parseGridCoreFile, +} from '../grid_core/parser'; +import { + DATA_GRID_ROOT, + DATA_SOURCE_ADAPTER_PROVIDER, + EXCLUDED_DIRS, + EXCLUDED_FILE_NAMES, + GRID_CORE_IMPORT_PATTERNS, + REGISTER_MODULE_RECEIVERS, + WIDGET_BASE_FILE, +} from './constants'; +import type { + DataGridClassRef, + DataGridParsedFile, + ExtenderRef, + GridCoreModuleInfo, + RegisterModuleCall, +} from './types'; + +// ─── File Discovery ────────────────────────────────────────────────────────── + +export function discoverDataGridFiles(rootDir: string): string[] { + const results: string[] = []; + + function walk(dir: string): void { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (!EXCLUDED_DIRS.has(entry.name)) { + walk(fullPath); + } + } else if ( + entry.isFile() + && !EXCLUDED_FILE_NAMES.has(entry.name) + && entry.name.endsWith('.ts') + && !entry.name.includes('.test.') + ) { + results.push(fullPath); + } + } + } + + walk(rootDir); + return results.sort(); +} + +export function getRelativePath(filePath: string): string { + return path.relative(DATA_GRID_ROOT, filePath).replace(/\\/g, '/'); +} + +// ─── AST Helpers ───────────────────────────────────────────────────────────── + +function getNodeText(node: ts.Node, sf: ts.SourceFile): string { + return node.getText(sf).trim(); +} + +function isGridCoreImport(fromPath: string): boolean { + return GRID_CORE_IMPORT_PATTERNS.some((p) => fromPath.includes(p)); +} + +function hasExportModifier(node: ts.Node): boolean { + if (!ts.canHaveModifiers(node)) return false; + const modifiers = ts.getModifiers(node); + return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; +} + +function parseHeritageString(text: string): { baseClass: string; mixins: string[] } { + const mixins: string[] = []; + let current = text; + while (true) { + const match = /^(\w+)\((.+)\)$/.exec(current); + if (match) { + const [, mixinName, inner] = match; + mixins.push(mixinName); + current = inner; + } else { + break; + } + } + return { + baseClass: mixins.length > 0 ? `${mixins[mixins.length - 1]}(${current})` : current, + mixins, + }; +} + +function getClassHeritage( + node: ts.ClassDeclaration | ts.ClassExpression, + sf: ts.SourceFile, + localVars: Map, +): { baseClass: string; mixins: string[] } { + if (!node.heritageClauses) return { baseClass: '', mixins: [] }; + for (const clause of node.heritageClauses) { + if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length > 0) { + const text = getNodeText(clause.types[0].expression, sf); + if (ts.isIdentifier(clause.types[0].expression) && localVars.has(text)) { + return parseHeritageString(localVars.get(text) ?? ''); + } + return parseHeritageString(text); + } + } + return { baseClass: '', mixins: [] }; +} + +// ─── registerModule Call Detection ─────────────────────────────────────────── + +function isRegisterModuleCall(node: ts.Node, sf: ts.SourceFile): node is ts.CallExpression { + if (!ts.isCallExpression(node)) return false; + const expr = node.expression; + if (!ts.isPropertyAccessExpression(expr)) return false; + const methodName = expr.name.text; + if (methodName !== 'registerModule') return false; + const receiverText = getNodeText(expr.expression, sf); + return REGISTER_MODULE_RECEIVERS.has(receiverText); +} + +function collectSpreadSources( + obj: ts.ObjectLiteralExpression, + sf: ts.SourceFile, +): string[] { + const sources: string[] = []; + for (const prop of obj.properties) { + if (ts.isSpreadAssignment(prop)) { + sources.push(getNodeText(prop.expression, sf)); + } + } + return sources; +} + +function parseInlineControllerViews( + obj: ts.ObjectLiteralExpression, + sf: ts.SourceFile, + parsedFile: DataGridParsedFile, +): Record { + const result: Record = {}; + for (const prop of obj.properties) { + let regName = ''; + let classRef = ''; + + if (ts.isPropertyAssignment(prop) && prop.name) { + regName = ts.isIdentifier(prop.name) ? prop.name.text : getNodeText(prop.name, sf); + classRef = getNodeText(prop.initializer, sf); + } else if (ts.isShorthandPropertyAssignment(prop)) { + regName = prop.name.text; + classRef = prop.name.text; + } + + if (regName) { + const localClass = parsedFile.classes.get(classRef); + const importInfo = parsedFile.imports.get(classRef); + + result[regName] = { + regName, + className: classRef, + isImportedFromGridCore: importInfo?.isFromGridCore ?? false, + isDefinedLocally: !!localClass, + baseClass: localClass?.baseClass ?? '', + mixins: localClass?.mixins ?? [], + sourceFile: localClass?.sourceFile ?? parsedFile.relPath, + }; + } + } + return result; +} + +function parseInlineExtenders( + obj: ts.ObjectLiteralExpression, + sf: ts.SourceFile, + parsedFile: DataGridParsedFile, +): { controllers: Record; views: Record } { + const result = { + controllers: {} as Record, + views: {} as Record, + }; + + const processSection = ( + sectionName: 'controllers' | 'views', + initializer: ts.ObjectLiteralExpression, + ): void => { + for (const extProp of initializer.properties) { + if (!ts.isSpreadAssignment(extProp)) { + let targetName = ''; + let extenderName = ''; + + if (ts.isPropertyAssignment(extProp) && extProp.name) { + targetName = ts.isIdentifier(extProp.name) + ? extProp.name.text + : getNodeText(extProp.name, sf); + extenderName = getNodeText(extProp.initializer, sf); + } else if (ts.isShorthandPropertyAssignment(extProp)) { + targetName = extProp.name.text; + extenderName = extProp.name.text; + } + + if (targetName) { + const importInfo = parsedFile.imports.get(extenderName); + const isLocal = parsedFile.classes.has(extenderName) + || parsedFile.localVars.has(extenderName); + + result[sectionName][targetName] = { + targetName, + extenderName, + isImportedFromGridCore: importInfo?.isFromGridCore ?? false, + isDefinedLocally: isLocal, + }; + } + } + } + }; + + for (const prop of obj.properties) { + if ( + !ts.isSpreadAssignment(prop) + && ts.isPropertyAssignment(prop) + && prop.name + && ts.isIdentifier(prop.name) + ) { + const sectionName = prop.name.text; + if ( + (sectionName === 'controllers' || sectionName === 'views') + && ts.isObjectLiteralExpression(prop.initializer) + ) { + processSection(sectionName, prop.initializer); + } + } + } + + return result; +} + +function parseRegisterModuleCall( + call: ts.CallExpression, + sf: ts.SourceFile, + parsedFile: DataGridParsedFile, +): RegisterModuleCall | null { + if (call.arguments.length < 2) return null; + + const nameArg = call.arguments[0]; + if (!ts.isStringLiteral(nameArg)) return null; + const moduleName = nameArg.text; + + const moduleArg = call.arguments[1]; + const reg: RegisterModuleCall = { + moduleName, + sourceFile: parsedFile.filePath, + relPath: parsedFile.relPath, + argIsIdentifier: false, + argIdentifierName: null, + spreadSources: [], + hasInlineControllers: false, + hasInlineViews: false, + hasInlineExtenders: false, + hasDefaultOptions: false, + referencesGridCoreModule: false, + gridCoreRefs: [], + controllers: {}, + views: {}, + extenders: { controllers: {}, views: {} }, + }; + + if (ts.isIdentifier(moduleArg)) { + reg.argIsIdentifier = true; + reg.argIdentifierName = moduleArg.text; + const imp = parsedFile.imports.get(moduleArg.text); + if (imp?.isFromGridCore) { + reg.referencesGridCoreModule = true; + reg.gridCoreRefs.push(moduleArg.text); + } + return reg; + } + + if (ts.isObjectLiteralExpression(moduleArg)) { + reg.spreadSources = collectSpreadSources(moduleArg, sf); + + // Check spreads for grid_core references + for (const src of reg.spreadSources) { + const imp = parsedFile.imports.get(src); + if (imp?.isFromGridCore) { + reg.referencesGridCoreModule = true; + reg.gridCoreRefs.push(src); + } + } + + const processModuleProp = ( + prop: ts.ObjectLiteralElementLike, + ): void => { + if (ts.isSpreadAssignment(prop)) return; + if ( + !ts.isPropertyAssignment(prop) + && !ts.isMethodDeclaration(prop) + && !ts.isShorthandPropertyAssignment(prop) + ) { + return; + } + + const propName = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : ''; + + if (propName === 'defaultOptions') { + reg.hasDefaultOptions = true; + // Check if defaultOptions references a grid_core module + if (ts.isPropertyAssignment(prop)) { + const initText = getNodeText(prop.initializer, sf); + for (const [name, imp] of parsedFile.imports) { + if (imp.isFromGridCore && initText.includes(name)) { + reg.referencesGridCoreModule = true; + if (!reg.gridCoreRefs.includes(name)) reg.gridCoreRefs.push(name); + } + } + } + } + + if (propName === 'controllers' && ts.isPropertyAssignment(prop)) { + if (ts.isObjectLiteralExpression(prop.initializer)) { + reg.hasInlineControllers = true; + reg.controllers = parseInlineControllerViews(prop.initializer, sf, parsedFile); + // Check if inline controllers reference grid_core + for (const ref of Object.values(reg.controllers)) { + if (ref.isImportedFromGridCore) { + reg.referencesGridCoreModule = true; + if (!reg.gridCoreRefs.includes(ref.className)) reg.gridCoreRefs.push(ref.className); + } + } + } else { + // controllers: someModule.controllers — forwarded reference + const refText = getNodeText(prop.initializer, sf); + const baseIdent = refText.split('.')[0]; + const imp = parsedFile.imports.get(baseIdent); + if (imp?.isFromGridCore) { + reg.referencesGridCoreModule = true; + if (!reg.gridCoreRefs.includes(baseIdent)) reg.gridCoreRefs.push(baseIdent); + } + } + } + + if (propName === 'views' && ts.isPropertyAssignment(prop)) { + if (ts.isObjectLiteralExpression(prop.initializer)) { + reg.hasInlineViews = true; + reg.views = parseInlineControllerViews(prop.initializer, sf, parsedFile); + for (const ref of Object.values(reg.views)) { + if (ref.isImportedFromGridCore) { + reg.referencesGridCoreModule = true; + if (!reg.gridCoreRefs.includes(ref.className)) reg.gridCoreRefs.push(ref.className); + } + } + } else { + const refText = getNodeText(prop.initializer, sf); + const baseIdent = refText.split('.')[0]; + const imp = parsedFile.imports.get(baseIdent); + if (imp?.isFromGridCore) { + reg.referencesGridCoreModule = true; + if (!reg.gridCoreRefs.includes(baseIdent)) reg.gridCoreRefs.push(baseIdent); + } + } + } + + if ( + propName === 'extenders' + && ts.isPropertyAssignment(prop) + && ts.isObjectLiteralExpression(prop.initializer) + ) { + reg.hasInlineExtenders = true; + reg.extenders = parseInlineExtenders(prop.initializer, sf, parsedFile); + // Check if extenders reference grid_core + const allExts = [ + ...Object.values(reg.extenders.controllers), + ...Object.values(reg.extenders.views), + ]; + for (const ext of allExts) { + if (ext.isImportedFromGridCore) { + reg.referencesGridCoreModule = true; + if (!reg.gridCoreRefs.includes(ext.extenderName)) { + reg.gridCoreRefs.push(ext.extenderName); + } + } + } + } + }; + + for (const prop of moduleArg.properties) { + processModuleProp(prop); + } + } + + return reg; +} + +// ─── DataSourceAdapter Extension Detection ─────────────────────────────────── + +function isDataSourceAdapterExtendCall( + node: ts.Node, + sf: ts.SourceFile, +): node is ts.CallExpression { + if (!ts.isCallExpression(node)) return false; + const expr = node.expression; + if (!ts.isPropertyAccessExpression(expr)) return false; + if (expr.name.text !== 'extend') return false; + const receiverText = getNodeText(expr.expression, sf); + return receiverText === DATA_SOURCE_ADAPTER_PROVIDER; +} + +// ─── Main Parse Function ───────────────────────────────────────────────────── + +export function parseDataGridFile(filePath: string): DataGridParsedFile { + const content = fs.readFileSync(filePath, 'utf-8'); + const sf = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + + const relPath = getRelativePath(filePath); + const result: DataGridParsedFile = { + filePath, + relPath, + registerModuleCalls: [], + dataSourceAdapterExtensions: [], + classes: new Map(), + imports: new Map(), + localVars: new Map(), + }; + + // 1. Collect imports + ts.forEachChild(sf, (node) => { + if (!ts.isImportDeclaration(node)) return; + if ( + !node.importClause + || !node.moduleSpecifier + || !ts.isStringLiteral(node.moduleSpecifier) + ) { + return; + } + + const fromPath = node.moduleSpecifier.text; + const fromGridCore = isGridCoreImport(fromPath); + const { namedBindings } = node.importClause; + + if (namedBindings && ts.isNamedImports(namedBindings)) { + for (const spec of namedBindings.elements) { + const localName = spec.name.text; + const originalName = spec.propertyName ? spec.propertyName.text : localName; + result.imports.set(localName, { + localName, + originalName, + fromPath, + isFromGridCore: fromGridCore, + }); + } + } + + if (node.importClause.name) { + const localName = node.importClause.name.text; + result.imports.set(localName, { + localName, + originalName: localName, + fromPath, + isFromGridCore: fromGridCore, + }); + } + }); + + // 2. Collect local variables + ts.forEachChild(sf, (node) => { + if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + if (ts.isIdentifier(decl.name) && decl.initializer) { + result.localVars.set(decl.name.text, getNodeText(decl.initializer, sf)); + } + } + } + }); + + // 3. Collect class declarations + ts.forEachChild(sf, (node) => { + if (ts.isClassDeclaration(node) && node.name) { + const className = node.name.text; + const heritage = getClassHeritage(node, sf, result.localVars); + result.classes.set(className, { + className, + baseClass: heritage.baseClass, + mixins: heritage.mixins, + sourceFile: relPath, + isExported: hasExportModifier(node), + isDefinedInDataGrid: true, + }); + } + + // Also check for class expressions in variable declarations: + // export const X = () => class extends Y {} + if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + if (ts.isIdentifier(decl.name) && decl.initializer) { + // Arrow function returning a class expression (mixin/extender pattern) + if (ts.isArrowFunction(decl.initializer) && ts.isClassExpression(decl.initializer.body)) { + const classExpr = decl.initializer.body; + const heritage = getClassHeritage(classExpr, sf, result.localVars); + const className = classExpr.name?.text ?? decl.name.text; + result.classes.set(decl.name.text, { + className, + baseClass: heritage.baseClass, + mixins: heritage.mixins, + sourceFile: relPath, + isExported: hasExportModifier(node), + isDefinedInDataGrid: true, + }); + } + } + } + } + }); + + // 4. Collect registerModule calls and DataSourceAdapter extensions + let dsaOrder = 0; + function visitStatements(node: ts.Node): void { + if (isRegisterModuleCall(node, sf)) { + const reg = parseRegisterModuleCall(node, sf, result); + if (reg) { + result.registerModuleCalls.push(reg); + } + } + + if (isDataSourceAdapterExtendCall(node, sf)) { + const arg = node.arguments[0]; + if (arg) { + const extenderName = getNodeText(arg, sf); + const importInfo = result.imports.get(extenderName); + const order = dsaOrder; + dsaOrder += 1; + result.dataSourceAdapterExtensions.push({ + sourceFile: filePath, + relPath, + extenderName, + isImportedFromGridCore: importInfo?.isFromGridCore ?? false, + order, + }); + } + } + + ts.forEachChild(node, visitStatements); + } + + ts.forEachChild(sf, visitStatements); + + return result; +} + +// ─── Module Order Parsing ──────────────────────────────────────────────────── + +export function parseModulesOrder(): string[] { + const content = fs.readFileSync(WIDGET_BASE_FILE, 'utf-8'); + const sf = ts.createSourceFile( + WIDGET_BASE_FILE, + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + + const order: string[] = []; + + function visit(node: ts.Node): void { + if ( + ts.isCallExpression(node) + && ts.isPropertyAccessExpression(node.expression) + && node.expression.name.text === 'registerModulesOrder' + && node.arguments.length === 1 + && ts.isArrayLiteralExpression(node.arguments[0]) + ) { + for (const elem of node.arguments[0].elements) { + if (ts.isStringLiteral(elem)) { + order.push(elem.text); + } + } + } + ts.forEachChild(node, visit); + } + + ts.forEachChild(sf, visit); + return order; +} + +// ─── Grid Core Module Parsing ──────────────────────────────────────────────── + +const MODULE_SUFFIX = 'Module'; + +function guessRegisteredName(moduleName: string): string | null { + if (moduleName.endsWith(MODULE_SUFFIX)) { + return moduleName.slice(0, -MODULE_SUFFIX.length); + } + return null; +} + +/** + * Parse all grid_core source files and extract module definitions. + * Returns a list of GridCoreModuleInfo — one per exported `*Module` constant. + */ +export function parseGridCoreModules(gridCoreRoot: string): GridCoreModuleInfo[] { + const sourceFiles = discoverGridCoreFiles(gridCoreRoot); + const results: GridCoreModuleInfo[] = []; + + // Derive featureArea from a path relative to gridCoreRoot + const CORE_DIR_FEATURE_MAP: Record = { + data_controller: 'Data', + views: 'Core', + editor_factory: 'Core', + error_handling: 'Core', + editing: 'Editing', + validating: 'Editing', + selection: 'Selection', + filter: 'Filtering', + header_filter: 'Filtering', + search: 'Filtering', + keyboard_navigation: 'Navigation', + focus: 'Navigation', + columns_controller: 'Columns Core', + column_headers: 'Columns Core', + header_panel: 'Columns Core', + column_chooser: 'Column Management', + column_fixing: 'Column Management', + sticky_columns: 'Column Management', + virtual_columns: 'Column Management', + columns_resizing_reordering: 'Column Management', + adaptivity: 'Column Management', + virtual_scrolling: 'Scrolling', + ai_column: 'AI', + ai_prompt_editor: 'AI', + }; + + function featureAreaFromFile(filePath: string): string { + const rel = path.relative(gridCoreRoot, filePath).replace(/\\/g, '/'); + const firstSegment = rel.split('/')[0]; + return CORE_DIR_FEATURE_MAP[firstSegment] ?? 'Other'; + } + + function relPathFromFile(filePath: string): string { + return path.relative(gridCoreRoot, filePath).replace(/\\/g, '/'); + } + + for (const file of sourceFiles) { + try { + const parsed = parseGridCoreFile(file); + const relPath = relPathFromFile(file); + const area = featureAreaFromFile(file); + + for (const mod of parsed.modules) { + const controllers: Record = {}; + for (const [regName, ctrl] of Object.entries(mod.controllers)) { + controllers[regName] = { + regName, + className: ctrl.className, + baseClass: ctrl.baseClass, + mixins: ctrl.mixins, + sourceFile: ctrl.sourceFile, + }; + } + + const views: Record = {}; + for (const [regName, view] of Object.entries(mod.views)) { + views[regName] = { + regName, + className: view.className, + baseClass: view.baseClass, + mixins: view.mixins, + sourceFile: view.sourceFile, + }; + } + + results.push({ + moduleName: mod.moduleName, + registeredAs: guessRegisteredName(mod.moduleName), + sourceFile: relPath, + featureArea: area, + controllers, + views, + extenders: mod.extenders, + hasDefaultOptions: mod.hasDefaultOptions, + }); + } + } catch { + // Skip files that fail to parse + } + } + + return results.sort((a, b) => a.moduleName.localeCompare(b.moduleName)); +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts new file mode 100644 index 000000000000..1e8c07548d3e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts @@ -0,0 +1,366 @@ +/* eslint-disable spellcheck/spell-checker,no-restricted-syntax,max-depth */ +import type { ModificationCategory } from './constants'; +import { getFeatureAreaFromPath } from './constants'; +import type { + ClassifiedModule, + DataGridParsedFile, + DataSourceAdapterExtension, + ExtenderPipeline, + ExtenderPipelineStep, + InheritanceEntry, + RegisterModuleCall, +} from './types'; + +// ─── Module Classification ─────────────────────────────────────────────────── + +function hasLocallyDefinedExtenders( + reg: RegisterModuleCall, + parsedFile: DataGridParsedFile, +): boolean { + const allExtenders = [ + ...Object.values(reg.extenders.controllers), + ...Object.values(reg.extenders.views), + ]; + + return allExtenders.some((ext) => { + if (ext.isDefinedLocally) return true; + if (ext.isImportedFromGridCore) return false; + + // Local variable (arrow-function extender) or local class + return parsedFile.localVars.has(ext.extenderName) + || parsedFile.classes.has(ext.extenderName); + }); +} + +function classifyModule( + reg: RegisterModuleCall, + parsedFile: DataGridParsedFile, +): ModificationCategory { + // If the module doesn't reference any grid_core module at all → new + if (!reg.referencesGridCoreModule) { + return 'new'; + } + + // Pattern 1: direct identifier reference → passthrough + // e.g. gridCore.registerModule('sorting', sortingModule) + if (reg.argIsIdentifier) { + return 'passthrough'; + } + + // Pattern 2: object literal with spread of grid_core module + const hasGridCoreSpreads = reg.spreadSources.some((src) => { + const importInfo = parsedFile.imports.get(src); + return importInfo?.isFromGridCore ?? false; + }); + + if (hasGridCoreSpreads) { + // Spread + locally defined extenders → extended + if (reg.hasInlineExtenders && hasLocallyDefinedExtenders(reg, parsedFile)) { + return 'extended'; + } + // Spread but no local modifications → passthrough + return 'passthrough'; + } + + // Pattern 3: object literal with inline controllers/views + if (reg.hasInlineControllers || reg.hasInlineViews) { + const hasLocalControllers = Object.values(reg.controllers).some((c) => c.isDefinedLocally); + const hasLocalViews = Object.values(reg.views).some((v) => v.isDefinedLocally); + + if (hasLocalControllers || hasLocalViews) { + return 'replaced'; + } + + // All controllers/views are from grid_core (reassembled module) + if (reg.hasInlineExtenders && hasLocallyDefinedExtenders(reg, parsedFile)) { + return 'extended'; + } + return 'passthrough'; + } + + // Pattern 4: only defaultOptions override with forwarded controllers → replaced + if (reg.hasDefaultOptions) { + return 'replaced'; + } + + // Pattern 5: extenders modifying grid_core module → extended + if (reg.hasInlineExtenders && hasLocallyDefinedExtenders(reg, parsedFile)) { + return 'extended'; + } + + return 'passthrough'; +} + +// ─── Module Details ────────────────────────────────────────────────────────── + +function buildDetails( + category: ModificationCategory, + reg: RegisterModuleCall, +): string { + switch (category) { + case 'passthrough': + return 'Re-registers grid_core module as-is'; + + case 'replaced': { + const replaced: string[] = []; + for (const [name, ref] of Object.entries(reg.controllers)) { + if (ref.isDefinedLocally) { + replaced.push(`controller '${name}' → ${ref.className} extends ${ref.baseClass}`); + } + } + for (const [name, ref] of Object.entries(reg.views)) { + if (ref.isDefinedLocally) { + replaced.push(`view '${name}' → ${ref.className} extends ${ref.baseClass}`); + } + } + if (reg.hasDefaultOptions) { + replaced.push('defaultOptions overridden'); + } + return replaced.join('; ') || 'Controller/view replacement'; + } + + case 'extended': { + const exts: string[] = []; + for (const [target, ext] of Object.entries(reg.extenders.controllers)) { + if (!ext.isImportedFromGridCore) { + exts.push(`extends controller '${target}' via ${ext.extenderName}`); + } + } + for (const [target, ext] of Object.entries(reg.extenders.views)) { + if (!ext.isImportedFromGridCore) { + exts.push(`extends view '${target}' via ${ext.extenderName}`); + } + } + return exts.join('; ') || 'Extends grid_core module with new extenders'; + } + + case 'new': { + const parts: string[] = []; + for (const [name, ref] of Object.entries(reg.controllers)) { + parts.push(`new controller '${name}': ${ref.className}`); + } + for (const [name, ref] of Object.entries(reg.views)) { + parts.push(`new view '${name}': ${ref.className}`); + } + for (const [target] of Object.entries(reg.extenders.controllers)) { + parts.push(`extends controller '${target}'`); + } + for (const [target] of Object.entries(reg.extenders.views)) { + parts.push(`extends view '${target}'`); + } + return parts.join('; ') || 'New data_grid module'; + } + + default: + return ''; + } +} + +// ─── Inheritance Chain Building ────────────────────────────────────────────── + +function buildInheritanceChain( + className: string, + allClasses: Map, + visited: Set, +): string[] { + if (visited.has(className)) return []; + visited.add(className); + + const info = allClasses.get(className); + if (!info || !info.baseClass) return []; + + const chain: string[] = []; + + for (const mixin of info.mixins) { + chain.push(`[mixin] ${mixin}`); + } + + let rawBase = info.baseClass; + const mixinMatch = /^\w+\((.+)\)$/.exec(rawBase); + if (mixinMatch) { + const [, inner] = mixinMatch; + rawBase = inner; + } + + chain.push(rawBase); + + if (allClasses.has(rawBase)) { + chain.push(...buildInheritanceChain(rawBase, allClasses, visited)); + } + + return chain; +} + +// ─── Public API ────────────────────────────────────────────────────────────── + +export function classifyModules( + parsedFiles: DataGridParsedFile[], + modulesOrder: string[], +): ClassifiedModule[] { + const results: ClassifiedModule[] = []; + + for (const pf of parsedFiles) { + for (const reg of pf.registerModuleCalls) { + const category = classifyModule(reg, pf); + const orderIndex = modulesOrder.indexOf(reg.moduleName); + + const newControllers = Object.entries(reg.controllers) + .filter(([, ref]) => ref.isDefinedLocally && !ref.isImportedFromGridCore) + .map(([name]) => name); + + const newViews = Object.entries(reg.views) + .filter(([, ref]) => ref.isDefinedLocally && !ref.isImportedFromGridCore) + .map(([name]) => name); + + const overriddenControllers = Object.entries(reg.controllers) + .filter(([, ref]) => ref.isDefinedLocally && ref.baseClass) + .map(([name]) => name); + + const overriddenExtenderControllers = Object.entries(reg.extenders.controllers) + .filter(([, ext]) => !ext.isImportedFromGridCore && (ext.isDefinedLocally || true)) + .map(([name]) => name); + + const overriddenExtenderViews = Object.entries(reg.extenders.views) + .filter(([, ext]) => !ext.isImportedFromGridCore && (ext.isDefinedLocally || true)) + .map(([name]) => name); + + // Determine grid_core source module + let gridCoreSourceModule: string | null = null; + for (const ref of reg.gridCoreRefs) { + const imp = pf.imports.get(ref); + if (imp?.isFromGridCore) { + gridCoreSourceModule = imp.fromPath; + break; + } + } + + results.push({ + moduleName: reg.moduleName, + category, + sourceFile: pf.filePath, + relPath: reg.relPath, + featureArea: getFeatureAreaFromPath(reg.relPath), + registrationOrder: orderIndex >= 0 ? orderIndex : 999, + + gridCoreModuleName: reg.argIsIdentifier ? reg.argIdentifierName : null, + gridCoreSourceModule, + + controllers: reg.controllers, + views: reg.views, + extenders: reg.extenders, + + newControllers, + newViews, + overriddenControllers, + overriddenExtenderControllers, + overriddenExtenderViews, + hasDefaultOptionsOverride: reg.hasDefaultOptions && category !== 'passthrough', + + details: buildDetails(category, reg), + }); + } + } + + results.sort((a, b) => a.registrationOrder - b.registrationOrder); + return results; +} + +export function collectDataSourceAdapterChain( + parsedFiles: DataGridParsedFile[], +): DataSourceAdapterExtension[] { + const allExtensions: DataSourceAdapterExtension[] = []; + for (const pf of parsedFiles) { + allExtensions.push(...pf.dataSourceAdapterExtensions); + } + // Assign global order based on collection order + allExtensions.forEach((ext, i) => { ext.order = i; }); + return allExtensions; +} + +export function buildExtenderPipelines( + modules: ClassifiedModule[], +): ExtenderPipeline[] { + const controllerSteps = new Map(); + const viewSteps = new Map(); + + for (const mod of modules) { + for (const [targetName, ext] of Object.entries(mod.extenders.controllers)) { + const step: ExtenderPipelineStep = { + moduleName: mod.moduleName, + relPath: mod.relPath, + extenderName: ext.extenderName, + isFromGridCore: ext.isImportedFromGridCore, + registrationOrder: mod.registrationOrder, + }; + + const existing = controllerSteps.get(targetName); + if (existing) { + existing.push(step); + } else { + controllerSteps.set(targetName, [step]); + } + } + for (const [targetName, ext] of Object.entries(mod.extenders.views)) { + const step: ExtenderPipelineStep = { + moduleName: mod.moduleName, + relPath: mod.relPath, + extenderName: ext.extenderName, + isFromGridCore: ext.isImportedFromGridCore, + registrationOrder: mod.registrationOrder, + }; + + const existing = viewSteps.get(targetName); + if (existing) { + existing.push(step); + } else { + viewSteps.set(targetName, [step]); + } + } + } + + const pipelines: ExtenderPipeline[] = []; + + for (const [targetName, steps] of controllerSteps) { + steps.sort((a, b) => a.registrationOrder - b.registrationOrder); + pipelines.push({ targetName, targetType: 'controller', steps }); + } + for (const [targetName, steps] of viewSteps) { + steps.sort((a, b) => a.registrationOrder - b.registrationOrder); + pipelines.push({ targetName, targetType: 'view', steps }); + } + + return pipelines.sort((a, b) => a.targetName.localeCompare(b.targetName)); +} + +export function buildInheritanceChains( + parsedFiles: DataGridParsedFile[], +): InheritanceEntry[] { + // Build a unified class map from all parsed data_grid files + const allClasses = new Map(); + for (const pf of parsedFiles) { + for (const [name, info] of pf.classes) { + allClasses.set(name, { + baseClass: info.baseClass, + mixins: info.mixins, + sourceFile: info.sourceFile, + }); + } + } + + const entries: InheritanceEntry[] = []; + for (const [className, info] of allClasses) { + if (info.baseClass) { + const visited = new Set(); + const chain = buildInheritanceChain(className, allClasses, visited); + if (chain.length > 0) { + entries.push({ + className, + chain, + sourceFile: info.sourceFile, + }); + } + } + } + + return entries.sort((a, b) => a.className.localeCompare(b.className)); +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts new file mode 100644 index 000000000000..f80a5b31e679 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts @@ -0,0 +1,168 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { ModificationCategory } from './constants'; + +export interface ImportInfo { + localName: string; + originalName: string; + fromPath: string; + isFromGridCore: boolean; +} + +export interface DataGridClassInfo { + className: string; + baseClass: string; + mixins: string[]; + sourceFile: string; + isExported: boolean; + isDefinedInDataGrid: boolean; +} + +export interface RegisterModuleCall { + moduleName: string; + sourceFile: string; + relPath: string; + argIsIdentifier: boolean; + argIdentifierName: string | null; + spreadSources: string[]; + hasInlineControllers: boolean; + hasInlineViews: boolean; + hasInlineExtenders: boolean; + hasDefaultOptions: boolean; + referencesGridCoreModule: boolean; + gridCoreRefs: string[]; + controllers: Record; + views: Record; + extenders: { + controllers: Record; + views: Record; + }; +} + +export interface DataGridClassRef { + regName: string; + className: string; + isImportedFromGridCore: boolean; + isDefinedLocally: boolean; + baseClass: string; + mixins: string[]; + sourceFile: string; +} + +export interface ExtenderRef { + targetName: string; + extenderName: string; + isImportedFromGridCore: boolean; + isDefinedLocally: boolean; +} + +export interface DataSourceAdapterExtension { + sourceFile: string; + relPath: string; + extenderName: string; + isImportedFromGridCore: boolean; + order: number; +} + +export interface DataGridParsedFile { + filePath: string; + relPath: string; + registerModuleCalls: RegisterModuleCall[]; + dataSourceAdapterExtensions: DataSourceAdapterExtension[]; + classes: Map; + imports: Map; + localVars: Map; +} + +export interface ClassifiedModule { + moduleName: string; + category: ModificationCategory; + sourceFile: string; + relPath: string; + featureArea: string; + registrationOrder: number; + + gridCoreModuleName: string | null; + gridCoreSourceModule: string | null; + + controllers: Record; + views: Record; + extenders: { + controllers: Record; + views: Record; + }; + + newControllers: string[]; + newViews: string[]; + overriddenControllers: string[]; + overriddenExtenderControllers: string[]; + overriddenExtenderViews: string[]; + hasDefaultOptionsOverride: boolean; + + details: string; +} + +export interface InheritanceEntry { + className: string; + chain: string[]; + sourceFile: string; +} + +export interface ExtenderPipelineStep { + moduleName: string; + relPath: string; + extenderName: string; + isFromGridCore: boolean; + registrationOrder: number; +} + +export interface ExtenderPipeline { + targetName: string; + targetType: 'controller' | 'view'; + steps: ExtenderPipelineStep[]; +} + +export interface GridCoreControllerOrView { + regName: string; + className: string; + baseClass: string; + mixins: string[]; + sourceFile: string; +} + +export interface GridCoreExtenderInfo { + extenderName: string; + pattern: 'mixin-function' | 'object'; +} + +export interface GridCoreModuleInfo { + moduleName: string; + registeredAs: string | null; + sourceFile: string; + featureArea: string; + controllers: Record; + views: Record; + extenders: { + controllers: Record; + views: Record; + }; + hasDefaultOptions: boolean; +} + +export interface DataGridArchitectureData { + generatedAt: string; + dataGridRoot: string; + gridCoreRoot: string; + modulesOrder: string[]; + modules: ClassifiedModule[]; + gridCoreModules: GridCoreModuleInfo[]; + extenderPipelines: ExtenderPipeline[]; + dataSourceAdapterChain: DataSourceAdapterExtension[]; + inheritanceChains: InheritanceEntry[]; + summary: { + total: number; + passthrough: number; + extended: number; + replaced: number; + new: number; + }; +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/cli.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/cli.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/cli.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/cli.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/constants.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/constants.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/constants.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/constants.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/generate-architecture-doc.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate-architecture-doc.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/generate-architecture-doc.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate-architecture-doc.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/graph-builder.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/graph-builder.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/graph-builder.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/graph-builder.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/html-template.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/html-template.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/html-template.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/html-template.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/parser.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/parser.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/parser.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/parser.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/resolver.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/resolver.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/resolver.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/resolver.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/types.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/types.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/types.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/types.ts From 21f1503a7a21f3a065e8ab89b7df2aabeeac878e Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Wed, 11 Mar 2026 13:02:43 +0400 Subject: [PATCH 02/10] add mixins and cross-deps --- .../__docs__/scripts/data_grid/generate.ts | 14 +- .../scripts/data_grid/graph-builder.ts | 35 ++++ .../scripts/data_grid/html-template.ts | 39 ++++- .../__docs__/scripts/data_grid/resolver.ts | 153 +++++++++++++++++- .../grids/__docs__/scripts/data_grid/types.ts | 11 ++ 5 files changed, 246 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts index af7b95969300..21bdc87603e1 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts @@ -13,6 +13,7 @@ import { parseModulesOrder, } from './parser'; import { + buildCrossDependencies, buildExtenderPipelines, buildInheritanceChains, classifyModules, @@ -136,7 +137,15 @@ function main(): void { const inheritanceChains = buildInheritanceChains(allParsedFiles); console.log(`\nBuilt ${inheritanceChains.length} inheritance chains`); - // 10. Build output data + // 10. Build cross-dependencies between data_grid modules + const crossDependencies = buildCrossDependencies(allParsedFiles, allModules); + console.log(`\nFound ${crossDependencies.length} cross-dependencies:`); + for (const dep of crossDependencies) { + const toLabel = dep.toModule ?? dep.toRelPath; + console.log(` ${dep.fromModule} → ${toLabel} [${dep.importedNames.join(', ')}]`); + } + + // 11. Build output data const data: DataGridArchitectureData = { generatedAt: new Date().toISOString(), dataGridRoot: 'packages/devextreme/js/__internal/grids/data_grid', @@ -147,13 +156,14 @@ function main(): void { extenderPipelines, dataSourceAdapterChain: dsaChain, inheritanceChains, + crossDependencies, summary: { total: allModules.length, ...counts, }, }; - // 11. Write output files + // 12. Write output files if (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts index 2975b1d49e12..10af8a49b389 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts @@ -207,5 +207,40 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap ); } + // ─── Cross-dependency edges ─────────────────────────────────────────────── + for (const dep of data.crossDependencies) { + const sourceId = `mod-${dep.fromModule}`; + + let targetId: string | null = null; + if (dep.toModule) { + targetId = `mod-${dep.toModule}`; + } else { + // Non-module file (shared mixin, utility, etc.) — create a utility node + const utilId = `util-${dep.toRelPath}`; + const fileName = dep.toRelPath.split('/').pop() ?? dep.toRelPath; + const shortName = fileName.replace(/\.ts$/, '').replace(/^m_/, ''); + + addNode(utilId, { + label: shortName, + nodeType: 'utility', + sourceFile: dep.toRelPath, + featureArea: 'Shared', + details: `Shared file: ${dep.toRelPath}`, + moduleName: shortName, + }, 'module utility'); + targetId = utilId; + } + + if (targetId && nodeIds.has(sourceId) && nodeIds.has(targetId)) { + addEdge(sourceId, targetId, { + edgeType: 'cross-dep', + label: dep.importedNames.join(', '), + targetName: dep.importedNames.join(', '), + importPath: dep.importPath, + toRelPath: dep.toRelPath, + }, 'edge-cross-dep'); + } + } + return elements; } diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts index b9739cd8df1e..75d8fe2a70f3 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts @@ -130,7 +130,8 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- - + +
@@ -171,11 +172,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
Extended
New
Grid Core Source
+
Shared Mixin / Utility

Legend — Edges

Registration Order
Grid Core → DataGrid
Controller Chain
-
View Chain
+
DSA Chain
+
Cross-dependency
DataSourceAdapter
@@ -192,6 +195,7 @@ var PIPELINES = ${pipelinesJson}; var DSA = ${dsaJson}; var MODULES = ${modulesJson}; var GC_MODULES = ${gridCoreModulesJson}; +var CROSS_DEPS = ${JSON.stringify(data.crossDependencies)}; var cy = cytoscape({ container: document.getElementById('cy'), @@ -221,6 +225,10 @@ var cy = cytoscape({ 'background-color': '#1a1a2e', 'border-width': 2, 'border-style': 'dashed', 'border-color': '#f59e0b', 'color': '#f5c040', 'background-opacity': 0.5, 'shape': 'barrel', }}, + { selector: 'node.module.utility', style: { + 'background-color': '#0a2a1a', 'border-width': 2, 'border-style': 'dashed', 'border-color': '#10b981', 'color': '#6ee7b7', + 'shape': 'diamond', + }}, { selector: 'edge.edge-gc-source', style: { 'line-color': '#f59e0b', 'target-arrow-color': '#f59e0b', 'target-arrow-shape': 'triangle', @@ -261,6 +269,16 @@ var cy = cytoscape({ 'text-rotation': 'none', }}, + { selector: 'edge.edge-cross-dep', style: { + 'line-color': '#10b981', 'target-arrow-color': '#10b981', 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', 'width': 2, 'arrow-scale': .7, + 'line-style': 'dashed', 'opacity': .6, + 'label': 'data(label)', 'font-size': 8, 'color': '#6ee7b7', + 'text-background-color': '#0a2a1a', 'text-background-opacity': .9, + 'text-background-padding': '3px', 'text-background-shape': 'round-rectangle', + 'text-rotation': 'none', + }}, + { selector: '.highlighted', style: { 'opacity': 1, 'z-index': 999 }}, { selector: 'edge.highlighted', style: { 'opacity': 1, 'z-index': 999, 'width': 4 }}, { selector: '.faded', style: { 'opacity': .05 }}, @@ -417,6 +435,17 @@ function showInfo(t) { } } } catch(e) { /* ignore parse errors */ } + } else if (t.isNode() && d.nodeType === 'utility') { + h = '

' + d.label + ' SHARED

'; + h += '

Source: ' + d.sourceFile + '

'; + h += '

This file does not register a module. It provides shared code (mixin, utility, base class) used by other modules.

'; + var crossDepsTo = CROSS_DEPS.filter(function(cd) { return cd.toRelPath === d.sourceFile; }); + if (crossDepsTo.length > 0) { + h += '

Used by:

'; + for (var ui = 0; ui < crossDepsTo.length; ui++) { + h += '

' + crossDepsTo[ui].fromModule + ' imports ' + crossDepsTo[ui].importedNames.join(', ') + '

'; + } + } } else if (t.isNode() && d.nodeType === 'module') { h = '

#' + (d.registrationOrder + 1) + ' ' + d.moduleName + ' ' + tagFor(d.category) + '

'; h += '

Source: ' + d.sourceFile + '

'; @@ -482,6 +511,12 @@ function showInfo(t) { if (et === 'grid-core-source') { h += '

The data_grid module ' + (d.target || '').replace('mod-', '').replace('gc-', '') + ' is derived from the grid_core module ' + (d.source || '').replace('gc-', '').replace('mod-', '') + '.

'; } + if (et === 'cross-dep') { + h += '

Module ' + (d.source || '').replace('mod-', '') + ' imports ' + (d.label || '') + ' from ' + (d.toRelPath || (d.target || '').replace('mod-', '')) + ''; + if (d.importPath) h += ' (import path: ' + d.importPath + ')'; + h += '

'; + h += '

This is a direct code dependency between data_grid modules — the source file imports a class, mixin, or utility from the target.

'; + } } infoP.innerHTML = h; } diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts index 1e8c07548d3e..1ab67cb4a9a4 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts @@ -1,8 +1,9 @@ -/* eslint-disable spellcheck/spell-checker,no-restricted-syntax,max-depth */ +/* eslint-disable spellcheck/spell-checker,max-depth */ import type { ModificationCategory } from './constants'; import { getFeatureAreaFromPath } from './constants'; import type { ClassifiedModule, + CrossDependency, DataGridParsedFile, DataSourceAdapterExtension, ExtenderPipeline, @@ -167,7 +168,7 @@ function buildInheritanceChain( visited.add(className); const info = allClasses.get(className); - if (!info || !info.baseClass) return []; + if (!info?.baseClass) return []; const chain: string[] = []; @@ -364,3 +365,151 @@ export function buildInheritanceChains( return entries.sort((a, b) => a.className.localeCompare(b.className)); } + +// ─── Cross-Dependency Detection ────────────────────────────────────────────── + +function resolveImportToRelPath( + fromRelPath: string, + importPath: string, +): string | null { + const fromDir = fromRelPath.split('/').slice(0, -1).join('/'); + const segments = importPath.split('/'); + const resolved: string[] = fromDir ? fromDir.split('/') : []; + + for (const seg of segments) { + if (seg === '.') { + // current dir + } else if (seg === '..') { + resolved.pop(); + } else { + resolved.push(seg); + } + } + + return resolved.join('/') || null; +} + +function findTargetFile( + resolvedPath: string, + relPathToFile: Map, +): string | null { + // Try exact match + if (relPathToFile.has(resolvedPath)) return resolvedPath; + + // Try with .ts extension + const withTs = `${resolvedPath}.ts`; + if (relPathToFile.has(withTs)) return withTs; + + // Try index file + const asIndex = `${resolvedPath}/index.ts`; + if (relPathToFile.has(asIndex)) return asIndex; + + return null; +} + +/** + * Detect import dependencies between data_grid files. + * Returns edges where one data_grid module file imports from another data_grid file. + * This captures patterns like: + * - Shared mixins (e.g. ColumnKeyboardNavigationMixin used by multiple modules) + * - Direct class imports between modules (e.g. importing a base controller) + * - Utility imports shared across modules + */ +export function buildCrossDependencies( + parsedFiles: DataGridParsedFile[], + modules: ClassifiedModule[], +): CrossDependency[] { + // Build map: relPath → moduleName (for files that register modules) + const relPathToModule = new Map(); + for (const mod of modules) { + relPathToModule.set(mod.relPath, mod.moduleName); + } + + // Build map: relPath → file data + const relPathToFile = new Map(); + for (const pf of parsedFiles) { + relPathToFile.set(pf.relPath, pf); + } + + const deps: CrossDependency[] = []; + const seen = new Set(); + + for (const pf of parsedFiles) { + // Find which module(s) this file belongs to + const fromModule = relPathToModule.get(pf.relPath); + if (!fromModule) { + // eslint-disable-next-line no-continue + continue; + } + + for (const [localName, imp] of pf.imports) { + // Skip grid_core imports — those are already handled + if (imp.isFromGridCore) { + // eslint-disable-next-line no-continue + continue; + } + + // Only track imports from other data_grid files (relative paths) + if (!imp.fromPath.startsWith('.') && !imp.fromPath.startsWith('..')) { + // eslint-disable-next-line no-continue + continue; + } + + // Resolve the import to a data_grid relPath + const resolvedTarget = resolveImportToRelPath(pf.relPath, imp.fromPath); + if (!resolvedTarget) { + // eslint-disable-next-line no-continue + continue; + } + + // Find the target file + const targetFile = findTargetFile(resolvedTarget, relPathToFile); + if (!targetFile) { + // eslint-disable-next-line no-continue + continue; + } + + // Determine the target module + const toModule = relPathToModule.get(targetFile) ?? null; + + // Don't include self-imports or imports to the same module + if (toModule === fromModule) { + // eslint-disable-next-line no-continue + continue; + } + + // Skip m_core imports (boring internal wiring) + if (resolvedTarget.includes('m_core')) { + // eslint-disable-next-line no-continue + continue; + } + + const key = `${fromModule}→${targetFile}`; + if (seen.has(key)) { + // Merge importedNames + const existing = deps.find( + (d) => d.fromModule === fromModule && d.toRelPath === targetFile, + ); + if (existing && !existing.importedNames.includes(localName)) { + existing.importedNames.push(localName); + existing.label = existing.importedNames.join(', '); + } + // eslint-disable-next-line no-continue + continue; + } + seen.add(key); + + deps.push({ + fromModule, + fromRelPath: pf.relPath, + toRelPath: targetFile, + toModule, + importedNames: [localName], + importPath: imp.fromPath, + label: localName, + }); + } + } + + return deps.sort((a, b) => a.fromModule.localeCompare(b.fromModule)); +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts index f80a5b31e679..06c90fb71a3c 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts @@ -148,6 +148,16 @@ export interface GridCoreModuleInfo { hasDefaultOptions: boolean; } +export interface CrossDependency { + fromModule: string; + fromRelPath: string; + toRelPath: string; + toModule: string | null; + importedNames: string[]; + importPath: string; + label: string; +} + export interface DataGridArchitectureData { generatedAt: string; dataGridRoot: string; @@ -158,6 +168,7 @@ export interface DataGridArchitectureData { extenderPipelines: ExtenderPipeline[]; dataSourceAdapterChain: DataSourceAdapterExtension[]; inheritanceChains: InheritanceEntry[]; + crossDependencies: CrossDependency[]; summary: { total: number; passthrough: number; From e4eaf77b6a822063363cdb04ff0c5265a9539191 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Wed, 11 Mar 2026 13:16:55 +0400 Subject: [PATCH 03/10] remove extra DSA --- .../__internal/grids/__docs__/scripts/data_grid/resolver.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts index 1ab67cb4a9a4..31eb29ceef1c 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts @@ -484,6 +484,12 @@ export function buildCrossDependencies( continue; } + // Skip m_data_source_adapter imports — already shown as DSA chain + if (resolvedTarget.includes('m_data_source_adapter')) { + // eslint-disable-next-line no-continue + continue; + } + const key = `${fromModule}→${targetFile}`; if (seen.has(key)) { // Merge importedNames From 0064969a654905c40f51bfcfe2e8b4e3c7e44646 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Wed, 11 Mar 2026 18:43:22 +0400 Subject: [PATCH 04/10] regenerated version --- .../__docs__/scripts/data_grid/constants.ts | 6 - .../__docs__/scripts/data_grid/generate.ts | 33 +- .../scripts/data_grid/graph-builder.ts | 102 +-- .../scripts/data_grid/html-template.ts | 610 ++++++-------- .../__docs__/scripts/data_grid/parser.ts | 744 ++++++++++-------- .../__docs__/scripts/data_grid/resolver.ts | 259 ++---- .../grids/__docs__/scripts/data_grid/types.ts | 76 +- 7 files changed, 785 insertions(+), 1045 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts index 2cc6cd845603..30e8476c4b9c 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts @@ -27,24 +27,18 @@ export type ModificationCategory = 'passthrough' | 'extended' | 'replaced' | 'ne const DATA_GRID_FEATURE_MAP: Record = { m_data_controller: 'Data', m_data_source_adapter: 'Data', - m_core: 'Core', m_widget: 'Core', m_widget_base: 'Core', m_utils: 'Core', - m_editing: 'Editing', - grouping: 'Grouping', summary: 'Summary', export: 'Export', - keyboard_navigation: 'Navigation', focus: 'Navigation', - m_columns_controller: 'Columns', m_aggregate_calculator: 'Data', - module_not_extended: 'Passthrough', }; diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts index 21bdc87603e1..4da432b6c91c 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts @@ -19,7 +19,7 @@ import { classifyModules, collectDataSourceAdapterChain, } from './resolver'; -import type { DataGridArchitectureData, DataGridParsedFile } from './types'; +import type { ArchitectureData, ParsedFile } from './types'; interface CliArgs { jsonOnly: boolean; @@ -52,7 +52,7 @@ function parseArgs(): CliArgs { return result; } -function appendMissingModuleNames(modulesOrder: string[], parsedFiles: DataGridParsedFile[]): void { +function appendMissingModuleNames(modulesOrder: string[], parsedFiles: ParsedFile[]): void { for (const pf of parsedFiles) { for (const reg of pf.registerModuleCalls) { if (!modulesOrder.includes(reg.moduleName)) { @@ -63,7 +63,7 @@ function appendMissingModuleNames(modulesOrder: string[], parsedFiles: DataGridP } function main(): void { - console.log('DataGrid Extensions Architecture Documentation Generator'); + console.log('DataGrid Architecture Documentation Generator'); console.log(`DataGrid root: ${DATA_GRID_ROOT}`); console.log(`Output dir: ${OUTPUT_DIR}`); @@ -72,8 +72,7 @@ function main(): void { // NOTE: registerModulesOrder defines ascending priority. // processModules (m_modules.ts) sorts by: orderIndex1 - orderIndex2, // which means index 0 processes first and the last index processes last. - // Extenders are applied in the same ascending order, so earlier modules - // are extended first, and later ones wrap on top. + // Extenders are applied in the same ascending order. const modulesOrder = parseModulesOrder(); console.log(`Parsed ${modulesOrder.length} modules from registerModulesOrder (ascending order)`); @@ -97,27 +96,20 @@ function main(): void { } }); - // 5. Discover all registered module names and build full order + // 5. Build full module order appendMissingModuleNames(modulesOrder, allParsedFiles); - // 6. Classify modules (using full order) + // 6. Classify modules const allModules = classifyModules(allParsedFiles, modulesOrder); - console.log(`\nClassified ${allModules.length} modules:`); const counts = { - passthrough: 0, - extended: 0, - replaced: 0, - new: 0, + passthrough: 0, extended: 0, replaced: 0, new: 0, }; for (const mod of allModules) { counts[mod.category] += 1; console.log(` [${mod.category.toUpperCase().padEnd(11)}] ${mod.moduleName} (${mod.relPath})`); } - console.log(`\n Passthrough: ${counts.passthrough}`); - console.log(` Replaced: ${counts.replaced}`); - console.log(` Extended: ${counts.extended}`); - console.log(` New: ${counts.new}`); + console.log(` Passthrough: ${counts.passthrough}, Replaced: ${counts.replaced}, Extended: ${counts.extended}, New: ${counts.new}`); // 7. Build extender pipelines const extenderPipelines = buildExtenderPipelines(allModules); @@ -137,7 +129,7 @@ function main(): void { const inheritanceChains = buildInheritanceChains(allParsedFiles); console.log(`\nBuilt ${inheritanceChains.length} inheritance chains`); - // 10. Build cross-dependencies between data_grid modules + // 10. Build cross-dependencies const crossDependencies = buildCrossDependencies(allParsedFiles, allModules); console.log(`\nFound ${crossDependencies.length} cross-dependencies:`); for (const dep of crossDependencies) { @@ -146,7 +138,7 @@ function main(): void { } // 11. Build output data - const data: DataGridArchitectureData = { + const data: ArchitectureData = { generatedAt: new Date().toISOString(), dataGridRoot: 'packages/devextreme/js/__internal/grids/data_grid', gridCoreRoot: 'packages/devextreme/js/__internal/grids/grid_core', @@ -157,10 +149,7 @@ function main(): void { dataSourceAdapterChain: dsaChain, inheritanceChains, crossDependencies, - summary: { - total: allModules.length, - ...counts, - }, + summary: { total: allModules.length, ...counts }, }; // 12. Write output files diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts index 10af8a49b389..82a1322b91e8 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts @@ -1,5 +1,5 @@ /* eslint-disable spellcheck/spell-checker */ -import type { DataGridArchitectureData, GridCoreModuleInfo } from './types'; +import type { ArchitectureData, GridCoreModuleInfo } from './types'; interface CytoscapeElement { group: 'nodes' | 'edges'; @@ -12,30 +12,14 @@ interface EdgeData extends Record { targetName?: string; } -/** - * Match a grid_core module to a data_grid module by comparing - * the module's registeredAs name with the data_grid module name. - */ function findGridCoreModule( dgModuleName: string, gridCoreModules: GridCoreModuleInfo[], ): GridCoreModuleInfo | undefined { - return gridCoreModules.find( - (gc) => gc.registeredAs === dgModuleName, - ); + return gridCoreModules.find((gc) => gc.registeredAs === dgModuleName); } -/** - * Builds a unified graph where: - * - Nodes = registered modules (data_grid) + grid_core source modules - * - Edges show direct extension chains between modules: - * - grid_core → data_grid source edges - * - Controller extender chains (e.g. grouping → editing for 'data' ctrl) - * - View extender chains - * - DataSourceAdapter chain - * - Registration order (subtle dotted) - */ -export function buildCytoscapeElements(data: DataGridArchitectureData): CytoscapeElement[] { +export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement[] { const elements: CytoscapeElement[] = []; const nodeIds = new Set(); const edgeIds = new Set(); @@ -46,14 +30,9 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap elements.push({ group: 'nodes', data: { id, ...nodeData }, classes }); } - function addEdge( - source: string, - target: string, - edgeData: EdgeData, - classes: string, - ): void { - const targetName = edgeData.targetName ?? ''; - const id = `e-${source}-${target}-${edgeData.edgeType}-${targetName}`; + function addEdge(source: string, target: string, edgeData: EdgeData, classes: string): void { + const tName = edgeData.targetName ?? ''; + const id = `e-${source}-${target}-${edgeData.edgeType}-${tName}`; if (!nodeIds.has(source) || !nodeIds.has(target) || edgeIds.has(id)) return; edgeIds.add(id); elements.push({ @@ -65,14 +44,11 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap }); } - // ─── Grid Core module nodes ───────────────────────────────────────────────── - // Add grid_core modules that are referenced by data_grid modules + // ─── Grid Core module nodes (barrel shape, dashed orange) ───────────────── const usedGcModules = new Set(); for (const mod of data.modules) { - const gcMod = findGridCoreModule(mod.moduleName, data.gridCoreModules); - if (gcMod) { - usedGcModules.add(gcMod.moduleName); - } + const gc = findGridCoreModule(mod.moduleName, data.gridCoreModules); + if (gc) usedGcModules.add(gc.moduleName); } for (const gcMod of data.gridCoreModules) { @@ -80,9 +56,7 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap // eslint-disable-next-line no-continue continue; } - const gcId = `gc-${gcMod.moduleName}`; - const labelParts: string[] = [gcMod.registeredAs ?? gcMod.moduleName]; const ctrls = Object.keys(gcMod.controllers); const vws = Object.keys(gcMod.views); @@ -100,8 +74,6 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap sourceFile: gcMod.sourceFile, featureArea: gcMod.featureArea, registrationOrder: -1, - details: `grid_core module: ${gcMod.sourceFile}`, - gridCoreSource: '', moduleName: gcMod.registeredAs ?? gcMod.moduleName, controllers: JSON.stringify(gcMod.controllers), views: JSON.stringify(gcMod.views), @@ -109,15 +81,12 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap }, 'module grid-core'); } - // ─── Data Grid module nodes ───────────────────────────────────────────────── + // ─── Data Grid module nodes ─────────────────────────────────────────────── for (const mod of data.modules) { const moduleId = `mod-${mod.moduleName}`; const orderNum = mod.registrationOrder + 1; - const labelParts: string[] = [`#${orderNum} ${mod.moduleName}`]; - if (mod.category !== 'passthrough') { - labelParts.push(`[${mod.category}]`); - } + if (mod.category !== 'passthrough') labelParts.push(`[${mod.category}]`); const extCtrl = mod.overriddenExtenderControllers; const extView = mod.overriddenExtenderViews; @@ -139,20 +108,18 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap }, `module ${mod.category}`); } - // ─── Grid Core → Data Grid source edges ───────────────────────────────────── + // ─── Grid Core → Data Grid source edges ─────────────────────────────────── for (const mod of data.modules) { const gcMod = findGridCoreModule(mod.moduleName, data.gridCoreModules); if (gcMod) { - const gcId = `gc-${gcMod.moduleName}`; - const dgId = `mod-${mod.moduleName}`; - addEdge(gcId, dgId, { + addEdge(`gc-${gcMod.moduleName}`, `mod-${mod.moduleName}`, { edgeType: 'grid-core-source', label: mod.category === 'passthrough' ? 'passthrough' : mod.category, }, 'edge-gc-source'); } } - // ─── Registration order spine (subtle) ───────────────────────────────────── + // ─── Registration order spine ───────────────────────────────────────────── for (let i = 0; i < data.modules.length - 1; i += 1) { addEdge( `mod-${data.modules[i].moduleName}`, @@ -162,15 +129,12 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap ); } - // ─── Extender chain edges (direct inter-module edges) ────────────────────── + // ─── Extender chain edges ───────────────────────────────────────────────── for (const pipeline of data.extenderPipelines) { const { targetName, targetType, steps } = pipeline; const edgeClass = targetType === 'controller' ? 'edge-ext-ctrl' : 'edge-ext-view'; - for (let i = 0; i < steps.length - 1; i += 1) { - const src = `mod-${steps[i].moduleName}`; - const tgt = `mod-${steps[i + 1].moduleName}`; - addEdge(src, tgt, { + addEdge(`mod-${steps[i].moduleName}`, `mod-${steps[i + 1].moduleName}`, { edgeType: 'extender-chain', targetName, targetType, @@ -181,51 +145,34 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap } } - // ─── DataSourceAdapter chain edges ───────────────────────────────────────── - const dsaModuleOrder: { moduleName: string; relPath: string; isFromGridCore: boolean }[] = []; + // ─── DataSourceAdapter chain edges ──────────────────────────────────────── + const dsaOrder: { moduleName: string }[] = []; for (const ext of data.dataSourceAdapterChain) { const mod = data.modules.find((m) => m.relPath === ext.relPath); - if (mod) { - dsaModuleOrder.push({ - moduleName: mod.moduleName, - relPath: ext.relPath, - isFromGridCore: ext.isImportedFromGridCore, - }); - } + if (mod) dsaOrder.push({ moduleName: mod.moduleName }); } - - for (let i = 0; i < dsaModuleOrder.length - 1; i += 1) { - addEdge( - `mod-${dsaModuleOrder[i].moduleName}`, - `mod-${dsaModuleOrder[i + 1].moduleName}`, - { - edgeType: 'dsa-chain', - targetName: 'DataSourceAdapter', - label: 'DSA', - }, - 'edge-dsa', - ); + for (let i = 0; i < dsaOrder.length - 1; i += 1) { + addEdge(`mod-${dsaOrder[i].moduleName}`, `mod-${dsaOrder[i + 1].moduleName}`, { + edgeType: 'dsa-chain', targetName: 'DataSourceAdapter', label: 'DSA', + }, 'edge-dsa'); } // ─── Cross-dependency edges ─────────────────────────────────────────────── for (const dep of data.crossDependencies) { const sourceId = `mod-${dep.fromModule}`; - let targetId: string | null = null; + if (dep.toModule) { targetId = `mod-${dep.toModule}`; } else { - // Non-module file (shared mixin, utility, etc.) — create a utility node const utilId = `util-${dep.toRelPath}`; const fileName = dep.toRelPath.split('/').pop() ?? dep.toRelPath; const shortName = fileName.replace(/\.ts$/, '').replace(/^m_/, ''); - addNode(utilId, { label: shortName, nodeType: 'utility', sourceFile: dep.toRelPath, featureArea: 'Shared', - details: `Shared file: ${dep.toRelPath}`, moduleName: shortName, }, 'module utility'); targetId = utilId; @@ -236,7 +183,6 @@ export function buildCytoscapeElements(data: DataGridArchitectureData): Cytoscap edgeType: 'cross-dep', label: dep.importedNames.join(', '), targetName: dep.importedNames.join(', '), - importPath: dep.importPath, toRelPath: dep.toRelPath, }, 'edge-cross-dep'); } diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts index 75d8fe2a70f3..4ed576609383 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts @@ -1,12 +1,13 @@ /* eslint-disable spellcheck/spell-checker */ import { buildCytoscapeElements } from './graph-builder'; -import type { DataGridArchitectureData } from './types'; +import type { ArchitectureData } from './types'; -export function generateHtml(data: DataGridArchitectureData): string { +export function generateHtml(data: ArchitectureData): string { const cytoscapeElements = buildCytoscapeElements(data); const elementsJson = JSON.stringify(cytoscapeElements, null, 2); const pipelinesJson = JSON.stringify(data.extenderPipelines); const dsaJson = JSON.stringify(data.dataSourceAdapterChain); + const crossDepsJson = JSON.stringify(data.crossDependencies); const gridCoreModulesJson = JSON.stringify(data.gridCoreModules.map((gc) => ({ moduleName: gc.moduleName, registeredAs: gc.registeredAs, @@ -31,7 +32,12 @@ export function generateHtml(data: DataGridArchitectureData): string { overriddenExtenderControllers: m.overriddenExtenderControllers, overriddenExtenderViews: m.overriddenExtenderViews, hasDefaultOptionsOverride: m.hasDefaultOptionsOverride, + controllers: m.controllers, + views: m.views, + extenders: m.extenders, }))); + + const categories = ['passthrough', 'extended', 'replaced', 'new', 'grid-core', 'utility']; const featureAreas = [...new Set(data.modules.map((m) => m.featureArea))].sort(); return ` @@ -39,246 +45,189 @@ export function generateHtml(data: DataGridArchitectureData): string { -DataGrid Extensions Architecture - +DataGrid Architecture + +
@@ -154,10 +179,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;d - - @@ -104,14 +75,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- ${featureAreas.map((area) => ``).join('\n ')}
-

Orientation

-
- - - - -
-

Edge Routing

+

Edge Routing

@@ -123,14 +87,14 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-

Legend

-
Controller
-
View
-
Module (compound)
-
Inheritance (ctrl)
-
Inheritance (view)
-
Extender (ctrl)
-
Extender (view)
-
Runtime Dependency
+
Controller
+
View
+
Module (compound)
+
Inheritance (ctrl)
+
Inheritance (view)
+
Extender (ctrl)
+
Extender (view)
+
Runtime Dependency
@@ -312,41 +276,31 @@ const cy = cytoscape({ layout: { name: 'preset' }, }); -// ── ELK Layout Helper ─────────────────────────── -function getOrientation() { - const checked = document.querySelector('input[name="orientation"]:checked'); - return checked ? checked.value : 'TB'; -} - -function toElkDirection(orient) { - return { TB: 'DOWN', BT: 'UP', LR: 'RIGHT', RL: 'LEFT' }[orient] || 'DOWN'; -} - -function toTaxiDirection(orient) { - return { TB: 'downward', BT: 'upward', LR: 'rightward', RL: 'leftward' }[orient] || 'downward'; -} +// ── Edge Routing Helper ───────────────────────── function getEdgeRouting() { const checked = document.querySelector('input[name="edge-routing"]:checked'); return checked ? checked.value : 'taxi'; } -function toElkEdgeRouting(curveStyle) { - return curveStyle === 'taxi' ? 'ORTHOGONAL' : 'POLYLINE'; -} - -function updateEdgeStyles(orient) { +function updateEdgeStyles() { const curveStyle = getEdgeRouting(); if (curveStyle === 'taxi') { - const dir = toTaxiDirection(orient); - cy.edges().style({ + // Apply taxi only to non-cross-compound edges + cy.edges().not('.cross-compound').style({ 'curve-style': 'taxi', - 'taxi-direction': dir, + 'taxi-direction': 'downward', 'taxi-turn': '50%', }); + // Force bezier for cross-compound edges (siblings / parent-child) + cy.edges('.cross-compound').style({ + 'curve-style': 'bezier', + 'taxi-direction': null, + 'taxi-turn': null, + }); // Revert to bezier for edges with overlapping/adjacent endpoints - cy.edges().forEach(function(edge) { + cy.edges().not('.cross-compound').forEach(function(edge) { const src = edge.source(); const tgt = edge.target(); if (tgt.data('parent') === src.id() || src.data('parent') === tgt.id()) { @@ -361,40 +315,371 @@ function updateEdgeStyles(orient) { } }); } else { - cy.edges().style({ + cy.edges().not('.cross-compound').style({ 'curve-style': curveStyle, 'taxi-direction': null, 'taxi-turn': null, }); + // Cross-compound and overlapping edges always use regular bezier + cy.edges('.cross-compound').style({ + 'curve-style': 'bezier', + 'taxi-direction': null, + 'taxi-turn': null, + }); + cy.edges().not('.cross-compound').forEach(function(edge) { + var src = edge.source(); + var tgt = edge.target(); + if (tgt.data('parent') === src.id() || src.data('parent') === tgt.id()) { + edge.style({ 'curve-style': 'bezier' }); + return; + } + var sb = src.boundingBox(); + var tb = tgt.boundingBox(); + var overlaps = !(sb.x2 < tb.x1 || tb.x2 < sb.x1 || sb.y2 < tb.y1 || tb.y2 < sb.y1); + if (overlaps) { + edge.style({ 'curve-style': 'bezier' }); + } + }); } } -function buildLayoutOpts(layoutName, orient) { - const elkEdgeRouting = toElkEdgeRouting(getEdgeRouting()); - return { - name: 'elk', - elk: { - algorithm: 'layered', - 'elk.direction': toElkDirection(orient), - 'elk.layered.spacing.nodeNodeBetweenLayers': '100', - 'elk.layered.spacing.edgeNodeBetweenLayers': '50', - 'elk.spacing.nodeNode': '40', - 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', - 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP', - 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', - 'elk.layered.mergeEdges': 'true', - 'elk.edgeRouting': elkEdgeRouting, +// ── Dependency-levels custom layout ────────────── + +function runDepLevelsLayout() { + // 1. Collect targets (leaf nodes: controllers & views) + var targets = cy.nodes('.gc-target-controller, .gc-target-view'); + var modules = cy.nodes('.module'); + + // 2. Build dependency map: target → set of target ids it depends on + // via inheritance edges and extension edges (module extends target → module's children depend on that target) + var deps = {}; + targets.forEach(function(n) { deps[n.id()] = new Set(); }); + + // Inheritance: target → target + cy.edges('.edge-inherit-ctrl, .edge-inherit-view').forEach(function(e) { + var src = e.source().id(); + var tgt = e.target().id(); + if (deps[src]) deps[src].add(tgt); + }); + + // Extension: module → target. All children of that module depend on the extended target. + cy.edges('.edge-ext-ctrl, .edge-ext-view').forEach(function(e) { + var modNode = e.source(); + var extTarget = e.target().id(); + modNode.children().forEach(function(child) { + if (deps[child.id()] && child.id() !== extTarget) { + deps[child.id()].add(extTarget); + } + }); + }); + + // 3. Compute global levels via recursive topological sort + var level = {}; + function getLevel(id, visiting) { + if (level[id] !== undefined) return level[id]; + if (!visiting) visiting = {}; + if (visiting[id]) return 0; // cycle guard + visiting[id] = true; + var maxDep = -1; + deps[id].forEach(function(depId) { + var dl = getLevel(depId, visiting); + if (dl > maxDep) maxDep = dl; + }); + level[id] = maxDep + 1; + delete visiting[id]; + return level[id]; + } + targets.forEach(function(n) { getLevel(n.id()); }); + + // 4. Determine module level = max level of its children; ext-only modules = max level of targets they extend + 1 + var moduleLevel = {}; + modules.forEach(function(mod) { + var children = mod.children(); + if (children.length > 0) { + var maxLv = 0; + children.forEach(function(c) { + var lv = level[c.id()] || 0; + if (lv > maxLv) maxLv = lv; + }); + moduleLevel[mod.id()] = maxLv; + } else { + var maxExt = -1; + mod.connectedEdges('.edge-ext-ctrl, .edge-ext-view').forEach(function(e) { + if (e.source().id() === mod.id()) { + var tgtLv = level[e.target().id()] || 0; + if (tgtLv > maxExt) maxExt = tgtLv; + } + }); + moduleLevel[mod.id()] = maxExt >= 0 ? maxExt + 1 : 0; + } + }); + + targets.forEach(function(n) { + if (!n.data('parent')) { + moduleLevel[n.id()] = level[n.id()] || 0; + } + }); + + // 5. Compute inner levels for children within each module. + // Inner level is based only on inheritance edges between siblings in the same module. + // Children that don't inherit from any sibling are at inner level 0 (bottom). + var innerLevel = {}; // childId → inner level within its module + modules.forEach(function(mod) { + var children = mod.children(); + if (children.length <= 1) { + children.forEach(function(c) { innerLevel[c.id()] = 0; }); + return; + } + var childIds = new Set(); + children.forEach(function(c) { childIds.add(c.id()); }); + + // Build sibling inheritance deps (only edges between children of this module) + var sibDeps = {}; + children.forEach(function(c) { sibDeps[c.id()] = new Set(); }); + cy.edges('.edge-inherit-ctrl, .edge-inherit-view').forEach(function(e) { + var src = e.source().id(); + var tgt = e.target().id(); + if (childIds.has(src) && childIds.has(tgt)) { + sibDeps[src].add(tgt); + } + }); + + // Compute inner levels + var innerLv = {}; + function getInnerLevel(id, vis) { + if (innerLv[id] !== undefined) return innerLv[id]; + if (!vis) vis = {}; + if (vis[id]) return 0; + vis[id] = true; + var maxD = -1; + sibDeps[id].forEach(function(did) { + var dl = getInnerLevel(did, vis); + if (dl > maxD) maxD = dl; + }); + innerLv[id] = maxD + 1; + delete vis[id]; + return innerLv[id]; + } + children.forEach(function(c) { getInnerLevel(c.id()); }); + children.forEach(function(c) { innerLevel[c.id()] = innerLv[c.id()] || 0; }); + }); + + // 6. Group top-level items by level + var byLevel = {}; + modules.forEach(function(mod) { + var lv = moduleLevel[mod.id()] || 0; + if (!byLevel[lv]) byLevel[lv] = []; + byLevel[lv].push(mod); + }); + targets.forEach(function(n) { + if (!n.data('parent')) { + var lv = level[n.id()] || 0; + if (!byLevel[lv]) byLevel[lv] = []; + byLevel[lv].push(n); + } + }); + + // 7. Compute child sub-layouts within each module to determine real module dimensions. + // For each module, arrange children in sub-rows by inner level. + var CHILD_COL_GAP = 16; + var CHILD_ROW_GAP = 12; + var CHILD_PAD = 24; // padding inside module for label at top + border + + // childLayout[modId] = { width, height, childPositions: { childId: {dx, dy} } } + var childLayout = {}; + modules.forEach(function(mod) { + var children = mod.children(); + if (children.length === 0) { + childLayout[mod.id()] = { width: mod.outerWidth() || 100, height: mod.outerHeight() || 50, childPositions: {} }; + return; + } + + // Group children by inner level + var byInner = {}; + var maxInner = 0; + children.forEach(function(c) { + var il = innerLevel[c.id()] || 0; + if (!byInner[il]) byInner[il] = []; + byInner[il].push(c); + if (il > maxInner) maxInner = il; + }); + + // Lay out each inner row + var innerYAccum = CHILD_PAD; + var maxRowWidth = 0; + var cp = {}; + + for (var il = maxInner; il >= 0; il--) { + var row = byInner[il]; + if (!row) continue; + row.sort(function(a, b) { return (a.data('label') || '').localeCompare(b.data('label') || ''); }); + var rowX = 0; + var rowH = 0; + for (var ri = 0; ri < row.length; ri++) { + var child = row[ri]; + var cw = child.outerWidth() || 80; + var ch = child.outerHeight() || 30; + if (ch > rowH) rowH = ch; + cp[child.id()] = { dx: rowX + cw / 2, dy: innerYAccum + ch / 2 }; + rowX += cw + CHILD_COL_GAP; + } + var rw = rowX - CHILD_COL_GAP; + if (rw > maxRowWidth) maxRowWidth = rw; + innerYAccum += rowH + CHILD_ROW_GAP; + } + + var modW = Math.max(maxRowWidth + CHILD_PAD * 2, 100); + var modH = innerYAccum - CHILD_ROW_GAP + CHILD_PAD; + + // Center each inner row horizontally within modW + for (var il2 = 0; il2 <= maxInner; il2++) { + var row2 = byInner[il2]; + if (!row2) continue; + var rowMinX = Infinity, rowMaxX = -Infinity; + row2.forEach(function(c) { + var p = cp[c.id()]; + var hw = (c.outerWidth() || 80) / 2; + if (p.dx - hw < rowMinX) rowMinX = p.dx - hw; + if (p.dx + hw > rowMaxX) rowMaxX = p.dx + hw; + }); + var rowW2 = rowMaxX - rowMinX; + var rowOff = (modW - CHILD_PAD * 2 - rowW2) / 2 - rowMinX; + row2.forEach(function(c) { cp[c.id()].dx += rowOff; }); + } + + childLayout[mod.id()] = { width: modW, height: modH, childPositions: cp }; + }); + + // 8. Position top-level items in rows by level. + // All items on the same level share the same Y center. + // Ext-only modules (no children) are sized as squares matching the row height. + var ROW_GAP = 120; + var COL_GAP = 50; + var positions = {}; + var maxGlobalLevel = 0; + Object.keys(byLevel).forEach(function(k) { if (+k > maxGlobalLevel) maxGlobalLevel = +k; }); + + // First pass: compute row heights (find the tallest item per level) + var rowHeights = {}; + for (var lvH = 0; lvH <= maxGlobalLevel; lvH++) { + var itemsH = byLevel[lvH]; + if (!itemsH || itemsH.length === 0) continue; + var maxH = 0; + for (var iH = 0; iH < itemsH.length; iH++) { + var nH = itemsH[iH]; + var isModH = nH.data('nodeType') === 'module'; + var clH = isModH ? childLayout[nH.id()] : null; + var hH = clH ? clH.height : (nH.outerHeight() || 40); + if (hH > maxH) maxH = hH; + } + rowHeights[lvH] = maxH; + } + + // Resize ext-only modules (no children) to squares matching their row height + modules.forEach(function(mod) { + var cl = childLayout[mod.id()]; + if (cl && Object.keys(cl.childPositions).length === 0) { + var modLv = moduleLevel[mod.id()] || 0; + var rh = rowHeights[modLv] || 50; + cl.width = rh; + cl.height = rh; + } + }); + + // Second pass: position items, center-aligning vertically within each row + var yAccum = 0; + for (var lv = 0; lv <= maxGlobalLevel; lv++) { + var items = byLevel[lv]; + if (!items || items.length === 0) continue; + var rowHeight = rowHeights[lv] || 40; + + items.sort(function(a, b) { + var aIsModule = a.data('nodeType') === 'module' ? 0 : 1; + var bIsModule = b.data('nodeType') === 'module' ? 0 : 1; + if (aIsModule !== bIsModule) return aIsModule - bIsModule; + return (a.data('label') || '').localeCompare(b.data('label') || ''); + }); + + var rowCenterY = -yAccum - rowHeight / 2; + var xAccum = 0; + for (var i = 0; i < items.length; i++) { + var node = items[i]; + var isModule = node.data('nodeType') === 'module'; + var cl = isModule ? childLayout[node.id()] : null; + var w = cl ? cl.width : (node.outerWidth() || 80); + var h = cl ? cl.height : (node.outerHeight() || 40); + + var nodeCenterX = xAccum + w / 2; + positions[node.id()] = { x: nodeCenterX, y: rowCenterY }; + + // Position children using computed sub-layout offsets, centered within the module + if (isModule && cl && Object.keys(cl.childPositions).length > 0) { + var originX = xAccum; + var originY = rowCenterY - h / 2; + Object.keys(cl.childPositions).forEach(function(cid) { + var off = cl.childPositions[cid]; + positions[cid] = { x: originX + CHILD_PAD + off.dx, y: originY + off.dy }; + }); + } + + xAccum += w + COL_GAP; + } + yAccum += rowHeight + ROW_GAP; + } + + // 9. Center rows horizontally + var globalMaxX = 0; + Object.keys(byLevel).forEach(function(k) { + var items = byLevel[k]; + if (!items) return; + items.forEach(function(n) { + var p = positions[n.id()]; + var cl = childLayout[n.id()]; + var hw = cl ? cl.width / 2 : ((n.outerWidth() || 80) / 2); + if (p && p.x + hw > globalMaxX) globalMaxX = p.x + hw; + }); + }); + + for (var lv2 = 0; lv2 <= maxGlobalLevel; lv2++) { + var rowItems = byLevel[lv2]; + if (!rowItems || rowItems.length === 0) continue; + var minX = Infinity, maxX = -Infinity; + rowItems.forEach(function(n) { + var p = positions[n.id()]; + if (p) { + var cl = childLayout[n.id()]; + var hw = cl ? cl.width / 2 : ((n.outerWidth() || 80) / 2); + if (p.x - hw < minX) minX = p.x - hw; + if (p.x + hw > maxX) maxX = p.x + hw; + } + }); + var rowWidth = maxX - minX; + var offset = (globalMaxX - rowWidth) / 2 - minX; + rowItems.forEach(function(n) { + var p = positions[n.id()]; + if (p) p.x += offset; + // Shift children too + n.children().forEach(function(child) { + var cp = positions[child.id()]; + if (cp) cp.x += offset; + }); + }); + } + + // 10. Apply positions + cy.layout({ + name: 'preset', + positions: function(node) { + return positions[node.id()] || { x: 0, y: 0 }; }, - nodeDimensionsIncludeLabels: true, animate: true, animationDuration: 400, - }; + stop: function() { updateEdgeStyles(); }, + }).run(); } -// Run initial layout, then apply taxi edge routing where safe -const initialLayoutOpts = buildLayoutOpts('elk-layered', 'TB'); -initialLayoutOpts.stop = function() { updateEdgeStyles('TB'); }; -cy.layout(initialLayoutOpts).run(); +// Run initial layout +runDepLevelsLayout(); // ── Interactivity: hover / click-to-pin / edge highlight ── @@ -471,27 +756,27 @@ cy.on('tap', 'node', function(e) { const d = e.target.data(); let html = '

' + (d.label || d.id) + '

'; if (d.nodeType === 'module') { - html += '

Source: ' + (d.sourceFile || '') + '

'; - html += '

Area: ' + (d.featureArea || '') + '

'; - if (d.definesControllers) html += '

Controllers: ' + d.definesControllers + '

'; - if (d.definesViews) html += '

Views: ' + d.definesViews + '

'; - if (d.extendsControllers) html += '

Extends (ctrl): ' + d.extendsControllers + '

'; - if (d.extendsViews) html += '

Extends (view): ' + d.extendsViews + '

'; + html += '

Source: ' + (d.sourceFile || '') + '

'; + html += '

Area: ' + (d.featureArea || '') + '

'; + if (d.definesControllers) html += '

Controllers: ' + d.definesControllers + '

'; + if (d.definesViews) html += '

Views: ' + d.definesViews + '

'; + if (d.extendsControllers) html += '

Extends (ctrl): ' + d.extendsControllers + '

'; + if (d.extendsViews) html += '

Extends (view): ' + d.extendsViews + '

'; } else if (d.nodeType === 'controller' || d.nodeType === 'view') { - html += '

Type: ' + d.nodeType + '

'; - html += '

Class: ' + (d.className || '') + '

'; - html += '

Base: ' + (d.baseClass || '') + '

'; - if (d.mixins) html += '

Mixins: ' + d.mixins + '

'; - html += '

Source: ' + (d.sourceFile || '') + '

'; + html += '

Type: ' + d.nodeType + '

'; + html += '

Class: ' + (d.className || '') + '

'; + html += '

Base: ' + (d.baseClass || '') + '

'; + if (d.mixins) html += '

Mixins: ' + d.mixins + '

'; + html += '

Source: ' + (d.sourceFile || '') + '

'; } infoPanel.innerHTML = html; }); cy.on('tap', 'edge', function(e) { const d = e.target.data(); infoPanel.innerHTML = '

Edge: ' + d.edgeType + '

' - + '

From: ' + d.source + '

' - + '

To: ' + d.target + '

' - + (d.extenderName ? '

Extender: ' + d.extenderName + '

' : ''); + + '

From: ' + d.source + '

' + + '

To: ' + d.target + '

' + + (d.extenderName ? '

Extender: ' + d.extenderName + '

' : ''); }); // ── Search ────────────────────────────────────── @@ -566,17 +851,8 @@ document.querySelectorAll('.area-toggle').forEach(cb => { }); // ── Layout Selector ───────────────────────────── -function rerunLayout() { - const orient = getOrientation(); - const opts = buildLayoutOpts('elk-layered', orient); - opts.stop = function() { updateEdgeStyles(orient); }; - cy.layout(opts).run(); -} -document.querySelectorAll('input[name="orientation"]').forEach(radio => { - radio.addEventListener('change', rerunLayout); -}); document.querySelectorAll('input[name="edge-routing"]').forEach(radio => { - radio.addEventListener('change', rerunLayout); + radio.addEventListener('change', function() { runDepLevelsLayout(); }); }); // ── Buttons ───────────────────────────────────── @@ -590,12 +866,8 @@ document.getElementById('btn-reset').addEventListener('click', () => { document.querySelectorAll('.area-toggle').forEach(cb => { cb.checked = true; }); cy.elements().style('display', 'element'); document.querySelectorAll('.edge-toggle').forEach(function(cb) { cb.checked = true; }); - document.querySelector('input[name="orientation"][value="TB"]').checked = true; document.querySelector('input[name="edge-routing"][value="taxi"]').checked = true; - updateEdgeStyles('TB'); - const resetOpts = buildLayoutOpts('elk-layered', 'TB'); - resetOpts.stop = function() { updateEdgeStyles('TB'); }; - cy.layout(resetOpts).run(); + runDepLevelsLayout(); }); From b24d71ea4cf76e827065f72c40e95fdb25d91753 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Tue, 17 Mar 2026 10:50:18 +0100 Subject: [PATCH 09/10] review and refactor --- .../__docs__/scripts/data_grid/constants.ts | 16 +- .../__docs__/scripts/data_grid/generate.ts | 69 +-- .../scripts/data_grid/graph-builder.ts | 131 +++-- .../scripts/data_grid/html-template.ts | 114 +--- .../__docs__/scripts/data_grid/parser.ts | 204 ++----- .../__docs__/scripts/data_grid/resolver.ts | 532 ++++++------------ .../grids/__docs__/scripts/data_grid/types.ts | 57 +- ...nerate-architecture-doc.ts => generate.ts} | 43 +- .../scripts/grid_core/graph-builder.ts | 33 +- .../scripts/grid_core/html-template.ts | 291 ++-------- .../__docs__/scripts/grid_core/parser.ts | 169 +----- .../__docs__/scripts/grid_core/resolver.ts | 173 +++--- .../grids/__docs__/scripts/grid_core/types.ts | 25 +- .../__docs__/scripts/shared/ast-helpers.ts | 169 ++++++ .../scripts/{grid_core => shared}/cli.ts | 16 +- .../__docs__/scripts/shared/file-discovery.ts | 43 ++ .../__docs__/scripts/shared/graph-context.ts | 56 ++ .../__docs__/scripts/shared/html-helpers.ts | 251 +++++++++ .../__docs__/scripts/shared/inheritance.ts | 79 +++ .../__docs__/scripts/shared/output-writer.ts | 37 ++ .../grids/__docs__/scripts/shared/types.ts | 15 + 21 files changed, 1224 insertions(+), 1299 deletions(-) rename packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/{generate-architecture-doc.ts => generate.ts} (77%) create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/shared/ast-helpers.ts rename packages/devextreme/js/__internal/grids/__docs__/scripts/{grid_core => shared}/cli.ts (62%) create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/shared/file-discovery.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/shared/graph-context.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/shared/html-helpers.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/shared/inheritance.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/shared/output-writer.ts create mode 100644 packages/devextreme/js/__internal/grids/__docs__/scripts/shared/types.ts diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts index 30e8476c4b9c..c43bccabe4a1 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/constants.ts @@ -16,11 +16,17 @@ export const REGISTER_MODULE_RECEIVERS = new Set([ export const DATA_SOURCE_ADAPTER_PROVIDER = 'dataSourceAdapterProvider'; -export const GRID_CORE_IMPORT_PATTERNS = [ - '@ts/grids/grid_core/', - '../../grid_core/', - '../../../grid_core/', -]; +export const GRID_CORE_IMPORT_REGEXP = /grid_core\//; + +/** + * Import path segments that are considered "boring" internal imports + * and should be excluded from cross-dependency analysis. + * Matched as exact path segments (not substrings). + */ +export const CROSS_DEP_IGNORED_SEGMENTS = new Set([ + 'm_core', + 'm_data_source_adapter', +]); export type ModificationCategory = 'passthrough' | 'extended' | 'replaced' | 'new'; diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts index 410e11b22b3e..b28825e298da 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts @@ -3,11 +3,14 @@ import * as fs from 'fs'; import * as path from 'path'; -import { DATA_GRID_ROOT, OUTPUT_DIR } from './constants'; +import { discoverSourceFiles, getRelativePath } from '../shared/file-discovery'; +import { writeOutputFiles } from '../shared/output-writer'; +import { + DATA_GRID_ROOT, EXCLUDED_DIRS, EXCLUDED_FILE_NAMES, type ModificationCategory, + OUTPUT_DIR, +} from './constants'; import { generateHtml } from './html-template'; import { - discoverDataGridFiles, - getRelativePath, parseDataGridFile, parseModulesOrder, } from './parser'; @@ -22,42 +25,11 @@ import type { ArchitectureData, GridCoreModuleInfo, ParsedFile } from './types'; const GC_JSON_PATH = path.join(OUTPUT_DIR, 'grid_core_architecture.generated.json'); -interface CliArgs { - jsonOnly: boolean; - htmlOnly: boolean; -} - -function parseArgs(): CliArgs { - const args = process.argv.slice(2); - const result: CliArgs = { jsonOnly: false, htmlOnly: false }; - - for (const arg of args) { - switch (arg) { - case '--json': - result.jsonOnly = true; - break; - case '--html': - result.htmlOnly = true; - break; - default: - console.error(`Error: Unknown argument "${arg}"`); - process.exit(1); - } - } - - if (result.jsonOnly && result.htmlOnly) { - console.error('Error: Cannot specify both --json and --html. Use neither to generate both.'); - process.exit(1); - } - - return result; -} - function loadGridCoreModules(): GridCoreModuleInfo[] { if (!fs.existsSync(GC_JSON_PATH)) { console.error(`ERROR: grid_core_architecture.generated.json not found at ${GC_JSON_PATH}`); console.error('Please run the grid_core architecture script first:'); - console.error(' npx tsx __docs__/scripts/grid_core/generate-architecture-doc.ts'); + console.error(' npx tsx __docs__/scripts/grid_core/generate.ts --json'); process.exit(1); } @@ -106,17 +78,18 @@ function main(): void { const gridCoreModules = loadGridCoreModules(); // 3. Discover data_grid source files - const sourceFiles = discoverDataGridFiles(DATA_GRID_ROOT); + const sourceFiles = discoverSourceFiles(DATA_GRID_ROOT, EXCLUDED_DIRS, EXCLUDED_FILE_NAMES); console.log(`Discovered ${sourceFiles.length} data_grid source files`); // 4. Parse all files const allParsedFiles = sourceFiles.flatMap((file) => { - console.log(` Parsing: ${getRelativePath(file)}`); + const relPath = getRelativePath(file, DATA_GRID_ROOT); + console.log(` Parsing: ${relPath}`); try { return [parseDataGridFile(file)]; } catch (e) { const msg = e instanceof Error ? e.message : String(e); - console.warn(` WARN: Failed to parse ${getRelativePath(file)}: ${msg}`); + console.warn(` WARN: Failed to parse ${relPath}: ${msg}`); return []; } }); @@ -127,7 +100,7 @@ function main(): void { // 6. Classify modules const allModules = classifyModules(allParsedFiles, modulesOrder, gridCoreModules); console.log(`\nClassified ${allModules.length} modules:`); - const counts = { + const counts: Record = { passthrough: 0, extended: 0, replaced: 0, new: 0, }; for (const mod of allModules) { @@ -178,23 +151,7 @@ function main(): void { }; // 12. Write output files - if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); - } - - const args = parseArgs(); - - if (!args.htmlOnly) { - const jsonPath = path.join(OUTPUT_DIR, 'data_grid_architecture.generated.json'); - fs.writeFileSync(jsonPath, `${JSON.stringify(data, null, 2)}\n`); - console.log(`\nJSON written to: ${jsonPath}`); - } - - if (!args.jsonOnly) { - const htmlPath = path.join(OUTPUT_DIR, 'data_grid_architecture.generated.html'); - fs.writeFileSync(htmlPath, generateHtml(data)); - console.log(`HTML written to: ${htmlPath}`); - } + writeOutputFiles(OUTPUT_DIR, 'data_grid_architecture', data, generateHtml); console.log('\nDone.'); } catch (e) { diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts index fd40c248679d..7f944d479be0 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts @@ -1,10 +1,24 @@ /* eslint-disable spellcheck/spell-checker */ -import type { ArchitectureData, GridCoreModuleInfo } from './types'; +import type { CytoscapeElement } from '../shared/graph-context'; +import { createGraphContext } from '../shared/graph-context'; +import { buildGcRegistrationLookup } from './resolver'; +import type { + ArchitectureData, ClassifiedModule, ControllerViewRef, GridCoreModuleInfo, +} from './types'; -interface CytoscapeElement { - group: 'nodes' | 'edges'; - data: Record; - classes?: string; +/** + * A controller/view is "locally new" when it's authored + * in data_grid, not forwarded from grid_core. + */ +function isLocallyNew(ref: ControllerViewRef): boolean { + return ref.isDefinedLocally && !ref.isImportedFromGridCore; +} + +/** Returns names of controllers/views that are locally defined and not imported from grid_core. */ +function getLocalNames(refs: Record): string[] { + return Object.entries(refs) + .filter(([, ref]) => isLocallyNew(ref)) + .map(([name]) => name); } interface EdgeData extends Record { @@ -12,14 +26,10 @@ interface EdgeData extends Record { targetName?: string; } -function findGridCoreModule( - dgModuleName: string, - gridCoreModules: GridCoreModuleInfo[], -): GridCoreModuleInfo | undefined { - return gridCoreModules.find((gc) => gc.registeredAs === dgModuleName); -} +/** Target name used for the DataSourceAdapter extender pipeline visualization. */ +const DSA_TARGET_NAME = 'dataSourceAdapter'; -interface ClassInfo { +interface GraphClassInfo { className: string; baseClass: string; sourceFile: string; @@ -29,8 +39,15 @@ function buildGcLabel(gcMod: GridCoreModuleInfo): string { const parts: string[] = [gcMod.registeredAs ?? gcMod.moduleName]; const ctrls = Object.keys(gcMod.controllers); const vws = Object.keys(gcMod.views); - if (ctrls.length > 0) parts.push(`ctrl: ${ctrls.join(', ')}`); - if (vws.length > 0) parts.push(`view: ${vws.join(', ')}`); + + if (ctrls.length > 0) { + parts.push(`ctrl: ${ctrls.join(', ')}`); + } + + if (vws.length > 0) { + parts.push(`view: ${vws.join(', ')}`); + } + return parts.join('\n'); } @@ -40,9 +57,13 @@ function buildSynthGcData( ): { id: string; data: Record } | null { const gcCtrls = Object.entries(mod.controllers) .filter(([, ref]) => ref.isImportedFromGridCore); + const gcViews = Object.entries(mod.views) .filter(([, ref]) => ref.isImportedFromGridCore); - if (gcCtrls.length === 0 && gcViews.length === 0) return null; + + if (gcCtrls.length === 0 && gcViews.length === 0) { + return null; + } const labelParts: string[] = [mod.moduleName]; if (gcCtrls.length > 0) { @@ -52,7 +73,7 @@ function buildSynthGcData( labelParts.push(`view: ${gcViews.map(([n]) => n).join(', ')}`); } - const ctrlInfo: Record = {}; + const ctrlInfo: Record = {}; for (const [regName, ref] of gcCtrls) { ctrlInfo[regName] = { className: ref.className, @@ -60,7 +81,7 @@ function buildSynthGcData( sourceFile: ref.sourceFile, }; } - const viewInfo: Record = {}; + const viewInfo: Record = {}; for (const [regName, ref] of gcViews) { viewInfo[regName] = { className: ref.className, @@ -87,15 +108,24 @@ function buildSynthGcData( }; } -export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement[] { - const elements: CytoscapeElement[] = []; - const nodeIds = new Set(); - const edgeIds = new Set(); +export function buildCytoscapeElements( + data: ArchitectureData, + modulesByRelPath: Map, +): CytoscapeElement[] { + const { + elements, nodeIds, edgeIds, addNode, + } = createGraphContext(); - function addNode(id: string, nodeData: Record, classes: string): void { - if (nodeIds.has(id)) return; - nodeIds.add(id); - elements.push({ group: 'nodes', data: { id, ...nodeData }, classes }); + // ─── Pre-built lookup maps (O(1) instead of repeated linear scans) ──────── + const gcModuleLookup = buildGcRegistrationLookup(data.gridCoreModules); + + // Cache getLocalNames per module to avoid recomputing across sections + const localCtrlCache = new Map(); + const localViewCache = new Map(); + + for (const mod of data.modules) { + localCtrlCache.set(mod.moduleName, getLocalNames(mod.controllers)); + localViewCache.set(mod.moduleName, getLocalNames(mod.views)); } function addEdge(source: string, target: string, edgeData: EdgeData, classes: string): void { @@ -120,10 +150,16 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement defClass: string, ): void { const synthId = `gc-synth-${dgMod.moduleName}`; - if (!nodeIds.has(synthId)) return; + if (!nodeIds.has(synthId)) { + return; + } const ref = targetType === 'controller' ? dgMod.controllers[targetName] : dgMod.views[targetName]; - if (!ref?.isImportedFromGridCore) return; + + if (!ref?.isImportedFromGridCore) { + return; + } + addEdge(synthId, targetId, { edgeType: 'gc-defines', label: 'defines', @@ -152,18 +188,19 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement } // DataSourceAdapter has the same mixin nature as other extender targets if (data.dataSourceAdapterChain.length > 0) { - allTargets.set('dataSourceAdapter', { + allTargets.set(DSA_TARGET_NAME, { type: 'controller', origin: 'gc', }); } for (const mod of data.modules) { - for (const name of mod.newControllers) { + for (const name of localCtrlCache.get(mod.moduleName) ?? []) { if (!allTargets.has(name)) { const origin = gcDefinedNames.has(name) ? 'gc' : 'dg'; allTargets.set(name, { type: 'controller', origin }); } } - for (const name of mod.newViews) { + + for (const name of localViewCache.get(mod.moduleName) ?? []) { if (!allTargets.has(name)) { const origin = gcDefinedNames.has(name) ? 'gc' : 'dg'; allTargets.set(name, { type: 'view', origin }); @@ -176,6 +213,7 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement const targetId = `gc-target-${targetName}`; const typeLabel = info.type === 'controller' ? 'ctrl' : 'view'; const originClass = info.origin === 'dg' ? 'dg-target' : 'gc-target'; + addNode(targetId, { label: `${targetName}\n(${typeLabel})`, nodeType: 'gcTarget', @@ -191,7 +229,7 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement // ─── Identify which gc modules are used ─────────────────────────────────── const usedGcModules = new Set(); for (const mod of data.modules) { - const gc = findGridCoreModule(mod.moduleName, data.gridCoreModules); + const gc = gcModuleLookup.get(mod.moduleName); if (gc) { usedGcModules.add(gc.moduleName); } @@ -205,11 +243,20 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement ? `#${orderNum} ${mod.moduleName} (${mod.category})` : `#${orderNum} ${mod.moduleName}`; const labelParts: string[] = [namePart]; - if (mod.newControllers.length > 0 || mod.newViews.length > 0) { + const localCtrls = localCtrlCache.get(mod.moduleName) ?? []; + const localViews = localViewCache.get(mod.moduleName) ?? []; + + if (localCtrls.length > 0 || localViews.length > 0) { labelParts.push(''); } - if (mod.newControllers.length > 0) labelParts.push(`ctrl: ${mod.newControllers.join(', ')}`); - if (mod.newViews.length > 0) labelParts.push(`view: ${mod.newViews.join(', ')}`); + + if (localCtrls.length > 0) { + labelParts.push(`ctrl: ${localCtrls.join(', ')}`); + } + + if (localViews.length > 0) { + labelParts.push(`view: ${localViews.join(', ')}`); + } addNode(moduleId, { label: labelParts.join('\n'), @@ -218,13 +265,12 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement sourceFile: mod.relPath, featureArea: mod.featureArea, registrationOrder: mod.registrationOrder, - details: mod.details, gridCoreSource: mod.gridCoreSourceModule ?? '', moduleName: mod.moduleName, }, `module ${mod.category}`); // GC module node — always embedded inside the dg module - const gc = findGridCoreModule(mod.moduleName, data.gridCoreModules); + const gc = gcModuleLookup.get(mod.moduleName); if (gc) { const gcId = `gc-${gc.moduleName}`; addNode(gcId, { @@ -288,8 +334,9 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement // eslint-disable-next-line no-continue continue; } - const defines = (targetType === 'controller' && dgMod.newControllers.includes(targetName)) - || (targetType === 'view' && dgMod.newViews.includes(targetName)); + const ref = targetType === 'controller' + ? dgMod.controllers[targetName] : dgMod.views[targetName]; + const defines = ref !== undefined && isLocallyNew(ref); if (defines) { addEdge(`mod-${dgMod.moduleName}`, targetId, { edgeType: 'gc-defines', @@ -330,18 +377,20 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement } // ─── DataSourceAdapter extender edges (same pattern as other targets) ────── - const dsaTargetId = 'gc-target-dataSourceAdapter'; + const dsaTargetId = `gc-target-${DSA_TARGET_NAME}`; if (nodeIds.has(dsaTargetId)) { for (let i = 0; i < data.dataSourceAdapterChain.length; i += 1) { const ext = data.dataSourceAdapterChain[i]; - const mod = data.modules.find((m) => m.relPath === ext.relPath); + const mod = modulesByRelPath.get(ext.relPath); + if (!mod) { // eslint-disable-next-line no-continue continue; } + addEdge(`mod-${mod.moduleName}`, dsaTargetId, { edgeType: 'extender-target', - targetName: 'dataSourceAdapter', + targetName: DSA_TARGET_NAME, targetType: 'controller', label: `#${i + 1} ${mod.moduleName}`, chainIndex: i, diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts index 6f1a85b2ba6e..a06259429ea0 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts @@ -1,9 +1,12 @@ /* eslint-disable spellcheck/spell-checker */ +import { BASE_CSS, HIGHLIGHT_CYTOSCAPE_STYLES, SHARED_INTERACTIVE_JS } from '../shared/html-helpers'; import { buildCytoscapeElements } from './graph-builder'; +import { buildModulesByRelPath } from './resolver'; import type { ArchitectureData } from './types'; export function generateHtml(data: ArchitectureData): string { - const cytoscapeElements = buildCytoscapeElements(data); + const modulesByRelPath = buildModulesByRelPath(data.modules); + const cytoscapeElements = buildCytoscapeElements(data, modulesByRelPath); const elementsJson = JSON.stringify(cytoscapeElements, null, 2); const pipelines = data.extenderPipelines.map((p) => ({ targetName: p.targetName, @@ -23,7 +26,7 @@ export function generateHtml(data: ArchitectureData): string { targetName: 'dataSourceAdapter', targetType: 'controller', steps: data.dataSourceAdapterChain.map((ext) => { - const mod = data.modules.find((m) => m.relPath === ext.relPath); + const mod = modulesByRelPath.get(ext.relPath); return { moduleName: mod?.moduleName ?? ext.relPath, relPath: ext.relPath, @@ -52,13 +55,7 @@ export function generateHtml(data: ArchitectureData): string { relPath: m.relPath, featureArea: m.featureArea, registrationOrder: m.registrationOrder, - details: m.details, gridCoreSourceModule: m.gridCoreSourceModule, - newControllers: m.newControllers, - newViews: m.newViews, - overriddenControllers: m.overriddenControllers, - overriddenExtenderControllers: m.overriddenExtenderControllers, - overriddenExtenderViews: m.overriddenExtenderViews, hasDefaultOptionsOverride: m.hasDefaultOptionsOverride, controllers: m.controllers, views: m.views, @@ -78,24 +75,7 @@ export function generateHtml(data: ArchitectureData): string { @@ -132,12 +107,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;d

Categories

- + ${categories.map((c) => ``).join('\n ')}

Feature Areas

- + ${featureAreas.map((a) => ``).join('\n ')}
@@ -270,26 +245,20 @@ var cy = cytoscape({ 'text-background-color': '#1a1a2e', 'text-background-opacity': .9, 'text-background-padding': '2px', 'text-background-shape': 'round-rectangle', }}, - { selector: '.highlighted', style: { 'opacity': 1, 'z-index': 999 }}, - { selector: 'edge.highlighted', style: { 'opacity': 1, 'z-index': 999, 'width': 3 }}, - { selector: '.faded', style: { 'opacity': 0.08 }}, - { selector: 'node.search-match', style: { 'border-width': 3, 'border-color': '#FF6B6B' }}, + ${HIGHLIGHT_CYTOSCAPE_STYLES} ], layout: { name: 'preset' }, }); /* ── Custom Layout ── */ -function getEdgeRouting() { var c = document.querySelector('input[name="edge-routing"]:checked'); return c ? c.value : 'bezier'; } +// Note: getEdgeRouting() and hasOverlappingBounds() are provided by SHARED_INTERACTIVE_JS function updateEdgeStyles() { var routing = getEdgeRouting(); if (routing === 'taxi') { cy.edges().style({ 'curve-style': 'taxi', 'taxi-direction': 'downward', 'taxi-turn': '50%' }); cy.edges().forEach(function(edge) { - var sb = edge.source().boundingBox(); - var tb = edge.target().boundingBox(); - var overlaps = !(sb.x2 < tb.x1 || tb.x2 < sb.x1 || sb.y2 < tb.y1 || tb.y2 < sb.y1); - if (overlaps) edge.style({ 'curve-style': 'bezier', 'taxi-direction': null, 'taxi-turn': null }); + if (hasOverlappingBounds(edge)) edge.style({ 'curve-style': 'bezier', 'taxi-direction': null, 'taxi-turn': null }); }); } else { cy.edges().style({ 'curve-style': 'bezier', 'taxi-direction': null, 'taxi-turn': null }); @@ -454,17 +423,6 @@ function staggerExtenderLabels() { } runLayout(); -/* ── Edge routing ── */ -document.querySelectorAll('input[name="edge-routing"]').forEach(function(r) { r.addEventListener('change', function() { updateEdgeStyles(); }); }); - -/* ── Edge toggles ── */ -document.querySelectorAll('.edge-toggle').forEach(function(cb) { - cb.addEventListener('change', function() { - var cls = this.getAttribute('data-cls'); - cy.edges('.' + cls).style('display', this.checked ? 'element' : 'none'); - }); - if (!cb.checked) { var cls = cb.getAttribute('data-cls'); cy.edges('.' + cls).style('display', 'none'); } -}); /* ── Category toggles ── */ function applyCatFilters() { @@ -531,51 +489,22 @@ function connSet(seeds) { }); return seeds.union(edges).union(nodes).union(extra); } -function highlightSet(t) { +function computeHighlightSet(t) { if (t.isEdge()) return cy.collection().union(t).union(t.source()).union(t.target()); var group = compoundGroup(t); return connSet(group); } -function applyHighlight(s) { cy.elements().addClass('faded').removeClass('highlighted'); s.removeClass('faded').addClass('highlighted'); } -function clearHighlight() { cy.elements().removeClass('faded').removeClass('highlighted'); } -cy.on('tap', 'node, edge', function(e) { - var t = e.target; - // For passthrough pair, normalize: clicking embedded gc → treat as clicking dg parent - var infoTarget = t; +// Normalize: clicking embedded gc child → treat as clicking dg parent +function normalizeClickTarget(t) { if (t.isNode() && t.data('nodeType') === 'gridCoreModule' && t.data('parent')) { var p = t.parent(); - if (p && p.nonempty()) infoTarget = p; + if (p && p.nonempty()) return p; } - var checkId = infoTarget.id(); - if (selectedTarget && selectedTarget.id() === checkId) { selectedTarget = null; clearHighlight(); infoP.innerHTML = '

Click a node or edge to see details.

'; return; } - selectedTarget = infoTarget; - applyHighlight(highlightSet(t)); - showInfo(infoTarget); -}); -cy.on('tap', function(e) { - if (e.target === cy && selectedTarget) { selectedTarget = null; clearHighlight(); infoP.innerHTML = '

Click a node or edge to see details.

'; } -}); - -/* ── Buttons ── */ -document.getElementById('btn-fit').addEventListener('click', function() { cy.fit(undefined, 30); }); -document.getElementById('btn-reset').addEventListener('click', function() { clearHighlight(); runLayout(); }); - -/* ── Search ── */ -var searchInput = document.getElementById('search'); -searchInput.addEventListener('input', function() { - var q = this.value.toLowerCase().trim(); - cy.nodes().removeClass('search-match'); - if (!q) return; - cy.nodes().forEach(function(n) { - var label = (n.data('label') || '').toLowerCase(); - var name = (n.data('moduleName') || '').toLowerCase(); - if (label.indexOf(q) >= 0 || name.indexOf(q) >= 0) n.addClass('search-match'); - }); -}); + return t; +} /* ── Info Panel ── */ -var infoP = document.getElementById('info-panel'); function tagFor(cat) { var map = { passthrough: 'c-pt', extended: 'c-ext', replaced: 'c-rep', 'new': 'c-new', 'grid-core': 'c-gc', 'gc-target': 'c-gc' }; @@ -643,7 +572,6 @@ function showInfo(t) { h = '

#' + (d.registrationOrder + 1) + ' ' + d.moduleName + ' ' + tagFor(d.category) + '

'; h += '

Source: ' + d.sourceFile + '

'; h += '

Area: ' + d.featureArea + '

'; - if (d.details) h += '

Details: ' + d.details + '

'; if (d.gridCoreSource) h += '

Grid Core Source: ' + d.gridCoreSource + '

'; var gc = findGcModule(d.moduleName); @@ -680,8 +608,14 @@ function showInfo(t) { h += '

Grid core module ' + (d.source || '').replace('gc-', '') + ' defines ' + d.targetName + '.

'; } } - infoP.innerHTML = h; + document.getElementById('info-panel').innerHTML = h; } + +// ── Shared interactive JS (highlight, edge toggles, search, click handlers, fit button, routing radio) ── +${SHARED_INTERACTIVE_JS} + +// ── Data_grid-specific: Reset Button ── +document.getElementById('btn-reset').addEventListener('click', function() { clearHighlight(); runLayout(); }); `; diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts index bc9dde3604f2..46d0581ff0a8 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/parser.ts @@ -1,15 +1,19 @@ /* eslint-disable spellcheck/spell-checker,max-depth */ import * as fs from 'fs'; -import * as path from 'path'; // eslint-disable-next-line import/no-extraneous-dependencies import ts from 'typescript'; +import { + collectImportSpecs, + getClassHeritage, + getNodeText, + hasExportModifier, +} from '../shared/ast-helpers'; +import { getRelativePath } from '../shared/file-discovery'; import { DATA_GRID_ROOT, DATA_SOURCE_ADAPTER_PROVIDER, - EXCLUDED_DIRS, - EXCLUDED_FILE_NAMES, - GRID_CORE_IMPORT_PATTERNS, + GRID_CORE_IMPORT_REGEXP, REGISTER_MODULE_RECEIVERS, WIDGET_BASE_FILE, } from './constants'; @@ -20,102 +24,29 @@ import type { RegisterModuleCall, } from './types'; -// ─── File Discovery ────────────────────────────────────────────────────────── - -export function discoverDataGridFiles(rootDir: string): string[] { - const results: string[] = []; - - function walk(dir: string): void { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - if (!EXCLUDED_DIRS.has(entry.name)) { - walk(fullPath); - } - } else if ( - entry.isFile() - && !EXCLUDED_FILE_NAMES.has(entry.name) - && entry.name.endsWith('.ts') - && !entry.name.includes('.test.') - ) { - results.push(fullPath); - } - } - } - - walk(rootDir); - return results.sort(); -} - -export function getRelativePath(filePath: string): string { - return path.relative(DATA_GRID_ROOT, filePath).replace(/\\/g, '/'); -} - -// ─── AST Helpers ───────────────────────────────────────────────────────────── - -function getNodeText(node: ts.Node, sf: ts.SourceFile): string { - return node.getText(sf).trim(); -} - function isGridCoreImport(fromPath: string): boolean { - return GRID_CORE_IMPORT_PATTERNS.some((p) => fromPath.includes(p)); + return GRID_CORE_IMPORT_REGEXP.test(fromPath); } -function hasExportModifier(node: ts.Node): boolean { - if (!ts.canHaveModifiers(node)) return false; - const modifiers = ts.getModifiers(node); - return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; -} +// ─── registerModule Call Detection ─────────────────────────────────────────── -function parseHeritageString(text: string): { baseClass: string; mixins: string[] } { - const mixins: string[] = []; - let current = text; - while (true) { - const match = /^(\w+)\((.+)\)$/.exec(current); - if (match) { - const [, mixinName, inner] = match; - mixins.push(mixinName); - current = inner; - } else { - break; - } +function isRegisterModuleCall(node: ts.Node, sf: ts.SourceFile): node is ts.CallExpression { + if (!ts.isCallExpression(node)) { + return false; } - return { - baseClass: mixins.length > 0 ? `${mixins[mixins.length - 1]}(${current})` : current, - mixins, - }; -} -function getClassHeritage( - node: ts.ClassDeclaration | ts.ClassExpression, - sf: ts.SourceFile, - localVars: Map, -): { baseClass: string; mixins: string[] } { - if (!node.heritageClauses) return { baseClass: '', mixins: [] }; - for (const clause of node.heritageClauses) { - if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length > 0) { - const text = getNodeText(clause.types[0].expression, sf); - if (ts.isIdentifier(clause.types[0].expression) && localVars.has(text)) { - return parseHeritageString(localVars.get(text) ?? ''); - } - return parseHeritageString(text); - } + const expr = node.expression; + if (!ts.isPropertyAccessExpression(expr)) { + return false; } - return { baseClass: '', mixins: [] }; -} -// ─── registerModule Call Detection ─────────────────────────────────────────── - -function isRegisterModuleCall(node: ts.Node, sf: ts.SourceFile): node is ts.CallExpression { - if (!ts.isCallExpression(node)) return false; - const expr = node.expression; - if (!ts.isPropertyAccessExpression(expr)) return false; const methodName = expr.name.text; - if (methodName !== 'registerModule') return false; + if (methodName !== 'registerModule') { + return false; + } + + const baseObj = getNodeText(expr.expression, sf).split('.')[0]; - const obj = getNodeText(expr.expression, sf); - const baseObj = obj.split('.')[0]; return REGISTER_MODULE_RECEIVERS.has(baseObj); } @@ -123,17 +54,22 @@ function isDataSourceAdapterExtendCall( node: ts.Node, sf: ts.SourceFile, imports: Map, -): boolean { - if (!ts.isCallExpression(node)) return false; +): node is ts.CallExpression { + if (!ts.isCallExpression(node)) { + return false; + } + const expr = node.expression; - if (!ts.isPropertyAccessExpression(expr)) return false; - if (expr.name.text !== 'extend') return false; + if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== 'extend') { + return false; + } const obj = getNodeText(expr.expression, sf); - if (obj === DATA_SOURCE_ADAPTER_PROVIDER) return true; + if (obj === DATA_SOURCE_ADAPTER_PROVIDER) { + return true; + } - const imp = imports.get(obj); - return imp?.localName === DATA_SOURCE_ADAPTER_PROVIDER; + return imports.get(obj)?.localName === DATA_SOURCE_ADAPTER_PROVIDER; } // ─── Inline Controllers/Views/Extenders Parsing ───────────────────────────── @@ -262,12 +198,6 @@ function parseInlineExtenders( if (ts.isObjectLiteralExpression(initExpr)) { for (const innerProp of initExpr.properties) { if (ts.isSpreadAssignment(innerProp)) { - const spreadText = getNodeText(innerProp.expression, sf); - const baseIdent = spreadText.split('.')[0]; - const imp = parsedFile.imports.get(baseIdent); - if (imp?.isFromGridCore) { - // Mark spread entries as grid_core - } // eslint-disable-next-line no-continue continue; } @@ -303,14 +233,6 @@ function parseInlineExtenders( isDefinedLocally, }; } - } else if (ts.isIdentifier(initExpr) || ts.isPropertyAccessExpression(initExpr)) { - // extenders: { controllers: someModule.extenders.controllers } - const text = getNodeText(initExpr, sf); - const baseIdent = text.split('.')[0]; - const imp = parsedFile.imports.get(baseIdent); - if (imp?.isFromGridCore) { - // forwarded from grid_core — mark as imported - } } } @@ -493,7 +415,7 @@ function parseRegisterModuleCall( // ─── Main File Parser ──────────────────────────────────────────────────────── export function parseDataGridFile(filePath: string): ParsedFile { - const relPath = getRelativePath(filePath); + const relPath = getRelativePath(filePath, DATA_GRID_ROOT); const content = fs.readFileSync(filePath, 'utf-8'); const sf = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); @@ -508,49 +430,14 @@ export function parseDataGridFile(filePath: string): ParsedFile { }; // ── Pass 1: Collect imports ── - for (const stmt of sf.statements) { - if ( - ts.isImportDeclaration(stmt) - && stmt.moduleSpecifier - && ts.isStringLiteral(stmt.moduleSpecifier) - ) { - const fromPath = stmt.moduleSpecifier.text; - const isFromGc = isGridCoreImport(fromPath); - const { importClause } = stmt; - - if (importClause) { - if (importClause.name) { - parsedFile.imports.set(importClause.name.text, { - localName: importClause.name.text, - originalName: 'default', - fromPath, - isFromGridCore: isFromGc, - }); - } - if (importClause.namedBindings) { - if (ts.isNamedImports(importClause.namedBindings)) { - for (const spec of importClause.namedBindings.elements) { - const localName = spec.name.text; - const originalName = spec.propertyName ? spec.propertyName.text : localName; - parsedFile.imports.set(localName, { - localName, - originalName, - fromPath, - isFromGridCore: isFromGc, - }); - } - } else if (ts.isNamespaceImport(importClause.namedBindings)) { - parsedFile.imports.set(importClause.namedBindings.name.text, { - localName: importClause.namedBindings.name.text, - originalName: '*', - fromPath, - isFromGridCore: isFromGc, - }); - } - } - } - } - } + collectImportSpecs(sf).forEach((spec) => { + parsedFile.imports.set(spec.localName, { + localName: spec.localName, + originalName: spec.originalName, + fromPath: spec.fromPath, + isFromGridCore: isGridCoreImport(spec.fromPath), + }); + }); // ── Pass 2: Collect classes and local variables ── function collectClassesAndVars(node: ts.Node): void { @@ -626,7 +513,7 @@ export function parseDataGridFile(filePath: string): ParsedFile { ts.forEachChild(sf, collectClassesAndVars); - // ── Pass 3: Find registerModule calls and DSA extensions ── + // ── Pass 3: Find registerModule calls and DataSourceAdapter extensions ── function findCalls(node: ts.Node): void { if (isRegisterModuleCall(node, sf)) { const call = node; @@ -645,9 +532,8 @@ export function parseDataGridFile(filePath: string): ParsedFile { } if (isDataSourceAdapterExtendCall(node, sf, parsedFile.imports)) { - const call = node as ts.CallExpression; - if (call.arguments.length >= 1) { - const extenderArg = call.arguments[0]; + if (node.arguments.length >= 1) { + const extenderArg = node.arguments[0]; const extenderName = getNodeText(extenderArg, sf); const imp = parsedFile.imports.get(extenderName); parsedFile.dataSourceAdapterExtensions.push({ diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts index 769658aa71c1..9e3110db27bd 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/resolver.ts @@ -1,11 +1,8 @@ -/* eslint-disable spellcheck/spell-checker,max-depth */ -import * as fs from 'fs'; -import * as path from 'path'; -// eslint-disable-next-line import/no-extraneous-dependencies -import ts from 'typescript'; - +/* eslint-disable max-depth,no-continue */ +import { buildInheritanceChainCore } from '../shared/inheritance'; +import type { HeritageInfo } from '../shared/types'; import type { ModificationCategory } from './constants'; -import { getFeatureAreaFromPath, GRID_CORE_ROOT } from './constants'; +import { CROSS_DEP_IGNORED_SEGMENTS, getFeatureAreaFromPath } from './constants'; import type { ClassifiedModule, CrossDependency, @@ -28,9 +25,16 @@ function hasLocallyDefinedExtenders( ...Object.values(reg.extenders.controllers), ...Object.values(reg.extenders.views), ]; + return allExtenders.some((ext) => { - if (ext.isDefinedLocally) return true; - if (ext.isImportedFromGridCore) return false; + if (ext.isDefinedLocally) { + return true; + } + + if (ext.isImportedFromGridCore) { + return false; + } + return parsedFile.localVars.has(ext.extenderName) || parsedFile.classes.has(ext.extenderName); }); @@ -40,16 +44,22 @@ function classifyModule( reg: RegisterModuleCall, parsedFile: ParsedFile, ): ModificationCategory { - // 1. No grid_core reference at all → new (data_grid-only module) - if (!reg.referencesGridCoreModule) return 'new'; + // No grid_core reference at all → new (data_grid-only module) + if (!reg.referencesGridCoreModule) { + return 'new'; + } - // 2. Direct passthrough: gridCore.registerModule('sorting', sortingModule) - if (reg.argIsIdentifier) return 'passthrough'; + // Direct passthrough: gridCore.registerModule('sorting', sortingModule) + if (reg.argIsIdentifier) { + return 'passthrough'; + } // Check for locally-defined controllers/views (→ replaced) const hasLocalControllers = Object.values(reg.controllers).some((c) => c.isDefinedLocally); const hasLocalViews = Object.values(reg.views).some((v) => v.isDefinedLocally); - if (hasLocalControllers || hasLocalViews) return 'replaced'; + if (hasLocalControllers || hasLocalViews) { + return 'replaced'; + } // Check for locally-defined extenders (→ extended) if (reg.hasInlineExtenders && hasLocallyDefinedExtenders(reg, parsedFile)) { @@ -61,202 +71,78 @@ function classifyModule( return 'passthrough'; } -// ─── Module Details ────────────────────────────────────────────────────────── - -function buildDetails(category: ModificationCategory, reg: RegisterModuleCall): string { - switch (category) { - case 'passthrough': - return 'Re-registers grid_core module as-is'; - case 'replaced': { - const replaced: string[] = []; - for (const [name, ref] of Object.entries(reg.controllers)) { - if (ref.isDefinedLocally) { - replaced.push(`controller '${name}' → ${ref.className} extends ${ref.baseClass}`); - } - } - for (const [name, ref] of Object.entries(reg.views)) { - if (ref.isDefinedLocally) { - replaced.push(`view '${name}' → ${ref.className} extends ${ref.baseClass}`); - } - } - return replaced.join('; ') || 'Controller/view replacement'; - } - case 'extended': { - const exts: string[] = []; - for (const [target, ext] of Object.entries(reg.extenders.controllers)) { - if (!ext.isImportedFromGridCore) { - exts.push(`extends controller '${target}' via ${ext.extenderName}`); - } - } - for (const [target, ext] of Object.entries(reg.extenders.views)) { - if (!ext.isImportedFromGridCore) { - exts.push(`extends view '${target}' via ${ext.extenderName}`); - } - } - return exts.join('; ') || 'Extends grid_core module with new extenders'; - } - case 'new': { - const parts: string[] = []; - for (const [name, ref] of Object.entries(reg.controllers)) { - parts.push(`new controller '${name}': ${ref.className}`); - } - for (const [name, ref] of Object.entries(reg.views)) { - parts.push(`new view '${name}': ${ref.className}`); - } - for (const [target] of Object.entries(reg.extenders.controllers)) { - parts.push(`extends controller '${target}'`); - } - for (const [target] of Object.entries(reg.extenders.views)) { - parts.push(`extends view '${target}'`); - } - return parts.join('; ') || 'New data_grid module'; - } - default: - return ''; - } -} - // ─── Resolve forwarded controllers/views from grid_core module data ────────── -function findGcModuleByImport( - fromPath: string, +function buildGcSourceLookup( gridCoreModules: GridCoreModuleInfo[], -): GridCoreModuleInfo | undefined { - // fromPath is like "@ts/grids/grid_core/columns_controller/m_columns_controller" - // gc sourceFile is like "columns_controller/m_columns_controller.ts" - // Normalize: strip prefix, add .ts suffix - const normalized = fromPath - .replace(/^@ts\/grids\/grid_core\//, '') - .replace(/\.ts$/, ''); - return gridCoreModules.find((gc) => { - const gcNorm = gc.sourceFile.replace(/\.ts$/, ''); - return gcNorm === normalized; - }); +): Map { + const map = new Map(); + for (const gc of gridCoreModules) { + const key = gc.sourceFile.replace(/\.ts$/, ''); + map.set(key, gc); + } + return map; } -/** - * Fallback: when a gc module isn't in the gc JSON (not registered via registerModule), - * parse the source file directly to extract controller/view names from the exported - * module constant (e.g. `export const columnsControllerModule = { controllers: { ... } }`). - */ -function parseGcSourceControllerViews( - importedName: string, - fromPath: string, -): { controllers: string[]; views: string[] } { - const result: { controllers: string[]; views: string[] } = { controllers: [], views: [] }; - // Resolve the file path from the import - const relToGc = fromPath.replace(/^@ts\/grids\/grid_core\//, ''); - const filePath = path.resolve(GRID_CORE_ROOT, `${relToGc}.ts`); - if (!fs.existsSync(filePath)) return result; - - const content = fs.readFileSync(filePath, 'utf-8'); - const sf = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); - - // Find the exported variable `importedName` and extract its controllers/views property keys - ts.forEachChild(sf, (node) => { - if (!ts.isVariableStatement(node)) return; - for (const decl of node.declarationList.declarations) { - if (!ts.isIdentifier(decl.name) || decl.name.text !== importedName) { - // eslint-disable-next-line no-continue - continue; - } - if (!decl.initializer || !ts.isObjectLiteralExpression(decl.initializer)) { - // eslint-disable-next-line no-continue - continue; - } - for (const prop of decl.initializer.properties) { - if (!ts.isPropertyAssignment(prop)) { - // eslint-disable-next-line no-continue - continue; - } - const propName = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : ''; - if ((propName === 'controllers' || propName === 'views') - && ts.isObjectLiteralExpression(prop.initializer)) { - for (const inner of prop.initializer.properties) { - if (ts.isPropertyAssignment(inner) || ts.isShorthandPropertyAssignment(inner)) { - const regName = inner.name && ts.isIdentifier(inner.name) ? inner.name.text : ''; - if (regName) { - result[propName].push(regName); - } - } - } - } - } +/** Build a lookup map: registeredAs → GridCoreModuleInfo for O(1) access. */ +export function buildGcRegistrationLookup( + gridCoreModules: GridCoreModuleInfo[], +): Map { + const map = new Map(); + for (const gc of gridCoreModules) { + if (gc.registeredAs) { + map.set(gc.registeredAs, gc); } - }); - return result; + } + return map; +} + +/** Build a lookup map: relPath → ClassifiedModule for O(1) access. */ +export function buildModulesByRelPath( + modules: ClassifiedModule[], +): Map { + return new Map(modules.map((m) => [m.relPath, m])); } -function resolveForwardedControllersViews( +function resolveForwardedRefs( reg: RegisterModuleCall, pf: ParsedFile, - gridCoreModules: GridCoreModuleInfo[], + gcSourceLookup: Map, + kind: 'controllers' | 'views', ): void { - if (reg.forwardedControllersRef && Object.keys(reg.controllers).length === 0) { - const imp = pf.imports.get(reg.forwardedControllersRef); - if (imp?.isFromGridCore) { - const gcMod = findGcModuleByImport(imp.fromPath, gridCoreModules); - if (gcMod) { - for (const [regName, info] of Object.entries(gcMod.controllers)) { - reg.controllers[regName] = { - regName, - className: info.className, - isImportedFromGridCore: true, - isDefinedLocally: false, - baseClass: info.baseClass, - mixins: info.mixins ?? [], - sourceFile: info.sourceFile, - }; - } - } else { - // Fallback: parse the gc source file directly - const parsed = parseGcSourceControllerViews(reg.forwardedControllersRef, imp.fromPath); - for (const regName of parsed.controllers) { - reg.controllers[regName] = { - regName, - className: regName, - isImportedFromGridCore: true, - isDefinedLocally: false, - baseClass: '', - mixins: [], - sourceFile: imp.fromPath, - }; - } - } - } + const forwardedRef = kind === 'controllers' + ? reg.forwardedControllersRef + : reg.forwardedViewsRef; + const target = reg[kind]; + + if (!forwardedRef || Object.keys(target).length > 0) { + return; } - if (reg.forwardedViewsRef && Object.keys(reg.views).length === 0) { - const imp = pf.imports.get(reg.forwardedViewsRef); - if (imp?.isFromGridCore) { - const gcMod = findGcModuleByImport(imp.fromPath, gridCoreModules); - if (gcMod) { - for (const [regName, info] of Object.entries(gcMod.views)) { - reg.views[regName] = { - regName, - className: info.className, - isImportedFromGridCore: true, - isDefinedLocally: false, - baseClass: info.baseClass, - mixins: info.mixins ?? [], - sourceFile: info.sourceFile, - }; - } - } else { - // Fallback: parse the gc source file directly - const parsed = parseGcSourceControllerViews(reg.forwardedViewsRef, imp.fromPath); - for (const regName of parsed.views) { - reg.views[regName] = { - regName, - className: regName, - isImportedFromGridCore: true, - isDefinedLocally: false, - baseClass: '', - mixins: [], - sourceFile: imp.fromPath, - }; - } - } + + const imp = pf.imports.get(forwardedRef); + if (!imp?.isFromGridCore) { + return; + } + + const normalized = imp.fromPath + .replace(/^@ts\/grids\/grid_core\//, '') + .replace(/\.ts$/, ''); + const gcMod = gcSourceLookup.get(normalized); + + if (gcMod) { + for (const [regName, info] of Object.entries(gcMod[kind])) { + target[regName] = { + regName, + className: info.className, + isImportedFromGridCore: true, + isDefinedLocally: false, + baseClass: info.baseClass, + mixins: info.mixins ?? [], + sourceFile: info.sourceFile, + }; } + } else { + console.warn(`WARN: gc module not found for forwarded ${kind} ref '${forwardedRef}' (from ${imp.fromPath}). Ensure grid_core JSON is up-to-date.`); } } @@ -268,30 +154,17 @@ export function classifyModules( gridCoreModules: GridCoreModuleInfo[], ): ClassifiedModule[] { const results: ClassifiedModule[] = []; + const orderMap = new Map(modulesOrder.map((name, i) => [name, i])); + const gcSourceLookup = buildGcSourceLookup(gridCoreModules); for (const pf of parsedFiles) { for (const reg of pf.registerModuleCalls) { // Resolve forwarded controllers/views from gc module data before classification - resolveForwardedControllersViews(reg, pf, gridCoreModules); + resolveForwardedRefs(reg, pf, gcSourceLookup, 'controllers'); + resolveForwardedRefs(reg, pf, gcSourceLookup, 'views'); const category = classifyModule(reg, pf); - const orderIndex = modulesOrder.indexOf(reg.moduleName); - - const newControllers = Object.entries(reg.controllers) - .filter(([, ref]) => ref.isDefinedLocally && !ref.isImportedFromGridCore) - .map(([name]) => name); - const newViews = Object.entries(reg.views) - .filter(([, ref]) => ref.isDefinedLocally && !ref.isImportedFromGridCore) - .map(([name]) => name); - const overriddenControllers = Object.entries(reg.controllers) - .filter(([, ref]) => ref.isDefinedLocally && ref.baseClass) - .map(([name]) => name); - const overriddenExtenderControllers = Object.entries(reg.extenders.controllers) - .filter(([, ext]) => !ext.isImportedFromGridCore) - .map(([name]) => name); - const overriddenExtenderViews = Object.entries(reg.extenders.views) - .filter(([, ext]) => !ext.isImportedFromGridCore) - .map(([name]) => name); + const orderIndex = orderMap.get(reg.moduleName) ?? -1; let gridCoreSourceModule: string | null = null; for (const ref of reg.gridCoreRefs) { @@ -308,19 +181,13 @@ export function classifyModules( sourceFile: pf.filePath, relPath: reg.relPath, featureArea: getFeatureAreaFromPath(reg.relPath), - registrationOrder: orderIndex >= 0 ? orderIndex : 999, + registrationOrder: orderIndex >= 0 ? orderIndex : modulesOrder.length, gridCoreModuleName: reg.argIsIdentifier ? reg.argIdentifierName : null, gridCoreSourceModule, controllers: reg.controllers, views: reg.views, extenders: reg.extenders, - newControllers, - newViews, - overriddenControllers, - overriddenExtenderControllers, - overriddenExtenderViews, hasDefaultOptionsOverride: reg.hasDefaultOptions && category !== 'passthrough', - details: buildDetails(category, reg), }); } } @@ -338,91 +205,71 @@ export function collectDataSourceAdapterChain( for (const pf of parsedFiles) { all.push(...pf.dataSourceAdapterExtensions); } - all.forEach((ext, i) => { ext.order = i; }); - return all; + return all.map((ext, i) => ({ ...ext, order: i })); } // ─── Public: buildExtenderPipelines ────────────────────────────────────────── +function addPipelineStep( + stepsMap: Map, + targetName: string, + step: ExtenderPipelineStep, +): void { + const existing = stepsMap.get(targetName); + if (existing) { + existing.push(step); + } else { + stepsMap.set(targetName, [step]); + } +} + +function collectExtenderSteps( + mod: ClassifiedModule, + gcMod: GridCoreModuleInfo | undefined, + kind: 'controllers' | 'views', + stepsMap: Map, +): void { + // 1. Extenders explicitly declared in data_grid registerModule call + const declaredTargets = new Set(Object.keys(mod.extenders[kind])); + for (const [targetName, ext] of Object.entries(mod.extenders[kind])) { + addPipelineStep(stepsMap, targetName, { + moduleName: mod.moduleName, + relPath: mod.relPath, + extenderName: ext.extenderName, + isFromGridCore: ext.isImportedFromGridCore, + registrationOrder: mod.registrationOrder, + category: mod.category, + }); + } + // 2. GC extenders from passthrough/extended modules (not already declared by dg) + if (gcMod && (mod.category === 'passthrough' || mod.category === 'extended')) { + for (const [targetName, ext] of Object.entries(gcMod.extenders[kind])) { + if (!declaredTargets.has(targetName)) { + addPipelineStep(stepsMap, targetName, { + moduleName: mod.moduleName, + relPath: mod.relPath, + extenderName: ext.extenderName, + isFromGridCore: true, + registrationOrder: mod.registrationOrder, + category: mod.category, + }); + } + } + } +} + export function buildExtenderPipelines( modules: ClassifiedModule[], gridCoreModules: GridCoreModuleInfo[], ): ExtenderPipeline[] { const controllerSteps = new Map(); const viewSteps = new Map(); - - // Helper: find gc module by registration name - function findGcModule(dgModuleName: string): GridCoreModuleInfo | undefined { - return gridCoreModules.find((gc) => gc.registeredAs === dgModuleName); - } + const gcRegLookup = buildGcRegistrationLookup(gridCoreModules); for (const mod of modules) { - const gcMod = findGcModule(mod.moduleName); - - // Collect controller extenders - // 1. Extenders explicitly declared in data_grid registerModule call - const declaredCtrlTargets = new Set(Object.keys(mod.extenders.controllers)); - for (const [targetName, ext] of Object.entries(mod.extenders.controllers)) { - const step: ExtenderPipelineStep = { - moduleName: mod.moduleName, - relPath: mod.relPath, - extenderName: ext.extenderName, - isFromGridCore: ext.isImportedFromGridCore, - registrationOrder: mod.registrationOrder, - category: mod.category, - }; - const existing = controllerSteps.get(targetName); - if (existing) { existing.push(step); } else { controllerSteps.set(targetName, [step]); } - } - // 2. GC extenders from passthrough modules (not already declared by dg) - if (gcMod && (mod.category === 'passthrough' || mod.category === 'extended')) { - for (const [targetName, ext] of Object.entries(gcMod.extenders.controllers)) { - if (!declaredCtrlTargets.has(targetName)) { - const step: ExtenderPipelineStep = { - moduleName: mod.moduleName, - relPath: mod.relPath, - extenderName: ext.extenderName, - isFromGridCore: true, - registrationOrder: mod.registrationOrder, - category: mod.category, - }; - const existing = controllerSteps.get(targetName); - if (existing) { existing.push(step); } else { controllerSteps.set(targetName, [step]); } - } - } - } - - // Collect view extenders - const declaredViewTargets = new Set(Object.keys(mod.extenders.views)); - for (const [targetName, ext] of Object.entries(mod.extenders.views)) { - const step: ExtenderPipelineStep = { - moduleName: mod.moduleName, - relPath: mod.relPath, - extenderName: ext.extenderName, - isFromGridCore: ext.isImportedFromGridCore, - registrationOrder: mod.registrationOrder, - category: mod.category, - }; - const existing = viewSteps.get(targetName); - if (existing) { existing.push(step); } else { viewSteps.set(targetName, [step]); } - } - // 2. GC extenders from passthrough modules (not already declared by dg) - if (gcMod && (mod.category === 'passthrough' || mod.category === 'extended')) { - for (const [targetName, ext] of Object.entries(gcMod.extenders.views)) { - if (!declaredViewTargets.has(targetName)) { - const step: ExtenderPipelineStep = { - moduleName: mod.moduleName, - relPath: mod.relPath, - extenderName: ext.extenderName, - isFromGridCore: true, - registrationOrder: mod.registrationOrder, - category: mod.category, - }; - const existing = viewSteps.get(targetName); - if (existing) { existing.push(step); } else { viewSteps.set(targetName, [step]); } - } - } - } + const gcMod = gcRegLookup.get(mod.moduleName); + collectExtenderSteps(mod, gcMod, 'controllers', controllerSteps); + collectExtenderSteps(mod, gcMod, 'views', viewSteps); } const pipelines: ExtenderPipeline[] = []; @@ -439,34 +286,16 @@ export function buildExtenderPipelines( // ─── Public: buildInheritanceChains ────────────────────────────────────────── -function buildInheritanceChain( - className: string, - allClasses: Map, - visited: Set, -): string[] { - if (visited.has(className)) return []; - visited.add(className); - const info = allClasses.get(className); - if (!info?.baseClass) return []; - - const chain: string[] = []; - for (const mixin of info.mixins) { chain.push(`[mixin] ${mixin}`); } - - let rawBase = info.baseClass; - const mixinMatch = /^\w+\((.+)\)$/.exec(rawBase); - if (mixinMatch) { [, rawBase] = mixinMatch; } - - chain.push(rawBase); - if (allClasses.has(rawBase)) { - chain.push(...buildInheritanceChain(rawBase, allClasses, visited)); - } - return chain; -} +const MAX_INHERITANCE_DEPTH = 50; export function buildInheritanceChains(parsedFiles: ParsedFile[]): InheritanceEntry[] { - const allClasses = new Map(); + const allClasses = new Map(); for (const pf of parsedFiles) { for (const [name, info] of pf.classes) { + if (allClasses.has(name)) { + const existing = allClasses.get(name); + console.warn(`WARN: Duplicate class "${name}" found in ${existing?.sourceFile ?? 'unknown'} and ${info.sourceFile}; keeping last-seen entry.`); + } allClasses.set(name, { baseClass: info.baseClass, mixins: info.mixins, @@ -479,7 +308,12 @@ export function buildInheritanceChains(parsedFiles: ParsedFile[]): InheritanceEn for (const [className, info] of allClasses) { if (info.baseClass) { const visited = new Set(); - const chain = buildInheritanceChain(className, allClasses, visited); + const chain = buildInheritanceChainCore( + className, + (name) => allClasses.get(name), + visited, + { maxDepth: MAX_INHERITANCE_DEPTH, formatMixin: (m) => `[mixin] ${m}` }, + ); if (chain.length > 0) { entries.push({ className, chain, sourceFile: info.sourceFile }); } @@ -490,7 +324,7 @@ export function buildInheritanceChains(parsedFiles: ParsedFile[]): InheritanceEn // ─── Public: buildCrossDependencies ────────────────────────────────────────── -function resolveImportToRelPath(fromRelPath: string, importPath: string): string | null { +function resolveRelativeImportPath(fromRelPath: string, importPath: string): string | null { const fromDir = fromRelPath.split('/').slice(0, -1).join('/'); const segments = importPath.split('/'); const resolved: string[] = fromDir ? fromDir.split('/') : []; @@ -501,11 +335,20 @@ function resolveImportToRelPath(fromRelPath: string, importPath: string): string } function findTargetFile(resolved: string, relPaths: Set): string | null { - if (relPaths.has(resolved)) return resolved; + if (relPaths.has(resolved)) { + return resolved; + } + const withTs = `${resolved}.ts`; - if (relPaths.has(withTs)) return withTs; + if (relPaths.has(withTs)) { + return withTs; + } + const asIndex = `${resolved}/index.ts`; - if (relPaths.has(asIndex)) return asIndex; + if (relPaths.has(asIndex)) { + return asIndex; + } + return null; } @@ -514,80 +357,71 @@ export function buildCrossDependencies( modules: ClassifiedModule[], ): CrossDependency[] { const relPathToModule = new Map(); - for (const mod of modules) { relPathToModule.set(mod.relPath, mod.moduleName); } + for (const mod of modules) { + relPathToModule.set(mod.relPath, mod.moduleName); + } const allRelPaths = new Set(); - for (const pf of parsedFiles) { allRelPaths.add(pf.relPath); } + for (const pf of parsedFiles) { + allRelPaths.add(pf.relPath); + } - const deps: CrossDependency[] = []; - const seen = new Set(); + const depsMap = new Map(); for (const pf of parsedFiles) { const fromModule = relPathToModule.get(pf.relPath); if (!fromModule) { - // eslint-disable-next-line no-continue continue; } for (const [localName, imp] of pf.imports) { if (imp.isFromGridCore) { - // eslint-disable-next-line no-continue continue; } if (!imp.fromPath.startsWith('.') && !imp.fromPath.startsWith('..')) { - // eslint-disable-next-line no-continue continue; } - const resolvedTarget = resolveImportToRelPath(pf.relPath, imp.fromPath); + const resolvedTarget = resolveRelativeImportPath(pf.relPath, imp.fromPath); if (!resolvedTarget) { - // eslint-disable-next-line no-continue continue; } const targetFile = findTargetFile(resolvedTarget, allRelPaths); if (!targetFile) { - // eslint-disable-next-line no-continue continue; } const toModule = relPathToModule.get(targetFile) ?? null; if (toModule === fromModule) { - // eslint-disable-next-line no-continue continue; } - // Skip boring internal imports - if (resolvedTarget.includes('m_core') || resolvedTarget.includes('m_data_source_adapter')) { - // eslint-disable-next-line no-continue + // Skip boring internal imports (exact path segment match) + const targetSegments = resolvedTarget.split('/'); + if (targetSegments.some((seg) => CROSS_DEP_IGNORED_SEGMENTS.has(seg))) { continue; } const key = `${fromModule}→${targetFile}`; - if (seen.has(key)) { - const existing = deps.find( - (d) => d.fromModule === fromModule && d.toRelPath === targetFile, - ); - if (existing && !existing.importedNames.includes(localName)) { + const existing = depsMap.get(key); + if (existing) { + if (!existing.importedNames.includes(localName)) { existing.importedNames.push(localName); - existing.label = existing.importedNames.join(', '); } - // eslint-disable-next-line no-continue continue; } - seen.add(key); - deps.push({ + depsMap.set(key, { fromModule, fromRelPath: pf.relPath, toRelPath: targetFile, toModule, importedNames: [localName], importPath: imp.fromPath, - label: localName, }); } } - return deps.sort((a, b) => a.fromModule.localeCompare(b.fromModule)); + return [...depsMap.values()].sort((a, b) => a.fromModule.localeCompare(b.fromModule)); } diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts index 0d3cd2dcbf88..7703015fd23b 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/types.ts @@ -1,4 +1,4 @@ -/* eslint-disable spellcheck/spell-checker */ +import type { BaseClassInfo, HeritageInfo, InheritanceEntry as SharedInheritanceEntry } from '../shared/types'; import type { ModificationCategory } from './constants'; // ─── Parsed file data ──────────────────────────────────────────────────────── @@ -10,13 +10,7 @@ export interface ImportInfo { isFromGridCore: boolean; } -export interface ClassInfo { - className: string; - baseClass: string; - mixins: string[]; - sourceFile: string; - isExported: boolean; -} +export interface ClassInfo extends BaseClassInfo {} export interface RegisterModuleCall { moduleName: string; @@ -43,13 +37,11 @@ export interface RegisterModuleCall { forwardedViewsRef: string | null; } -export interface ControllerViewRef { +export interface ControllerViewRef extends HeritageInfo { regName: string; className: string; isImportedFromGridCore: boolean; isDefinedLocally: boolean; - baseClass: string; - mixins: string[]; sourceFile: string; } @@ -98,14 +90,7 @@ export interface ClassifiedModule { views: Record; }; - newControllers: string[]; - newViews: string[]; - overriddenControllers: string[]; - overriddenExtenderControllers: string[]; - overriddenExtenderViews: string[]; hasDefaultOptionsOverride: boolean; - - details: string; } export interface ExtenderPipelineStep { @@ -123,9 +108,7 @@ export interface ExtenderPipeline { steps: ExtenderPipelineStep[]; } -export interface InheritanceEntry { - className: string; - chain: string[]; +export interface InheritanceEntry extends SharedInheritanceEntry { sourceFile: string; } @@ -136,7 +119,13 @@ export interface CrossDependency { toModule: string | null; importedNames: string[]; importPath: string; - label: string; +} + +/** Controller/view entry as serialized in the grid_core JSON output. */ +export interface GridCoreControllerViewInfo extends HeritageInfo { + regName: string; + className: string; + sourceFile: string; } export interface GridCoreModuleInfo { @@ -144,20 +133,8 @@ export interface GridCoreModuleInfo { registeredAs: string | null; sourceFile: string; featureArea: string; - controllers: Record; - views: Record; + controllers: Record; + views: Record; extenders: { controllers: Record; views: Record; @@ -176,11 +153,5 @@ export interface ArchitectureData { dataSourceAdapterChain: DataSourceAdapterExtension[]; inheritanceChains: InheritanceEntry[]; crossDependencies: CrossDependency[]; - summary: { - total: number; - passthrough: number; - extended: number; - replaced: number; - new: number; - }; + summary: Record & { total: number }; } diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate-architecture-doc.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate.ts similarity index 77% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate-architecture-doc.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate.ts index ead74165dec3..1c573cca5b4c 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate-architecture-doc.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/generate.ts @@ -1,12 +1,13 @@ #!/usr/bin/env tsx -/* eslint-disable no-console, no-restricted-syntax */ -import * as fs from 'fs'; -import * as path from 'path'; +/* eslint-disable no-console */ -import { parseArgs } from './cli'; -import { GRID_CORE_ROOT, OUTPUT_DIR } from './constants'; +import { discoverSourceFiles, getRelativePath } from '../shared/file-discovery'; +import { writeOutputFiles } from '../shared/output-writer'; +import { + EXCLUDED_DIRS, EXCLUDED_FILE_NAMES, GRID_CORE_ROOT, OUTPUT_DIR, +} from './constants'; import { generateHtml } from './html-template'; -import { discoverSourceFiles, getRelativePath, parseFile } from './parser'; +import { parseFile } from './parser'; import { buildGlobalAliasMap, buildGlobalClassRegistry, @@ -30,17 +31,18 @@ function main(): void { try { // 1. Discover source files - const sourceFiles = discoverSourceFiles(GRID_CORE_ROOT); + const sourceFiles = discoverSourceFiles(GRID_CORE_ROOT, EXCLUDED_DIRS, EXCLUDED_FILE_NAMES); console.log(`Discovered ${sourceFiles.length} source files`); // 2. Parse all files const allParsedFiles = sourceFiles.flatMap((file) => { - console.log(`Parsing: ${getRelativePath(file)}`); + const relPath = getRelativePath(file, GRID_CORE_ROOT); + console.log(`Parsing: ${relPath}`); try { return [parseFile(file)]; } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); - console.warn(`WARN: Failed to parse ${getRelativePath(file)}: ${errorMessage}`); + console.warn(`WARN: Failed to parse ${relPath}: ${errorMessage}`); return []; } @@ -58,10 +60,11 @@ function main(): void { resolveAliasesInClasses(globalClasses, globalAliasMap); // 3. Collect modules and re-resolve cross-file class references + const fileByRelPath = new Map(allParsedFiles.map((pf) => [pf.relPath, pf])); const modules: ModuleInfo[] = []; for (const pf of allParsedFiles) { for (const mod of pf.modules) { - resolveModuleClassRefs(mod, pf, globalClasses, allParsedFiles, globalAliasMap); + resolveModuleClassRefs(mod, pf, globalClasses, fileByRelPath, globalAliasMap); modules.push(mod); } } @@ -103,22 +106,7 @@ function main(): void { }; // 9. Write output files - if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); - } - - const args = parseArgs(); - if (!args.htmlOnly) { - const jsonPath = path.join(OUTPUT_DIR, 'grid_core_architecture.generated.json'); - fs.writeFileSync(jsonPath, `${JSON.stringify(data, null, 2)}\n`); - console.log(`✓ JSON written to: ${jsonPath}`); - } - - if (!args.jsonOnly) { - const htmlPath = path.join(OUTPUT_DIR, 'grid_core_architecture.generated.html'); - fs.writeFileSync(htmlPath, generateHtml(data)); - console.log(`✓ HTML written to: ${htmlPath}`); - } + writeOutputFiles(OUTPUT_DIR, 'grid_core_architecture', data, generateHtml); console.log('\nSummary:'); console.log(` Modules: ${modules.length}`); @@ -132,6 +120,9 @@ function main(): void { } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); console.error(`ERROR: ${errorMessage}`); + if (e instanceof Error && e.stack) { + console.error(e.stack); + } process.exit(1); } } diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/graph-builder.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/graph-builder.ts index b8d9e841989f..42d6d75ce22c 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/graph-builder.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/graph-builder.ts @@ -1,13 +1,9 @@ /* eslint-disable spellcheck/spell-checker, max-depth */ +import type { CytoscapeElement } from '../shared/graph-context'; +import { createGraphContext } from '../shared/graph-context'; import { MODULE_ITEM_CLASS, MODULES_PREFIX } from './constants'; import type { ArchitectureData } from './types'; -interface CytoscapeElement { - group: 'nodes' | 'edges'; - data: Record; - classes?: string; -} - function nonEmpty(value: string): string | undefined { return value || undefined; } @@ -43,30 +39,21 @@ function buildNodeIdMap(data: ArchitectureData): Map { } export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement[] { - const elements: CytoscapeElement[] = []; - const nodeIds = new Set(); - const edgeIds = new Set(); - const nodeParent = new Map(); // nodeId → parentId + const ctx = createGraphContext({ trackParent: true }); + const { + elements, nodeIds, edgeIds, nodeParent, addNode, + } = ctx; const nodeIdMap = buildNodeIdMap(data); - function addNode(id: string, nodeData: Record, classes: string): void { - if (nodeIds.has(id)) { - return; - } - - nodeIds.add(id); - if (nodeData.parent) { - nodeParent.set(id, nodeData.parent as string); - } - elements.push({ group: 'nodes', data: { id, ...nodeData }, classes }); - } - function addEdge( source: string, target: string, edgeData: Record, classes: string, ): void { + // ID keyed by classes — safe because each edge type (inheritance, extension, + // runtime) uses a distinct class string, and at most one edge of each type + // exists per source→target pair. const id = `e-${source}-${target}-${classes}`; if (!nodeIds.has(source) || !nodeIds.has(target) || edgeIds.has(id)) { @@ -170,7 +157,7 @@ export function buildCytoscapeElements(data: ArchitectureData): CytoscapeElement // 3. Add inheritance edges for (const entry of data.inheritanceChains) { - const sourceId = nodeIdMap.get(entry.class); + const sourceId = nodeIdMap.get(entry.className); if (!sourceId || !nodeIds.has(sourceId)) { // eslint-disable-next-line no-continue continue; diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/html-template.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/html-template.ts index b058fba7865b..b2e9beeff8b9 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/html-template.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/html-template.ts @@ -4,6 +4,13 @@ * Generates an interactive Cytoscape.js visualization. */ +import { + BASE_CSS, + EXTENDER_EDGE_BASE_STYLES, + GC_TARGET_CYTOSCAPE_STYLES, + HIGHLIGHT_CYTOSCAPE_STYLES, + SHARED_INTERACTIVE_JS, +} from '../shared/html-helpers'; import { buildCytoscapeElements } from './graph-builder'; import type { ArchitectureData } from './types'; @@ -23,36 +30,13 @@ export function generateHtml(data: ArchitectureData): string { Grid Core Architecture - @@ -120,7 +104,7 @@ const cy = cytoscape({ container: document.getElementById('cy'), elements: ELEMENTS, style: [ - // Compound nodes (modules) — styled like data_grid's node.module.grid-core + // Compound nodes (modules) — grid-core barrel style with label overrides { selector: 'node.module', style: { 'shape': 'barrel', @@ -139,149 +123,61 @@ const cy = cytoscape({ 'text-margin-y': -4, } }, - // Extension-only modules — same style as regular modules + // Extension-only modules { selector: 'node.module.ext-only', - style: { - 'min-width': 80, - 'min-height': 30, - } + style: { 'min-width': 80, 'min-height': 30 } }, - // Controller nodes — styled like data_grid's gc-target-controller + // GC-target nodes (shared styles for controller/view) + ${GC_TARGET_CYTOSCAPE_STYLES} + // Grid_core-specific: explicit sizing for gc-target nodes { selector: 'node.gc-target-controller', - style: { - 'shape': 'hexagon', - 'background-color': '#1e1e3a', - 'background-opacity': 0.6, - 'border-width': 2, - 'border-style': 'dashed', - 'border-color': '#7dd3fc', - 'label': 'data(label)', - 'text-valign': 'center', - 'text-halign': 'center', - 'font-size': 9, - 'color': '#bae6fd', - 'text-wrap': 'wrap', - 'text-max-width': '120px', - 'width': labelWidth(9), - 'min-width': 30, - 'height': 30, - 'padding': '12px', - } + style: { 'width': labelWidth(9), 'min-width': 30, 'height': 30 } }, - // View nodes — styled like data_grid's gc-target-view { selector: 'node.gc-target-view', - style: { - 'shape': 'ellipse', - 'background-color': '#1e1e3a', - 'background-opacity': 0.6, - 'border-width': 2, - 'border-style': 'dashed', - 'border-color': '#c084fc', - 'label': 'data(label)', - 'text-valign': 'center', - 'text-halign': 'center', - 'font-size': 9, - 'color': '#d8b4fe', - 'text-wrap': 'wrap', - 'text-max-width': '120px', - 'width': labelWidth(9), - 'min-width': 30, - 'height': 26, - 'padding': '12px', - } + style: { 'width': labelWidth(9), 'min-width': 30, 'height': 26 } }, // Inheritance edges (ctrl) — dashed, same color as extension ctrl { selector: 'edge.edge-inherit-ctrl', style: { - 'line-color': '#0ea5e9', - 'target-arrow-color': '#0ea5e9', - 'target-arrow-shape': 'triangle', - 'line-style': 'dashed', - 'curve-style': 'bezier', - 'width': 2, - 'arrow-scale': 0.8, - 'opacity': 0.8, + 'line-color': '#0ea5e9', 'target-arrow-color': '#0ea5e9', + 'target-arrow-shape': 'triangle', 'line-style': 'dashed', + 'curve-style': 'bezier', 'width': 2, 'arrow-scale': 0.8, 'opacity': 0.8, } }, // Inheritance edges (view) — dashed, same color as extension view { selector: 'edge.edge-inherit-view', style: { - 'line-color': '#a855f7', - 'target-arrow-color': '#a855f7', - 'target-arrow-shape': 'triangle', - 'line-style': 'dashed', - 'curve-style': 'bezier', - 'width': 2, - 'arrow-scale': 0.8, - 'opacity': 0.8, - } - }, - // Extension edges (controller) — styled like data_grid's edge-ext-ctrl - { selector: 'edge.edge-ext-ctrl', - style: { - 'line-color': '#0ea5e9', - 'target-arrow-color': '#0ea5e9', - 'target-arrow-shape': 'triangle', - 'curve-style': 'bezier', - 'width': 2, - 'opacity': 0.8, - 'arrow-scale': 0.8, + 'line-color': '#a855f7', 'target-arrow-color': '#a855f7', + 'target-arrow-shape': 'triangle', 'line-style': 'dashed', + 'curve-style': 'bezier', 'width': 2, 'arrow-scale': 0.8, 'opacity': 0.8, } }, - // Extension edges (view) — styled like data_grid's edge-ext-view - { selector: 'edge.edge-ext-view', - style: { - 'line-color': '#a855f7', - 'target-arrow-color': '#a855f7', - 'target-arrow-shape': 'triangle', - 'curve-style': 'bezier', - 'width': 2, - 'opacity': 0.8, - 'arrow-scale': 0.8, - } + // Extension edges (shared base styles) + ${EXTENDER_EDGE_BASE_STYLES} + // Grid_core-specific: add opacity to extender edges + { selector: 'edge.edge-ext-ctrl, edge.edge-ext-view', + style: { 'opacity': 0.8 } }, // Runtime dependency edges — dotted, yellow { selector: 'edge.edge-runtime', style: { - 'line-color': '#f6e05e', - 'target-arrow-color': '#f6e05e', - 'target-arrow-shape': 'triangle', - 'line-style': 'dotted', - 'curve-style': 'bezier', - 'width': 1, - 'arrow-scale': 0.6, - 'opacity': 0.5, + 'line-color': '#f6e05e', 'target-arrow-color': '#f6e05e', + 'target-arrow-shape': 'triangle', 'line-style': 'dotted', + 'curve-style': 'bezier', 'width': 1, 'arrow-scale': 0.6, 'opacity': 0.5, } }, // Highlighted state - { selector: '.highlighted', - style: { 'opacity': 1, 'z-index': 999 } - }, - { selector: 'edge.highlighted', - style: { 'opacity': 1, 'z-index': 999, 'width': 3 } - }, - { selector: '.faded', - style: { 'opacity': 0.08 } - }, - { selector: 'node.search-match', - style: { 'border-width': 3, 'border-color': '#FF6B6B' } - }, + ${HIGHLIGHT_CYTOSCAPE_STYLES} // Cross-compound edges must use bezier to avoid "invalid endpoints" warnings with taxi routing { selector: 'edge.cross-compound', - style: { - 'curve-style': 'bezier', - } + style: { 'curve-style': 'bezier' } }, ], layout: { name: 'preset' }, }); // ── Edge Routing Helper ───────────────────────── - -function getEdgeRouting() { - const checked = document.querySelector('input[name="edge-routing"]:checked'); - return checked ? checked.value : 'taxi'; -} +// Note: getEdgeRouting() and hasOverlappingBounds() are provided by SHARED_INTERACTIVE_JS function updateEdgeStyles() { const curveStyle = getEdgeRouting(); @@ -303,14 +199,7 @@ function updateEdgeStyles() { cy.edges().not('.cross-compound').forEach(function(edge) { const src = edge.source(); const tgt = edge.target(); - if (tgt.data('parent') === src.id() || src.data('parent') === tgt.id()) { - edge.style({ 'curve-style': 'bezier', 'taxi-direction': null, 'taxi-turn': null }); - return; - } - const sb = src.boundingBox(); - const tb = tgt.boundingBox(); - const overlaps = !(sb.x2 < tb.x1 || tb.x2 < sb.x1 || sb.y2 < tb.y1 || tb.y2 < sb.y1); - if (overlaps) { + if (tgt.data('parent') === src.id() || src.data('parent') === tgt.id() || hasOverlappingBounds(edge)) { edge.style({ 'curve-style': 'bezier', 'taxi-direction': null, 'taxi-turn': null }); } }); @@ -320,7 +209,6 @@ function updateEdgeStyles() { 'taxi-direction': null, 'taxi-turn': null, }); - // Cross-compound and overlapping edges always use regular bezier cy.edges('.cross-compound').style({ 'curve-style': 'bezier', 'taxi-direction': null, @@ -329,14 +217,7 @@ function updateEdgeStyles() { cy.edges().not('.cross-compound').forEach(function(edge) { var src = edge.source(); var tgt = edge.target(); - if (tgt.data('parent') === src.id() || src.data('parent') === tgt.id()) { - edge.style({ 'curve-style': 'bezier' }); - return; - } - var sb = src.boundingBox(); - var tb = tgt.boundingBox(); - var overlaps = !(sb.x2 < tb.x1 || tb.x2 < sb.x1 || sb.y2 < tb.y1 || tb.y2 < sb.y1); - if (overlaps) { + if (tgt.data('parent') === src.id() || src.data('parent') === tgt.id() || hasOverlappingBounds(edge)) { edge.style({ 'curve-style': 'bezier' }); } }); @@ -716,46 +597,19 @@ function computeHighlightSet(target) { return connectedSet(leafSet); } -function applyHighlight(highlightSet) { - cy.elements().addClass('faded').removeClass('highlighted'); - highlightSet.removeClass('faded').addClass('highlighted'); -} - -function clearHighlight() { - cy.elements().removeClass('faded').removeClass('highlighted'); -} - // ── State ────────────── -let selectedTarget = null; - -// ── Click to highlight ─────── -cy.on('tap', 'node, edge', function(e) { - const target = e.target; - if (selectedTarget && selectedTarget.id() === target.id()) { - selectedTarget = null; - clearHighlight(); - infoPanel.innerHTML = '

Click a node or edge to see details.

'; - return; - } - selectedTarget = target; - const set = computeHighlightSet(target); - applyHighlight(set); -}); - -cy.on('tap', function(e) { - if (e.target === cy && selectedTarget) { - selectedTarget = null; - clearHighlight(); - infoPanel.innerHTML = '

Click a node or edge to see details.

'; - } -}); +var selectedTarget = null; -// ── Info Panel ───── -const infoPanel = document.getElementById('info-panel'); -cy.on('tap', 'node', function(e) { - const d = e.target.data(); - let html = '

' + (d.label || d.id) + '

'; - if (d.nodeType === 'module') { +// ── Info Panel (grid_core-specific) ───── +function showInfo(target) { + var d = target.data(); + var html = '

' + (d.label || d.id) + '

'; + if (target.isEdge()) { + html = '

Edge: ' + d.edgeType + '

' + + '

From: ' + d.source + '

' + + '

To: ' + d.target + '

' + + (d.extenderName ? '

Extender: ' + d.extenderName + '

' : ''); + } else if (d.nodeType === 'module') { html += '

Source: ' + (d.sourceFile || '') + '

'; html += '

Area: ' + (d.featureArea || '') + '

'; if (d.definesControllers) html += '

Controllers: ' + d.definesControllers + '

'; @@ -769,45 +623,13 @@ cy.on('tap', 'node', function(e) { if (d.mixins) html += '

Mixins: ' + d.mixins + '

'; html += '

Source: ' + (d.sourceFile || '') + '

'; } - infoPanel.innerHTML = html; -}); -cy.on('tap', 'edge', function(e) { - const d = e.target.data(); - infoPanel.innerHTML = '

Edge: ' + d.edgeType + '

' - + '

From: ' + d.source + '

' - + '

To: ' + d.target + '

' - + (d.extenderName ? '

Extender: ' + d.extenderName + '

' : ''); -}); - -// ── Search ────────────────────────────────────── -document.getElementById('search').addEventListener('input', function(e) { - const q = e.target.value.toLowerCase(); - cy.nodes().removeClass('search-match'); - if (!q) return; - const matches = cy.nodes().filter(n => (n.data('label') || '').toLowerCase().includes(q) || n.data('id').toLowerCase().includes(q)); - matches.addClass('search-match'); - if (matches.length === 1) { - cy.animate({ center: { eles: matches }, zoom: 1.5 }, { duration: 300 }); - } else if (matches.length > 0) { - cy.animate({ fit: { eles: matches, padding: 50 } }, { duration: 300 }); - } -}); + document.getElementById('info-panel').innerHTML = html; +} -// ── Edge Type Toggles ─────────────────────────── -document.querySelectorAll('.edge-toggle').forEach(function(cb) { - cb.addEventListener('change', function() { - var cls = this.getAttribute('data-cls'); - cy.edges('.' + cls).style('display', this.checked ? 'element' : 'none'); - if (selectedTarget) { - const set = computeHighlightSet(selectedTarget); - cy.elements().removeClass('highlighted').addClass('faded'); - set.removeClass('faded').addClass('highlighted'); - } - }); - if (!cb.checked) { var cls = cb.getAttribute('data-cls'); cy.edges('.' + cls).style('display', 'none'); } -}); +// ── Shared interactive JS (highlight, edge toggles, search, click handlers, fit button, routing radio) ── +${SHARED_INTERACTIVE_JS} -// ── Feature Area Toggles ──────────────────────── +// ── Grid_core-specific: Feature Area Toggles ──────────────────────── document.getElementById('toggle-all-areas').addEventListener('change', function() { const checked = this.checked; document.querySelectorAll('.area-toggle').forEach(cb => { @@ -850,16 +672,11 @@ document.querySelectorAll('.area-toggle').forEach(cb => { }); }); -// ── Layout Selector ───────────────────────────── -document.querySelectorAll('input[name="edge-routing"]').forEach(radio => { - radio.addEventListener('change', function() { runDepLevelsLayout(); }); -}); - -// ── Buttons ───────────────────────────────────── -document.getElementById('btn-fit').addEventListener('click', () => cy.fit(undefined, 40)); +// ── Grid_core-specific: Reset Button ───────────────────────────── document.getElementById('btn-reset').addEventListener('click', () => { selectedTarget = null; - cy.elements().removeClass('faded highlighted search-match'); + clearHighlight(); + cy.elements().removeClass('search-match'); document.getElementById('search').value = ''; document.getElementById('toggle-all-areas').checked = true; document.getElementById('toggle-all-areas').indeterminate = false; diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/parser.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/parser.ts index 7c27c23c2c29..72516fe05717 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/parser.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/parser.ts @@ -1,12 +1,16 @@ -/* eslint-disable spellcheck/spell-checker,no-restricted-syntax,max-depth */ +/* eslint-disable spellcheck/spell-checker,max-depth */ import * as fs from 'fs'; -import * as path from 'path'; // eslint-disable-next-line import/no-extraneous-dependencies import ts from 'typescript'; import { - EXCLUDED_DIRS, - EXCLUDED_FILE_NAMES, + collectImportSpecs, + getClassHeritage, + getNodeText, + hasExportModifier, +} from '../shared/ast-helpers'; +import { getRelativePath } from '../shared/file-discovery'; +import { getFeatureAreaFromPath, GRID_CORE_ROOT, MODULE_SUFFIX, @@ -15,97 +19,7 @@ import type { ClassRegistrationInfo, ExtenderInfo, ModuleInfo, ParsedFile, RuntimeDependency, } from './types'; -// ─── File Discovery ────────────────────────────────────────────────────────── - -export function discoverSourceFiles(rootDir: string): string[] { - const results: string[] = []; - - function walk(dir: string): void { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - if (EXCLUDED_DIRS.has(entry.name)) { - // eslint-disable-next-line no-continue - continue; - } - walk(fullPath); - } else if ( - entry.isFile() - && !EXCLUDED_FILE_NAMES.has(entry.name) - && entry.name.endsWith('.ts') - && !entry.name.includes('.test.') - ) { - results.push(fullPath); - } - } - } - - walk(rootDir); - return results.sort(); -} - -// ─── AST Helpers ───────────────────────────────────────────────────────────── - -export function getRelativePath(filePath: string): string { - return path.relative(GRID_CORE_ROOT, filePath).replace(/\\/g, '/'); -} - -function getNodeText(node: ts.Node, sourceFile: ts.SourceFile): string { - return node.getText(sourceFile).trim(); -} - -/** - * Parse a heritage string like "Mixin(Base)" or "Mixin(Mixin2(Base))". - * - * Note: only handles single-argument mixin calls (which is the pattern used - * throughout the grid_core codebase). Multi-argument patterns are not supported. - */ -export function parseHeritageString(text: string): { baseClass: string; mixins: string[] } { - const mixins: string[] = []; - - let current = text; - while (true) { - const match = /^(\w+)\((.+)\)$/.exec(current); - if (match) { - const [, mixinName, innerExpr] = match; - mixins.push(mixinName); - current = innerExpr; - } else { - break; - } - } - - return { baseClass: mixins.length > 0 ? `${mixins[mixins.length - 1]}(${current})` : current, mixins }; -} - -/** - * Parse a class heritage expression like: - * - `modules.Controller` - * - `ColumnsView` - * - `ColumnStateMixin(modules.View)` - * - `ColumnContextMenuMixin(ColumnsView)` - * - `EditorFactoryMixin(modules.ViewController)` - * - * Returns { baseClass, mixins } - */ -function parseHeritageExpression( - expr: ts.Expression, - sourceFile: ts.SourceFile, - localVars: Map, -): { baseClass: string; mixins: string[] } { - const text = getNodeText(expr, sourceFile); - - if (ts.isIdentifier(expr) && localVars.has(text)) { - const resolved = localVars.get(text) ?? ''; - return parseHeritageString(resolved); - } - - return parseHeritageString(text); -} - // ─── Runtime Dependency Collection ─────────────────────────────────────────── - function collectRuntimeDeps( node: ts.Node, sourceFile: ts.SourceFile, @@ -158,32 +72,6 @@ function collectRuntimeDeps( // ─── Module Parsing ────────────────────────────────────────────────────────── -function hasExportModifier(node: ts.Node): boolean { - if (!ts.canHaveModifiers(node)) { - return false; - } - const modifiers = ts.getModifiers(node); - return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; -} - -function getClassHeritage( - node: ts.ClassDeclaration | ts.ClassExpression, - sourceFile: ts.SourceFile, - localVars: Map, -): { baseClass: string; mixins: string[] } { - if (!node.heritageClauses) { - return { baseClass: '', mixins: [] }; - } - - for (const clause of node.heritageClauses) { - if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length > 0) { - return parseHeritageExpression(clause.types[0].expression, sourceFile, localVars); - } - } - - return { baseClass: '', mixins: [] }; -} - function guessRegisteredName(moduleName: string): string | null { if (moduleName.endsWith(MODULE_SUFFIX)) { return moduleName.slice(0, -MODULE_SUFFIX.length); @@ -343,7 +231,7 @@ export function parseFile(filePath: string): ParsedFile { ts.ScriptKind.TS, ); - const relPath = getRelativePath(filePath); + const relPath = getRelativePath(filePath, GRID_CORE_ROOT); const result: ParsedFile = { filePath, relPath, @@ -356,32 +244,19 @@ export function parseFile(filePath: string): ParsedFile { }; // Collect import aliases (import { X as Y } from '...') - ts.forEachChild(sourceFile, (node) => { - if (ts.isImportDeclaration(node) - && node.importClause && node.moduleSpecifier - && ts.isStringLiteral(node.moduleSpecifier) - ) { - const fromPath = node.moduleSpecifier.text; - const { namedBindings } = node.importClause; - if (namedBindings && ts.isNamedImports(namedBindings)) { - for (const spec of namedBindings.elements) { - const localName = spec.name.text; - const originalName = spec.propertyName ? spec.propertyName.text : spec.name.text; - result.importedNames.set(localName, originalName); - if (spec.propertyName) { - result.importAliases.set(localName, { - localName, - originalName, - fromPath, - }); - } - } - } - // Handle default imports: import X from '...' - if (node.importClause.name) { - const localName = node.importClause.name.text; - result.importedNames.set(localName, localName); - } + collectImportSpecs(sourceFile).forEach((spec) => { + if (!spec.isNamespace) { + result.importedNames.set( + spec.localName, + spec.isDefault ? spec.localName : spec.originalName, + ); + } + if (spec.isRenamed) { + result.importAliases.set(spec.localName, { + localName: spec.localName, + originalName: spec.originalName, + fromPath: spec.fromPath, + }); } }); diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/resolver.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/resolver.ts index b347176efc87..850b587d37cb 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/resolver.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/resolver.ts @@ -1,4 +1,7 @@ -/* eslint-disable spellcheck/spell-checker, no-restricted-syntax, max-depth */ +/* eslint-disable spellcheck/spell-checker, max-depth */ +import { parseMixinCall, stripAllMixins } from '../shared/ast-helpers'; +import { buildInheritanceChainCore } from '../shared/inheritance'; +import type { HeritageInfo } from '../shared/types'; import { BARE_MODULE_BASES, getFeatureAreaFromPath, @@ -33,7 +36,7 @@ export function buildGlobalClassRegistry( for (const pf of allParsedFiles) { for (const [className, info] of pf.classes) { - const entry: GlobalClassInfo = { ...info, sourceFile: pf.relPath }; + const entry: GlobalClassInfo = { ...info, className, sourceFile: pf.relPath }; const existingEntry = registry.get(className); if (!existingEntry) { registry.set(className, entry); @@ -57,6 +60,8 @@ export function buildGlobalClassRegistry( }); if (baseEntry) { registry.set(className, baseEntry); + } else { + console.warn(`WARN: Duplicate class "${className}" found in ${entries.map((e) => e.sourceFile).join(', ')}; keeping first-seen entry (no entry matched the base-class heuristic).`); } } @@ -104,11 +109,10 @@ export function resolveAliasInString(text: string, aliasMap: Map } // Mixin pattern: "Mixin(AliasName)" -> "Mixin(RealName)" - const mixinMatch = /^(\w+)\((.+)\)$/.exec(text); - if (mixinMatch) { - const [, mixinPart, innerPart] = mixinMatch; - const mixin = resolveAliasInString(mixinPart, aliasMap); - const inner = resolveAliasInString(innerPart, aliasMap); + const parsed = parseMixinCall(text); + if (parsed) { + const mixin = resolveAliasInString(parsed.mixinName, aliasMap); + const inner = resolveAliasInString(parsed.inner, aliasMap); return `${mixin}(${inner})`; } @@ -143,10 +147,9 @@ export function normalizeModuleRef(baseClass: string): string { result = result.replace(/^core\./, 'modules.'); // Also normalize inside mixin calls: "Mixin(Modules.View)" → "Mixin(modules.View)" - const mixinMatch = /^(\w+)\((.+)\)$/.exec(result); - if (mixinMatch) { - const [, mixinName, mixinInner] = mixinMatch; - return `${mixinName}(${normalizeModuleRef(mixinInner)})`; + const parsed = parseMixinCall(result); + if (parsed) { + return `${parsed.mixinName}(${normalizeModuleRef(parsed.inner)})`; } return result; @@ -222,12 +225,7 @@ function isModuleBaseDescendant( } // Strip mixin wrapper to get the innermost base - let rawBase = baseClass; - let match = /^\w+\((.+)\)$/.exec(rawBase); - while (match) { - [, rawBase] = match; - match = /^\w+\((.+)\)$/.exec(rawBase); - } + const rawBase = stripAllMixins(baseClass); if (rawBase.startsWith(MODULES_PREFIX)) { return true; @@ -262,6 +260,19 @@ export function findStandaloneRegistrations( } } + // Pre-build a set of class names that are used as a base class or mixin by any other class. + // Uses exact name matching (via stripAllMixins) instead of substring includes(). + const usedAsBaseOrMixin = new Set(); + for (const [, info] of globalClasses) { + const rawBase = stripAllMixins(info.baseClass); + if (rawBase) { + usedAsBaseOrMixin.add(rawBase); + } + for (const mixin of info.mixins) { + usedAsBaseOrMixin.add(mixin); + } + } + const controllers: Record = {}; const views: Record = {}; @@ -271,16 +282,8 @@ export function findStandaloneRegistrations( continue; } - // Include if used as a base class OR if it descends from a module base - let isUsed = false; - for (const [otherName, otherInfo] of globalClasses) { - if (otherName !== className - && (otherInfo.baseClass.includes(className) || otherInfo.mixins.includes(className)) - ) { - isUsed = true; - break; - } - } + // Include if used as a base class or mixin OR if it descends from a module base + const isUsed = usedAsBaseOrMixin.has(className); if (!isUsed && !isModuleBaseDescendant(className, globalClasses)) { // eslint-disable-next-line no-continue continue; @@ -324,7 +327,7 @@ export function buildInheritanceChains( standaloneViews: Record, globalClasses: Map, ): InheritanceEntry[] { - const allClasses = new Map(); + const allClasses = new Map(); const bareModuleBasesSet = new Set(BARE_MODULE_BASES); for (const [className, info] of globalClasses) { @@ -336,61 +339,41 @@ export function buildInheritanceChains( } const entries: InheritanceEntry[] = []; - const processed = new Set(); - - function buildChain(className: string): string[] { - if (processed.has(className)) { - console.warn(`Circular inheritance detected in class hierarchy involving "${className}".`); - return []; - } - processed.add(className); - const info = allClasses.get(className); - if (!info?.baseClass) { - return []; - } - - const chain: string[] = []; - - for (const mixin of info.mixins) { - chain.push(mixin); - } - - let rawBase = info.baseClass; - const mixinMatch = /^\w+\((.+)\)$/.exec(rawBase); - if (mixinMatch) { - [, rawBase] = mixinMatch; - } - - chain.push(rawBase); - - const baseName = rawBase.replace(MODULES_PREFIX, ''); - if (allClasses.has(baseName)) { - chain.push(...buildChain(baseName)); - } else if (rawBase.startsWith(MODULES_PREFIX)) { - if (rawBase === 'modules.View' || rawBase === 'modules.Controller') { - chain.push('ModuleItem'); - } else if (rawBase === 'modules.ViewController') { - chain.push('modules.Controller', 'ModuleItem'); + const visited = new Set(); + + const getClassInfo = (name: string): HeritageInfo | undefined => allClasses.get(name); + const chainOptions = { + resolveNext: (rawBase: string): string => rawBase.replace(MODULES_PREFIX, ''), + onTerminal: (rawBase: string): string[] => { + switch (rawBase) { + case 'modules.View': + case 'modules.Controller': + return ['ModuleItem']; + case 'modules.ViewController': + return ['modules.Controller', 'ModuleItem']; + default: + return []; } - } - - return chain; - } + }, + onCycle: (name: string): void => { + console.warn(`Circular inheritance detected in class hierarchy involving "${name}".`); + }, + }; // Build chains for all controllers and views in modules for (const mod of modules) { for (const ctrl of Object.values(mod.controllers)) { - processed.clear(); - const chain = buildChain(ctrl.className); + visited.clear(); + const chain = buildInheritanceChainCore(ctrl.className, getClassInfo, visited, chainOptions); if (chain.length > 0) { - entries.push({ class: ctrl.className, chain }); + entries.push({ className: ctrl.className, chain }); } } for (const view of Object.values(mod.views)) { - processed.clear(); - const chain = buildChain(view.className); + visited.clear(); + const chain = buildInheritanceChainCore(view.className, getClassInfo, visited, chainOptions); if (chain.length > 0) { - entries.push({ class: view.className, chain }); + entries.push({ className: view.className, chain }); } } } @@ -401,14 +384,14 @@ export function buildInheritanceChains( ...Object.values(standaloneViews), ]; for (const entry of standAloneEntries) { - processed.clear(); - const chain = buildChain(entry.className); + visited.clear(); + const chain = buildInheritanceChainCore(entry.className, getClassInfo, visited, chainOptions); if (chain.length > 0) { - entries.push({ class: entry.className, chain }); + entries.push({ className: entry.className, chain }); } } - return entries.sort((a, b) => a.class.localeCompare(b.class)); + return entries.sort((a, b) => a.className.localeCompare(b.className)); } // ─── Runtime Dependency Resolution ─────────────────────────────────────────── @@ -441,20 +424,18 @@ export function resolveRuntimeDeps( } } + // Build a map: sourceFile → moduleName (for fallback lookup) + const sourceFileToModule = new Map(); + for (const mod of modules) { + sourceFileToModule.set(mod.sourceFile, mod.moduleName); + } + for (const pf of allParsedFiles) { for (const dep of pf.runtimeDeps) { - let fromModule = classToModule.get(dep.from) ?? ''; - if (!fromModule) { - fromModule = extenderToModule.get(dep.from) ?? ''; - } - if (!fromModule) { - for (const mod of modules) { - if (mod.sourceFile === pf.relPath) { - fromModule = mod.moduleName; - break; - } - } - } + const fromModule = classToModule.get(dep.from) + ?? extenderToModule.get(dep.from) + ?? sourceFileToModule.get(pf.relPath) + ?? ''; allDeps.push({ ...dep, fromModule }); } @@ -480,19 +461,21 @@ export function resolveRuntimeDeps( * Re-resolve controllers and views in a module definition using the global class registry. * This fixes the case where a class is defined in one file and referenced * in the module definition in another file. + * + * @param mod + * @param parsedFile + * @param globalClasses + * @param fileByRelPath - Pre-built map from relPath to ParsedFile. + * Build once with: `new Map(allParsedFiles.map(pf => [pf.relPath, pf]))` + * @param globalAliasMap */ export function resolveModuleClassRefs( mod: ModuleInfo, parsedFile: ParsedFile, globalClasses: Map, - allParsedFiles: ParsedFile[], + fileByRelPath: Map, globalAliasMap: Map, ): void { - const fileByRelPath = new Map(); - for (const pf of allParsedFiles) { - fileByRelPath.set(pf.relPath, pf); - } - const resolveEntry = ( entry: { baseClass: string; diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/types.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/types.ts index 7a931c6a1c8c..43fa08362ac3 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/types.ts @@ -1,9 +1,8 @@ -export interface ClassRegistrationInfo { - className: string; - baseClass: string; - mixins: string[]; - sourceFile: string; - isExported: boolean; +import type { BaseClassInfo, HeritageInfo, InheritanceEntry } from '../shared/types'; + +export type { InheritanceEntry }; + +export interface ClassRegistrationInfo extends BaseClassInfo { featureArea: string; } @@ -35,11 +34,6 @@ export interface RuntimeDependency { location: string; } -export interface InheritanceEntry { - class: string; - chain: string[]; -} - export interface ArchitectureData { generatedAt: string; sourceRoot: string; @@ -60,16 +54,11 @@ export interface ParsedFile { filePath: string; relPath: string; modules: ModuleInfo[]; - classes: Map; + classes: Map; runtimeDeps: RuntimeDependency[]; localVars: Map; importAliases: Map; importedNames: Map; } -export interface GlobalClassInfo { - baseClass: string; - mixins: string[]; - sourceFile: string; - isExported: boolean; -} +export interface GlobalClassInfo extends BaseClassInfo {} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/ast-helpers.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/ast-helpers.ts new file mode 100644 index 000000000000..f28510de9143 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/ast-helpers.ts @@ -0,0 +1,169 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import ts from 'typescript'; + +import type { HeritageInfo } from './types'; + +export function getNodeText(node: ts.Node, sourceFile: ts.SourceFile): string { + return node.getText(sourceFile).trim(); +} + +export function hasExportModifier(node: ts.Node): boolean { + if (!ts.canHaveModifiers(node)) { + return false; + } + + const modifiers = ts.getModifiers(node); + + return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; +} + +export interface RawImportSpec { + localName: string; + originalName: string; + fromPath: string; + isDefault: boolean; + isNamespace: boolean; + isRenamed: boolean; +} + +/** + * Collect all import specifiers from a source file. + * Returns a flat list of raw import specs that callers can map into their own structures. + */ +export function collectImportSpecs(sourceFile: ts.SourceFile): RawImportSpec[] { + const specs: RawImportSpec[] = []; + + ts.forEachChild(sourceFile, (node) => { + if ( + !ts.isImportDeclaration(node) + || !node.moduleSpecifier + || !ts.isStringLiteral(node.moduleSpecifier) + ) { + return; + } + + const fromPath = node.moduleSpecifier.text; + const { importClause } = node; + + if (!importClause) { + return; + } + + // Default import: import X from '...' + if (importClause.name) { + specs.push({ + localName: importClause.name.text, + originalName: 'default', + fromPath, + isDefault: true, + isNamespace: false, + isRenamed: false, + }); + } + + if (!importClause.namedBindings) { + return; + } + + // Named imports: import { X, Y as Z } from '...' + if (ts.isNamedImports(importClause.namedBindings)) { + importClause.namedBindings.elements.forEach((spec) => { + const localName = spec.name.text; + const originalName = spec.propertyName ? spec.propertyName.text : localName; + specs.push({ + localName, + originalName, + fromPath, + isDefault: false, + isNamespace: false, + isRenamed: !!spec.propertyName, + }); + }); + } else if (ts.isNamespaceImport(importClause.namedBindings)) { + // Namespace import: import * as X from '...' + specs.push({ + localName: importClause.namedBindings.name.text, + originalName: '*', + fromPath, + isDefault: false, + isNamespace: true, + isRenamed: false, + }); + } + }); + + return specs; +} + +// ─── Mixin-wrapper utilities ───────────────────────────────────────────────── + +const MIXIN_CALL_RE = /^(\w+)\((.+)\)$/; + +/** Decompose the outermost mixin call: "Mixin(Inner)" → { mixinName, inner } */ +export function parseMixinCall(text: string): { mixinName: string; inner: string } | null { + const match = MIXIN_CALL_RE.exec(text); + return match ? { mixinName: match[1], inner: match[2] } : null; +} + +/** Strip the outermost mixin wrapper: "Mixin(Base)" → "Base" */ +export function stripOuterMixin(text: string): string { + const parsed = parseMixinCall(text); + return parsed ? parsed.inner : text; +} + +/** Strip all mixin wrappers: "Mixin(Mixin2(Base))" → "Base" */ +export function stripAllMixins(text: string): string { + let current = text; + let parsed = parseMixinCall(current); + while (parsed) { + current = parsed.inner; + parsed = parseMixinCall(current); + } + return current; +} + +/** + * Parse a heritage string like "Mixin(Base)" or "Mixin(Mixin2(Base))". + * Returns { baseClass, mixins }. + */ +export function parseHeritageString(text: string): HeritageInfo { + const mixins: string[] = []; + let current = text; + + let parsed = parseMixinCall(current); + while (parsed) { + mixins.push(parsed.mixinName); + current = parsed.inner; + parsed = parseMixinCall(current); + } + + return { + baseClass: mixins.length > 0 ? `${mixins[mixins.length - 1]}(${current})` : current, + mixins, + }; +} + +/** + * Extract heritage (base class + mixins) from a class declaration/expression. + * Resolves local variable aliases before parsing. + */ +export function getClassHeritage( + node: ts.ClassDeclaration | ts.ClassExpression, + sourceFile: ts.SourceFile, + localVars: Map, +): HeritageInfo { + if (!node.heritageClauses) { + return { baseClass: '', mixins: [] }; + } + const extendsClause = node.heritageClauses.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length > 0, + ); + if (extendsClause) { + const text = getNodeText(extendsClause.types[0].expression, sourceFile); + if (ts.isIdentifier(extendsClause.types[0].expression) && localVars.has(text)) { + return parseHeritageString(localVars.get(text) ?? ''); + } + return parseHeritageString(text); + } + return { baseClass: '', mixins: [] }; +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/cli.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/cli.ts similarity index 62% rename from packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/cli.ts rename to packages/devextreme/js/__internal/grids/__docs__/scripts/shared/cli.ts index b3f3411b21e7..88b4128ad803 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/grid_core/cli.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/cli.ts @@ -1,18 +1,14 @@ -interface CliArgs { +export interface CliArgs { jsonOnly: boolean; htmlOnly: boolean; } export function parseArgs(): CliArgs { const args = process.argv.slice(2); - const result: CliArgs = { - jsonOnly: false, - htmlOnly: false, - }; + const result: CliArgs = { jsonOnly: false, htmlOnly: false }; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < args.length; i += 1) { - switch (args[i]) { + args.forEach((arg) => { + switch (arg) { case '--json': result.jsonOnly = true; break; @@ -20,10 +16,10 @@ export function parseArgs(): CliArgs { result.htmlOnly = true; break; default: - console.error(`Error: Unknown argument "${args[i]}"`); + console.error(`Error: Unknown argument "${arg}". Valid options are: --json, --html`); process.exit(1); } - } + }); if (result.jsonOnly && result.htmlOnly) { console.error('Error: Cannot specify both --json and --html. Use neither to generate both.'); diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/file-discovery.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/file-discovery.ts new file mode 100644 index 000000000000..112d71ae1795 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/file-discovery.ts @@ -0,0 +1,43 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Recursively discover TypeScript source files under `rootDir`, + * excluding specified directories and file names. + */ +export function discoverSourceFiles( + rootDir: string, + excludedDirs: Set, + excludedFileNames: Set, +): string[] { + const results: string[] = []; + + function walk(dir: string): void { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + entries.forEach((entry) => { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (!excludedDirs.has(entry.name)) { + walk(fullPath); + } + } else if ( + entry.isFile() + && !excludedFileNames.has(entry.name) + && entry.name.endsWith('.ts') + && !entry.name.includes('.test.') + ) { + results.push(fullPath); + } + }); + } + + walk(rootDir); + return results.sort(); +} + +/** + * Get a forward-slash-separated relative path from `rootDir` to `filePath`. + */ +export function getRelativePath(filePath: string, rootDir: string): string { + return path.relative(rootDir, filePath).replace(/\\/g, '/'); +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/graph-context.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/graph-context.ts new file mode 100644 index 000000000000..3b571aa8b1da --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/graph-context.ts @@ -0,0 +1,56 @@ +/* eslint-disable spellcheck/spell-checker */ +/** + * Shared types used by both grid_core and data_grid graph builders. + */ + +export interface CytoscapeElement { + group: 'nodes' | 'edges'; + data: Record; + classes?: string; +} + +/** + * Provides shared addNode/addEdge helpers backed by common state (elements, nodeIds, edgeIds). + * Each graph-builder creates its own context and can extend edge creation via `buildEdgeId`. + */ +export interface GraphContext { + elements: CytoscapeElement[]; + nodeIds: Set; + edgeIds: Set; + addNode: (id: string, nodeData: Record, classes: string) => void; +} + +export interface GraphContextOptions { + /** + * If true, track each node's parent in a Map for compound-graph queries. + * The Map is returned in the context as `nodeParent`. + */ + trackParent?: boolean; +} + +export interface GraphContextWithParent extends GraphContext { + nodeParent: Map; +} + +export function createGraphContext(opts?: GraphContextOptions): GraphContextWithParent { + const elements: CytoscapeElement[] = []; + const nodeIds = new Set(); + const edgeIds = new Set(); + const nodeParent = new Map(); + const trackParent = opts?.trackParent ?? false; + + function addNode(id: string, nodeData: Record, classes: string): void { + if (nodeIds.has(id)) { + return; + } + nodeIds.add(id); + if (trackParent && nodeData.parent) { + nodeParent.set(id, nodeData.parent as string); + } + elements.push({ group: 'nodes', data: { id, ...nodeData }, classes }); + } + + return { + elements, nodeIds, edgeIds, nodeParent, addNode, + }; +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/html-helpers.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/html-helpers.ts new file mode 100644 index 000000000000..66bc1452173f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/html-helpers.ts @@ -0,0 +1,251 @@ +/** + * Shared CSS for html-templates. + */ +export const BASE_CSS = ` +* { + box-sizing: border-box; margin: 0; padding: 0 +} +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; height: 100vh; background: #1a1a1a; color: #e8e8e8 +} +#sidebar { + width: 270px; min-width: 270px; background: #2a2a2a; border-right: 1px solid #555; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px +} +#sidebar h2 { + font-size: 13px; color: #aaa; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 4px +} +#sidebar label { + display: flex; align-items: center; gap: 6px; font-size: 12px; cursor: pointer; padding: 1px 0; color: #e8e8e8 +} +#sidebar input[type="checkbox"] { + accent-color: #5B9BD5 +} +#sidebar input[type="text"] { + width: 100%; padding: 6px 10px; border: 1px solid #444; border-radius: 4px; font-size: 13px; background: #333; color: #e8e8e8; outline: none +} +#sidebar input[type="text"]:focus-visible { + border-color: #777 +} +#sidebar button { + padding: 5px 10px; border: 1px solid #555; border-radius: 4px; background: #333; cursor: pointer; font-size: 12px; color: #e8e8e8 +} +#sidebar button:hover { + background: #444 +} +.radio-group { + display: flex; flex-direction: column; gap: 3px +} +.radio-group label { + font-size: 11px +} +#main { + flex: 1; display: flex; flex-direction: column +} +#cy { + flex: 1; background: #212121 +} +#info-panel { + height: 180px; min-height: 80px; background: #2a2a2a; border-top: 1px solid #555; padding: 12px 16px; overflow-y: auto; font-size: 12px; line-height: 1.5; color: #e8e8e8 +} +#info-panel h3 { + font-size: 13px; margin-bottom: 4px +} +#info-panel .lbl { + color: #aaa; font-weight: 500 +} +.tag { + display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; margin: 1px 2px +} +#legend { + padding: 8px; font-size: 12px; border-top: 1px solid #555 +} +.leg-item { + display: flex; align-items: center; gap: 6px; margin: 2px 0 +} +.leg-sw { + width: 18px; height: 12px; border-radius: 2px; flex-shrink: 0 +} +.leg-ln { + width: 24px; height: 0; flex-shrink: 0 +} +.select-all-row { + border-bottom: 1px solid #444; padding-bottom: 3px; margin-bottom: 2px +} +`; + +/** + * Cytoscape styles for highlight/faded/search states. + */ +export const HIGHLIGHT_CYTOSCAPE_STYLES = ` + { selector: '.highlighted', + style: { 'opacity': 1, 'z-index': 999 } + }, + { selector: 'edge.highlighted', + style: { 'opacity': 1, 'z-index': 999, 'width': 3 } + }, + { selector: '.faded', + style: { 'opacity': 0.08 } + }, + { selector: 'node.search-match', + style: { 'border-width': 3, 'border-color': '#FF6B6B' } + },`; + +/** + * Cytoscape styles for gc-target nodes (controllers & views). + */ +export const GC_TARGET_CYTOSCAPE_STYLES = ` + { selector: 'node.gc-target', + style: { + 'background-color': '#1e1e3a', + 'background-opacity': 0.6, + 'border-width': 2, + 'border-style': 'dashed', + 'border-color': '#c084fc', + 'color': '#d8b4fe', + 'font-size': 9, + 'text-valign': 'center', + 'text-halign': 'center', + 'text-wrap': 'wrap', + 'text-max-width': '120px', + 'padding': '12px', + 'label': 'data(label)', + } + }, + { selector: 'node.gc-target-controller', + style: { + 'border-color': '#7dd3fc', + 'color': '#bae6fd', + 'shape': 'hexagon', + } + }, + { selector: 'node.gc-target-view', + style: { + 'border-color': '#c084fc', + 'color': '#d8b4fe', + 'shape': 'ellipse', + } + },`; + +/** + * Base Cytoscape styles for extender edges (controller & view). + */ +export const EXTENDER_EDGE_BASE_STYLES = ` + { selector: 'edge.edge-ext-ctrl', + style: { + 'line-color': '#0ea5e9', + 'target-arrow-color': '#0ea5e9', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + 'width': 2, + 'arrow-scale': 0.8, + } + }, + { selector: 'edge.edge-ext-view', + style: { + 'line-color': '#a855f7', + 'target-arrow-color': '#a855f7', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + 'width': 2, + 'arrow-scale': 0.8, + } + },`; + +/** + * Shared interactive JavaScript helpers for diagram templates. + * Contains: highlight/clear, edge toggle wiring, search, click handlers, + * fit button, getEdgeRouting(), and overlap-detection for taxi routing. + * + * NOT included (template-specific): reset button, feature area/category toggles. + * + * Templates must define before interpolating: + * - `cy` — the cytoscape instance + * - `computeHighlightSet(target)` — returns the collection to highlight for a given target + * - `showInfo(target)` — renders info panel content for a given target + * - `selectedTarget` — mutable state variable (will be set/cleared by this code) + * - `updateEdgeStyles()` — applies current edge routing to all edges + * + * Optional hooks: + * - `normalizeClickTarget(target)` — transform click target before + * info/selection (e.g. gc child → dg parent) + */ +export const SHARED_INTERACTIVE_JS = ` +/* ── Shared: Highlight helpers ── */ +function applyHighlight(s) { + cy.elements().addClass('faded').removeClass('highlighted'); + s.removeClass('faded').addClass('highlighted'); +} +function clearHighlight() { + cy.elements().removeClass('faded').removeClass('highlighted'); +} + +/* ── Shared: Edge routing helper ── */ +function getEdgeRouting() { + var c = document.querySelector('input[name="edge-routing"]:checked'); + return c ? c.value : 'bezier'; +} + +function hasOverlappingBounds(edge) { + var sb = edge.source().boundingBox(); + var tb = edge.target().boundingBox(); + return !(sb.x2 < tb.x1 || tb.x2 < sb.x1 || sb.y2 < tb.y1 || tb.y2 < sb.y1); +} + +/* ── Shared: Edge type toggles ── */ +document.querySelectorAll('.edge-toggle').forEach(function(cb) { + cb.addEventListener('change', function() { + var cls = this.getAttribute('data-cls'); + cy.edges('.' + cls).style('display', this.checked ? 'element' : 'none'); + }); + if (!cb.checked) { + var cls = cb.getAttribute('data-cls'); + cy.edges('.' + cls).style('display', 'none'); + } +}); + +/* ── Shared: Search ── */ +document.getElementById('search').addEventListener('input', function() { + var q = this.value.toLowerCase().trim(); + cy.nodes().removeClass('search-match'); + if (!q) return; + cy.nodes().forEach(function(n) { + var label = (n.data('label') || '').toLowerCase(); + var name = (n.data('moduleName') || n.data('id') || '').toLowerCase(); + if (label.indexOf(q) >= 0 || name.indexOf(q) >= 0) n.addClass('search-match'); + }); +}); + +/* ── Shared: Click-to-select / deselect ── */ +var infoPanel = document.getElementById('info-panel'); +var defaultInfoHtml = '

Click a node or edge to see details.

'; + +cy.on('tap', 'node, edge', function(e) { + var t = e.target; + var infoTarget = typeof normalizeClickTarget === 'function' ? normalizeClickTarget(t) : t; + var checkId = infoTarget.id(); + if (selectedTarget && selectedTarget.id() === checkId) { + selectedTarget = null; + clearHighlight(); + infoPanel.innerHTML = defaultInfoHtml; + return; + } + selectedTarget = infoTarget; + applyHighlight(computeHighlightSet(t)); + showInfo(infoTarget); +}); +cy.on('tap', function(e) { + if (e.target === cy && selectedTarget) { + selectedTarget = null; + clearHighlight(); + infoPanel.innerHTML = defaultInfoHtml; + } +}); + +/* ── Shared: Fit button ── */ +document.getElementById('btn-fit').addEventListener('click', function() { cy.fit(undefined, 30); }); + +/* ── Shared: Edge routing radio ── */ +document.querySelectorAll('input[name="edge-routing"]').forEach(function(r) { + r.addEventListener('change', function() { updateEdgeStyles(); }); +}); +`; diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/inheritance.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/inheritance.ts new file mode 100644 index 000000000000..92e4ba9bea36 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/inheritance.ts @@ -0,0 +1,79 @@ +import { stripOuterMixin } from './ast-helpers'; +import type { HeritageInfo } from './types'; + +export interface BuildChainOptions { + /** Maximum recursion depth. Default: no limit (relies on visited set). */ + maxDepth?: number; + /** Format a mixin name before pushing to chain (e.g. add "[mixin] " prefix). */ + formatMixin?: (mixin: string) => string; + /** + * Map rawBase to the key used for class-map lookup and recursion. + * Default: identity (use rawBase as-is). + */ + resolveNext?: (rawBase: string) => string | null; + /** + * Called when the resolved key is not found in the class map. + * Return extra chain entries for well-known terminal nodes. + * Default: no extra entries. + */ + onTerminal?: (rawBase: string) => string[]; + /** Called when a cycle is detected (className already in visited set). */ + onCycle?: (className: string) => void; +} + +/** + * Core recursive algorithm for building an inheritance chain. + * + * Steps: + * 1. Check visited set (+ optional depth limit) + * 2. Look up class info via `getClassInfo` + * 3. Collect mixins into chain + * 4. Strip outermost mixin wrapper from baseClass + * 5. Push rawBase into chain + * 6. Recurse if the resolved key exists in the class map + * + * Both `grid_core` and `data_grid` resolvers use this with different hooks. + */ +export function buildInheritanceChainCore( + className: string, + getClassInfo: (name: string) => HeritageInfo | undefined, + visited: Set, + options: BuildChainOptions = {}, + depth = 0, +): string[] { + const { + maxDepth, formatMixin, resolveNext, onTerminal, onCycle, + } = options; + + if (visited.has(className)) { + onCycle?.(className); + return []; + } + if (maxDepth !== undefined && depth > maxDepth) { + return []; + } + visited.add(className); + + const info = getClassInfo(className); + if (!info?.baseClass) { + return []; + } + + const chain: string[] = []; + + info.mixins.forEach((mixin) => { + chain.push(formatMixin ? formatMixin(mixin) : mixin); + }); + + const rawBase = stripOuterMixin(info.baseClass); + chain.push(rawBase); + + const nextKey = resolveNext ? resolveNext(rawBase) : rawBase; + if (nextKey !== null && getClassInfo(nextKey)) { + chain.push(...buildInheritanceChainCore(nextKey, getClassInfo, visited, options, depth + 1)); + } else if (onTerminal) { + chain.push(...onTerminal(rawBase)); + } + + return chain; +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/output-writer.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/output-writer.ts new file mode 100644 index 000000000000..cbb218ff73f8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/output-writer.ts @@ -0,0 +1,37 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { parseArgs } from './cli'; + +/** + * Writes architecture output files (JSON and/or HTML) to `outputDir`. + * + * Respects CLI flags `--json` (JSON only) and `--html` (HTML only). + * When neither flag is set, both files are written. + */ +export function writeOutputFiles( + outputDir: string, + baseName: string, + data: T, + generateHtml: (d: T) => string, +): void { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const args = parseArgs(); + + if (!args.htmlOnly) { + const jsonPath = path.join(outputDir, `${baseName}.generated.json`); + fs.writeFileSync(jsonPath, `${JSON.stringify(data, null, 2)}\n`); + // eslint-disable-next-line no-console + console.log(`✓ JSON written to: ${jsonPath}`); + } + + if (!args.jsonOnly) { + const htmlPath = path.join(outputDir, `${baseName}.generated.html`); + fs.writeFileSync(htmlPath, generateHtml(data)); + // eslint-disable-next-line no-console + console.log(`✓ HTML written to: ${htmlPath}`); + } +} diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/types.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/types.ts new file mode 100644 index 000000000000..06a66e267af9 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/shared/types.ts @@ -0,0 +1,15 @@ +export interface HeritageInfo { + baseClass: string; + mixins: string[]; +} + +export interface BaseClassInfo extends HeritageInfo { + className: string; + sourceFile: string; + isExported: boolean; +} + +export interface InheritanceEntry { + className: string; + chain: string[]; +} From 47f9d88d90e0529bc956517e1bdcffb2172d576c Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Tue, 17 Mar 2026 15:40:07 +0100 Subject: [PATCH 10/10] improve ui --- .../__docs__/scripts/data_grid/generate.ts | 2 +- .../scripts/data_grid/graph-builder.ts | 8 +- .../scripts/data_grid/html-template.ts | 56 +++++++++----- .../__docs__/scripts/data_grid/parser.ts | 4 +- .../__docs__/scripts/data_grid/resolver.ts | 17 ++--- .../grids/__docs__/scripts/data_grid/types.ts | 1 - .../scripts/grid_core/graph-builder.ts | 4 +- .../scripts/grid_core/html-template.ts | 74 ++++++++++++++++--- .../__docs__/scripts/shared/html-helpers.ts | 46 ++++++++++-- 9 files changed, 160 insertions(+), 52 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts index b28825e298da..cd6ecde7d9f9 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/generate.ts @@ -105,7 +105,7 @@ function main(): void { }; for (const mod of allModules) { counts[mod.category] += 1; - console.log(` [${mod.category.toUpperCase().padEnd(11)}] ${mod.moduleName} (${mod.relPath})`); + console.log(` [${mod.category.toUpperCase().padEnd(11)}] ${mod.moduleName} (${mod.sourceFile})`); } console.log(` Passthrough: ${counts.passthrough}, Replaced: ${counts.replaced}, Extended: ${counts.extended}, New: ${counts.new}`); diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts index 7f944d479be0..61e43e1fbd39 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/graph-builder.ts @@ -96,7 +96,7 @@ function buildSynthGcData( label: labelParts.join('\n'), nodeType: 'gridCoreModule', category: 'grid-core', - sourceFile: mod.gridCoreSourceModule ?? mod.relPath, + sourceFile: mod.gridCoreSourceModule ?? mod.sourceFile, featureArea: mod.featureArea, registrationOrder: -1, moduleName: mod.moduleName, @@ -110,7 +110,7 @@ function buildSynthGcData( export function buildCytoscapeElements( data: ArchitectureData, - modulesByRelPath: Map, + modulesBySourceFile: Map, ): CytoscapeElement[] { const { elements, nodeIds, edgeIds, addNode, @@ -262,7 +262,7 @@ export function buildCytoscapeElements( label: labelParts.join('\n'), nodeType: 'module', category: mod.category, - sourceFile: mod.relPath, + sourceFile: mod.sourceFile, featureArea: mod.featureArea, registrationOrder: mod.registrationOrder, gridCoreSource: mod.gridCoreSourceModule ?? '', @@ -381,7 +381,7 @@ export function buildCytoscapeElements( if (nodeIds.has(dsaTargetId)) { for (let i = 0; i < data.dataSourceAdapterChain.length; i += 1) { const ext = data.dataSourceAdapterChain[i]; - const mod = modulesByRelPath.get(ext.relPath); + const mod = modulesBySourceFile.get(ext.relPath); if (!mod) { // eslint-disable-next-line no-continue diff --git a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts index a06259429ea0..3bbb62c538c1 100644 --- a/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts +++ b/packages/devextreme/js/__internal/grids/__docs__/scripts/data_grid/html-template.ts @@ -1,12 +1,12 @@ /* eslint-disable spellcheck/spell-checker */ import { BASE_CSS, HIGHLIGHT_CYTOSCAPE_STYLES, SHARED_INTERACTIVE_JS } from '../shared/html-helpers'; import { buildCytoscapeElements } from './graph-builder'; -import { buildModulesByRelPath } from './resolver'; +import { buildModulesBySourceFile } from './resolver'; import type { ArchitectureData } from './types'; export function generateHtml(data: ArchitectureData): string { - const modulesByRelPath = buildModulesByRelPath(data.modules); - const cytoscapeElements = buildCytoscapeElements(data, modulesByRelPath); + const modulesBySourceFile = buildModulesBySourceFile(data.modules); + const cytoscapeElements = buildCytoscapeElements(data, modulesBySourceFile); const elementsJson = JSON.stringify(cytoscapeElements, null, 2); const pipelines = data.extenderPipelines.map((p) => ({ targetName: p.targetName, @@ -26,7 +26,7 @@ export function generateHtml(data: ArchitectureData): string { targetName: 'dataSourceAdapter', targetType: 'controller', steps: data.dataSourceAdapterChain.map((ext) => { - const mod = modulesByRelPath.get(ext.relPath); + const mod = modulesBySourceFile.get(ext.relPath); return { moduleName: mod?.moduleName ?? ext.relPath, relPath: ext.relPath, @@ -52,7 +52,7 @@ export function generateHtml(data: ArchitectureData): string { const modulesJson = JSON.stringify(data.modules.map((m) => ({ moduleName: m.moduleName, category: m.category, - relPath: m.relPath, + sourceFile: m.sourceFile, featureArea: m.featureArea, registrationOrder: m.registrationOrder, gridCoreSourceModule: m.gridCoreSourceModule, @@ -63,6 +63,13 @@ export function generateHtml(data: ArchitectureData): string { }))); const categories = ['passthrough', 'extended', 'replaced', 'new', 'gc-target']; + const categoryDisplayNames: Record = { + passthrough: 'Passthrough', + extended: 'Extended', + replaced: 'Replaced', + new: 'New', + 'gc-target': 'Grid Core Target', + }; const featureAreas = [...new Set(data.modules.map((m) => m.featureArea))].sort(); return ` @@ -108,7 +115,7 @@ ${BASE_CSS}

Categories

- ${categories.map((c) => ``).join('\n ')} + ${categories.map((c) => ``).join('\n ')}

Feature Areas

@@ -133,8 +140,8 @@ ${BASE_CSS}
Replaced
New
Grid Core Module
-
GC Target (ctrl/view)
-
DG Target (ctrl/view)
+
Grid Core Target (ctrl/view)
+
DataGrid Target (ctrl/view)
Registration Order
Extender (ctrl → target)
Extender (view → target)
@@ -144,7 +151,12 @@ ${BASE_CSS}
-

Click a node or edge to see details.

+
+
+ +

Click a node or edge to see details.

+
+