Skip to content
Draft
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
13 changes: 6 additions & 7 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,7 @@ pnpm nx run-many -t lint # all packages
pnpm run lint # devextreme package: js, ts, dts, texts
pnpm run lint-js -- --fix # auto-fix JS

# Regenerate (after changes to generators, TS declarations, or devextreme-internal-tools)
pnpm run regenerate-all
# Regenerate TS reexports and bundle (after changes to public .d.ts)
pnpm run update-ts-reexports # from packages/devextreme
pnpm run update-ts-bundle # from packages/devextreme

Expand All @@ -183,7 +182,7 @@ pnpm nx clean:artifacts devextreme # build artifacts only
```
packages/
devextreme/ # core library: ui/, viz/, core/, data/, __internal/
devextreme-{angular,react,vue}/ # framework wrappers (generated)
devextreme-{angular,react,vue}/ # framework wrappers (hand/AI-maintained)
devextreme-scss/ # SCSS themes
devextreme-themebuilder/ # theme builder
devextreme-metadata/ # metadata for wrapper generation
Expand All @@ -209,22 +208,22 @@ clean (`devextreme-nx-infra-plugin:clean` preserving CSS and npm metadata) → l

- Use `pnpm nx <target> <project>` rather than raw npm scripts so Nx caching and the dependency graph stay correct.
- Build before testing: `pnpm nx build:dev devextreme`; QUnit and TestCafe both require an up-to-date build.
- Run `pnpm run regenerate-all` after editing wrapper generators, TypeScript declarations, or `devextreme-internal-tools`.
- Edit source files only under `packages/devextreme/js/**`, `packages/devextreme-scss/scss/**`, and `packages/devextreme-metadata/**`.
- The Angular/React/Vue wrappers are maintained by hand (with AI agents), not regenerated from metadata. When public API in `packages/devextreme/js/{ui,viz}/**/*.d.ts` changes, update all three wrappers in the same PR following `.github/instructions/wrapper-{angular,react,vue}.instructions.md` and `.github/instructions/public-api-wrappers.instructions.md`.
- Edit source files under `packages/devextreme/js/**`, `packages/devextreme-scss/scss/**`, `packages/devextreme-metadata/**`, and the wrapper `packages/devextreme-{angular,react,vue}/src/**` (except each package's `src/core/**`).
- Match the Node and pnpm versions declared in `package.json` (`engines`, `packageManager`); mismatched versions cause CI failure.
- Set `DEVEXTREME_TEST_CI=true` for test-mode builds and `BUILD_TEST_INTERNAL_PACKAGE=true` for wrapper test prep.

## Constraints

- NEVER edit generated wrappers under `packages/devextreme-{angular,react,vue}/src/` (templates excepted); update the generators and run `pnpm run regenerate-all` instead.
- The wrappers under `packages/devextreme-{angular,react,vue}/src/` are maintained by hand — keep them in sync with the public API per the wrapper instructions; NEVER edit each package's `src/core/**` (hand-written base classes) to accommodate a single component.
- NEVER hand-edit `packages/devextreme/js/__internal/core/localization/{default_messages.ts,cldr-data/**}`; regenerate via the localization executor.
- NEVER run `pnpm install` without `--frozen-lockfile`; use `pnpm install --frozen-lockfile` to match CI.

## Compact Instructions

- Always install with `pnpm install --frozen-lockfile`; never plain `pnpm install`.
- Build before test: `pnpm nx build:dev devextreme`.
- Generated wrappers under `packages/devextreme-{angular,react,vue}/src/` are read-onlymodify generators and run `pnpm run regenerate-all`.
- Wrappers under `packages/devextreme-{angular,react,vue}/src/` are hand/AI-maintainedupdate them per the wrapper instructions when public API changes; leave each `src/core/**` alone.
- Prefer `pnpm nx <target>` over direct npm scripts for caching.
- Consult @.github/instructions/ for file-specific coding rules before editing.

Expand Down
64 changes: 64 additions & 0 deletions .github/instructions/public-api-wrappers.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
applyTo: "**/packages/devextreme/js/{ui,viz}/**/*.d.ts"
---

# Public API changes must be propagated to the framework wrappers

