Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<string>();

export const REGISTER_MODULE_RECEIVERS = new Set([
'gridCore',
'core',
'treeListCore',
'dataGridCore',
]);

export const DATA_SOURCE_ADAPTER_PROVIDER = 'dataSourceAdapterProvider';

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';

const DATA_GRID_FEATURE_MAP: Record<string, string> = {
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<string, string> = {
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');
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env tsx
/* eslint-disable no-console, spellcheck/spell-checker */
import * as fs from 'fs';
import * as path from 'path';

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 {
parseDataGridFile,
parseModulesOrder,
} from './parser';
import {
buildCrossDependencies,
buildExtenderPipelines,
buildInheritanceChains,
classifyModules,
collectDataSourceAdapterChain,
} from './resolver';
import type { ArchitectureData, GridCoreModuleInfo, ParsedFile } from './types';

const GC_JSON_PATH = path.join(OUTPUT_DIR, 'grid_core_architecture.generated.json');

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.ts --json');
process.exit(1);
}

const raw = JSON.parse(fs.readFileSync(GC_JSON_PATH, 'utf-8'));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const modules: GridCoreModuleInfo[] = (raw.modules ?? []).map((m: any) => ({
moduleName: m.moduleName,
registeredAs: m.registeredAs ?? null,
sourceFile: m.sourceFile,
featureArea: m.featureArea,
controllers: m.controllers ?? {},
views: m.views ?? {},
extenders: m.extenders ?? { controllers: {}, views: {} },
hasDefaultOptions: m.hasDefaultOptions ?? false,
}));

console.log(`Loaded ${modules.length} grid_core modules from ${GC_JSON_PATH}`);
return modules;
}

function appendMissingModuleNames(modulesOrder: string[], parsedFiles: ParsedFile[]): 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 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.
const modulesOrder = parseModulesOrder();
console.log(`Parsed ${modulesOrder.length} modules from registerModulesOrder (ascending order)`);

// 2. Load grid_core modules from pre-generated JSON (prerequisite check)
const gridCoreModules = loadGridCoreModules();

// 3. Discover data_grid source files
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) => {
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 ${relPath}: ${msg}`);
return [];
}
});

// 5. Build full module order
appendMissingModuleNames(modulesOrder, allParsedFiles);

// 6. Classify modules
const allModules = classifyModules(allParsedFiles, modulesOrder, gridCoreModules);
console.log(`\nClassified ${allModules.length} modules:`);
const counts: Record<ModificationCategory, number> = {
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.sourceFile})`);
}
console.log(` Passthrough: ${counts.passthrough}, Replaced: ${counts.replaced}, Extended: ${counts.extended}, New: ${counts.new}`);

// 7. Build extender pipelines (including gc extenders from passthrough modules)
const extenderPipelines = buildExtenderPipelines(allModules, gridCoreModules);
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}${s.isFromGridCore ? '(gc)' : '(dg)'}`).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 cross-dependencies
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: ArchitectureData = {
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,
crossDependencies,
summary: { total: allModules.length, ...counts },
};

// 12. Write output files
writeOutputFiles(OUTPUT_DIR, 'data_grid_architecture', data, generateHtml);

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();
Loading
Loading