From 6e77801c7709f701d6a8567703b2020217bb6302 Mon Sep 17 00:00:00 2001 From: Stephen Cooper Date: Fri, 16 Jan 2026 09:38:55 +0000 Subject: [PATCH 1/5] Fix auto detect latest branch (#12895) * Fix auto detect latest branch * use stable release tag --- .github/workflows/module-size-vs-release.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/module-size-vs-release.yml b/.github/workflows/module-size-vs-release.yml index f95586ceaa5..1b2611d88e3 100644 --- a/.github/workflows/module-size-vs-release.yml +++ b/.github/workflows/module-size-vs-release.yml @@ -8,9 +8,9 @@ on: workflow_dispatch: inputs: release_tag: - description: 'Release tag to compare against (e.g., b35.1.0)' + description: 'Release tag to compare against (e.g., b35.1.0) or leave empty to auto-detect latest release' type: string - required: true + required: false env: NX_NO_CLOUD: true @@ -57,12 +57,13 @@ jobs: per_page: 10 }); - // Find the latest non-prerelease tag matching bXX.X.X pattern - const release = releases.find(r => !r.prerelease && /^b\d+\.\d+\.\d+$/.test(r.tag_name)); + // Find the latest non-prerelease tag matching release-XX.X.X pattern + const release = releases.find(r => !r.prerelease && /^release-\d+\.\d+\.\d+$/.test(r.tag_name)); if (!release) { core.setFailed('Could not find a valid release tag'); return; } + // Use the latest release tag releaseTag = release.tag_name; console.log(`Using latest release tag: ${releaseTag}`); } From 03077e8b4ff3319df532867b46b1096e61426520 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Fri, 16 Jan 2026 07:14:47 -0300 Subject: [PATCH 2/5] AG-16441 - AutoComplete Feature for FormulaInputField (#12896) * AG-16441 - [formula-editor] - added auto complete * added autoSizeList to AutoCompleteList * moved AutoComplete to a Feature * simplified runStartsWithSearch --- .../src/interfaces/formulas.ts | 1 + .../autocomplete/agAutocompleteList.ts | 46 ++- .../src/formula/formulaService.ts | 25 ++ .../src/widgets/agFormulaInputField.ts | 30 +- .../formulaInputAutocompleteFeature.ts | 283 ++++++++++++++++++ .../src/widgets/formulaInputTokenUtils.ts | 12 + 6 files changed, 378 insertions(+), 19 deletions(-) create mode 100644 packages/ag-grid-enterprise/src/widgets/formulaInputAutocompleteFeature.ts create mode 100644 packages/ag-grid-enterprise/src/widgets/formulaInputTokenUtils.ts diff --git a/packages/ag-grid-community/src/interfaces/formulas.ts b/packages/ag-grid-community/src/interfaces/formulas.ts index 6c906b33ab7..940dbd7d2b2 100644 --- a/packages/ag-grid-community/src/interfaces/formulas.ts +++ b/packages/ag-grid-community/src/interfaces/formulas.ts @@ -89,4 +89,5 @@ export interface IFormulaService extends Bean { }): string; refreshFormulas(refreshRows: boolean): void; getFunction(name: string): ((params: FormulaFunctionParams) => unknown) | undefined; + getFunctionNames(): string[]; } diff --git a/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocompleteList.ts b/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocompleteList.ts index e8b2eb09888..7b109fa2273 100644 --- a/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocompleteList.ts +++ b/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocompleteList.ts @@ -49,6 +49,9 @@ export class AgAutocompleteList extends AgPopupComponent< autocompleteEntries: AutocompleteEntry[]; onConfirmed: () => void; useFuzzySearch?: boolean; + useStartsWithSearch?: boolean; + autoSizeList?: boolean; + maxVisibleItems?: number; forceLastSelection?: (lastSelection: AutocompleteEntry, searchString: string) => boolean; } ) { @@ -75,6 +78,7 @@ export class AgAutocompleteList extends AgPopupComponent< }); this.setSelectedValue(0); + this.updateListHeight(); } public onNavigationKeyDown(event: any, key: string): void { @@ -95,6 +99,7 @@ export class AgAutocompleteList extends AgPopupComponent< this.autocompleteEntries = this.params.autocompleteEntries; this.virtualList.refresh(); this.checkSetSelectedValue(0); + this.updateListHeight(); } this.updateSearchInList(); } @@ -129,8 +134,20 @@ export class AgAutocompleteList extends AgPopupComponent< return { topMatch, allMatches }; } + private runStartsWithSearch( + searchString: string, + searchStrings: string[] + ): { topMatch: string | undefined; allMatches: string[] } { + const lowerCaseSearchString = searchString.toLocaleLowerCase(); + const allMatches = searchStrings.filter((string) => + string.toLocaleLowerCase().startsWith(lowerCaseSearchString) + ); + const topMatch = allMatches[0]; + return { topMatch, allMatches }; + } + private runSearch() { - const { autocompleteEntries, useFuzzySearch, forceLastSelection } = this.params; + const { autocompleteEntries, useFuzzySearch, useStartsWithSearch, forceLastSelection } = this.params; const searchStrings = autocompleteEntries.map((v) => v.displayValue ?? v.key); let matchingStrings: string[]; @@ -143,9 +160,11 @@ export class AgAutocompleteList extends AgPopupComponent< }).values; topSuggestion = matchingStrings.length ? matchingStrings[0] : undefined; } else { - const containsMatches = this.runContainsSearch(this.searchString, searchStrings); - matchingStrings = containsMatches.allMatches; - topSuggestion = containsMatches.topMatch; + const matches = useStartsWithSearch + ? this.runStartsWithSearch(this.searchString, searchStrings) + : this.runContainsSearch(this.searchString, searchStrings); + matchingStrings = matches.allMatches; + topSuggestion = matches.topMatch; } let filteredEntries = autocompleteEntries.filter(({ key, displayValue }) => @@ -160,6 +179,7 @@ export class AgAutocompleteList extends AgPopupComponent< } this.autocompleteEntries = filteredEntries; this.virtualList.refresh(); + this.updateListHeight(); if (!topSuggestion) { return; @@ -174,6 +194,24 @@ export class AgAutocompleteList extends AgPopupComponent< this.virtualList.forEachRenderedRow((row: AgAutocompleteRow) => row.setSearchString(this.searchString)); } + private updateListHeight(): void { + if (!this.params.autoSizeList) { + return; + } + + const rowCount = this.autocompleteEntries.length; + const rowHeight = this.virtualList.getRowHeight(); + const maxItems = this.params.maxVisibleItems ?? rowCount; + const visibleCount = Math.min(rowCount, maxItems); + let height = visibleCount * rowHeight; + + if (rowCount === 0) { + height = rowHeight; + } + + this.eList.style.height = `${height}px`; + } + private checkSetSelectedValue(index: number): void { if (index >= 0 && index < this.autocompleteEntries.length) { this.setSelectedValue(index); diff --git a/packages/ag-grid-enterprise/src/formula/formulaService.ts b/packages/ag-grid-enterprise/src/formula/formulaService.ts index 07b6ec6353a..a614e8e3737 100644 --- a/packages/ag-grid-enterprise/src/formula/formulaService.ts +++ b/packages/ag-grid-enterprise/src/formula/formulaService.ts @@ -21,6 +21,7 @@ import type { Addr } from './functions/resolver'; import { evalAst, unresolvedDeps } from './functions/resolver'; import SUPPORTED_FUNCTIONS from './functions/supportedFuncs'; import { shiftNode } from './functions/utils'; +import { isFormulaIdentChar, isFormulaIdentStart } from './refUtils'; /** * Cell Formula Cache @@ -106,6 +107,7 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe /** Built-in operations (extendable via gridOptions.formulaFuncs). */ private supportedOperations: Map unknown>; + private functionNames: string[] | null = null; // Track the active editor instance per grid/cell to avoid overlapping syncs on editor restarts. public activeEditor: number | null = null; @@ -224,6 +226,7 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe private setupFunctions() { // eslint-disable-next-line no-restricted-properties this.supportedOperations = new Map(Object.entries(SUPPORTED_FUNCTIONS)); + this.functionNames = null; // Register custom functions, not reactive. const customFuncs = this.gos.get('formulaFuncs'); @@ -234,6 +237,28 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe } } + public getFunctionNames(): string[] { + if (this.functionNames) { + return this.functionNames; + } + + const names: string[] = []; + + for (const name of this.supportedOperations.keys()) { + if (!isFormulaIdentStart(name[0])) { + continue; + } + if (![...name].every((char) => isFormulaIdentChar(char))) { + continue; + } + names.push(name); + } + + names.sort((a, b) => a.localeCompare(b)); + this.functionNames = names; + return names; + } + private setupColRefMap() { if (!this.active) { this.colRefMap = new Map(); diff --git a/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts b/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts index ba9451b3db3..558a1e4ecb4 100644 --- a/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts +++ b/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts @@ -8,9 +8,12 @@ import type { } from 'ag-grid-community'; import { AgContentEditableField, _createElement, _getDocument, _getWindow } from 'ag-grid-community'; +import { agAutocompleteCSS } from '../advancedFilter/autocomplete/agAutocomplete.css-GENERATED'; import { getRefTokenMatches } from '../formula/refUtils'; import { agFormulaInputFieldCSS } from './agFormulaInputField.css-GENERATED'; +import { FormulaInputAutocompleteFeature } from './formulaInputAutocompleteFeature'; import { FormulaInputRangeSyncFeature } from './formulaInputRangeSyncFeature'; +import { TOKEN_INSERT_AFTER_CHARS, getPreviousNonSpaceChar } from './formulaInputTokenUtils'; import { getColorClassesForRef } from './formulaRangeUtils'; const FORMULA_TOKEN_COLOR_COUNT = 7; @@ -26,9 +29,6 @@ type RangeInsertAction = 'insert' | 'replace' | 'none'; type TokenMatch = { ref: string; start: number; end: number; index: number }; type TokenInsertResult = { previousRef?: string; tokenIndex?: number | null }; -// only insert new refs after operators/argument separators; otherwise treat clicks as normal edit completion. -const TOKEN_INSERT_AFTER_CHARS = new Set(['=', '+', '-', '*', '/', '^', ',', '(', ';', '<', '>', '&']); - export class AgFormulaInputField extends AgContentEditableField< BeanCollection, GridOptionsWithDefaults, @@ -46,13 +46,16 @@ export class AgFormulaInputField extends AgContentEditableField< private lastTokenCaretOffset: number | null = null; private lastTokenRef?: string; private rangeSyncFeature?: FormulaInputRangeSyncFeature; + private autocompleteFeature?: FormulaInputAutocompleteFeature; // fallback color assignment per ref when a token index is unavailable. + private readonly formulaColorByRef = new Map(); constructor() { // keep renderValueToElement false so we fully control DOM rendering. super({ renderValueToElement: false, className: 'ag-formula-input-field' } as any); this.registerCSS(agFormulaInputFieldCSS); + this.registerCSS(agAutocompleteCSS); } public override postConstruct(): void { @@ -63,6 +66,7 @@ export class AgFormulaInputField extends AgContentEditableField< }); this.rangeSyncFeature = this.createManagedBean(new FormulaInputRangeSyncFeature(this)); + this.autocompleteFeature = this.createManagedBean(new FormulaInputAutocompleteFeature(this)); } public override setValue(value?: string | null, silent?: boolean): this { @@ -384,6 +388,10 @@ export class AgFormulaInputField extends AgContentEditableField< }); } + public getCaretOffsetsForAutocomplete(value: string): { caretOffset: number; valueOffset: number } | null { + return this.getCaretOffsets(value); + } + private getCaretOffsets( value: string, options: { useCachedCaret: boolean; useCachedValueOffset: boolean } = { @@ -445,6 +453,7 @@ export class AgFormulaInputField extends AgContentEditableField< if (params.dispatch) { this.dispatchValueChanged(); } + this.autocompleteFeature?.onPlainValueUpdated(); } private applyFormulaValue( @@ -463,9 +472,10 @@ export class AgFormulaInputField extends AgContentEditableField< if (params.dispatch) { this.dispatchValueChanged(); } + this.autocompleteFeature?.onFormulaValueUpdated(); } - private applyFormulaValueChange(params: { + public applyFormulaValueChange(params: { currentValue: string; nextValue: string; caret: number; @@ -480,6 +490,7 @@ export class AgFormulaInputField extends AgContentEditableField< caret: params.caret, }); this.dispatchValueChanged(); + this.autocompleteFeature?.onFormulaValueUpdated(); } public replaceTokenRef( @@ -580,17 +591,6 @@ const shouldInsertTokenAtOffset = (value: string, offset: number): boolean => { return previousChar == null || TOKEN_INSERT_AFTER_CHARS.has(previousChar); }; -const getPreviousNonSpaceChar = (value: string, offset: number): string | null => { - // skip whitespace to detect the meaningful character before the caret. - for (let i = offset - 1; i >= 0; i--) { - const char = value[i]; - if (char != null && char.trim() !== '') { - return char; - } - } - return null; -}; - // Rendering & caret helpers const tokenize = ( beans: BeanCollection, diff --git a/packages/ag-grid-enterprise/src/widgets/formulaInputAutocompleteFeature.ts b/packages/ag-grid-enterprise/src/widgets/formulaInputAutocompleteFeature.ts new file mode 100644 index 00000000000..e368a76784b --- /dev/null +++ b/packages/ag-grid-enterprise/src/widgets/formulaInputAutocompleteFeature.ts @@ -0,0 +1,283 @@ +import type { AgComponentPopupPositionParams, PopupPositionParams } from 'ag-grid-community'; +import { BeanStub, KeyCode, _getDocument } from 'ag-grid-community'; + +import { AgAutocompleteList } from '../advancedFilter/autocomplete/agAutocompleteList'; +import type { AutocompleteEntry } from '../advancedFilter/autocomplete/autocompleteParams'; +import { isFormulaIdentChar, isFormulaIdentStart } from '../formula/refUtils'; +import type { AgFormulaInputField } from './agFormulaInputField'; +import { TOKEN_INSERT_AFTER_CHARS, getPreviousNonSpaceChar } from './formulaInputTokenUtils'; + +type FunctionTokenMatch = { start: number; end: number; prefix: string }; + +export class FormulaInputAutocompleteFeature extends BeanStub { + private functionAutocompleteList: AgAutocompleteList | null = null; + private functionAutocompleteHidePopup?: () => void; + private functionAutocompleteToken: FunctionTokenMatch | null = null; + private functionAutocompleteEntries: AutocompleteEntry[] | null = null; + private functionAutocompleteSearch: string | null = null; + + constructor(private readonly field: AgFormulaInputField) { + super(); + } + + public postConstruct(): void { + this.addManagedElementListeners(this.field.getContentElement(), { + keydown: this.onContentKeyDown.bind(this), + mouseup: this.updateFunctionAutocomplete.bind(this), + focusin: this.updateFunctionAutocomplete.bind(this), + focusout: this.closeFunctionAutocomplete.bind(this), + }); + this.addDestroyFunc(() => this.closeFunctionAutocomplete()); + } + + public onPlainValueUpdated(): void { + this.closeFunctionAutocomplete(); + } + + public onFormulaValueUpdated(): void { + this.updateFunctionAutocomplete(); + } + + private onContentKeyDown(event: KeyboardEvent): void { + if (this.functionAutocompleteList) { + switch (event.key) { + case KeyCode.ENTER: + case KeyCode.TAB: + event.preventDefault(); + event.stopPropagation(); + this.confirmFunctionAutocomplete(); + return; + case KeyCode.ESCAPE: + event.preventDefault(); + event.stopPropagation(); + this.closeFunctionAutocomplete(); + return; + case KeyCode.UP: + case KeyCode.DOWN: + this.functionAutocompleteList.onNavigationKeyDown(event, event.key); + return; + } + } + + switch (event.key) { + case KeyCode.LEFT: + case KeyCode.RIGHT: + case KeyCode.PAGE_HOME: + case KeyCode.PAGE_END: + this.scheduleFunctionAutocompleteUpdate(); + break; + } + } + + private scheduleFunctionAutocompleteUpdate(): void { + setTimeout(() => { + if (!this.isAlive()) { + return; + } + this.updateFunctionAutocomplete(); + }); + } + + private updateFunctionAutocomplete(): void { + if (!this.isContentFocused()) { + this.closeFunctionAutocomplete(); + return; + } + + const value = this.field.getCurrentValue(); + const hasFormulaPrefix = value.trimStart().startsWith('='); + + if (!hasFormulaPrefix) { + this.closeFunctionAutocomplete(); + return; + } + + const caretOffsets = this.field.getCaretOffsetsForAutocomplete(value); + if (!caretOffsets) { + this.closeFunctionAutocomplete(); + return; + } + + const token = getFunctionTokenAtOffset(value, caretOffsets.valueOffset); + if (!token) { + this.closeFunctionAutocomplete(); + return; + } + + const entries = this.getFunctionAutocompleteEntries(); + if (!entries.length) { + this.closeFunctionAutocomplete(); + return; + } + + const searchLower = token.prefix.toLocaleLowerCase(); + const hasMatch = entries.some((entry) => entry.key.toLocaleLowerCase().startsWith(searchLower)); + + if (!hasMatch) { + this.closeFunctionAutocomplete(); + return; + } + + this.functionAutocompleteToken = token; + this.openFunctionAutocomplete(entries); + + if (this.functionAutocompleteList && this.functionAutocompleteSearch !== token.prefix) { + this.functionAutocompleteList.setSearch(token.prefix); + this.functionAutocompleteSearch = token.prefix; + } + } + + private getFunctionAutocompleteEntries(): AutocompleteEntry[] { + const formula = this.beans.formula; + const names = formula?.active ? formula.getFunctionNames?.() ?? [] : []; + + if (!this.functionAutocompleteEntries || this.functionAutocompleteEntries.length !== names.length) { + this.functionAutocompleteEntries = names.map((name) => ({ key: name })); + } + + return this.functionAutocompleteEntries; + } + + private openFunctionAutocomplete(entries: AutocompleteEntry[]): void { + if (this.functionAutocompleteList || !entries.length) { + return; + } + + const popupSvc = this.beans.popupSvc; + if (!popupSvc) { + return; + } + + this.functionAutocompleteList = this.createManagedBean( + new AgAutocompleteList({ + autocompleteEntries: entries, + onConfirmed: () => this.confirmFunctionAutocomplete(), + useStartsWithSearch: true, + autoSizeList: true, + maxVisibleItems: 10, + }) + ); + + const ePopupGui = this.functionAutocompleteList.getGui(); + + const positionParams: AgComponentPopupPositionParams = { + ePopup: ePopupGui, + type: 'autocomplete', + eventSource: this.field.getGui(), + position: 'under', + alignSide: this.gos.get('enableRtl') ? 'right' : 'left', + keepWithinBounds: true, + }; + + const addPopupRes = popupSvc.addPopup({ + eChild: ePopupGui, + anchorToElement: this.field.getGui(), + positionCallback: () => popupSvc.positionPopupByComponent(positionParams), + ariaLabel: 'Formula functions', + }); + + this.functionAutocompleteHidePopup = addPopupRes.hideFunc; + this.functionAutocompleteList.afterGuiAttached(); + } + + private closeFunctionAutocomplete(): void { + this.functionAutocompleteToken = null; + this.functionAutocompleteSearch = null; + + if (!this.functionAutocompleteList) { + return; + } + + this.functionAutocompleteHidePopup?.(); + this.functionAutocompleteHidePopup = undefined; + this.destroyBean(this.functionAutocompleteList); + this.functionAutocompleteList = null; + } + + private confirmFunctionAutocomplete(): void { + const token = this.functionAutocompleteToken; + const selected = this.functionAutocompleteList?.getSelectedValue(); + + if (!token || !selected) { + this.closeFunctionAutocomplete(); + return; + } + + const value = this.field.getCurrentValue(); + const functionName = selected.key; + const baseValue = value.slice(0, token.start) + functionName + value.slice(token.end); + const insertPos = token.start + functionName.length; + const nextValue = + baseValue[insertPos] === '(' ? baseValue : baseValue.slice(0, insertPos) + '(' + baseValue.slice(insertPos); + + this.field.getContentElement().focus({ preventScroll: true }); + this.field.applyFormulaValueChange({ + currentValue: value, + nextValue, + caret: insertPos + 1, + }); + + this.closeFunctionAutocomplete(); + } + + private isContentFocused(): boolean { + return _getDocument(this.beans).activeElement === this.field.getContentElement(); + } +} + +const getFunctionTokenAtOffset = (value: string, caretOffset: number): FunctionTokenMatch | null => { + // show functions only when the caret is at the end of a formula identifier. + if (caretOffset < 0 || caretOffset > value.length || isInsideStringLiteral(value, caretOffset)) { + return null; + } + + let start = caretOffset; + while (start > 0 && isFormulaIdentChar(value[start - 1])) { + start--; + } + + let end = caretOffset; + while (end < value.length && isFormulaIdentChar(value[end])) { + end++; + } + + if (start === end || caretOffset !== end) { + return null; + } + + const token = value.slice(start, end); + if (!token || !isFormulaIdentStart(token[0])) { + return null; + } + + if (value[start - 1] === '$') { + return null; + } + + const previousChar = getPreviousNonSpaceChar(value, start); + if (previousChar != null && !TOKEN_INSERT_AFTER_CHARS.has(previousChar)) { + return null; + } + + return { + start, + end, + prefix: value.slice(start, caretOffset), + }; +}; + +const isInsideStringLiteral = (value: string, offset: number): boolean => { + // treat doubled quotes as escaped quotes when scanning. + let inString = false; + for (let i = 0; i < offset && i < value.length; i++) { + if (value[i] !== '"') { + continue; + } + if (value[i + 1] === '"') { + i++; + continue; + } + inString = !inString; + } + return inString; +}; diff --git a/packages/ag-grid-enterprise/src/widgets/formulaInputTokenUtils.ts b/packages/ag-grid-enterprise/src/widgets/formulaInputTokenUtils.ts new file mode 100644 index 00000000000..bc6f4c6c54d --- /dev/null +++ b/packages/ag-grid-enterprise/src/widgets/formulaInputTokenUtils.ts @@ -0,0 +1,12 @@ +export const TOKEN_INSERT_AFTER_CHARS = new Set(['=', '+', '-', '*', '/', '^', ',', '(', ';', '<', '>', '&']); + +export const getPreviousNonSpaceChar = (value: string, offset: number): string | null => { + // skip whitespace to detect the meaningful character before the caret. + for (let i = offset - 1; i >= 0; i--) { + const char = value[i]; + if (char != null && char.trim() !== '') { + return char; + } + } + return null; +}; From a6d83a7228f8f63c3d34896cbc56178875c5815f Mon Sep 17 00:00:00 2001 From: Elias Date: Fri, 16 Jan 2026 10:55:49 +0000 Subject: [PATCH 3/5] AG-14709 (algolia) Generate entries for more nested fields (#10753) * wip * fix * ignore css-GENERATED * fix condition * getText avoids need to trim * tidy --------- Co-authored-by: Stephen Cooper --- .../src/executors/generate/executor.ts | 6 +- .../generate/generate-code-reference-files.ts | 118 ++++++++++++++---- 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/plugins/ag-grid-generate-code-reference-files/src/executors/generate/executor.ts b/plugins/ag-grid-generate-code-reference-files/src/executors/generate/executor.ts index e97933bbc12..36739d2e7cf 100644 --- a/plugins/ag-grid-generate-code-reference-files/src/executors/generate/executor.ts +++ b/plugins/ag-grid-generate-code-reference-files/src/executors/generate/executor.ts @@ -71,14 +71,10 @@ async function generateFile(options: ExecutorOptions) { await writeJSONFile(distFolder + '/doc-interfaces.AUTO.json', buildInterfaceProps(INTERFACE_GLOBS)); }; - console.log(`--------------------------------------------------------------------------------`); - console.log(`Generate docs reference files...`); - console.log('Using Typescript version: ', ts.version); - await generateMetaFiles(); console.log(`Generated OK.`); - console.log(`--------------------------------------------------------------------------------`); + console.log('-'.repeat(80)); } // Run the executor for degugging diff --git a/plugins/ag-grid-generate-code-reference-files/src/executors/generate/generate-code-reference-files.ts b/plugins/ag-grid-generate-code-reference-files/src/executors/generate/generate-code-reference-files.ts index 5b734fb3064..a68ad9b75a9 100644 --- a/plugins/ag-grid-generate-code-reference-files/src/executors/generate/generate-code-reference-files.ts +++ b/plugins/ag-grid-generate-code-reference-files/src/executors/generate/generate-code-reference-files.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import path from 'path'; import ts from 'typescript'; import { _GET_ALL_EVENTS } from './_copiedFromCore/eventTypes'; @@ -47,13 +48,13 @@ function toCamelCase(value) { return value[0].toLowerCase() + value.substring(1); } -function silentFindNode(text: string, srcFile: ts.SourceFile): ts.Node | undefined { +function silentFindNode(text: string, srcFile: ts.SourceFile, auxSrcFiles: AuxSrcFiles): ts.Node | undefined { let typeRef: ts.Node | undefined = undefined; try { - typeRef = findNode(text, srcFile); + typeRef = findInAllTrees(text, srcFile, auxSrcFiles); } catch { try { - typeRef = findNode(text, srcFile, 'TypeAliasDeclaration'); + typeRef = findInAllTrees(text, srcFile, auxSrcFiles, 'TypeAliasDeclaration'); } catch { // Do nothing } @@ -66,71 +67,93 @@ function extractNestedTypes( srcFile: ts.SourceFile, includeQuestionMark: boolean, results: Record, - visited: Set + visited: Set, + auxSrcFiles: ts.SourceFile[] ): void { if (visited.has(node)) { return; } if (ts.isTypeReferenceNode(node)) { - const typeRef = silentFindNode(node.typeName.getText(), srcFile); + const typeRef = silentFindNode(node.typeName.getText(), srcFile, auxSrcFiles); if (typeRef === undefined) { + console.error('failed to find', node.typeName.getText()); return; } visited.add(node); - extractNestedTypes(typeRef, srcFile, includeQuestionMark, results, visited); + extractNestedTypes(typeRef, srcFile, includeQuestionMark, results, visited, auxSrcFiles); return; } if (ts.isTypeAliasDeclaration(node)) { visited.add(node); - extractNestedTypes(node.type, srcFile, includeQuestionMark, results, visited); + extractNestedTypes(node.type, srcFile, includeQuestionMark, results, visited, auxSrcFiles); return; } if (ts.isInterfaceDeclaration(node)) { visited.add(node); - node.heritageClauses?.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited)); - node.members.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited)); + node.heritageClauses?.map((n) => + extractNestedTypes(n, srcFile, includeQuestionMark, results, visited, auxSrcFiles) + ); + node.members.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited, auxSrcFiles)); return; } if (ts.isHeritageClause(node)) { - node.types.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited)); + node.types.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited, auxSrcFiles)); return; } if (ts.isUnionTypeNode(node)) { - node.types.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited)); + node.types.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited, auxSrcFiles)); + return; + } + + if (ts.isArrayTypeNode(node)) { + extractNestedTypes(node.elementType, srcFile, includeQuestionMark, results, visited, auxSrcFiles); + return; + } + + if (ts.isParenthesizedTypeNode(node)) { + extractNestedTypes(node.type, srcFile, includeQuestionMark, results, visited, auxSrcFiles); return; } if (ts.isExpressionWithTypeArguments(node)) { - extractNestedTypes(node.expression, srcFile, includeQuestionMark, results, visited); + extractNestedTypes(node.expression, srcFile, includeQuestionMark, results, visited, auxSrcFiles); return; } if (ts.isPropertySignature(node)) { results[node.name.getText()] = getJsDoc(node); if (node.type) { - extractNestedTypes(node.type, srcFile, includeQuestionMark, results, visited); + extractNestedTypes(node.type, srcFile, includeQuestionMark, results, visited, auxSrcFiles); } return; } if (ts.isIdentifier(node)) { - const ref = findNode(node.escapedText, srcFile); - extractNestedTypes(ref, srcFile, includeQuestionMark, results, visited); + const ref = silentFindNode(node.getText(), srcFile, auxSrcFiles); + if (ref) { + extractNestedTypes(ref, srcFile, includeQuestionMark, results, visited, auxSrcFiles); + } return; } if (ts.isTypeLiteralNode(node)) { - node.members.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited)); + node.members.map((n) => extractNestedTypes(n, srcFile, includeQuestionMark, results, visited, auxSrcFiles)); return; } } -function extractTypesFromNode(node, srcFile, includeQuestionMark, extractNested = false) { +function extractTypesFromNode( + node, + srcFile: ts.SourceFile, + includeQuestionMark: boolean, + extractNested = false, + auxSrcFiles: AuxSrcFiles = [] +) { const nodeMembers = {}; const kind = ts.SyntaxKind[node.kind]; @@ -155,7 +178,7 @@ function extractTypesFromNode(node, srcFile, includeQuestionMark, extractNested }; if (extractNested) { const nested = {}; - extractNestedTypes(node.type, srcFile, includeQuestionMark, nested, new Set()); + extractNestedTypes(node.type, srcFile, includeQuestionMark, nested, new Set(), auxSrcFiles); type.nested = nested; } nodeMembers[name] = { meta: getJsDoc(node), type }; @@ -182,12 +205,12 @@ function extractTypesFromNode(node, srcFile, includeQuestionMark, extractNested return nodeMembers; } -function parseFile(sourceFile) { +function parseFile(sourceFile: string): ts.SourceFile { const src = fs.readFileSync(sourceFile, 'utf8'); return ts.createSourceFile('tempFile.ts', src, ts.ScriptTarget.Latest, true); } -export function getInterfaces(globs) { +export function getInterfaces(globs: string[]) { let interfaces = {}; const extensions = {}; globs.forEach((file) => { @@ -238,11 +261,11 @@ function getAncestors(extensions, child) { return ancestors; } -function isBuiltinUtilityType(type) { +function isBuiltinUtilityType(type: string): type is 'Required' | 'Omit' | 'Pick' | 'Readonly' | 'Optional' { return type === 'Required' || type === 'Omit' || type === 'Pick' || type === 'Readonly' || type === 'Optional'; } -function mergeAncestorProps(isDocStyle, parent, child, getProps) { +function mergeAncestorProps(isDocStyle: boolean, parent, child, getProps) { const props = { ...getProps(child) }; const mergedProps = props; // If the parent has a generic params lets apply the child's specific types @@ -502,7 +525,7 @@ function extractInterfaces(srcFile, extension) { } /** Build the interface file in the format that can be used by */ -export function buildInterfaceProps(globs) { +export function buildInterfaceProps(globs: string[]) { const interfaces = { _config_: {}, }; @@ -543,13 +566,58 @@ export function buildInterfaceProps(globs) { return interfaces; } +function parseImportedDefinitions( + dir: string, + srcFile: ts.SourceFile, + definitions = new Map() +): AuxSrcFiles { + srcFile.forEachChild((child) => { + if (ts.isImportDeclaration(child)) { + const modulePath = child.moduleSpecifier.getFullText().trim().replaceAll("'", ''); + // only look at local imports for now + if (modulePath.startsWith('.') && !modulePath.includes('css-GENERATED')) { + const absPath = require.resolve(path.resolve(dir, `${modulePath}.ts`)); + if (definitions.has(absPath)) { + return; + } + const parsed = parseFile(absPath); + definitions.set(absPath, parsed); + parseImportedDefinitions(path.dirname(absPath), parsed, definitions); + return; + } + } + }); + + return Array.from(definitions.values()); +} + +type AuxSrcFiles = ts.SourceFile[]; + +function findInAllTrees( + typeName: string, + sourceFile: ts.SourceFile, + auxSrcFiles: AuxSrcFiles, + type = 'InterfaceDeclaration' +): ts.TypeNode | undefined { + try { + return findNode(typeName, sourceFile, type); + } catch (error) { + if (auxSrcFiles.length > 0) { + return findInAllTrees(typeName, auxSrcFiles[0], auxSrcFiles.slice(1), type); + } else { + throw error; + } + } +} + export function getGridOptions(gridOpsFile: string) { const srcFile = parseFile(gridOpsFile); + const otherTrees = parseImportedDefinitions(path.dirname(gridOpsFile), srcFile); const gridOptionsNode = findNode('GridOptions', srcFile); - let gridOpsMembers = {}; + const gridOpsMembers = {}; ts.forEachChild(gridOptionsNode, (n) => { - gridOpsMembers = { ...gridOpsMembers, ...extractTypesFromNode(n, srcFile, false, true) }; + Object.assign(gridOpsMembers, extractTypesFromNode(n, srcFile, false, true, otherTrees)); }); return gridOpsMembers; From b1b3019b1ed971c4893efb1d50be64ef375ee5c0 Mon Sep 17 00:00:00 2001 From: Stephen Cooper Date: Fri, 16 Jan 2026 12:37:12 +0000 Subject: [PATCH 4/5] Module comparison include gzip numbers (#12897) * Module comparison include gzip numbers * Test Report output * update gzip to be self size * Handle missing gzip sizes while they are not on latest * temp work around for new gzipSelf property * revert test change --- scripts/ci/compare-module-sizes.mjs | 60 ++++++++++++++--------- testing/module-size/moduleCombinations.ts | 15 +++++- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/scripts/ci/compare-module-sizes.mjs b/scripts/ci/compare-module-sizes.mjs index c174626d6b9..74adf45f600 100644 --- a/scripts/ci/compare-module-sizes.mjs +++ b/scripts/ci/compare-module-sizes.mjs @@ -91,11 +91,15 @@ for (const key of allKeys) { const pr = prMap.get(key); if (base && pr) { + // Not available on latest so default to 0 for now + pr.gzipSelfSize = pr.gzipSelfSize || 0; + base.gzipSelfSize = base.gzipSelfSize || 0; + const selfSizeDiff = pr.selfSize - base.selfSize; const fileSizeDiff = pr.fileSize - base.fileSize; - const gzipSizeDiff = pr.gzipSize - base.gzipSize; + const gzipSelfSizeDiff = pr.gzipSelfSize - base.gzipSelfSize; const selfSizePercent = calcPercent(base.selfSize, selfSizeDiff); - const gzipSizePercent = calcPercent(base.gzipSize, gzipSizeDiff); + const gzipSelfSizePercent = calcPercent(base.gzipSelfSize, gzipSelfSizeDiff); diffs.push({ modules: pr.modules, @@ -107,14 +111,16 @@ for (const key of allKeys) { baseFileSize: base.fileSize, prFileSize: pr.fileSize, fileSizeDiff, - baseGzipSize: base.gzipSize, - prGzipSize: pr.gzipSize, - gzipSizeDiff, - gzipSizePercent, + baseGzipSelfSize: base.gzipSelfSize, + prGzipSelfSize: pr.gzipSelfSize, + gzipSelfSizeDiff, + gzipSelfSizePercent, isNew: false, isRemoved: false, }); } else if (pr && !base) { + // Not available on latest so default to 0 for now + pr.gzipSelfSize = pr.gzipSelfSize || 0; diffs.push({ modules: pr.modules, key, @@ -125,14 +131,16 @@ for (const key of allKeys) { baseFileSize: 0, prFileSize: pr.fileSize, fileSizeDiff: pr.fileSize, - baseGzipSize: 0, - prGzipSize: pr.gzipSize, - gzipSizeDiff: pr.gzipSize, - gzipSizePercent: 100, + baseGzipSelfSize: 0, + prGzipSelfSize: pr.gzipSelfSize, + gzipSelfSizeDiff: pr.gzipSelfSize, + gzipSelfSizePercent: 100, isNew: true, isRemoved: false, }); } else if (base && !pr) { + // Not available on latest so default to 0 for now + base.gzipSelfSize = base.gzipSelfSize || 0; diffs.push({ modules: base.modules, key, @@ -143,10 +151,10 @@ for (const key of allKeys) { baseFileSize: base.fileSize, prFileSize: 0, fileSizeDiff: -base.fileSize, - baseGzipSize: base.gzipSize, - prGzipSize: 0, - gzipSizeDiff: -base.gzipSize, - gzipSizePercent: -100, + baseGzipSelfSize: base.gzipSelfSize, + prGzipSelfSize: 0, + gzipSelfSizeDiff: -base.gzipSelfSize, + gzipSelfSizePercent: -100, isNew: false, isRemoved: true, }); @@ -194,8 +202,10 @@ if (!showMaxIncrease && !showMaxDecrease) { // Significant changes table if (significantChanges.length > 0) { report += `### Significant Changes (≥ ${THRESHOLD_PERCENT}%)\n\n`; - report += '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Diff % | Gzip Diff (KB) | Gzip % |\n'; - report += '|-----------|-----------|---------|-----------|--------|----------------|--------|\n'; + report += + '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Diff % | Base Gzip (KB) | PR Gzip (KB) | Gzip Diff (KB) | Gzip % |\n'; + report += + '|-----------|-----------|---------|-----------|--------|----------------|--------------|----------------|--------|\n'; for (const diff of significantChanges) { const moduleName = diff.modules.length === 0 ? 'Base (no modules)' : diff.modules.join(', '); @@ -209,8 +219,10 @@ if (significantChanges.length > 0) { applyHighlight(formatSize(diff.prSelfSize), shouldHighlight), applyHighlight(`**${formatDiff(diff.selfSizeDiff)}**`, shouldHighlight), applyHighlight(formatPercent(diff.selfSizePercent), shouldHighlight), - applyHighlight(formatDiff(diff.gzipSizeDiff), shouldHighlight), - applyHighlight(formatPercent(diff.gzipSizePercent), shouldHighlight), + applyHighlight(formatSize(diff.baseGzipSelfSize), shouldHighlight), + applyHighlight(formatSize(diff.prGzipSelfSize), shouldHighlight), + applyHighlight(formatDiff(diff.gzipSelfSizeDiff), shouldHighlight), + applyHighlight(formatPercent(diff.gzipSelfSizePercent), shouldHighlight), ]; report += `| ${cells.join(' | ')} |\n`; @@ -254,8 +266,10 @@ report += `- **Modules unchanged:** ${diffs.filter((d) => d.selfSizeDiff === 0). const allChanges = diffs.filter((d) => d.selfSizeDiff !== 0); if (allChanges.length > 0) { report += '#### All Module Changes\n\n'; - report += '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Diff % | Gzip Diff (KB) | Gzip % |\n'; - report += '|-----------|-----------|---------|-----------|--------|----------------|--------|\n'; + report += + '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Diff % | Base Gzip (KB) | PR Gzip (KB) | Gzip Diff (KB) | Gzip % |\n'; + report += + '|-----------|-----------|---------|-----------|--------|----------------|--------------|----------------|--------|\n'; for (const diff of allChanges) { const moduleName = diff.modules.length === 0 ? 'Base (no modules)' : diff.modules.join(', '); @@ -269,8 +283,10 @@ if (allChanges.length > 0) { applyHighlight(formatSize(diff.prSelfSize), shouldHighlight), applyHighlight(`**${formatDiff(diff.selfSizeDiff)}**`, shouldHighlight), applyHighlight(formatPercent(diff.selfSizePercent), shouldHighlight), - applyHighlight(formatDiff(diff.gzipSizeDiff), shouldHighlight), - applyHighlight(formatPercent(diff.gzipSizePercent), shouldHighlight), + applyHighlight(formatSize(diff.baseGzipSelfSize), shouldHighlight), + applyHighlight(formatSize(diff.prGzipSelfSize), shouldHighlight), + applyHighlight(formatDiff(diff.gzipSelfSizeDiff), shouldHighlight), + applyHighlight(formatPercent(diff.gzipSelfSizePercent), shouldHighlight), ]; report += `| ${cells.join(' | ')} |\n`; diff --git a/testing/module-size/moduleCombinations.ts b/testing/module-size/moduleCombinations.ts index 77160b591da..738cf48da13 100644 --- a/testing/module-size/moduleCombinations.ts +++ b/testing/module-size/moduleCombinations.ts @@ -4,9 +4,17 @@ const fs = require('fs'); const { exec } = require('child_process'); const path = require('path'); -const results: { modules: string[]; expectedSize: number; selfSize: number; fileSize: number; gzipSize: number }[] = []; +const results: { + modules: string[]; + expectedSize: number; + selfSize: number; + gzipSelfSize: number; + fileSize: number; + gzipSize: number; +}[] = []; const updateModulesScript = path.join(__dirname, 'moduleUpdater.ts'); let baseSize = 0; +let baseGzipSize = 0; let moduleCombinationsToProcess = moduleCombinations; @@ -74,16 +82,21 @@ function runCombination(index) { const gzipSize = parseFloat(gzipSizeMatch[1]); let selfSize = 0; + let gzipSelfSize = 0; if (modules.length === 0) { baseSize = fileSize; + baseGzipSize = gzipSize; selfSize = fileSize; + gzipSelfSize = gzipSize; } else { selfSize = parseFloat((fileSize - baseSize).toFixed(2)); + gzipSelfSize = parseFloat((gzipSize - baseGzipSize).toFixed(2)); } results.push({ modules, selfSize, + gzipSelfSize, fileSize, gzipSize, expectedSize, From a74130933621e4df63da3d7c7f09c992c24ea6a1 Mon Sep 17 00:00:00 2001 From: Bernie Sumption Date: Fri, 16 Jan 2026 13:08:14 +0000 Subject: [PATCH 5/5] AG-16528 Use page over menu link labels for website search (#12899) --- .../update-algolia-indices/src/generators/docs-pages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/update-algolia-indices/src/generators/docs-pages.ts b/documentation/update-algolia-indices/src/generators/docs-pages.ts index 207239de3e5..d610e2644b6 100644 --- a/documentation/update-algolia-indices/src/generators/docs-pages.ts +++ b/documentation/update-algolia-indices/src/generators/docs-pages.ts @@ -85,7 +85,7 @@ export const parseDocPage = async (item: FlattenedMenuItem) => { objectID: hashPath, breadcrumb, - title, + title: pageTitle || title, heading, subHeading, path: hashPath,