The Angular, React, and Vue wrappers are **no longer regenerated** from
`devextreme-metadata` / `devextreme-internal-tools`. They are maintained by hand (with the
help of local AI agents). This means **every public API change in a wrapped component's
`.d.ts` must be applied, in the same pull request, to all three wrappers**.

This file applies to the public API surface (`js/ui/**/*.d.ts`, `js/viz/**/*.d.ts`). Use it
both when editing the API and when reviewing an API change.

## What counts as a wrapper-affecting change

A change to any of the following in a component's options interface (e.g. `dxButtonOptions`,
its `Properties` alias, or a nested option type such as `dxDataGridColumn`) affects the
wrappers:

- **Options** — a property added, removed, renamed, or retyped on the options interface.
- **Events** — an `onX` callback added, removed, renamed, or whose event type changed.
- **Event types** — a `*Event` type (e.g. `ClickEvent`) whose shape changed.
- **Nested option types** — a collection/object option type (e.g. `columns`, `items`,
`toolbar`) added, removed, renamed, or restructured.
- **Template options** — a `template` option added or removed.

Purely internal changes (implementation in `js/__internal/**`, JSDoc-only edits that do not
change types or names) do **not** require a wrapper change.

## Where each change lands in the wrappers

| Public API element | Angular | React | Vue |
|---|---|---|---|
| Option `foo?: T` | `@Input() get foo()/set foo()` in `ui/<comp>/index.ts` | field in the component's options type (`IXOptions`) | entry in `props` (correct runtime type + `PropType<T>`) and `emits` `update:foo` |
| Event `onX?: (e: XEvent) => void` | `@Output() onX: EventEmitter<XEvent>` + `{ subscribe, emit }` entry | narrowed field in `IXOptionsNarrowedEvents` + entry in `independentEvents` | `onX` prop typed `Function as PropType<(e: XEvent) => void>` |
| Nested option type | do **not** edit `ui/nested/**` (deprecated, removed in a parallel PR) | `NestedOption` config component | configuration component via `prepareConfigurationComponentConfig` |
| Template option | template input | entry in `templateProps` | `template` prop |

The per-framework anatomy and rules live in
[wrapper-angular.instructions.md](./wrapper-angular.instructions.md),
[wrapper-react.instructions.md](./wrapper-react.instructions.md), and
[wrapper-vue.instructions.md](./wrapper-vue.instructions.md).

## Reviewer checklist

When a PR changes a wrapped component's public `.d.ts`, verify and comment inline if any is
missing:

1. **Completeness** — every added, removed, or renamed option/event is reflected in
**all three** wrappers (`devextreme-angular/src`, `devextreme-react/src`,
`devextreme-vue/src`). A change present in one or two wrappers but not the third is a
defect — flag the missing wrapper(s) by name.
2. **Type fidelity** — the wrapper carries the same type as the API (including union
members, generics, and the correct Vue runtime type + `PropType`).
3. **Breaking changes** — explicitly label these as **breaking** in a review comment:
- an option or event **removed** or **renamed**;
- an option/event **type narrowed** or otherwise changed incompatibly;
- a nested option type removed or restructured.
Confirm the wrappers reflect the same removal/rename so they stay in sync; do not let a
breaking API change land with stale wrapper surface.

Only comment on the wrappers and the API contract. Do not propose regenerating the wrappers
or restoring the metadata pipeline — that workflow is retired.
96 changes: 96 additions & 0 deletions .github/instructions/wrapper-angular.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
applyTo: "**/packages/devextreme-angular/src/**"
---

# Maintaining the DevExtreme Angular wrapper by hand

The files under `packages/devextreme-angular/src/` are **no longer generated**. Edit them
directly to keep them in sync with the public API in
`packages/devextreme/js/{ui,viz}/**/*.d.ts`. See
[public-api-wrappers.instructions.md](./public-api-wrappers.instructions.md) for when a
wrapper change is required and what the reviewer checks.

## Do not edit

- `packages/devextreme-angular/src/core/**` — hand-written base classes
(`DxComponent`, `NestedOptionHost`, template/integration modules). Never change these to
accommodate a single component.
- `packages/devextreme-angular/src/ui/nested/**` — the legacy nested option components
("old nesteds"). This folder is **deprecated and being removed in a parallel PR**. Do not
add, edit, or reference files here, even when a nested/collection option changes.

## Adding a new component

Fastest path: **clone the closest existing wrapper and rename**, rather than writing a file
from scratch. For a plain Widget-based component copy `ui/load-indicator/index.ts`; for one
with many options/events copy `ui/button/index.ts`; for a container with nested options copy
`ui/sortable/` (the component plus its `nested/` folder).

Files to create and register (keep every list alphabetical):

- `packages/devextreme-angular/src/ui/<name>/index.ts` — the component (see anatomy below).
- `packages/devextreme-angular/src/ui/<name>/ng-package.json` —
`{ "lib": { "entryFile": "index.ts" } }`.
- `packages/devextreme-angular/src/index.ts` — add
`export { Dx<Name>Component, Dx<Name>Module } from 'devextreme-angular/ui/<name>';`.
- `packages/devextreme-angular/src/ui/all.ts` — add the `import` **and** add `Dx<Name>Module`
to **both** the declarations and exports arrays (the two arrays are identical; anchor on the
neighboring entries to place it in each).
- For a component with nested/object options, also create the `ui/<name>/nested/` folder
(cloning `ui/sortable/nested/` is the fastest way) and wire its module into the component's
`@NgModule` plus `export * from 'devextreme-angular/ui/<name>/nested';`.

Do **not** create anything under `src/metadata/generated/**` — it is gitignored generated
output, not source.

## Component file anatomy (`ui/<component>/index.ts`)

Use the widget's options interface (e.g. `dxButtonOptions` and its `WidgetOptions` base) as
the source of truth. A component file contains, in order:

1. **Imports** — Angular symbols (`Component`, `Input`, `Output`, `EventEmitter`,
`NgModule`, `OnDestroy`, …), `type` imports of the event types and enum/union types from
`devextreme/ui/<widget>` and `devextreme/common`, the default widget import
(`import DxButton from 'devextreme/ui/button'`), and base classes from
`devextreme-angular/core`.
2. **`@Component` decorator** — `selector: 'dx-<name>'`, `template` (`<ng-content>` for
container widgets, empty otherwise), `imports: [ DxIntegrationModule ]`, and the standard
`providers` (`DxTemplateHost`, `WatcherHelper`, `NestedOptionHost`).
3. **Class** `Dx<Name>Component extends DxComponent implements OnDestroy` with
`instance: Dx<Name> = null;`.
4. **One `@Input()` getter/setter pair per option**, in the same order as the options
interface (including inherited `WidgetOptions`/`DOMComponentOptions`):
```ts
@Input()
get text(): string { return this._getOption('text'); }
set text(value: string) { this._setOption('text', value); }
```
The getter/setter type must match the option's type exactly (keep `| undefined` and
union members).
5. **Events** — one `@Output()` per `onX` callback, named exactly as the option and typed
with the event type:
```ts
@Output() onClick: EventEmitter<ClickEvent>;
```
6. **Two-way binding outputs** — one `@Output() <option>Change: EventEmitter<T>;` for **every**
`@Input` option (type matches the input).
7. **Constructor** — calls `this._createEventEmitters([...])` with one entry per emitter:
`{ subscribe: '<widgetEventName>', emit: 'on<Event>' }` for events (the widget event name
is the `onX` name without the `on` prefix, camelCased: `onClick` → `click`) and
`{ emit: '<option>Change' }` for every option.
8. **`_createInstance(element, options)`** — returns `new Dx<Name>(element, options)`.
9. **`ngOnDestroy()`** — `this._destroyWidget();`.
10. **`@NgModule`** exporting the component (and `DxTemplateModule`).
11. **Types reexport** — `import type * as Dx<Name>Types from "devextreme/ui/<widget>_types"; export { Dx<Name>Types };`

## Applying an API change

- **Option added** → add the `@Input` getter/setter, its `<option>Change` output, and the
`{ emit: '<option>Change' }` entry in `_createEventEmitters`.
- **Option removed/renamed** → remove/rename all three (input, change output, emitter entry).
- **Event added** → add the `@Output() onX`, and a `{ subscribe, emit }` entry.
- **Nested/collection option** → do **not** touch `ui/nested/**` (deprecated, see "Do not
edit"). Leave nested option handling to the parallel removal PR.

Keep import lists, option order, and emitter order aligned with the options interface so
diffs stay reviewable.
83 changes: 83 additions & 0 deletions .github/instructions/wrapper-react.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
applyTo: "**/packages/devextreme-react/src/**"
---

# Maintaining the DevExtreme React wrapper by hand

The files under `packages/devextreme-react/src/` are **no longer generated**. Edit them
directly to keep them in sync with the public API in
`packages/devextreme/js/{ui,viz}/**/*.d.ts`. See
[public-api-wrappers.instructions.md](./public-api-wrappers.instructions.md) for when a
wrapper change is required and what the reviewer checks.

## Do not edit

- `packages/devextreme-react/src/core/**` — hand-written base
(`Component as BaseComponent`, `NestedOption`, `NestedComponentMeta`, `IHtmlOptions`,
`ComponentRef`). Never change these for a single component.

## Adding a new component

Fastest path: **clone the closest existing wrapper and rename**, rather than writing a file
from scratch. For a plain Widget-based component copy `load-indicator.ts`; for one with a
`template` copy `button.ts`; for a container with nested options copy `data-grid.ts` (it
contains the nested config-component pattern).

Files to create and register (keep every list alphabetical):

- `packages/devextreme-react/src/<name>.ts` — the component (see anatomy below), including any
nested config components in the same file.
- `packages/devextreme-react/src/index.ts` — add `export { <Name> } from "./<name>";`.

## Component file anatomy (`<component>.ts`)

The widget's `Properties` type (alias of `dx<Widget>Options`) is the source of truth.

1. **`"use client"`** directive on line 1, then React imports (`memo`, `forwardRef`,
`useImperativeHandle`, `useRef`, `useMemo`, …), the default widget import and its
`Properties` (`import dxButton, { Properties } from "devextreme/ui/button"`), the base
(`Component as BaseComponent`, `IHtmlOptions`, `ComponentRef`, and `NestedOption`/
`NestedComponentMeta` if there are nested options), and `type` imports of the event types.
2. **`ReplaceFieldTypes`** helper (copied verbatim).
3. **`I<Name>OptionsNarrowedEvents`** — one field per `onX` event, typed
`((e: XEvent) => void)`, so the wrapper exposes the concrete event type.
4. **`I<Name>Options`** — `React.PropsWithChildren<ReplaceFieldTypes<Properties,
I<Name>OptionsNarrowedEvents> & IHtmlOptions & { render?...; component?... }>` (template
props included only if the widget has a `template`).
5. **`<Name>Ref`** interface exposing `instance: () => dx<Widget>`.
6. **Component** — `memo(forwardRef((props, ref) => { … }))`. Inside:
- `useImperativeHandle` returning `{ instance() { return baseRef.current?.getInstance(); } }`;
- `useMemo` arrays describing widget behavior, each derived from the API:
- `independentEvents` — the list of every `onX` event name;
- `subscribableOptions` / `defaults` — for two-way–bindable options (`defaultX` maps to `x`);
- `expectedChildren` — nested option components (`{ optionName, isCollectionItem }`);
- `templateProps` — `{ tmplOption, render, component }` per `template` option;
- `React.createElement(BaseComponent<…>, { WidgetClass: dx<Widget>, ref: baseRef, …arrays, ...props })`.
7. **Exports** — `export default <Name>;`, `export { <Name>, I<Name>Options, <Name>Ref };`,
then `import type * as <Name>Types from 'devextreme/ui/<widget>_types'; export { <Name>Types };`.

## Nested option components (config components)

For each nested/object option, a config component is defined and exported:
```ts
const _componentColumn = (props: IColumnProps) =>
React.createElement(NestedOption<IColumnProps>, {
...props,
elementDescriptor: { OptionName: "columns", /* ExpectedChildren for deeper nesting */ },
});
const Column = Object.assign<typeof _componentColumn, NestedComponentMeta>(_componentColumn, {
componentType: "option",
});
```
`OptionName` is the parent option; `isCollectionItem: true` (in the owner's
`expectedChildren`) marks array options.

## Applying an API change

- **Option added** → it flows in through `Properties`; add it to `subscribableOptions`/
`defaults` only if it is two-way bindable, and to `templateProps` if it is a template.
- **Event added/removed/renamed** → update `I<Name>OptionsNarrowedEvents` and the
`independentEvents` array.
- **Nested option added/changed** → add/update the `_component*` + `Object.assign` export and
the owner's `expectedChildren` entry.
- **Type change** → the concrete event/enum type imports must be updated to match.
Loading
Loading