diff --git a/.github/workflows/qunit_tests.yml b/.github/workflows/qunit_tests.yml index 86d0994d64ce..2ae2c013fc1d 100644 --- a/.github/workflows/qunit_tests.yml +++ b/.github/workflows/qunit_tests.yml @@ -111,7 +111,7 @@ jobs: shell: bash env: DEVEXTREME_TEST_CI: "true" - run: pnpm exec nx build:systemjs + run: pnpm exec nx build:dev - name: Zip artifacts working-directory: ./packages/devextreme diff --git a/packages/devextreme/docker-ci.sh b/packages/devextreme/docker-ci.sh index 28f51484ee3c..4944b701684c 100755 --- a/packages/devextreme/docker-ci.sh +++ b/packages/devextreme/docker-ci.sh @@ -40,7 +40,8 @@ function run_test { function run_test_impl { local port=`node -e "console.log(require('./ports.json').qunit)"` - local url="http://0.0.0.0:$port/run?notimers=true" + # Use 127.0.0.1, not 0.0.0.0 — Chrome cannot fetch modules from 0.0.0.0. + local url="http://127.0.0.1:$port/run?notimers=true" local runner_pid local runner_result=0 diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 4e3ad735a89e..a069f831b981 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -11,6 +11,7 @@ import { Deferred, when } from '@js/core/utils/deferred'; import { each } from '@js/core/utils/iterator'; import { getBoundingRect } from '@js/core/utils/position'; import { getHeight, getWidth } from '@js/core/utils/size'; +import { setHeight } from '@js/core/utils/style'; import { isDefined, isNumeric, isString } from '@js/core/utils/type'; import { getWindow, hasWindow } from '@js/core/utils/window'; import * as accessibility from '@js/ui/shared/accessibility'; @@ -826,7 +827,7 @@ export class ResizingController extends modules.ViewController { // IE11 if (maxHeightHappened && !isMaxHeightApplied) { - $(groupElement).css('height', maxHeight); + setHeight($(groupElement), maxHeight); } if (!dataController.isLoaded()) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts index ee5eb9a8b98d..018c38129c5f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts @@ -536,7 +536,7 @@ export class RowsView extends ColumnsView { $cell = that._createCell({ column: columns[i], rowType: 'freeSpace', columnIndex: i, columns, }); - isNumeric(height) && $cell.css('height', height); + isNumeric(height) && setHeight($cell, height); $row.append($cell); } @@ -1035,7 +1035,7 @@ export class RowsView extends ColumnsView { if (showFreeSpaceRow) { deferRender(() => { - freeSpaceRowElements.css('height', resultHeight); + setHeight(freeSpaceRowElements, resultHeight); isFreeSpaceRowVisible = true; freeSpaceRowElements.show(); }); @@ -1044,7 +1044,7 @@ export class RowsView extends ColumnsView { }); } } else { - freeSpaceRowElements.css('height', 0); + setHeight(freeSpaceRowElements, 0); freeSpaceRowElements.show(); this._updateLastRowBorder(true); } diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 3ab3e909e6ba..1e11e1e80a2e 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -130,11 +130,6 @@ "minimist": "1.2.8", "qunit": "2.25.0", "sinon": "18.0.1", - "systemjs": "0.19.41", - "systemjs-plugin-babel": "0.0.25", - "systemjs-plugin-css": "0.1.37", - "systemjs-plugin-json": "0.3.0", - "systemjs-plugin-text": "0.0.11", "terser-webpack-plugin": "5.3.17", "ts-jest": "29.1.2", "tsc-alias": "1.8.16", diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 19efa18de2d0..7a92022c8bb3 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -152,6 +152,20 @@ "{projectRoot}/artifacts/npm/devextreme-internal/bundles/dx.custom.config.js" ] }, + "copy:qunit:dx-custom": { + "executor": "devextreme-nx-infra-plugin:copy-files", + "options": { + "files": [ + { + "from": "./build/bundle-templates/dx.custom.js", + "to": "./artifacts/transpiled-esm-npm/bundles/dx.custom.js" + } + ] + }, + "dependsOn": ["build:devextreme-bundler-config"], + "inputs": ["{projectRoot}/build/bundle-templates/dx.custom.js"], + "outputs": ["{projectRoot}/artifacts/transpiled-esm-npm/bundles/dx.custom.js"] + }, "build:devextreme-bundler-config:watch": { "executor": "devextreme-nx-infra-plugin:concatenate-files", "cache": false, @@ -322,6 +336,11 @@ { "from": "./js/viz/vector_map.utils/_settings.json", "to": "./viz/vector_map.utils/_settings.json" } ] }, + "configurations": { + "qunit": { + "removeDebug": false + } + }, "inputs": [ "jsSourcesProduction", "jsAssetsProduction" @@ -410,6 +429,11 @@ ".jsx": ".js" } }, + "configurations": { + "qunit": { + "removeDebug": false + } + }, "inputs": [ "internalTsArtifacts" ], @@ -417,6 +441,72 @@ "{projectRoot}/artifacts/transpiled-esm-npm/esm/__internal" ] }, + "build:npm:esm:watch": { + "executor": "devextreme-nx-infra-plugin:babel-transform", + "cache": false, + "options": { + "babelConfigPath": "./build/transpile-config.js", + "configKey": "esm", + "sourcePattern": "./js/**/*.{js,jsx}", + "excludePatterns": [ + "./js/**/*.d.ts", + "./js/__internal/**/*" + ], + "outDir": "./artifacts/transpiled-esm-npm/esm", + "removeDebug": true, + "watch": true, + "copyAssets": [ + { "from": "./js/localization/messages", "to": "./localization/messages" }, + { "from": "./js/viz/vector_map.utils/_settings.json", "to": "./viz/vector_map.utils/_settings.json" } + ] + }, + "configurations": { + "qunit": { + "removeDebug": false + } + } + }, + "build:npm:esm:internal:watch": { + "executor": "devextreme-nx-infra-plugin:babel-transform", + "cache": false, + "options": { + "babelConfigPath": "./build/transpile-config.js", + "configKey": "esm", + "sourcePattern": "./artifacts/dist_ts/__internal/**/*.{js,jsx}", + "outDir": "./artifacts/transpiled-esm-npm/esm/__internal", + "removeDebug": true, + "watch": true, + "renameExtensions": { + ".jsx": ".js" + } + }, + "configurations": { + "qunit": { + "removeDebug": false + } + } + }, + "build:qunit-esm": { + "executor": "nx:run-commands", + "options": { + "cwd": "{projectRoot}", + "parallel": true, + "commands": [ + "pnpm nx run devextreme:build:npm:esm -c qunit", + "pnpm nx run devextreme:build:npm:esm:internal -c qunit" + ] + }, + "dependsOn": [ + "build:ts:internal" + ], + "outputs": [ + "{projectRoot}/artifacts/transpiled-esm-npm/esm" + ], + "cache": true, + "metadata": { + "description": "ESM artifacts for QUnit native import-map loader (?loader=esm)." + } + }, "build:npm:cjs:internal": { "executor": "devextreme-nx-infra-plugin:babel-transform", "options": { @@ -530,14 +620,12 @@ "pnpm nx build:devextreme-bundler-config devextreme", "pnpm nx build:devextreme-bundler-config devextreme -c prod", "pnpm nx build:ts:internal devextreme", - "pnpm nx run-many --targets=build:cjs,build:cjs:internal,build:cjs:bundles --projects=devextreme --parallel", - "pnpm nx run-many --targets=build:cjs,build:cjs:internal,build:cjs:bundles --projects=devextreme --parallel -c production", - "pnpm nx run-many --targets=build:npm:cjs,build:npm:cjs:internal --projects=devextreme --parallel", + "pnpm nx run-many --targets=build:npm:esm,build:npm:esm:internal --projects=devextreme --parallel -c qunit", + "pnpm nx copy:qunit:dx-custom devextreme", "pnpm nx clean:dist-ts devextreme" ], "outputs": [ - "{projectRoot}/artifacts/transpiled", - "{projectRoot}/artifacts/transpiled-renovation-npm", + "{projectRoot}/artifacts/transpiled-esm-npm", "{projectRoot}/build/bundle-templates/dx.custom.js", "{projectRoot}/artifacts/npm/devextreme/bundles/dx.custom.config.js" ] @@ -570,10 +658,8 @@ "options": { "commands": [ "pnpm nx build:ts:internal:watch devextreme", - "pnpm nx build:cjs:watch devextreme", - "pnpm nx build:cjs:watch devextreme -c production", - "pnpm nx build:cjs:internal:watch devextreme", - "pnpm nx build:cjs:internal:watch devextreme -c production" + "pnpm nx build:npm:esm:watch devextreme -c qunit", + "pnpm nx build:npm:esm:internal:watch devextreme -c qunit" ], "cwd": "{projectRoot}", "parallel": true @@ -1729,7 +1815,7 @@ "pnpm nx clean:artifacts devextreme", "pnpm nx build:localization devextreme", "pnpm nx build:transpile devextreme -c ci", - "pnpm nx run-many --targets=bundle:debug,build:vectormap,copy:vendor --projects=devextreme --parallel" + "pnpm nx run-many --targets=build:vectormap,copy:vendor --projects=devextreme --parallel" ], "parallel": false }, @@ -1755,58 +1841,9 @@ ], "cache": true, "metadata": { - "description": "Dev/CI test build. Skips prod bundles, aspnet, declarations, npm, and license checks." + "description": "Dev/CI QUnit build (native ESM import-map). Skips all CJS transpile/bundle steps, npm CJS dual-mode, prod bundles, aspnet, declarations, npm packing, and license checks." } }, - "build:systemjs": { - "executor": "nx:run-commands", - "options": { - "cwd": "{projectRoot}", - "parallel": true, - "commands": [ - "node testing/systemjs-builder.js --transpile=modules", - "node testing/systemjs-builder.js --transpile=testing", - "node testing/systemjs-builder.js --transpile=css", - "node testing/systemjs-builder.js --transpile=js-vendors" - ] - }, - "dependsOn": [ - "build:dev" - ], - "inputs": [ - "internalPackageEnv", - { - "env": "DEVEXTREME_TEST_CI" - }, - { - "dependentTasksOutputFiles": "packages/devextreme/artifacts/transpiled/**/*" - }, - { - "dependentTasksOutputFiles": "packages/devextreme/artifacts/css/dx.light.css" - }, - { - "dependentTasksOutputFiles": "packages/devextreme/artifacts/css/dx.material.blue.light.css" - }, - { - "dependentTasksOutputFiles": "packages/devextreme/artifacts/css/dx.fluent.blue.light.css" - }, - { - "dependentTasksOutputFiles": "packages/devextreme/artifacts/css/dx-gantt.css" - }, - "{projectRoot}/testing/content/**/*", - "{projectRoot}/testing/helpers/**/*", - "{projectRoot}/testing/tests/**/*", - "{projectRoot}/testing/systemjs-builder.js", - "{workspaceRoot}/pnpm-lock.yaml" - ], - "outputs": [ - "{projectRoot}/artifacts/transpiled-systemjs", - "{projectRoot}/artifacts/transpiled-testing", - "{projectRoot}/artifacts/css-systemjs", - "{projectRoot}/artifacts/js-systemjs" - ], - "cache": true - }, "dev": { "executor": "nx:run-commands", "cache": false, @@ -1827,7 +1864,6 @@ "commands": [ "pnpm nx build:transpile:watch devextreme", "pnpm nx build:devextreme-bundler-config:watch devextreme", - "pnpm nx bundle:watch devextreme", "pnpm nx test-env devextreme" ] } diff --git a/packages/devextreme/testing/helpers/chartMocks.js b/packages/devextreme/testing/helpers/chartMocks.js index 5e7a2ba7c4d5..f7acef832d79 100644 --- a/packages/devextreme/testing/helpers/chartMocks.js +++ b/packages/devextreme/testing/helpers/chartMocks.js @@ -16,6 +16,8 @@ import { } from './vizMocks.js'; import { Range } from 'viz/translators/range'; +const mutableSeriesFamilyModule = seriesFamilyModule.default ?? seriesFamilyModule; + const LoadingIndicatorOrig = loadingIndicatorModule.LoadingIndicator; const firstCategory = 'First'; @@ -394,7 +396,7 @@ export const resetMockFactory = function resetMockFactory() { }; export const setupSeriesFamily = function() { - seriesFamilyModule.SeriesFamily = function(options) { + mutableSeriesFamilyModule.SeriesFamily = function(options) { return new MockSeriesFamily(options); }; }; diff --git a/packages/devextreme/testing/helpers/data.errorHandlingHelper.js b/packages/devextreme/testing/helpers/data.errorHandlingHelper.js index 9e4d4d445357..329c36b0f3cb 100644 --- a/packages/devextreme/testing/helpers/data.errorHandlingHelper.js +++ b/packages/devextreme/testing/helpers/data.errorHandlingHelper.js @@ -1,16 +1,8 @@ -(function(root, factory) { - root.DevExpress = root.DevExpress || {}; - root.DevExpress.data = root.DevExpress.data || {}; - root.DevExpress.data.testing = root.DevExpress.data.testing || {}; - - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - root.DevExpress.data.testing.ErrorHandlingHelper = module.exports = factory(require('jquery'), require('core/class'), require('common/data/errors')); - }); - } else { - root.DevExpress.data.testing.ErrorHandlingHelper = factory(window.jQuery, DevExpress.Class, DevExpress.data); - } -}(window, function($, Class, errorsModule) { +import $ from 'jquery'; +import Class from 'core/class'; +import * as errorsModule from 'common/data/errors'; + +const __moduleExports = (function($, Class, errorsModule) { return Class.inherit({ ctor: function() { @@ -76,4 +68,11 @@ }); } }); -})); +})($, Class, errorsModule); + +window.DevExpress = window.DevExpress || {}; +window.DevExpress.data = window.DevExpress.data || {}; +window.DevExpress.data.testing = window.DevExpress.data.testing || {}; +window.DevExpress.data.testing.ErrorHandlingHelper = __moduleExports; + +export default __moduleExports; diff --git a/packages/devextreme/testing/helpers/dataGridMocks.js b/packages/devextreme/testing/helpers/dataGridMocks.js index 966f4ccb369a..418f18963e79 100644 --- a/packages/devextreme/testing/helpers/dataGridMocks.js +++ b/packages/devextreme/testing/helpers/dataGridMocks.js @@ -1,32 +1,46 @@ -let gridBaseMock; +import $ from 'jquery'; +import gridCoreModule from '__internal/grids/data_grid/m_core'; +import columnResizingReorderingModule from '__internal/grids/data_grid/module_not_extended/columns_resizing_reordering'; +import domUtilsModule from '__internal/core/utils/m_dom'; +import commonUtilsModule from '__internal/core/utils/m_common'; +import typeUtilsModule from '__internal/core/utils/m_type'; +import ArrayStoreModule from 'common/data/array_store'; +import gridBaseMockModule from './gridBaseMocks.js'; -/* global jQuery */ -if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - gridBaseMock = require('./gridBaseMocks.js'); +const gridBaseMock = gridBaseMockModule.default ?? gridBaseMockModule; +const gridCore = gridCoreModule.default ?? gridCoreModule; +const columnResizingReordering = columnResizingReorderingModule.default ?? columnResizingReorderingModule; +const domUtils = domUtilsModule.default ?? domUtilsModule; +const commonUtils = commonUtilsModule.default ?? commonUtilsModule; +const typeUtils = typeUtilsModule.default ?? typeUtilsModule; +const ArrayStore = ArrayStoreModule.default ?? ArrayStoreModule; - window.dataGridMocks = module.exports = gridBaseMock( - require('jquery'), - require('__internal/grids/data_grid/m_core').default, - require('__internal/grids/data_grid/module_not_extended/columns_resizing_reordering').default, - require('__internal/core/utils/m_dom'), - require('__internal/core/utils/m_common'), - require('__internal/core/utils/m_type'), - require('common/data/array_store'), - 'DataGrid' - ); - }); -} else { - gridBaseMock = DevExpress.require('./gridBaseMocks.js'); +const dataGridMocks = gridBaseMock( + $, + gridCore, + columnResizingReordering, + domUtils, + commonUtils, + typeUtils, + ArrayStore, + 'DataGrid' +); - jQuery.extend(window, gridBaseMock( - jQuery, - DevExpress.require('__internal/grids/data_grid/m_core'), - DevExpress.require('__internal/grids/data_grid/module_not_extended/columns_resizing_reordering'), - DevExpress.require('__internal/core/utils/m_dom'), - DevExpress.require('__internal/core/utils/m_common'), - DevExpress.require('__internal/core/utils/m_type'), - DevExpress.require('common/data/array_store'), - 'DataGrid' - )); -} +window.dataGridMocks = dataGridMocks; + +export const setupDataGridModules = dataGridMocks.setupDataGridModules; +export const MockDataController = dataGridMocks.MockDataController; +export const MockEditingController = dataGridMocks.MockEditingController; +export const MockSelectionController = dataGridMocks.MockSelectionController; +export const MockColumnsController = dataGridMocks.MockColumnsController; +export const MockTablePositionViewController = dataGridMocks.MockTablePositionViewController; +export const MockGridDataSource = dataGridMocks.MockGridDataSource; +export const getCells = dataGridMocks.getCells; +export const MockColumnsSeparatorView = dataGridMocks.MockColumnsSeparatorView; +export const MockTrackerView = dataGridMocks.MockTrackerView; +export const MockDraggingPanel = dataGridMocks.MockDraggingPanel; +export const TestDraggingHeader = dataGridMocks.TestDraggingHeader; +export const generateItems = dataGridMocks.generateItems; +export const generateNestedData = dataGridMocks.generateNestedData; + +export default dataGridMocks; diff --git a/packages/devextreme/testing/helpers/esm-shims/fluent_blue_light.css.js b/packages/devextreme/testing/helpers/esm-shims/fluent_blue_light.css.js new file mode 100644 index 000000000000..28ba858f51a8 --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/fluent_blue_light.css.js @@ -0,0 +1,5 @@ +import { injectStylesheet } from './injectStylesheet.js'; + +await injectStylesheet('/packages/devextreme/artifacts/css/dx.fluent.blue.light.css', { + themeName: 'fluent.blue.light', +}); diff --git a/packages/devextreme/testing/helpers/esm-shims/gantt.css.js b/packages/devextreme/testing/helpers/esm-shims/gantt.css.js new file mode 100644 index 000000000000..e247d21d5527 --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/gantt.css.js @@ -0,0 +1,3 @@ +import { injectStylesheet } from './injectStylesheet.js'; + +await injectStylesheet('/packages/devextreme/artifacts/css/dx-gantt.css'); diff --git a/packages/devextreme/testing/helpers/esm-shims/injectStylesheet.js b/packages/devextreme/testing/helpers/esm-shims/injectStylesheet.js new file mode 100644 index 000000000000..55eacd3c7014 --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/injectStylesheet.js @@ -0,0 +1,57 @@ +/** + * Injects a stylesheet once for ESM import-map QUnit mode + * (`*.css!` suite imports resolve here). + * + * Only appends ``. + * Do not add dx-theme-* classes on body — that belongs to themes.attachCssClasses + * on `.dx-viewport` and would change typography/layout. + * + * Returns a Promise so importers can `await` load — otherwise tests that + * assert computed styles race the async fetch. + * + * @param {string} href + * @param {{ themeName?: string }} [options] + * themeName — optional `data-theme` (fluent.blue.light / generic.light / …) + */ +export function injectStylesheet(href, options = {}) { + const existing = document.querySelector(`link[data-dx-esm-css="${href}"]`); + if(existing) { + return waitForStylesheet(existing, href); + } + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + link.setAttribute('data-dx-esm-css', href); + if(options.themeName) { + link.setAttribute('data-theme', options.themeName); + } + document.head.appendChild(link); + return waitForStylesheet(link, href); +} + +function waitForStylesheet(link, href) { + if(link.sheet || link.dataset.dxEsmCssLoaded === '1') { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const onLoad = () => { + link.dataset.dxEsmCssLoaded = '1'; + resolve(); + }; + const onError = () => { + reject(new Error(`Failed to load stylesheet: ${href}`)); + }; + + link.addEventListener('load', onLoad, { once: true }); + link.addEventListener('error', onError, { once: true }); + + // Cached stylesheets may already be applied before listeners attach + if(link.sheet) { + link.removeEventListener('load', onLoad); + link.removeEventListener('error', onError); + onLoad(); + } + }); +} diff --git a/packages/devextreme/testing/helpers/esm-shims/jquery.js b/packages/devextreme/testing/helpers/esm-shims/jquery.js new file mode 100644 index 000000000000..a6a3df9f758a --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/jquery.js @@ -0,0 +1,17 @@ +/** + * ESM jquery shim for QUnit import-map loader. + * jQuery is loaded via classic script tag before modules run; + * run-suite calls `jQuery.noConflict()` which clears `window.$`. + * Re-attach `$` so suites that use the global alias (without importing + * jquery) keep working. + */ +const $ = window.jQuery; + +if(!$ || typeof $.fn === 'undefined') { + throw new Error('ESM jquery shim: window.jQuery is not available'); +} + +window.$ = $; + +export default $; +export { $ }; diff --git a/packages/devextreme/testing/helpers/esm-shims/jspdf_autotable.js b/packages/devextreme/testing/helpers/esm-shims/jspdf_autotable.js new file mode 100644 index 000000000000..cbd2e5ff587b --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/jspdf_autotable.js @@ -0,0 +1,37 @@ +/** + * jspdf-autotable side-effect import under native ESM. + * The vendor build attaches itself through a CJS require call, which is + * unavailable in the browser — call applyPlugin explicitly instead. + * + * Note: the `.mjs` build exports `autoTable` only as default + * (`export { …, autoTable as default }`), not as a named export. + */ +import { jsPDF } from 'jspdf'; +/* eslint-disable import/named -- vendor ESM re-exports include default + named applyPlugin */ +import autoTable, { + Cell, + CellHookData, + Column, + Row, + Table, + __createTable, + __drawTable, + applyPlugin, +} from '../../../node_modules/jspdf-autotable/dist/jspdf.plugin.autotable.mjs'; +/* eslint-enable import/named */ + +const JsPdfCtor = typeof jsPDF === 'function' ? jsPDF : jsPDF.jsPDF; +applyPlugin(JsPdfCtor); + +export { + Cell, + CellHookData, + Column, + Row, + Table, + __createTable, + __drawTable, + applyPlugin, + autoTable, +}; +export default autoTable; diff --git a/packages/devextreme/testing/helpers/esm-shims/knockout.js b/packages/devextreme/testing/helpers/esm-shims/knockout.js new file mode 100644 index 000000000000..b1f45f77edec --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/knockout.js @@ -0,0 +1,12 @@ +/** + * ESM knockout shim for QUnit import-map loader. + * Knockout is loaded via classic script tag before modules run. + */ +const ko = window.ko; + +if(!ko) { + throw new Error('ESM knockout shim: window.ko is not available'); +} + +export default ko; +export { ko }; diff --git a/packages/devextreme/testing/helpers/esm-shims/material_blue_light.css.js b/packages/devextreme/testing/helpers/esm-shims/material_blue_light.css.js new file mode 100644 index 000000000000..0c9ff647951b --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/material_blue_light.css.js @@ -0,0 +1,5 @@ +import { injectStylesheet } from './injectStylesheet.js'; + +await injectStylesheet('/packages/devextreme/artifacts/css/dx.material.blue.light.css', { + themeName: 'material.blue.light', +}); diff --git a/packages/devextreme/testing/helpers/esm-shims/mutable_facade.js b/packages/devextreme/testing/helpers/esm-shims/mutable_facade.js new file mode 100644 index 000000000000..8cc00339e1b3 --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/mutable_facade.js @@ -0,0 +1,50 @@ +/** + * Shared helpers for mutable ESM facades used by QUnit stubbing. + */ +export function wrapCtor(api, name) { + const ExportWrapper = function(...args) { + const Impl = api[name]; + if(new.target) { + return new Impl(...args); + } + return Impl.apply(this, args); + }; + Object.defineProperty(ExportWrapper, 'name', { value: name, configurable: true }); + // Point at the live implementation prototype so stubClass(import { X }) + // sees real methods (e.g. vizMocks Tooltip/Title/ExportMenu). + // Also inherit statics (Class.inherit / redefine / parent / …) so + // `BaseThemeManager.inherit(...)` keeps working under the facade. + const Impl = api[name]; + if(typeof Impl === 'function') { + Object.setPrototypeOf(ExportWrapper, Impl); + if(Impl.prototype) { + ExportWrapper.prototype = Impl.prototype; + } + } + return ExportWrapper; +} + +/** + * @param {object} original + * @param {string} globalKey + * @param {Record} [debugSets] map DEBUG_set_* → api property name + */ +export function createMutableApi(original, globalKey, debugSets = {}) { + if(globalThis[globalKey]) { + return globalThis[globalKey]; + } + + const api = { ...original }; + Object.entries(debugSets).forEach(([debugName, propName]) => { + const originalDebugSet = api[debugName]; + api[debugName] = (value) => { + api[propName] = value; + if(typeof originalDebugSet === 'function') { + originalDebugSet(value); + } + }; + }); + + globalThis[globalKey] = api; + return api; +} diff --git a/packages/devextreme/testing/helpers/esm-shims/themes.js b/packages/devextreme/testing/helpers/esm-shims/themes.js new file mode 100644 index 000000000000..3aa9ec10dfa9 --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/themes.js @@ -0,0 +1,48 @@ +/** + * Mutable facade for ui/themes — QUnit stubs replace api.isMaterial / isFluent / + * isMaterialBased / isGeneric / current on the default export object. + * + * Named exports always forward to the current api.* implementation so + * library `import { isMaterial }` keeps working after stubs. + * + * api is stored on globalThis so import-map and static-redirect URLs + * (cache-buster differences) still share one stubbable object. + */ +import * as original from '../../../artifacts/transpiled-esm-npm/esm/__internal/ui/themes.js?dx-original=1'; + +const GLOBAL_KEY = '__dxMutableUiThemes'; + +const api = globalThis[GLOBAL_KEY] ?? (globalThis[GLOBAL_KEY] = { + ...original, + // Keep composition live so stubbing isMaterial / isFluent affects isMaterialBased. + isMaterialBased(themeName) { + return api.isMaterial(themeName) || api.isFluent(themeName); + }, +}); + +function wrapExport(name) { + return function(...args) { + return api[name](...args); + }; +} + +export const setDefaultTimeout = wrapExport('setDefaultTimeout'); +export const init = wrapExport('init'); +export const initialized = wrapExport('initialized'); +export const resetTheme = wrapExport('resetTheme'); +export const ready = wrapExport('ready'); +export const waitWebFont = wrapExport('waitWebFont'); +export const isWebFontLoaded = wrapExport('isWebFontLoaded'); +export const isCompact = wrapExport('isCompact'); +export const isDark = wrapExport('isDark'); +export const isGeneric = wrapExport('isGeneric'); +export const isMaterial = wrapExport('isMaterial'); +export const isFluent = wrapExport('isFluent'); +export const isMaterialBased = wrapExport('isMaterialBased'); +export const detachCssClasses = wrapExport('detachCssClasses'); +export const attachCssClasses = wrapExport('attachCssClasses'); +export const current = wrapExport('current'); +export const waitForThemeLoad = wrapExport('waitForThemeLoad'); +export const isPendingThemeLoaded = wrapExport('isPendingThemeLoaded'); + +export default api; diff --git a/packages/devextreme/testing/helpers/esm-shims/tslib.js b/packages/devextreme/testing/helpers/esm-shims/tslib.js new file mode 100644 index 000000000000..377dace0c5bc --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/tslib.js @@ -0,0 +1,41 @@ +/** + * Minimal tslib fallback for QUnit when the package is not hoisted. + * Covers helpers used by rrule's ESM build. + */ +export function __assign(target) { + for(let i = 1; i < arguments.length; i++) { + const source = arguments[i]; + for(const key in source) { + if(Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; +} + +export function __extends(derived, base) { + Object.setPrototypeOf(derived, base); + function PrototypeBridge() { + this.constructor = derived; + } + PrototypeBridge.prototype = base === null ? Object.create(base) : base.prototype; + // eslint-disable-next-line new-cap, no-new + derived.prototype = new PrototypeBridge(); +} + +export function __spreadArray(to, from, pack) { + if(pack || arguments.length === 2) { + let packed; + for(let i = 0, length = from.length; i < length; i++) { + if(packed || !(i in from)) { + if(!packed) { + packed = Array.prototype.slice.call(from, 0, i); + } + packed[i] = from[i]; + } + } + return to.concat(packed || Array.prototype.slice.call(from)); + } + return to.concat(from); +} diff --git a/packages/devextreme/testing/helpers/esm-shims/zod-to-json-schema.js b/packages/devextreme/testing/helpers/esm-shims/zod-to-json-schema.js new file mode 100644 index 000000000000..c1ce5b58b1a2 --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/zod-to-json-schema.js @@ -0,0 +1,8 @@ +/** + * Minimal zod-to-json-schema stub for QUnit ESM / import-map loader. + */ +export function zodToJsonSchema() { + return { type: 'object' }; +} + +export default zodToJsonSchema; diff --git a/packages/devextreme/testing/helpers/esm-shims/zod.js b/packages/devextreme/testing/helpers/esm-shims/zod.js new file mode 100644 index 000000000000..489d0bbcf5ba --- /dev/null +++ b/packages/devextreme/testing/helpers/esm-shims/zod.js @@ -0,0 +1,35 @@ +/** + * Minimal zod stub for QUnit ESM / import-map loader. + */ +const z = { + object() { return z; }, + string() { return z; }, + boolean() { return z; }, + number() { return z; }, + date() { return z; }, + null() { return z; }, + enum() { return z; }, + union() { return z; }, + array() { return z; }, + tuple() { return z; }, + literal() { return z; }, + record() { return z; }, + lazy() { return z; }, + optional() { return z; }, + nullable() { return z; }, + // eslint-disable-next-line spellcheck/spell-checker + nullish() { return z; }, + strict() { return z; }, + int() { return z; }, + // eslint-disable-next-line spellcheck/spell-checker + nonnegative() { return z; }, + positive() { return z; }, + min() { return z; }, + max() { return z; }, + transform() { return z; }, + describe() { return z; }, + safeParse() { return { success: true, data: {} }; }, +}; + +export { z }; +export default z; diff --git a/packages/devextreme/testing/helpers/executeAsyncMock.js b/packages/devextreme/testing/helpers/executeAsyncMock.js index cd52380fd3a7..5da48be090e1 100644 --- a/packages/devextreme/testing/helpers/executeAsyncMock.js +++ b/packages/devextreme/testing/helpers/executeAsyncMock.js @@ -1,28 +1,20 @@ -(function(root, factory) { - root.DevExpress = root.DevExpress || {}; - root.DevExpress.testing = root.DevExpress.testing || {}; +import commonUtils from '__internal/core/utils/m_common'; - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - root.DevExpress.testing.executeAsyncMock = module.exports = factory(require('__internal/core/utils/m_common').default); - }); - } else { - root.DevExpress.testing.executeAsyncMock = factory(DevExpress.utils.common); - } -}(window, function(commonUtils) { - const originalExecuteAsync = commonUtils.executeAsync; - - return { - setup: function() { - commonUtils.executeAsync = function(action, context) { - return originalExecuteAsync.apply(this, [action, context, function(callback) { return callback.apply(this, arguments); }]); - }; - }, - teardown: function() { - commonUtils.executeAsync = originalExecuteAsync; - } - }; +const originalExecuteAsync = commonUtils.executeAsync; -})); +const executeAsyncMock = { + setup: function() { + commonUtils.executeAsync = function(action, context) { + return originalExecuteAsync.apply(this, [action, context, function(callback) { return callback.apply(this, arguments); }]); + }; + }, + teardown: function() { + commonUtils.executeAsync = originalExecuteAsync; + } +}; +window.DevExpress = window.DevExpress || {}; +window.DevExpress.testing = window.DevExpress.testing || {}; +window.DevExpress.testing.executeAsyncMock = executeAsyncMock; +export default executeAsyncMock; diff --git a/packages/devextreme/testing/helpers/includeThemesLinks.js b/packages/devextreme/testing/helpers/includeThemesLinks.js index a6e01e6d0104..6d8b3d53ee93 100644 --- a/packages/devextreme/testing/helpers/includeThemesLinks.js +++ b/packages/devextreme/testing/helpers/includeThemesLinks.js @@ -1,9 +1,12 @@ -const themesList = ['generic.light', 'material.blue.light']; +const themesList = [ + { name: 'generic.light', href: '/packages/devextreme/artifacts/css/dx.light.css' }, + { name: 'material.blue.light', href: '/packages/devextreme/artifacts/css/dx.material.blue.light.css' }, +]; -themesList.forEach(theme => { +themesList.forEach(({ name, href }) => { const link = document.createElement('link'); link.setAttribute('rel', 'dx-theme'); - link.setAttribute('data-theme', theme); - link.setAttribute('href', SystemJS.normalizeSync(theme.replace(/\./g, '_') + '.css')); + link.setAttribute('data-theme', name); + link.setAttribute('href', href); document.head.appendChild(link); }); diff --git a/packages/devextreme/testing/helpers/keyboardMock.js b/packages/devextreme/testing/helpers/keyboardMock.js index ade1eefd1974..1107d8978d43 100644 --- a/packages/devextreme/testing/helpers/keyboardMock.js +++ b/packages/devextreme/testing/helpers/keyboardMock.js @@ -1,16 +1,8 @@ -let focused; - -(function(root, factory) { - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - focused = require('__internal/core/utils/m_selectors').focused; - root.keyboardMock = module.exports = factory(require('jquery'), require('inferno')); - }); - } else { - focused = DevExpress.require('__internal/core/utils/m_selectors').focused; - root.keyboardMock = factory(root.jQuery); - } -}(window, function($, inferno) { +import $ from 'jquery'; +import * as inferno from 'inferno'; +import { focused } from '__internal/core/utils/m_selectors'; + +const keyboardMock = (function($, inferno) { let $element; let caret; @@ -427,4 +419,6 @@ let focused; } }; }; -})); +})($, inferno); + +export default keyboardMock; diff --git a/packages/devextreme/testing/helpers/memoryLeaksHelper.js b/packages/devextreme/testing/helpers/memoryLeaksHelper.js index 6a4efcca90a9..bab8140b8826 100644 --- a/packages/devextreme/testing/helpers/memoryLeaksHelper.js +++ b/packages/devextreme/testing/helpers/memoryLeaksHelper.js @@ -1,17 +1,6 @@ -(function(root, factory) { - /* global jQuery */ - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - root.memoryLeaksHelper = module.exports = factory( - require('jquery') - ); - }); - } else { - jQuery.extend(window, factory( - jQuery - )); - } -}(window, function($) { +import $ from 'jquery'; + +const __moduleExports = (function($) { const exports = {}; @@ -142,4 +131,8 @@ }; return exports; -})); +})($); + +window.memoryLeaksHelper = __moduleExports; + +export default __moduleExports; diff --git a/packages/devextreme/testing/helpers/mockModule.js b/packages/devextreme/testing/helpers/mockModule.js index 5864029817d4..2cbbd4521fbd 100644 --- a/packages/devextreme/testing/helpers/mockModule.js +++ b/packages/devextreme/testing/helpers/mockModule.js @@ -1,12 +1,13 @@ /* eslint-disable no-undef */ -const $ = require('jquery'); - -exports.mock = (module, value) => { - const normalizedName = System.normalizeSync(module); - System.delete(normalizedName); - value.__esModule = true; - $.extend({ default: value }); - System.set(normalizedName, System.newModule($.extend({ default: value }, value))); - return value; +/** + * Module replacement helper — not supported under native ESM + * (no module registry). Mutate a mutable import-map facade instead + * (see trackerMock.js / esm-shims/viz_chart_tracker.js). + */ +exports.mock = function mock() { + throw new Error( + 'mockModule.mock is not supported under native ESM; ' + + 'mutate the target module\'s mutable facade (default export) instead', + ); }; diff --git a/packages/devextreme/testing/helpers/nativePointerMock.js b/packages/devextreme/testing/helpers/nativePointerMock.js index fab9e3227981..c50749f0d0f5 100644 --- a/packages/devextreme/testing/helpers/nativePointerMock.js +++ b/packages/devextreme/testing/helpers/nativePointerMock.js @@ -1,13 +1,6 @@ -(function(root, factory) { - /* global jQuery */ - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - root.nativePointerMock = module.exports = factory(require('jquery')); - }); - } else { - root.nativePointerMock = factory(jQuery); - } -}(window, function($) { +import $ from 'jquery'; + +const __moduleExports = (function($) { const UA = (function() { const ua = window.navigator.userAgent; let matches; @@ -980,4 +973,8 @@ return result; -})); +})($); + +window.nativePointerMock = __moduleExports; + +export default __moduleExports; diff --git a/packages/devextreme/testing/helpers/noDiagram.js b/packages/devextreme/testing/helpers/noDiagram.js index 1d0e288bbae3..189f31a52683 100644 --- a/packages/devextreme/testing/helpers/noDiagram.js +++ b/packages/devextreme/testing/helpers/noDiagram.js @@ -1,4 +1,4 @@ if(window.DevExpress) { window.DevExpress.diagram = undefined; } -module.exports = null; +export default null; diff --git a/packages/devextreme/testing/helpers/noGantt.js b/packages/devextreme/testing/helpers/noGantt.js index ed0a8a552761..abc0e1f6294f 100644 --- a/packages/devextreme/testing/helpers/noGantt.js +++ b/packages/devextreme/testing/helpers/noGantt.js @@ -1,4 +1,4 @@ if(window.DevExpress) { window.DevExpress.Gantt = undefined; } -module.exports = null; +export default null; diff --git a/packages/devextreme/testing/helpers/noJQuery.js b/packages/devextreme/testing/helpers/noJQuery.js index 25af527694e7..7646bbd17d04 100644 --- a/packages/devextreme/testing/helpers/noJQuery.js +++ b/packages/devextreme/testing/helpers/noJQuery.js @@ -1 +1 @@ -window.jQuery = module.exports = null; +export default null; diff --git a/packages/devextreme/testing/helpers/pointerMock.js b/packages/devextreme/testing/helpers/pointerMock.js index 439c852ea5b6..6b89ba1b2301 100644 --- a/packages/devextreme/testing/helpers/pointerMock.js +++ b/packages/devextreme/testing/helpers/pointerMock.js @@ -1,17 +1,9 @@ -(function(root, factory) { - /* global jQuery */ - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - root.pointerMock = module.exports = factory( - require('jquery'), - require('inferno'), - require('common/core/events/gesture/emitter.gesture'), - require('common/core/events/click')); - }); - } else { - root.pointerMock = factory(jQuery, DevExpress.events.GestureEmitter, DevExpress.events.click); - } -}(window, function($, inferno, GestureEmitter, clickEvent) { +import $ from 'jquery'; +import * as inferno from 'inferno'; +import GestureEmitter from 'common/core/events/gesture/emitter.gesture'; +import * as clickEvent from 'common/core/events/click'; + +const pointerMock = (function($, inferno, GestureEmitter, clickEvent) { GestureEmitter.touchBoundary(0); @@ -236,4 +228,6 @@ } }; }; -})); +})($, inferno, GestureEmitter, clickEvent); + +export default pointerMock; diff --git a/packages/devextreme/testing/helpers/positionFixtures.js b/packages/devextreme/testing/helpers/positionFixtures.js index cada61dab25b..978a65f1fabc 100644 --- a/packages/devextreme/testing/helpers/positionFixtures.js +++ b/packages/devextreme/testing/helpers/positionFixtures.js @@ -1,12 +1,6 @@ -(function(root, factory) { - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - root.fixtures = module.exports = factory(require('jquery')); - }); - } else { - root.fixtures = factory(root.jQuery); - } -}(window, function($) { +import $ from 'jquery'; + +const __moduleExports = (function($) { const fixtures = { simple: { @@ -258,4 +252,8 @@ }; return fixtures; -})); +})($); + +window.fixtures = __moduleExports; + +export default __moduleExports; diff --git a/packages/devextreme/testing/helpers/publicModulesHelper.js b/packages/devextreme/testing/helpers/publicModulesHelper.js index bff3366338de..3c39a4f42755 100644 --- a/packages/devextreme/testing/helpers/publicModulesHelper.js +++ b/packages/devextreme/testing/helpers/publicModulesHelper.js @@ -1,12 +1,6 @@ -(function(root, factory) { - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - root.testGlobalExports = module.exports = factory(require('jquery')); - }); - } else { - root.testGlobalExports = factory(root.jQuery); - } -}(window, function($) { +import $ from 'jquery'; + +const __moduleExports = (function($) { return function(namespaces, fields) { $.each(namespaces, function(namespaceName, namespace) { $.each(fields, function(fieldName, fieldValue) { @@ -17,4 +11,8 @@ }); }); }; -})); +})($); + +window.testGlobalExports = __moduleExports; + +export default __moduleExports; diff --git a/packages/devextreme/testing/helpers/quillDependencies/noQuill.js b/packages/devextreme/testing/helpers/quillDependencies/noQuill.js index e45fa7973937..3b197fe204b6 100644 --- a/packages/devextreme/testing/helpers/quillDependencies/noQuill.js +++ b/packages/devextreme/testing/helpers/quillDependencies/noQuill.js @@ -1 +1,2 @@ -window.Quill = module.exports = null; +window.Quill = null; +export default null; diff --git a/packages/devextreme/testing/helpers/qunitExtensions.js b/packages/devextreme/testing/helpers/qunitExtensions.js index c7b3344d568b..75507728a1bb 100644 --- a/packages/devextreme/testing/helpers/qunitExtensions.js +++ b/packages/devextreme/testing/helpers/qunitExtensions.js @@ -514,7 +514,7 @@ if(timerType === 'timeouts') { if( callback.indexOf('.Deferred.exceptionHook') > -1 || // NOTE: jQuery.Deferred are now asynchronous - callback.indexOf('e._drain()') > -1 // NOTE: SystemJS Promise polyfill + callback.indexOf('e._drain()') > -1 // NOTE: legacy Promise polyfill ) { return true; } diff --git a/packages/devextreme/testing/helpers/stubs/zodStub.js b/packages/devextreme/testing/helpers/stubs/zodStub.js deleted file mode 100644 index 668e5fd8ecd2..000000000000 --- a/packages/devextreme/testing/helpers/stubs/zodStub.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Minimal zod stub for QUnit / SystemJS tests. - * - * Uses AMD define() when available (CSP mode) and falls back - * to global assignment for regular SystemJS (NoCsp mode). - */ - -(function() { - const z = { - // top-level constructors - object: function() { return z; }, - string: function() { return z; }, - boolean: function() { return z; }, - number: function() { return z; }, - date: function() { return z; }, - null: function() { return z; }, - enum: function() { return z; }, - union: function() { return z; }, - array: function() { return z; }, - tuple: function() { return z; }, - literal: function() { return z; }, - record: function() { return z; }, - lazy: function() { return z; }, - // chain modifiers - optional: function() { return z; }, - nullable: function() { return z; }, - // eslint-disable-next-line spellcheck/spell-checker - nullish: function() { return z; }, - strict: function() { return z; }, - int: function() { return z; }, - // eslint-disable-next-line spellcheck/spell-checker - nonnegative: function() { return z; }, - positive: function() { return z; }, - min: function() { return z; }, - max: function() { return z; }, - transform: function() { return z; }, - describe: function() { return z; }, - // validation - safeParse: function() { return { success: true, data: {} }; }, - }; - - if(typeof define === 'function') { - define(function(require, exports) { - Object.defineProperty(exports, '__esModule', { value: true }); - exports.z = z; - exports.default = z; - }); - } else { - const root = typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : globalThis; - root.z = z; - root.zod = z; - } -})(); diff --git a/packages/devextreme/testing/helpers/stubs/zodToJsonSchemaStub.js b/packages/devextreme/testing/helpers/stubs/zodToJsonSchemaStub.js deleted file mode 100644 index a7aa7e3e9d70..000000000000 --- a/packages/devextreme/testing/helpers/stubs/zodToJsonSchemaStub.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Minimal zod-to-json-schema stub for QUnit / SystemJS tests. - * - * Uses AMD define() when available (CSP mode) and falls back - * to global assignment for regular SystemJS (NoCsp mode). - */ - -(function() { - const zodToJsonSchema = function() { return { type: 'object' }; }; - - if(typeof define === 'function') { - define(function(require, exports) { - Object.defineProperty(exports, '__esModule', { value: true }); - exports.zodToJsonSchema = zodToJsonSchema; - exports.default = zodToJsonSchema; - }); - } else { - const root = typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : globalThis; - root.zodToJsonSchema = zodToJsonSchema; - } -})(); diff --git a/packages/devextreme/testing/helpers/trackerMock.js b/packages/devextreme/testing/helpers/trackerMock.js index 745abb5717e9..3a930364f4d5 100644 --- a/packages/devextreme/testing/helpers/trackerMock.js +++ b/packages/devextreme/testing/helpers/trackerMock.js @@ -1,13 +1,12 @@ -const mock = require('./mockModule.js').mock; const vizMocks = require('./vizMocks.js'); -const { ChartTracker, PieTracker } = require('viz/chart_components/tracker'); -const ChartTrackerStub = vizMocks.stubClass(ChartTracker); -const PieTrackerStub = vizMocks.stubClass(PieTracker); +// Mutate the import-map facade (see esm-shims/viz_chart_tracker.js). +const trackerModule = require('viz/chart_components/tracker'); -const trackerModule = mock('viz/chart_components/tracker', { - ChartTracker: sinon.spy((parameters) => new ChartTrackerStub(parameters)), - PieTracker: sinon.spy((parameters) => new PieTrackerStub(parameters)) -}); +const ChartTrackerStub = vizMocks.stubClass(trackerModule.ChartTracker); +const PieTrackerStub = vizMocks.stubClass(trackerModule.PieTracker); + +trackerModule.ChartTracker = sinon.spy((parameters) => new ChartTrackerStub(parameters)); +trackerModule.PieTracker = sinon.spy((parameters) => new PieTrackerStub(parameters)); exports.default = trackerModule; exports.__esModule = true; diff --git a/packages/devextreme/testing/helpers/treeListMocks.js b/packages/devextreme/testing/helpers/treeListMocks.js index 45f1d23b83e0..fcb1a8d606a1 100644 --- a/packages/devextreme/testing/helpers/treeListMocks.js +++ b/packages/devextreme/testing/helpers/treeListMocks.js @@ -1,32 +1,44 @@ -let gridBaseMock; +import $ from 'jquery'; +import treeListCoreModule from '__internal/grids/tree_list/m_core'; +import domUtilsModule from '__internal/core/utils/m_dom'; +import commonUtilsModule from '__internal/core/utils/m_common'; +import typeUtilsModule from '__internal/core/utils/m_type'; +import ArrayStoreModule from 'common/data/array_store'; +import gridBaseMockModule from './gridBaseMocks.js'; -/* global jQuery */ -if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - gridBaseMock = require('./gridBaseMocks.js'); +const gridBaseMock = gridBaseMockModule.default ?? gridBaseMockModule; +const treeListCore = treeListCoreModule.default ?? treeListCoreModule; +const domUtils = domUtilsModule.default ?? domUtilsModule; +const commonUtils = commonUtilsModule.default ?? commonUtilsModule; +const typeUtils = typeUtilsModule.default ?? typeUtilsModule; +const ArrayStore = ArrayStoreModule.default ?? ArrayStoreModule; - window.treeListMocks = module.exports = gridBaseMock( - require('jquery'), - require('__internal/grids/tree_list/m_core').default, - null, - require('__internal/core/utils/m_dom'), - require('__internal/core/utils/m_common'), - require('__internal/core/utils/m_type'), - require('common/data/array_store'), - 'TreeList' - ); - }); -} else { - gridBaseMock = require('./gridBaseMocks.js'); +const treeListMocks = gridBaseMock( + $, + treeListCore, + null, + domUtils, + commonUtils, + typeUtils, + ArrayStore, + 'TreeList' +); - jQuery.extend(window, gridBaseMock( - jQuery, - DevExpress.require('__internal/grids/tree_list/m_core'), - null, - DevExpress.require('__internal/core/utils/m_dom'), - DevExpress.require('__internal/core/utils/m_common'), - DevExpress.require('__internal/core/utils/m_type'), - DevExpress.require('common/data/array_store'), - 'TreeList' - )); -} +window.treeListMocks = treeListMocks; + +export const setupTreeListModules = treeListMocks.setupTreeListModules; +export const MockDataController = treeListMocks.MockDataController; +export const MockEditingController = treeListMocks.MockEditingController; +export const MockSelectionController = treeListMocks.MockSelectionController; +export const MockColumnsController = treeListMocks.MockColumnsController; +export const MockTablePositionViewController = treeListMocks.MockTablePositionViewController; +export const MockGridDataSource = treeListMocks.MockGridDataSource; +export const getCells = treeListMocks.getCells; +export const MockColumnsSeparatorView = treeListMocks.MockColumnsSeparatorView; +export const MockTrackerView = treeListMocks.MockTrackerView; +export const MockDraggingPanel = treeListMocks.MockDraggingPanel; +export const TestDraggingHeader = treeListMocks.TestDraggingHeader; +export const generateItems = treeListMocks.generateItems; +export const generateNestedData = treeListMocks.generateNestedData; + +export default treeListMocks; diff --git a/packages/devextreme/testing/helpers/vizMocks.js b/packages/devextreme/testing/helpers/vizMocks.js index 8a6838ea73a5..1d96bbb2face 100644 --- a/packages/devextreme/testing/helpers/vizMocks.js +++ b/packages/devextreme/testing/helpers/vizMocks.js @@ -10,7 +10,7 @@ import { Series } from 'viz/series/base_series'; import * as loadingIndicatorModule from 'viz/core/loading_indicator'; import * as exportMenuModule from 'viz/core/export'; import rendererModule from 'viz/core/renderers/renderer_default'; -import * as errors from 'viz/core/errors_warnings'; +import errors from 'viz/core/errors_warnings'; import * as baseWidgetUtils from '__internal/viz/core/base_widget.utils'; import * as typeUtils from 'core/utils/type'; @@ -367,7 +367,17 @@ const Point = stubClass(pointModule.Point); const Legend = stubClass(legendModule.Legend); const Title = stubClass(titleModule.Title); const Tooltip = stubClass(tooltipModule.Tooltip); -const Axis = stubClass(axisModule.Axis); +// ESM npm artifacts strip /// #DEBUG methods (removeDebug: true). +// Restore the ones gauges/charts call on mocks (kept in legacy CJS transpile). +const Axis = stubClass(axisModule.Axis, null, { + $extraFunctions: [ + 'shift', + '_getTickMarkPoints', + '_validateOverlappingMode', + '_getStep', + '_validateDisplayMode', + ], +}); const SeriesStub = stubClass(Series); export { diff --git a/packages/devextreme/testing/helpers/xmlHttpRequestMock.js b/packages/devextreme/testing/helpers/xmlHttpRequestMock.js index cc55e4440a93..604163d61f3f 100644 --- a/packages/devextreme/testing/helpers/xmlHttpRequestMock.js +++ b/packages/devextreme/testing/helpers/xmlHttpRequestMock.js @@ -1,4 +1,4 @@ -/* global $ */ +import $ from 'jquery'; const RealXMLHttpRequest = window.XMLHttpRequest; diff --git a/packages/devextreme/testing/runner/README.md b/packages/devextreme/testing/runner/README.md new file mode 100644 index 000000000000..825019920821 --- /dev/null +++ b/packages/devextreme/testing/runner/README.md @@ -0,0 +1,103 @@ +# QUnit test runner (native ESM) + +Developer notes for the Node HTTP runner that serves QUnit suites with **native ESM + import maps** (no SystemJS). + +Related layout: + +| Path | Role | +| --- | --- | +| `testing/runner/lib/` | Server-side request handling, source rewrites, import-map build | +| `testing/helpers/esm-shims/` | Browser-side shim modules wired through the import map / static redirects | + +After changing TypeScript under `testing/runner/`, recompile (`tsc -p testing/runner/tsconfig.json`) and **restart** the process on port `20060` — templates and rewrite logic are loaded at process start. + +--- + +## `lib/static.ts` + +HTTP static file server for the QUnit runner. + +**Responsibilities:** + +- Resolve and serve workspace files (tests, helpers, artifacts, vendors) with correct content types and cache headers. +- Apply **serve-time transforms** so the browser receives valid ESM: + - QUnit tests/helpers → `cjsInterop.rewriteQunitTestHelperSource` + - `aspnet.js` UMD artifact → `cjsInterop.rewriteAspnetArtifactToEsm` + - Vendor / Globalize / Intl / VectorMap bundles → wrap as ESM modules + - JSON (`?esm-export=1`) → `export default …` +- Serve **generated** mutable facades for modules in `MUTABLE_MODULE_GROUPS` / viz namespace-reexports; redirect only special hand-written cases (e.g. themes). +- For pure `import * as X; export default X` viz reexports, generate facades on the fly via `autoMutableFacade.tryBuildAutoMutableFacade`. +- Support `?dx-original=1` so a shim can import the **real** artifact without being redirected back to itself. + +This module is the integration point: almost every special-case rewrite for QUnit ESM loading goes through `tryServeStatic`. + +--- + +## `lib/cjsInterop.ts` + +Serve-time **CJS → ESM** source rewrites for QUnit tests, helpers, and bundle templates. + +Legacy suites still use `require()`, `module.exports` / `exports.*`, AMD `define(function () { … })`, and CJS-style `import x from 'bare-specifier'`. Native ESM cannot load those as-is. + +**What it does:** + +1. **`require('…')`** → hoisted `import * as __dxReq_N` plus `('default' in ns ? ns.default : { …ns })` at the call site (keeps explicit `default: null` for noJQuery/…; mutable shallow copy only when there is no default — needed when tests assign onto the module object). +2. **`module.exports` / `exports.*`** → wrap the file with a synthetic `module`/`exports` object and emit `export default` + named exports. +3. **Bare default / named imports** → namespace import + CJS default interop (`'default' in ns ? ns.default : …`, merge default object/function into named bindings when needed). +4. **AMD `define(function () { … })`** → IIFE, with imports hoisted to file top (imports inside `if (define.amd)` are illegal in ESM). +5. **Plugin-style JSON** (`file.json!` / `file.json!json`) → absolute URLs with `?esm-export=1`. +6. **`aspnet.js`** → dedicated UMD → ESM conversion (`rewriteAspnetArtifactToEsm`). + +`esm-shims/` files are **excluded** from this pipeline (`isQunitTestOrHelperPath`) — they are already real ESM. + +--- + +## `lib/autoMutableFacade.ts` + +Generates **mutable ESM facades** at request time so QUnit can `sinon.stub` module APIs without editing `packages/devextreme/js`. + +**Two sources of facades:** + +1. **`MUTABLE_MODULE_GROUPS`** ([`mutableModuleGroups.ts`](./lib/mutableModuleGroups.ts)) — explicit list of stub-able modules (animation frame, viz renderer, exporter, …). All aliases share one `globalThis` api; named exports use `wrapCtor` / live forwards. Import map points at the ESM artifact URL; `static.ts` serves the generated facade unless `?dx-original=1`. Codegen lives in `autoMutableFacade.ts`. +2. **Namespace-default reexports** (`import * as X; export default X`) under `viz/` — discovered automatically. + +Hand-written files under `esm-shims/` remain only for **non-generic** cases (themes composition, CSS inject, jquery/knockout globals, vendor stubs). + +Typical generated shape: + +```js +import * as original from '.../module.js?dx-original=1'; +import { createMutableApi, wrapCtor } from '.../mutable_facade.js'; + +const api = createMutableApi(original, '__dxAutoMutable_…'); +export const Foo = wrapCtor(api, 'Foo'); +export default api; +``` + +To stub a new module, add a group entry in [`mutableModuleGroups.ts`](./lib/mutableModuleGroups.ts): + +```ts +{ + internal: '__internal/viz/core/title.js', // real module under esm/ + also: ['viz/core/title.js'], // extra artifact URLs → same facade + extraKeys: ['animation/frame'], // optional bare import-map keys + apiFromDefault: true, // when default export is the stub target +} +``` + +Import-map keys are derived as `strip(.js)` of `internal`/`also`, plus `extraKeys`. Prefer this over a new hand-written shim. +--- + +## `testing/helpers/esm-shims/` + +Browser modules that the import map (and/or `static.ts` artifact redirects) substitute for real package / artifact specifiers during QUnit runs. + +**Why they exist:** + +- **Stubbing / mutation** — prefer `MUTABLE_MODULE_GROUPS` in `mutableModuleGroups.ts` (serve-time generated facades via `autoMutableFacade.ts`). Keep a hand-written shim only for custom composition (e.g. themes). +- **Globals bridge** — e.g. `jquery.js` / `knockout.js` re-export the classic ``; - const integrationImportPaths = getJQueryIntegrationImports(); + // Restore CSP for default runs; `?nocsp` keeps the meta off for suites + // that branch on QUnit.urlParams['nocsp'] (Knockout, aspnet, …). const cspMetaTag = runProps.NoCsp ? '' : ` string; rootDirectory: string; - setNoCacheHeaders: (res: ServerResponse) => void; setStaticCacheHeaders: (res: ServerResponse, searchParams: URLSearchParams) => void; } @@ -18,53 +25,99 @@ export interface StaticFileService { ) => boolean; } +const CONTENT_TYPES: Readonly> = { + '.html': 'text/html; charset=utf-8', + '.htm': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.mjs': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.xml': 'text/xml; charset=utf-8', + '.xsl': 'text/xml; charset=utf-8', + '.txt': 'text/plain; charset=utf-8', + '.md': 'text/plain; charset=utf-8', + '.log': 'text/plain; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.eot': 'application/vnd.ms-fontobject', + '.map': 'application/json; charset=utf-8', + '.wasm': 'application/wasm', +}; + +const JS_CONTENT_TYPE = 'application/javascript; charset=utf-8'; +const ESM_ARTIFACT_MARKER = '/artifacts/transpiled-esm-npm/esm/'; + +function normalizeUrlPath(filePath: string): string { + return filePath.split(path.sep).join('/'); +} + function getContentType(filePath: string): string { - const ext = path.extname(filePath).toLowerCase(); - - switch (ext) { - case '.html': - case '.htm': - return 'text/html; charset=utf-8'; - case '.css': - return 'text/css; charset=utf-8'; - case '.js': - case '.mjs': - return 'application/javascript; charset=utf-8'; - case '.json': - return 'application/json; charset=utf-8'; - case '.xml': - case '.xsl': - return 'text/xml; charset=utf-8'; - case '.txt': - case '.md': - case '.log': - return 'text/plain; charset=utf-8'; - case '.svg': - return 'image/svg+xml'; - case '.png': - return 'image/png'; - case '.jpg': - case '.jpeg': - return 'image/jpeg'; - case '.gif': - return 'image/gif'; - case '.ico': - return 'image/x-icon'; - case '.woff': - return 'font/woff'; - case '.woff2': - return 'font/woff2'; - case '.ttf': - return 'font/ttf'; - case '.eot': - return 'application/vnd.ms-fontobject'; - case '.map': - return 'application/json; charset=utf-8'; - case '.wasm': - return 'application/wasm'; - default: - return 'application/octet-stream'; + return CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream'; +} + +function sendError(res: ServerResponse, statusCode: number, message: string): boolean { + // Always override any prior Cache-Control (e.g. DX_HTTP_CACHE year-long + // headers set before a transform/read failure). + applyNoCacheHeaders(res); + res.statusCode = statusCode; + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.end(message); + return true; +} + +function sendJsModuleBody(res: ServerResponse, body: string): boolean { + const buffer = Buffer.from(body, 'utf8'); + res.statusCode = 200; + res.setHeader('Content-Type', JS_CONTENT_TYPE); + res.setHeader('Content-Length', String(buffer.length)); + res.end(buffer); + return true; +} + +function sendTransformedJs( + res: ServerResponse, + filePath: string, + transform: (raw: string) => string, + errorMessage: string, +): boolean { + try { + return sendJsModuleBody(res, transform(fs.readFileSync(filePath, 'utf8'))); + } catch { + return sendError(res, 500, errorMessage); + } +} + +/** + * Native ESM requires resolvable URLs. Our transpiled ESM tree uses + * extensionless relative imports (`from './wrapper'`). Resolve those + * to `.js` / `/index.js` on disk so import maps can load artifacts. + * + * Prefer `name.js` over a sibling directory `name/` — otherwise imports like + * `../__internal/integration/jquery` resolve to a directory listing (HTML) + * and the browser reports "Failed to fetch dynamically imported module". + * + * Also: files like `ui.collection_widget.edit` have a dotted basename; + * `path.extname` returns `.edit`, so we must still try appending `.js`. + */ +function resolveStaticFilePath(filePath: string): string | null { + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + return filePath; + } + + for (const candidate of [`${filePath}.js`, `${filePath}.mjs`, path.join(filePath, 'index.js')]) { + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { + return candidate; + } } + + return fs.existsSync(filePath) ? filePath : null; } function sendStaticFile(res: ServerResponse, filePath: string, fileSize: number): boolean { @@ -88,6 +141,326 @@ function sendStaticFile(res: ServerResponse, filePath: string, fileSize: number) return true; } +// --- Vendor / UMD → ESM wrappers ------------------------------------------------ + +const ESM_DEFAULT_FROM_CJS = `const __dxVendorExport = module.exports && module.exports.__esModule + && Object.prototype.hasOwnProperty.call(module.exports, 'default') + ? module.exports.default + : module.exports; +export default __dxVendorExport; +`; + +function forceVendorGlobalThis(source: string): string { + return source.replace(/}\(\s*this\s*,/g, '}(globalThis,'); +} + +function wrapVendorCjsBranch( + source: string, + options: { + preamble?: string; + requireShim?: string; + exportsInit?: string; + trailing?: string; + rewriteThis?: boolean; + } = {}, +): string { + const { + preamble = '', + requireShim = '', + exportsInit = '{}', + trailing = ESM_DEFAULT_FROM_CJS, + rewriteThis = true, + } = options; + + const vendorSource = rewriteThis ? forceVendorGlobalThis(source) : source; + + return [ + preamble, + preamble ? '\n' : '', + `const module = { exports: ${exportsInit} };\n`, + 'const exports = module.exports;\n', + 'var define;\n', + requireShim, + vendorSource, + '\n', + trailing, + ].join(''); +} + +/** + * `intl/dist/Intl.complete.js` appends locale data that expects a free + * `IntlPolyfill` binding from the UMD *browser* branch. Forcing CJS breaks + * that, so keep the global branch and re-export the polyfill. + */ +function wrapIntlVendorAsEsm(source: string): string { + return 'var define;\n' + + 'var IntlPolyfill;\n' + + `${source + .replace(/}\(this,/g, '}(globalThis,') + .replace(/e\.IntlPolyfill=r\(\)/g, 'e.IntlPolyfill=IntlPolyfill=r()')}\n` + + 'export default IntlPolyfill;\n'; +} + +/** + * Force the CJS branch of a UMD wrapper and re-export `module.exports` as default. + * Also emit synthetic named exports from the webpack entry module so + * `import Def, * as Ns from 'pkg'` gets CJS-style interop + * (needed by diagram.importer → `Ns.DiagramControl`). + */ +function collectWebpackEntryExportNames(source: string): string[] { + const entryMatch = /var __webpack_exports__ = __webpack_require__\((\d+)\);/.exec(source); + if (!entryMatch) { + return []; + } + + const entryId = entryMatch[1]; + const moduleStart = source.indexOf(`/***/ ${entryId}`); + if (moduleStart < 0) { + return []; + } + + const nextModule = source.indexOf('\n/***/ ', moduleStart + 1); + const moduleSource = nextModule < 0 + ? source.slice(moduleStart) + : source.slice(moduleStart, nextModule); + + const names = new Set(); + const definePropertyRe = /Object\.defineProperty\(\s*exports\s*,\s*["']([^"']+)["']/g; + let match = definePropertyRe.exec(moduleSource); + while (match) { + const name = match[1]; + if (name !== '__esModule' && name !== 'default' && /^[A-Za-z_$][\w$]*$/.test(name)) { + names.add(name); + } + match = definePropertyRe.exec(moduleSource); + } + + return [...names].sort(); +} + +function wrapWebpackVendorAsEsm(source: string): string { + const namedExports = collectWebpackEntryExportNames(source) + .map((name) => `export const ${name} = module.exports.${name};`) + .join('\n'); + + return wrapVendorCjsBranch(source, { + rewriteThis: false, + trailing: `${ESM_DEFAULT_FROM_CJS}${namedExports ? `${namedExports}\n` : ''}`, + }); +} + +/** globalize / cldrjs ship as UMD; native ESM needs a CJS-branch + require shim. */ +function wrapGlobalizeOrCldrAsEsm(source: string, relativeUrlPath: string): string { + const normalized = normalizeUrlPath(relativeUrlPath); + const isCldrMain = normalized.endsWith('/cldrjs/dist/cldr.js'); + const isCldrPlugin = /\/cldrjs\/dist\/cldr\/[^/]+\.js$/i.test(normalized); + const isGlobalizeMain = normalized.endsWith('/globalize/dist/globalize.js'); + const isGlobalizePlugin = normalized.includes('/globalize/dist/globalize/'); + const baseName = path.basename(normalized, '.js'); + const needsNumber = isGlobalizePlugin && (baseName === 'currency' || baseName === 'date'); + + const preamble: string[] = []; + if (isCldrPlugin) { + preamble.push('import __dxCldr from \'cldr\';'); + } else if (isGlobalizeMain || isGlobalizePlugin) { + preamble.push('import __dxCldr from \'cldr\';'); + preamble.push('import \'cldr/event\';'); + if (isGlobalizePlugin) { + preamble.push('import \'cldr/supplemental\';'); + preamble.push('import __dxGlobalize from \'globalize\';'); + if (needsNumber) { + // CJS factory skips `./number`; AMD/DevExtreme always load it first. + preamble.push('import \'./number.js\';'); + } + } + } + + const requireShim = isCldrMain + ? 'function require(id) { throw new Error(\'Unexpected require in cldr: \' + id); }\n' + : [ + 'function require(id) {\n', + ' if (id === \'cldrjs\' || id === \'cldr\' || id === \'../cldr\') {\n', + ' return __dxCldr;\n', + ' }\n', + ' if (id === \'../globalize\' || id === \'globalize\') {\n', + ' return __dxGlobalize;\n', + ' }\n', + ' throw new Error(\'Unhandled require in globalize/cldr UMD: \' + id);\n', + '}\n', + ].join(''); + + return wrapVendorCjsBranch(source, { + preamble: preamble.join('\n'), + requireShim, + }); +} + +/** + * Vector map geo data UMD: CJS writes into `exports`, browser branch expects + * bare `DevExpress`. Under ESM imports hoist above suite setup, so create the + * global sources bag and point `module.exports` at the same object. + */ +function wrapVectorMapDataAsEsm(source: string): string { + return wrapVendorCjsBranch(source, { + preamble: [ + 'globalThis.DevExpress = globalThis.DevExpress || {};', + 'globalThis.DevExpress.viz = globalThis.DevExpress.viz || {};', + 'globalThis.DevExpress.viz.map = globalThis.DevExpress.viz.map || {};', + 'globalThis.DevExpress.viz.map.sources = globalThis.DevExpress.viz.map.sources || {};', + ].join('\n'), + exportsInit: 'globalThis.DevExpress.viz.map.sources', + trailing: 'export default module.exports;\n', + }); +} + +/** `dx.vectormaputils.js` is UMD (`exports.parse = …`); tests do `import { parse }`. */ +function wrapVectorMapUtilsAsEsm(source: string): string { + return wrapVendorCjsBranch(source, { + rewriteThis: false, + trailing: 'export default module.exports;\nexport const parse = module.exports.parse;\n', + }); +} + +type VendorWrapper = (source: string, relativeUrlPath: string) => string; + +function resolveVendorEsmWrapper(relativeUrlPath: string): VendorWrapper | null { + const normalized = normalizeUrlPath(relativeUrlPath); + + if ( + normalized.endsWith('/intl/dist/Intl.complete.js') + || normalized.endsWith('/intl/dist/Intl.js') + ) { + return (source) => wrapIntlVendorAsEsm(source); + } + + if ( + normalized.endsWith('/globalize/dist/globalize.js') + || normalized.includes('/globalize/dist/globalize/') + || normalized.endsWith('/cldrjs/dist/cldr.js') + || /\/cldrjs\/dist\/cldr\/[^/]+\.js$/i.test(normalized) + ) { + return wrapGlobalizeOrCldrAsEsm; + } + + if (/\/artifacts\/js\/vectormap-data\/[^/]+\.js$/i.test(normalized)) { + return (source) => wrapVectorMapDataAsEsm(source); + } + + if (/\/artifacts\/js\/vectormap-utils\/dx\.vectormaputils\.js$/i.test(normalized)) { + return (source) => wrapVectorMapUtilsAsEsm(source); + } + + if ( + normalized.endsWith('/devextreme-quill/dist/dx-quill.js') + || normalized.endsWith('/artifacts/js/dx-diagram.js') + || normalized.endsWith('/artifacts/js/dx-gantt.js') + || normalized.endsWith('/artifacts/js/dx-exceljs-fork.js') + || normalized.endsWith('/artifacts/js/jszip.js') + ) { + return (source) => wrapWebpackVendorAsEsm(source); + } + + return null; +} + +/** Serve JSON as `export default …` for native ESM (`*.json!` replacement). */ +function sendJsonAsEsmModule(res: ServerResponse, filePath: string): boolean { + return sendTransformedJs( + res, + filePath, + (raw) => { + JSON.parse(raw); + return `export default ${raw};\n`; + }, + 'Failed to export JSON as ESM module', + ); +} + +// --- Mutable artifact facades --------------------------------------------------- + +function sendMutableFacadeModule(res: ServerResponse, shimUrl: string): boolean { + // Serve a re-export at the artifact URL so relative library imports and + // bare import-map entries share the same shim module graph. + return sendJsModuleBody( + res, + `export * from '${shimUrl}';\nexport { default } from '${shimUrl}';\n`, + ); +} + +// --- ESM artifact tweaks -------------------------------------------------------- + +/** + * Convert leftover `exports.foo = …` (from #DEBUG / dual CJS-ESM sources) + * into native ESM exports so the browser does not throw "exports is not defined". + */ +function rewriteLegacyCjsExportsInEsmArtifact(source: string): string { + if (!/\bexports\./.test(source)) { + return source; + } + + return source + .replace( + /^exports\.([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*);?\s*$/gm, + (_match, exportName: string, valueName: string) => (exportName === valueName + ? `export { ${exportName} };` + : `export { ${valueName} as ${exportName} };`), + ) + .replace( + /^exports\.([A-Za-z_$][\w$]*)\s*=\s*function\s*\(/gm, + 'export function $1(', + ) + .replace( + /^exports\.([A-Za-z_$][\w$]*)\s*=\s*async\s+function\s*\(/gm, + 'export async function $1(', + ); +} + +/** + * ESM npm artifacts are built with removeDebug:true, which strips QUnit-only + * hooks. Re-attach the ones still present as locals in the compiled module. + * + * When debug is kept (`-c qunit`), some sources still emit CJS `exports.*` + * assignments that throw under native ESM — rewrite those to ESM exports. + */ +function restoreEsmDebugTestHooks(relativeUrlPath: string, source: string): string { + const normalized = normalizeUrlPath(relativeUrlPath); + if (!normalized.includes(ESM_ARTIFACT_MARKER)) { + return source; + } + + let next = source; + + if (normalized.endsWith('/__internal/events/core/m_events_engine.js') + && !next.includes('eventsEngine.detectPassiveEventHandlersSupport')) { + next = next.replace( + /eventsEngine\.passiveEventHandlersSupported\s*=\s*passiveEventHandlersSupported;/, + 'eventsEngine.passiveEventHandlersSupported = passiveEventHandlersSupported;\n' + + 'eventsEngine.elementDataMap = elementDataMap;\n' + + 'eventsEngine.detectPassiveEventHandlersSupport = detectPassiveEventHandlersSupport;', + ); + } + + return rewriteLegacyCjsExportsInEsmArtifact(next); +} + +function sendEsmArtifactJs( + res: ServerResponse, + filePath: string, + relativeUrlPath: string, +): boolean { + try { + const raw = fs.readFileSync(filePath, 'utf8'); + let body = restoreEsmDebugTestHooks(relativeUrlPath, raw); + body = rewriteAspnetArtifactToEsm(body, relativeUrlPath); + if (body === raw) { + return sendStaticFile(res, filePath, fs.statSync(filePath).size); + } + return sendJsModuleBody(res, body); + } catch { + return sendError(res, 500, 'Failed to serve ESM artifact'); + } +} + function sendDirectoryListing( res: ServerResponse, requestPath: string, @@ -141,7 +514,6 @@ ${items.join('\n')} export function createStaticFileService({ escapeHtml, rootDirectory, - setNoCacheHeaders, setStaticCacheHeaders, }: StaticFileServiceDeps): StaticFileService { function tryServeStatic( @@ -156,30 +528,88 @@ export function createStaticFileService({ const relativeToRoot = path.relative(rootDirectory, filePath); if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) { - setNoCacheHeaders(res); - res.statusCode = 403; - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.end('Forbidden'); - return true; + return sendError(res, 403, 'Forbidden'); } - if (!fs.existsSync(filePath)) { + const resolvedFilePath = resolveStaticFilePath(filePath); + if (!resolvedFilePath) { return false; } setStaticCacheHeaders(res, searchParams); - const stat = fs.statSync(filePath); + const stat = fs.statSync(resolvedFilePath); if (stat.isDirectory()) { - return sendDirectoryListing(res, pathname, filePath, escapeHtml); + return sendDirectoryListing(res, pathname, resolvedFilePath, escapeHtml); } - if (stat.isFile()) { - return sendStaticFile(res, filePath, stat.size); + if (!stat.isFile()) { + return false; + } + + if (searchParams.has('esm-export') && path.extname(resolvedFilePath).toLowerCase() === '.json') { + return sendJsonAsEsmModule(res, resolvedFilePath); + } + + // Native ESM resolves relative imports against the request URL, not the + // on-disk file. Redirect extensionless URLs to the canonical file URL. + const resolvedUrlPath = `/${normalizeUrlPath(path.relative(rootDirectory, resolvedFilePath))}`; + if (resolvedUrlPath !== normalizedPath) { + const query = searchParams.toString(); + res.statusCode = 302; + res.setHeader('Location', query ? `${resolvedUrlPath}?${query}` : resolvedUrlPath); + res.end(); + return true; + } + + const relativeUrlPath = normalizeUrlPath(relativeToRoot); + const isJs = path.extname(resolvedFilePath).toLowerCase() === '.js'; + + if (isJs && isQunitTestOrHelperPath(relativeUrlPath)) { + return sendTransformedJs( + res, + resolvedFilePath, + (raw) => rewriteQunitTestHelperSource(raw, relativeUrlPath), + 'Failed to rewrite CJS-style test/helper module', + ); + } + + // Relative library imports bypass import maps — serve mutable facades + // at the artifact URL unless ?dx-original=1 (used by the facade itself). + // Hand-written shims (themes, …) win; otherwise auto-generate. + if (!searchParams.has('dx-original')) { + const shimUrl = findHandWrittenMutableFacade(relativeUrlPath); + if (shimUrl) { + return sendMutableFacadeModule(res, shimUrl); + } + const autoFacade = tryBuildAutoMutableFacade( + relativeUrlPath, + resolvedFilePath, + rootDirectory, + ); + if (autoFacade) { + return sendJsModuleBody(res, autoFacade); + } + } + + if (isJs) { + const vendorWrapper = resolveVendorEsmWrapper(relativeUrlPath); + if (vendorWrapper) { + return sendTransformedJs( + res, + resolvedFilePath, + (raw) => vendorWrapper(raw, relativeUrlPath), + 'Failed to wrap vendor bundle as ESM', + ); + } + + if (relativeUrlPath.includes(ESM_ARTIFACT_MARKER)) { + return sendEsmArtifactJs(res, resolvedFilePath, relativeUrlPath); + } } - return false; + return sendStaticFile(res, resolvedFilePath, stat.size); } return { diff --git a/packages/devextreme/testing/runner/templates/run-suite.template.html b/packages/devextreme/testing/runner/templates/run-suite.template.html index a7660dce96c2..d206af4ba123 100644 --- a/packages/devextreme/testing/runner/templates/run-suite.template.html +++ b/packages/devextreme/testing/runner/templates/run-suite.template.html @@ -99,9 +99,9 @@ - + - + + + {{{IMPORT_MAP_SCRIPT}}} +
- + diff --git a/packages/devextreme/testing/systemjs-builder.js b/packages/devextreme/testing/systemjs-builder.js deleted file mode 100644 index 940b87d249de..000000000000 --- a/packages/devextreme/testing/systemjs-builder.js +++ /dev/null @@ -1,272 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const babel = require('@babel/core'); -const parseArguments = require('minimist'); - - -const root = path.join(__dirname, '..'); -const transpilePath = path.join(root, '/artifacts/transpiled'); - -const getFileList = (dirName) => { - let files = []; - const items = fs.readdirSync(dirName, { withFileTypes: true }); - - // eslint-disable-next-line no-restricted-syntax - for(const item of items) { - if(item.isDirectory()) { - files = [...files, ...getFileList(path.join(dirName, item.name))]; - } else if( - item.name.endsWith('.js') || - (item.name.endsWith('.json') && !item.name.includes('tsconfig') && !item.name.includes('__meta')) - ) { - files.push(path.join(dirName, item.name)); - } - } - - return files; -}; - -const writeFileSync = (destPath, file) => { - const destDir = path.dirname(destPath); - if(!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }); - } - - fs.writeFileSync(destPath, file); -}; - -const buildAmdModule = (body) => ` -define(function(require, exports, module) { - ${body} -}); -`; - -const transpileCommonJSFile = (source, pathToFile) => { - const [pre, post] = path.extname(pathToFile) === '.json' - ? ['module.exports = ', ';'] - : ['', '']; - - writeFileSync( - pathToFile, - buildAmdModule( - `${pre}${source}${post}`.replace(/(\n|\r)/g, '$1 ') - ) - ); -}; - -const buildJsonModule = (body) => ` -define(function(require, exports, module) { - module.exports = ${body}; -}); -`; - -const buildSystemJSModule = (body, pre = '') => ` -SystemJS.register([], function(exports) { - ${pre} - - return { - setters: [], - execute: function() { - ${body} - } - }; -}); -`; - -const transpileFile = async(sourcePath, targetPath) => { - const code = fs.readFileSync(sourcePath) - .toString() - .replaceAll('/packages/devextreme/testing/helpers/wrapRenovatedWidget.js', '/packages/devextreme/artifacts/transpiled-testing/helpers/wrapRenovatedWidget.js') - .replaceAll(path.normalize('/testing/helpers/'), path.normalize('/artifacts/transpiled-testing/helpers/')) - // TODO see packages/devextreme/testing/tests/DevExpress.viz.vectorMap.utils/tests.js - // import { parse } from '../../../artifacts/js/vectormap-utils/dx.vectormaputils.js'; - // This used to work because the runner cwd was the same as the devextreme root folder - // remove next 3 lines after fix - .replaceAll( - path.normalize('../../../artifacts/js/vectormap-utils/dx.vectormaputils.js'), - path.normalize('../../../../artifacts/js/vectormap-utils/dx.vectormaputils.js')); - - if(sourcePath.includes('testing/helpers/includeThemesLinks.js')) { - writeFileSync(targetPath, buildSystemJSModule('', code.replaceAll('\n', ' '))); - return; - } - - if( - /(^|\s)System(JS)?\.register/gm.test(code) || - /(^|\s)define\(/gm.test(code) || - sourcePath.includes('helpers/forMap') - ) { - writeFileSync(targetPath, code); - } else if(/(\(|\s|^)require\(/.test(code) || /(module\.)?exports(\.\w+)?\s?=/.test(code)) { - transpileCommonJSFile(code, targetPath); - } else if(sourcePath.endsWith('.json')) { - writeFileSync(targetPath, buildJsonModule(code)); - } else { - await transpileWithBabel(code, targetPath); - } -}; - -const transpileModules = async() => { - await Promise.all( - getFileList(transpilePath).map((filePath) => { - return transpileFile( - filePath, - filePath.replace(path.normalize('/transpiled'), path.normalize('/transpiled-systemjs')), - ); - }) - ); -}; - -const buildCssAsSystemModule = (name, filePath) => ` -System.register('${filePath}', [], false, function() {}); -(function() { - if (typeof document == 'undefined') return; - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = '/packages/devextreme/${filePath}'; - link.setAttribute('data-theme', '${name}'); - document.getElementsByTagName('head')[0].appendChild(link); -})(); -`; - -const transpileCss = async() => { - const cssList = [ - ['artifacts/css/dx.light.css', 'generic.light'], - ['artifacts/css/dx.material.blue.light.css', 'material.blue.light'], - ['artifacts/css/dx.fluent.blue.light.css', 'fluent.blue.light'], - ['artifacts/css/dx-gantt.css', 'gantt'], - ]; - - // eslint-disable-next-line no-restricted-syntax - for(const [cssFile, styleName] of cssList) { - const destPath = path.join(root, cssFile.replace('css', 'css-systemjs')); - - writeFileSync(destPath, buildCssAsSystemModule(styleName, cssFile)); - } -}; - -const transpileWithBabel = async(sourceCode, destPath) => { - const { code } = await babel.transform(sourceCode, { - compact: false, - plugins: ['@babel/plugin-transform-modules-systemjs'], - sourceMaps: true, - }); - - writeFileSync(destPath, code); -}; - -const transpileIntl = async() => { - const listIntlFiles = [ - { - filePath: require.resolve('intl/lib/core.js'), - destPath: path.join(root, 'artifacts/js-systemjs/intl/intl.js'), - }, - { - filePath: require.resolve('intl/locale-data/complete.js'), - destPath: path.join(root, 'artifacts/js-systemjs/intl/intl.complete.js'), - }, - ]; - - await Promise.all(listIntlFiles.map(({ filePath, destPath }) => { - const code = fs.readFileSync(filePath).toString(); - - writeFileSync( - destPath, - buildAmdModule( - code.replace('IntlPolyfill', 'require("./intl.js")') - ) - ); - })); - - const intlIndex = ` - define(function(require, exports, module) { - window.IntlPolyfill = require('./intl.js'); - - require('./intl.complete.js'); - - if (!window.Intl) { - window.Intl = window.IntlPolyfill; - window.IntlPolyfill.__applyLocaleSensitivePrototypes(); - } - - module.exports = window.IntlPolyfill; - }); - `; - - writeFileSync(path.join(root, 'artifacts/js-systemjs/intl/index.js'), intlIndex); -}; - -const transpileJsVendors = async() => { - const pluginsList = [ - { - filePath: require.resolve('systemjs-plugin-css/css.js'), - destPath: path.join(root, 'artifacts/js-systemjs/css.js'), - }, - { - filePath: require.resolve('systemjs-plugin-json/json.js'), - destPath: path.join(root, 'artifacts/js-systemjs/json.js'), - }, - ]; - - await Promise.all( - pluginsList.map(({ filePath, destPath }) => { - const code = fs.readFileSync(filePath).toString(); - - return writeFileSync( - destPath, - buildSystemJSModule( - '', - code.replaceAll('module.exports', 'exports') - ) - ); - }), - ); - - await transpileIntl(); - - await transpileFile( - require.resolve('knockout/build/output/knockout-latest.debug.js'), - path.join(root, 'artifacts/js-systemjs/knockout.js') - ); - await transpileFile( - path.join(root, 'node_modules/@preact/signals-core/dist/signals-core.js'), - path.join(root, 'artifacts/js-systemjs/preact-signals.js') - ); - - - [].concat( - getFileList(path.join(root, 'node_modules/devextreme-cldr-data')), - getFileList(path.join(root, 'node_modules/cldr-core/supplemental')) - ) - .filter(filePath => filePath.endsWith('.json')) - .forEach((filePath) => { - transpileFile(filePath, filePath.replace(path.normalize('/node_modules'), path.normalize('/artifacts/js-systemjs'))); - }); -}; - -const transpileTesting = async() => { - const contentList = getFileList(path.join(root, 'testing/content')); - const helpersList = getFileList(path.join(root, 'testing/helpers')); - const testsList = getFileList(path.join(root, 'testing/tests')); - - [].concat(contentList, helpersList, testsList) - .forEach((filePath) => { - transpileFile(filePath, filePath.replace(path.normalize('/testing/'), path.normalize('/artifacts/transpiled-testing/'))); - }); -}; - -(async() => { - - const { transpile } = parseArguments(process.argv); - - switch(transpile) { - case 'modules': - return await transpileModules(); - case 'testing': - return await transpileTesting(); - case 'css': - return await transpileCss(); - case 'js-vendors': - return await transpileJsVendors(); - } -})(); diff --git a/packages/devextreme/testing/tests/DevExpress.aspnet/aspnet.tests.js b/packages/devextreme/testing/tests/DevExpress.aspnet/aspnet.tests.js index c61b8e313b3c..6d2fdbc87a94 100644 --- a/packages/devextreme/testing/tests/DevExpress.aspnet/aspnet.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.aspnet/aspnet.tests.js @@ -1,39 +1,28 @@ -(function(factory) { - if(typeof define === 'function' && define.amd) { - define(function(require, exports, module) { - require('integration/jquery'), - require('ui/button'); - require('ui/check_box'); - require('ui/drop_down_button'); - require('ui/form'); - require('ui/popup'); - require('ui/select_box'); - require('ui/text_box'); - require('ui/toolbar'); - require('ui/validator'); - require('ui/validation_summary'); - - const aspnet = require('aspnet'); - window.DevExpress = { aspnet: aspnet }; // for DevExpress.aspnet.createComponent in templates - - module.exports = factory( - require('jquery'), - require('core/templates/template_engine_registry').setTemplateEngine, - aspnet, - function() { return require('ui/widget/ui.errors'); }, - function() { return require('../../helpers/ajaxMock.js'); } - ); - }); - } else { - factory( - window.jQuery, - DevExpress.setTemplateEngine, - DevExpress.aspnet, - function() { return window.DevExpress_ui_widget_errors; }, - function() { return window.ajaxMock; } - ); - } -}(function($, setTemplateEngine, aspnet, errorsAccessor, ajaxMockAccessor) { +import 'integration/jquery'; +import 'ui/button'; +import 'ui/check_box'; +import 'ui/drop_down_button'; +import 'ui/form'; +import 'ui/popup'; +import 'ui/select_box'; +import 'ui/text_box'; +import 'ui/toolbar'; +import 'ui/validator'; +import 'ui/validation_summary'; + +import $ from 'jquery'; +import { setTemplateEngine } from 'core/templates/template_engine_registry'; +import aspnetModule from 'aspnet'; +import errorsModule from 'ui/widget/ui.errors'; +import ajaxMock from '../../helpers/ajaxMock.js'; + +// Templates call DevExpress.aspnet.createComponent / renderComponent. +// MVC-style templates also expect global `$` (runner calls jQuery.noConflict()). +window.DevExpress = window.DevExpress || {}; +window.DevExpress.aspnet = aspnetModule; +window.$ = $; + +(function($, setTemplateEngine, aspnet, errorsAccessor, ajaxMockAccessor) { if(QUnit.urlParams['nojquery']) { return; @@ -696,4 +685,4 @@ }); }); -})); +})($, setTemplateEngine, aspnetModule, function() { return errorsModule; }, function() { return ajaxMock; }); diff --git a/packages/devextreme/testing/tests/DevExpress.common/charts.tests.js b/packages/devextreme/testing/tests/DevExpress.common/charts.tests.js index 6c7ff287bb62..e1d72136d9ca 100644 --- a/packages/devextreme/testing/tests/DevExpress.common/charts.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.common/charts.tests.js @@ -1,16 +1,19 @@ import { registerPattern, registerGradient } from 'common/charts'; import graphicObjects from '__internal/common/m_charts'; -import utils from 'viz/core/utils_default'; +function clearGraphicObjects() { + const objects = graphicObjects.getGraphicObjects(); + Object.keys(objects).forEach((key) => { + delete objects[key]; + }); +} QUnit.module('Graphic objects', { beforeEach: function() { - this.getNextDefsStub = sinon.stub(utils, 'getNextDefsSvgId'); - this.getNextDefsStub.onCall(0).returns('DevExpressId_1'); - this.getNextDefsStub.onCall(1).returns('DevExpressId_2'); + clearGraphicObjects(); }, afterEach: function() { - this.getNextDefsStub.restore(); + clearGraphicObjects(); } }); @@ -18,12 +21,11 @@ QUnit.test('should register pattern', function(assert) { const id_1 = registerPattern({ key: 'test_key_1' }); const id_2 = registerPattern({ key: 'test_key_2' }); - assert.equal(this.getNextDefsStub.callCount, 2); - assert.equal(id_1, 'DevExpressId_1'); - assert.equal(id_2, 'DevExpressId_2'); + assert.ok(/^DevExpress_\d+$/.test(id_1), 'id has expected format'); + assert.notEqual(id_1, id_2, 'ids are unique'); assert.deepEqual(graphicObjects.getGraphicObjects(), { - 'DevExpressId_1': { key: 'test_key_1', type: 'pattern' }, - 'DevExpressId_2': { key: 'test_key_2', type: 'pattern' } + [id_1]: { key: 'test_key_1', type: 'pattern' }, + [id_2]: { key: 'test_key_2', type: 'pattern' } }); }); @@ -31,11 +33,10 @@ QUnit.test('should register gradient', function(assert) { const id_1 = registerGradient('gradient_type', { key: 'test_key_1' }); const id_2 = registerGradient('gradient_type', { key: 'test_key_2' }); - assert.equal(this.getNextDefsStub.callCount, 2); - assert.equal(id_1, 'DevExpressId_1'); - assert.equal(id_2, 'DevExpressId_2'); + assert.ok(/^DevExpress_\d+$/.test(id_1), 'id has expected format'); + assert.notEqual(id_1, id_2, 'ids are unique'); assert.deepEqual(graphicObjects.getGraphicObjects(), { - 'DevExpressId_1': { key: 'test_key_1', type: 'gradient_type' }, - 'DevExpressId_2': { key: 'test_key_2', type: 'gradient_type' } + [id_1]: { key: 'test_key_1', type: 'gradient_type' }, + [id_2]: { key: 'test_key_2', type: 'gradient_type' } }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.core/config_bundled.tests.js b/packages/devextreme/testing/tests/DevExpress.core/config_bundled.tests.js index 7800023e6c96..e7cdd80db419 100644 --- a/packages/devextreme/testing/tests/DevExpress.core/config_bundled.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.core/config_bundled.tests.js @@ -3,14 +3,14 @@ const useJQuery = !QUnit.urlParams['nojquery']; window.DevExpress = window.DevExpress || {}; window.DevExpress.config = { useJQuery: useJQuery }; -define(function(require) { - QUnit.test = QUnit.urlParams['nocsp'] ? QUnit.test : QUnit.skip; - require('bundles/dx.all.js'); +// Must stay dynamic: static imports hoist above the config assignment. +await import('bundles/dx.all.js'); - QUnit.module('config.useJQuery'); +QUnit.test = QUnit.urlParams['nocsp'] ? QUnit.test : QUnit.skip; - QUnit.test('config value useJQuery with jQuery in window', function(assert) { - const config = DevExpress.config; - assert.equal(config().useJQuery, useJQuery); - }); +QUnit.module('config.useJQuery'); + +QUnit.test('config value useJQuery with jQuery in window', function(assert) { + const config = DevExpress.config; + assert.equal(config().useJQuery, useJQuery); }); diff --git a/packages/devextreme/testing/tests/DevExpress.jquery/bundled.tests.js b/packages/devextreme/testing/tests/DevExpress.jquery/bundled.tests.js index 0fc86f98a8e9..26ba47daecfb 100644 --- a/packages/devextreme/testing/tests/DevExpress.jquery/bundled.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.jquery/bundled.tests.js @@ -1,18 +1,13 @@ -define(function(require) { - if(QUnit.urlParams['nojquery']) { - return; - } - - const $ = require('jquery'); - - require('bundles/dx.all.js'); - +import $ from 'jquery'; +import 'integration/jquery'; +import dxButton from 'ui/button'; +if(!QUnit.urlParams['nojquery']) { QUnit.module('jquery integration'); QUnit.test('renderer uses correct strategy', function(assert) { const node = document.createElement('div'); - const element = new DevExpress.ui.dxButton(node).element(); + const element = new dxButton(node).element(); assert.ok(element instanceof window.jQuery); }); @@ -22,4 +17,4 @@ define(function(require) { assert.equal(typeof $element.dxButton, 'function'); }); -}); +} diff --git a/packages/devextreme/testing/tests/DevExpress.jquery/template.tests.js b/packages/devextreme/testing/tests/DevExpress.jquery/template.tests.js index c072d222b479..06020db1cc88 100644 --- a/packages/devextreme/testing/tests/DevExpress.jquery/template.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.jquery/template.tests.js @@ -1,9 +1,3 @@ -SystemJS.config({ - map: { - 'jqueryify': SystemJS.map.jquery - } -}); - define(function(require) { const $ = require('jquery'); const Template = require('core/templates/template').Template; diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js index d028a628baf6..b1131dcb7c4d 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js @@ -1,26 +1,3 @@ -SystemJS.config({ - meta: { - './localization.base.tests.js': { - deps: [ - 'common/core/localization/globalize/core', - 'common/core/localization/globalize/number', - 'common/core/localization/globalize/currency', - 'common/core/localization/globalize/date', - 'common/core/localization/globalize/message' - ] - } - }, - packages: { - 'globalize': { - meta: { - '../globalize.js': { - deps: ['cldr/unresolved'] - } - } - } - } -}); - define(function(require, exports, module) { const cldrData = [ require('devextreme-cldr-data/ar.json!json'), @@ -547,7 +524,7 @@ define(function(require, exports, module) { }); QUnit.module('Exceljs format', () => { - ExcelJSLocalizationFormatTests.default.runCurrencyTests([ + ExcelJSLocalizationFormatTests.runCurrencyTests([ { value: 'USD', expected: '$#,##0_);\\($#,##0\\)' }, { value: 'RUB', expected: '\\R\\U\\B#,##0_);\\(\\R\\U\\B#,##0\\)' }, { value: 'JPY', expected: '\\¥#,##0_);\\(\\¥#,##0\\)' }, @@ -556,7 +533,7 @@ define(function(require, exports, module) { { value: 'SEK', expected: '\\S\\E\\K#,##0_);\\(\\S\\E\\K#,##0\\)' } ]); - ExcelJSLocalizationFormatTests.default.runPivotGridCurrencyTests([ + ExcelJSLocalizationFormatTests.runPivotGridCurrencyTests([ { value: 'USD', expected: '$#,##0_);\\($#,##0\\)' }, { value: 'RUB', expected: '\\R\\U\\B#,##0_);\\(\\R\\U\\B#,##0\\)' }, { value: 'JPY', expected: '\\¥#,##0_);\\(\\¥#,##0\\)' }, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/adaptiveColumns.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/adaptiveColumns.tests.js index 62f7c4e1a833..ec8e84cf6f59 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/adaptiveColumns.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/adaptiveColumns.tests.js @@ -105,19 +105,22 @@ QUnit.module('AdaptiveColumns', { if(name === 'width' || name === 'height') { ++cssInvokeCounter; } + return cssFunc.apply(this, arguments); }; - // arrange, act - $('.dx-datagrid').width(200); - setupDataGrid(this); - this.rowsView.render($('#container')); - this.resizingController.updateDimensions(); - this.clock.tick(10); - - // assert - assert.equal(cssInvokeCounter, 0, 'no $.css() invokes for width/height CSS properties'); - - renderer.fn.css = cssFunc; + try { + // arrange, act + $('.dx-datagrid').width(200); + setupDataGrid(this); + this.rowsView.render($('#container')); + this.resizingController.updateDimensions(); + this.clock.tick(10); + + // assert + assert.equal(cssInvokeCounter, 0, 'no $.css() invokes for width/height CSS properties'); + } finally { + renderer.fn.css = cssFunc; + } }); // T516888 diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/importQuill.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/importQuill.tests.js index 8ba22c6b1e98..954d86128ce6 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/importQuill.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/importQuill.tests.js @@ -1,22 +1,13 @@ +import quillImporter from 'ui/html_editor/quill_importer'; -SystemJS.config({ - map: { - 'devextreme-quill': '/packages/devextreme/testing/helpers/quillDependencies/noQuill.js' - } -}); - -define(function(require) { - const getQuill = require('ui/html_editor/quill_importer').getQuill; - - QUnit.module('Import 3rd party', function() { - QUnit.test('it throw an error if the quill script isn\'t referenced', function(assert) { - assert.throws( - function() { getQuill(); }, - function(e) { - return /(E1041)[\s\S]*(Quill)/.test(e.message); - }, - 'The Quill script isn\'t referenced' - ); - }); +QUnit.module('Import 3rd party', function() { + QUnit.test('it throw an error if the quill script is not referenced', function(assert) { + assert.throws( + function() { quillImporter.getQuill(); }, + function(e) { + return /(E1041)[\s\S]*(Quill)/.test(e.message); + }, + 'The Quill script is not referenced' + ); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/tableResizingModule.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/tableResizingModule.tests.js index 939672ded248..cf29a6de403a 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/tableResizingModule.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/tableResizingModule.tests.js @@ -72,6 +72,8 @@ const moduleConfig = { }, afterEach: function() { + this.clock.tick(1000); + resizeCallbacks.empty(); this.clock.restore(); } }; @@ -176,7 +178,7 @@ module('Table resizing module', moduleConfig, () => { resizeCallbacks.fire(); - assert.strictEqual(typeof resizingInstance._resizeHandlerWithContext, 'object', '_resizeHandler is an object'); + assert.ok(resizingInstance._resizeHandlerWithContext, '_resizeHandlerWithContext is registered'); }); test('Window resize callback should be cleaned after the widget dispose', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js index 3afd5c78e87e..bcc686b7c3b1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js @@ -1883,6 +1883,7 @@ testModule('Toolbar items state update', { test('state of the items in menu should be synchronized after toolbar repaint (t1117604)', function(assert) { this.options.items = this.mapToMenuItems(TABLE_OPERATIONS); + resizeCallbacks.empty(); const toolbar = new Toolbar(this.quillMock, this.options); this.quillMock.getFormat = () => ({ table: true }); toolbar.updateTableWidgets(); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js index 9205430c2753..55a3f263fb84 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { DesktopTooltipStrategy } from '__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy'; import { FunctionTemplate } from 'core/templates/function_template'; import { extend } from 'core/utils/extend'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/diagramParts/importDiagram.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/diagramParts/importDiagram.tests.js index ecc19597f49f..11113aa6afb5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/diagramParts/importDiagram.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/diagramParts/importDiagram.tests.js @@ -1,20 +1,12 @@ -SystemJS.config({ - map: { - 'devexpress-diagram': '/packages/devextreme/testing/helpers/noDiagram.js' - } -}); - -define(function(require) { - const getDiagram = require('__internal/ui/diagram/diagram.importer').getDiagram; +import { getDiagram } from '__internal/ui/diagram/diagram.importer'; - QUnit.module('Import devexpress-diagram', function() { - QUnit.test('throw an error if the devexpress-diagram script isn\'t referenced', function(assert) { - assert.throws( - function() { getDiagram(); }, - function(e) { - return /(E1041)[\s\S]*(devexpress-diagram)/.test(e.message); - } - ); - }); +QUnit.module('Import devexpress-diagram', function() { + QUnit.test('throw an error if the devexpress-diagram script isn\'t referenced', function(assert) { + assert.throws( + function() { getDiagram(); }, + function(e) { + return /(E1041)[\s\S]*(devexpress-diagram)/.test(e.message); + } + ); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/ganttParts/importGantt.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/ganttParts/importGantt.tests.js index 66e095bfe854..58162b185b4c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/ganttParts/importGantt.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/ganttParts/importGantt.tests.js @@ -1,21 +1,12 @@ +import { getGanttViewCore as getGantt } from '__internal/ui/gantt/gantt_importer'; -SystemJS.config({ - map: { - 'devexpress-gantt': '/packages/devextreme/testing/helpers/noGantt.js' - } -}); - -define(function(require) { - const getGantt = require('__internal/ui/gantt/gantt_importer').getGanttViewCore; - - QUnit.module('Import devexpress-gantt', function() { - QUnit.test('throw an error if the devexpress-gantt script isn\'t referenced', function(assert) { - assert.throws( - function() { getGantt(); }, - function(e) { - return /(E1041)[\s\S]*(devexpress-gantt)/.test(e.message); - } - ); - }); +QUnit.module('Import devexpress-gantt', function() { + QUnit.test('throw an error if the devexpress-gantt script isn\'t referenced', function(assert) { + assert.throws( + function() { getGantt(); }, + function(e) { + return /(E1041)[\s\S]*(devexpress-gantt)/.test(e.message); + } + ); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui/themes.tests.js b/packages/devextreme/testing/tests/DevExpress.ui/themes.tests.js index 3e98fd22e59e..dff805a7fd8e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui/themes.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui/themes.tests.js @@ -770,7 +770,7 @@ QUnit.module('initialized method', (hooks) => { test('initialized fires for ordinary link (init before link addition - should wait theme loading)', function(assert) { const done = assert.async(); - const url = ROOT_URL + 'packages/devextreme/testing' + '/helpers/themeMarker.css'; // WA for systemjs builder + const url = ROOT_URL + 'packages/devextreme/testing' + '/helpers/themeMarker.css'; const $frame = createFrame(); themes.setDefaultTimeout(30000); diff --git a/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.integration.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.integration.tests.js index 953ea43bdf2c..ee10eb4db407 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.integration.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.integration.tests.js @@ -12,7 +12,9 @@ import seriesFamilyModule from 'viz/core/series_family'; import { setupSeriesFamily } from '../../helpers/chartMocks.js'; import pointerMock from '../../helpers/pointerMock.js'; -const seriesFamilyNativeConstructor = { ...seriesFamilyModule }.SeriesFamily; +const mutableRendererModule = rendererModule.default ?? rendererModule; +const mutableSeriesFamilyModule = seriesFamilyModule.default ?? seriesFamilyModule; +const seriesFamilyNativeConstructor = mutableSeriesFamilyModule.SeriesFamily; setupSeriesFamily(); QUnit.testStart(function() { const markup = @@ -2325,7 +2327,7 @@ QUnit.test('check horizontal alignment === center', function(assert) { QUnit.module('Auto hide point markers', $.extend({}, moduleSetup, { beforeEach: function() { moduleSetup.beforeEach.call(this); - seriesFamilyModule.SeriesFamily = seriesFamilyNativeConstructor; + mutableSeriesFamilyModule.SeriesFamily = seriesFamilyNativeConstructor; const dataSource = []; for(let i = 0; i < 500000; i += 250) { const y1 = Math.sin(i); @@ -3300,14 +3302,14 @@ QUnit.module('Option changing in onDrawn after zooming', { beforeEach: function() { this.legendShiftSpy = sinon.spy(legendModule.Legend.prototype, 'move'); this.titleShiftSpy = sinon.spy(titleModule.Title.prototype, 'move'); - sinon.stub(rendererModule, 'Renderer').callsFake(function() { + sinon.stub(mutableRendererModule, 'Renderer').callsFake(function() { return new Renderer(); }); }, afterEach: function() { legendModule.Legend.prototype.move.restore(); titleModule.Title.prototype.move.restore(); - rendererModule.Renderer.restore(); + mutableRendererModule.Renderer.restore(); } }); @@ -4901,7 +4903,7 @@ QUnit.test('Reset axes animation before adjusting position of vertical axes (fix QUnit.module('SeriesFamily', $.extend({}, moduleSetup, { beforeEach: function() { moduleSetup.beforeEach.call(this); - seriesFamilyModule.SeriesFamily = seriesFamilyNativeConstructor; + mutableSeriesFamilyModule.SeriesFamily = seriesFamilyNativeConstructor; } })); diff --git a/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.part7.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.part7.tests.js index 358b3781265e..510f626adffa 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.part7.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.charts/chart.part7.tests.js @@ -10,12 +10,14 @@ import { createChartInstance, LabelCtor, } from './chartParts/commons.js'; -import { ERROR_MESSAGES as dxErrors } from 'viz/core/errors_warnings'; +import errorsWarnings from 'viz/core/errors_warnings'; import seriesModule from 'viz/series/base_series'; import dataValidatorModule from 'viz/components/data_validator'; import { MockSeries, categories, seriesMockData, MockTranslator } from '../../helpers/chartMocks.js'; import graphicObjects from '__internal/common/m_charts'; +const dxErrors = errorsWarnings.ERROR_MESSAGES; + $('
').appendTo('#qunit-fixture'); (function seriesCreationTests() { diff --git a/packages/devextreme/testing/tests/DevExpress.viz.core/axisDrawing.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.core/axisDrawing.tests.js index 5a818a865b72..a1cc51c986d2 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.core/axisDrawing.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.core/axisDrawing.tests.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { ERROR_MESSAGES as dxErrors } from 'viz/core/errors_warnings'; +import errorsWarnings from 'viz/core/errors_warnings'; import translator2DModule from 'viz/translators/translator2d'; import { Range } from 'viz/translators/range'; import tickGeneratorModule from 'viz/axes/tick_generator'; @@ -9,6 +9,8 @@ import { stubClass, } from '../../helpers/vizMocks.js'; +const dxErrors = errorsWarnings.ERROR_MESSAGES; + const StubTranslator = stubClass(translator2DModule.Translator2D, { updateBusinessRange: function(range) { this.getBusinessRange.returns(range); diff --git a/packages/devextreme/testing/tests/DevExpress.viz.gauges/barGauge.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.gauges/barGauge.tests.js index 6f4bb5c49201..9b44a0a266e2 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.gauges/barGauge.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.gauges/barGauge.tests.js @@ -61,12 +61,21 @@ QUnit.begin(function() { step(1); that.animationStep && that.animationStep(1); complete(); - that.animationComplete && that.animationComplete(); + // Drop the hook before calling it — overlapping animate() + // chains (or a second completion after done()) must not + // re-enter assert.async() under native ESM scheduling. + const onGroupComplete = that.animationComplete; + that.animationComplete = null; + onGroupComplete && onGroupComplete(); test.renderer.animationCompleted && test.renderer.animationCompleted(); } } if(arguments[1] && typeof arguments[1].step === 'function') { + // Real renderer replaces the in-flight animation; without this, + // a second animate() leaves an orphan setTimeout chain and + // animationComplete / assert.async() fire twice. + this.stopAnimation(); that = this; step = arguments[1].step; complete = arguments[1].complete || noop; @@ -77,6 +86,7 @@ QUnit.begin(function() { }; group.stopAnimation = function() { clearTimeout(this.__animation); + this.__animation = null; return this; }; return group; diff --git a/packages/devextreme/testing/tests/Memory Leaks/vizWidgets.tests.js b/packages/devextreme/testing/tests/Memory Leaks/vizWidgets.tests.js index 4799c4cef71f..56edcb6d686e 100644 --- a/packages/devextreme/testing/tests/Memory Leaks/vizWidgets.tests.js +++ b/packages/devextreme/testing/tests/Memory Leaks/vizWidgets.tests.js @@ -1,4 +1,4 @@ -window.DevExpress = { viz: { map: { sources: {} } } }; +window.DevExpress = window.DevExpress || { viz: { map: { sources: {} } } }; import $ from 'core/renderer'; import { getWidth, getHeight, setWidth, setHeight } from 'core/utils/size'; @@ -18,6 +18,8 @@ import 'viz/tree_map'; import '/packages/devextreme/artifacts/js/vectormap-data/world.js'; import '/packages/devextreme/artifacts/js/vectormap-data/usa.js'; +const DevExpress = window.DevExpress; + const chartTestsSignature = { getInitOptions() { return { diff --git a/packages/nx-infra-plugin/AGENTS.md b/packages/nx-infra-plugin/AGENTS.md index a54a48137d65..c0f41b0ac880 100644 --- a/packages/nx-infra-plugin/AGENTS.md +++ b/packages/nx-infra-plugin/AGENTS.md @@ -65,3 +65,24 @@ Each behavior is owned by exactly ONE executor's canonical tests; consumers must 3. Preserve exact functional parity. Verify with the executor's e2e spec before and after. 4. Update consumer imports in one batch. 5. Run the full validation pipeline. + +## Former gulp tasks (gulp fully removed) + +`gulpfile.js`, `build/gulp/`, and all gulp dependencies have been deleted outright — devextreme no longer depends on gulp or ships a gulp CLI. The Nx-consumed build assets that used to live under `build/gulp/` (`transpile-config.js`, `modules_metadata.json`, the `*.jst` templates) were relocated to purpose-named folders directly under `build/` (`build/transpile-config.js`, `build/modules_metadata.json`, `build/localization-templates/`, `build/vectormap-templates/`). The table below is a historical reference mapping each removed gulp task to its Nx replacement: + +| Former gulp task | Nx target | Notes | +| --------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `clean` | `clean:artifacts` | Uses `devextreme-nx-infra-plugin:clean` with `excludePatterns` to preserve `artifacts/css`, `artifacts/npm/devextreme/package.json`, and `artifacts/npm/devextreme-dist`. Run directly via `pnpm nx clean:artifacts devextreme`. | +| `bundler-config-watch` | `build:devextreme-bundler-config:watch` | Uses `devextreme-nx-infra-plugin:concatenate-files` in `watch` mode (chokidar over the `build/bundle-templates/modules/parts` sources) with `additionalPasses` so a change rebuilds both `dx.custom.js` and the derived `dx.custom.config.js`, matching the old gulp `bundler-config` chain. `cache: false`. | +| `bundler-config` (non-watch) | `build:devextreme-bundler-config` | Run via `pnpm nx build:devextreme-bundler-config devextreme` (add `-c prod` for parity with the old uglified variant). | +| `generate-community-locales` | `build:community-localization` | Uses `devextreme-nx-infra-plugin:generate-community-locales` to normalize `js/localization/messages/*.json` against `en.json` in place (fills translations, English fallback for missing/`TODO` values, escapes quotes, inherits en's key order/formatting). Target is `cache: false` with no `outputs` (input dir == output dir — a source-normalization task, not a cached build artifact). Run via `pnpm nx build:community-localization devextreme`. | +| `test-env` | `test-env` | Uses `nx:run-commands` to wrap the existing `node ./testing/launch` script (compiles the QUnit runner, starts the test server on port 20060, opens the browser). `cache: false`, no outputs (long-running server). `cwd` is `{projectRoot}`. | +| `transpile-watch` | `build:transpile:watch` | `nx:run-commands` (parallel, `cache: false`) fanning out to the incremental watch targets `build:ts:internal:watch` (TypeScript watch program emitting `dist_ts`), `build:cjs:watch` (+ `-c production`), and `build:cjs:internal:watch` (+ `-c production`). Together these keep `artifacts/transpiled` and `artifacts/transpiled-renovation-npm` fresh, matching the old gulp JS + TS watch pipes. Watch capability lives in the `babel-transform` (per-file, chokidar + debounce, `watch` option) and `build-typescript` (`ts.createWatchProgram`, `watch` option) executors via the shared `src/utils/watch.ts` helper. `babel-transform` watch is intentionally file-level incremental and does **not** do an initial full transform — it relies on a preceding build having already populated its source directory (mirroring gulp-watch, which fed babel from a single in-memory TS-compiler stream with no such gap). Because `build:transpile`'s last step (`clean:dist-ts`) deletes `artifacts/dist_ts` — the exact directory `build:cjs:internal:watch` reads from and that runs right before `dev-watch` starts — `build:transpile:watch` itself (not the leaf `build:cjs:internal:watch` target, to avoid two parallel invocations each re-running the compile) declares `dependsOn: ["build:ts:internal"]`, so a real (or Nx-cache-restored) TS compile always repopulates `dist_ts/__internal` once, up front, before any of the fanned-out watch processes start; without it, whichever of the parallel TS-watch/babel-watch processes started first would decide — nondeterministically — whether the initial compile burst is picked up. | +| `transpile-tests` | `transpile:tests` | Uses `devextreme-nx-infra-plugin:babel-transform` with the flat (keyless) `./testing/tests.babelrc.json` config to transpile `testing/**/*.js` in place. `dependsOn: ["build:devextreme-bundler-config"]` reproduces the old gulp `series('bundler-config', …)` prerequisite. `cache: false` (in-place source transform, not a cached artifact). Run via `pnpm nx transpile:tests devextreme`. | +| `transpile-systemjs` | _(removed)_ | QUnit uses native ESM + import maps only. `build:systemjs` and `testing/systemjs-builder.js` are gone. ESM for QUnit is `artifacts/transpiled-esm-npm` from `build:transpile -c ci` (via `build:dev` / CI) or the dedicated `build:qunit-esm` target. | +| `js-bundles-watch` | `bundle:watch` | Webpack watch via `devextreme-nx-infra-plugin:bundle` with `watch: true`. Does not run `compress:bundles`. | +| `js-bundles-prod` | `bundle:prod` | Use `pnpm nx bundle:prod devextreme` (add `-c production` for uglify/dist parity). | +| `js-bundles-debug` | `bundle:debug` | Use `pnpm nx bundle:debug devextreme` (add `-c production` for uglify/dist parity). | +| `dev-watch` | `dev-watch` | `nx:run-commands` (parallel, `cache: false`, `cwd: {projectRoot}`) fanning out to the four already-migrated watch targets — `build:transpile:watch`, `build:devextreme-bundler-config:watch`, `bundle:watch`, `test-env` — matching the old `gulp.parallel('transpile-watch', 'bundler-config-watch', 'js-bundles-watch', 'test-env')`. | +| `dev` | `dev` | `nx:run-commands` with `dependsOn: ["build:dev"]`; its own command then runs `pnpm nx dev-watch devextreme`, reproducing the old `gulp.series('default-dev', 'dev-watch')` build-then-watch sequencing. Reuses `build:dev` as the initial-build step rather than replicating gulp's lighter `default-dev` variant (which skipped `clean` and the one-shot `js-bundles-debug` build) — an accepted small startup-cost tradeoff. | +| `default` / `main-batch` / `misc-batch` | `build`, `build-dist`, `build:dev` | Gulp orchestration (`gulp.series` / `gulp-multi-process`) is gone. Native Nx `build` covers the non-uglify default batch; `build -c production` (+ `build:npm -c production`) matches the old `gulp default --uglify`; `build -c production-internal` matches uglify + `BUILD_INTERNAL_PACKAGE`; `build -c testing` matches `BUILD_TEST_INTERNAL_PACKAGE`. `build-dist` is a thin wrapper (`pnpm nx run devextreme:build -c production`, `-c internal` → `production-internal`). `build:dev` is the former `DEVEXTREME_TEST_CI` path: clean → localization → `build:transpile -c ci` → parallel `bundle:debug,build:vectormap,copy:vendor` (skips prod bundles, aspnet, declarations, npm, license checks). npm scripts `build:dev` / `build-dist` / `clean` / `transpile-tests` call Nx directly. | diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9582741b143d..ea330bec5fca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -362,7 +362,7 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: ^22.0.9 - version: 22.1.2(258b0df82f598bf0cbd063448ecf32b5) + version: 22.1.2(9ab32ed578a68caf92174a202d695664) '@angular/animations': specifier: ^22.0.8 version: 22.1.0(@angular/core@22.1.0(@angular/compiler@22.1.0)(rxjs@7.8.2)(zone.js@0.16.2)) @@ -1584,21 +1584,6 @@ importers: sinon: specifier: 18.0.1 version: 18.0.1 - systemjs: - specifier: 0.19.41 - version: 0.19.41 - systemjs-plugin-babel: - specifier: 0.0.25 - version: 0.0.25 - systemjs-plugin-css: - specifier: 0.1.37 - version: 0.1.37 - systemjs-plugin-json: - specifier: 0.3.0 - version: 0.3.0 - systemjs-plugin-text: - specifier: 0.0.11 - version: 0.0.11 terser-webpack-plugin: specifier: 5.3.17 version: 5.3.17(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.28.1)(webpack@5.105.4(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.28.1)) @@ -16243,18 +16228,9 @@ packages: systemjs-builder@0.16.15: resolution: {integrity: sha512-C18G//KWWwQpstAVBUDt0YbbqvSFVVtr0MFqtf2zB4U/cePOA00Btcja++mzlFLMnepVpDv0GdtfE/6A8lrxeA==} - systemjs-plugin-babel@0.0.25: - resolution: {integrity: sha512-RMKSizWWlw4+IpDB385ugxn7Owd9W+HEtjYDQ6yO1FpsnER/vk6FbXRweUF+mvRi6EHgk8vDdUdtui7ReDwX3w==} - - systemjs-plugin-css@0.1.37: - resolution: {integrity: sha512-wCGG62zYXuOlNji5FlBjeMFAnLeAO/HQmFg+8UBX/mlHoAKLHlGFYRstlhGKibRU2oxk/BH9DaihOuhhNLi7Kg==} - systemjs-plugin-json@0.3.0: resolution: {integrity: sha512-GPHZgc6bGIDIQsoNAkhthddApy4ErFhy30rMBrEepkoDidhs0JeSk821htUOSrtqJjnUPBf2gge325B5GfsW0w==} - systemjs-plugin-text@0.0.11: - resolution: {integrity: sha512-buWE27P6iM3WZYXcsiy6+fiulQ/x+Puux4ni5ejTlcUgqUg3/sUvoAUZ4GGPACC0acjxmnaCt3kHb0+uNs1ekw==} - systemjs@0.19.41: resolution: {integrity: sha512-8E9CmZ01dIr52po2LNhc3QuKyeSTyvQfshHMi3lekSbEOdR9OAUOX2X+wPKunZX3CpudM6w3r8eTCjGQrK79Wg==} @@ -17944,13 +17920,13 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-angular@22.1.2(258b0df82f598bf0cbd063448ecf32b5)': + '@angular-devkit/build-angular@22.1.2(9ab32ed578a68caf92174a202d695664)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2201.2(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2201.2(chokidar@5.0.0)(webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.109.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.28.1)(postcss@8.5.23)))(webpack@5.109.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.28.1)(postcss@8.5.23)) '@angular-devkit/core': 22.1.2(chokidar@5.0.0) - '@angular/build': 22.1.2(f076a2752836734f19dd852b068a7651) + '@angular/build': 22.1.2(9f8834ba6c8fdff652d4ac7537fafa50) '@angular/compiler-cli': 22.1.0(@angular/compiler@22.1.0)(typescript@6.0.3) '@babel/core': 8.0.1 '@babel/generator': 8.0.0 @@ -17970,7 +17946,7 @@ snapshots: copy-webpack-plugin: 14.0.0(webpack@5.109.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.28.1)(postcss@8.5.23)) css-loader: 7.1.4(webpack@5.109.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(esbuild@0.28.1)(postcss@8.5.23)) esbuild-wasm: 0.28.1 - http-proxy-middleware: 4.2.0 + http-proxy-middleware: 4.2.0(supports-color@7.2.0) istanbul-lib-instrument: 6.0.3 jsonc-parser: 3.3.1 karma-source-map-support: 1.4.0 @@ -18287,7 +18263,7 @@ snapshots: - tsx - yaml - '@angular/build@22.1.2(f076a2752836734f19dd852b068a7651)': + '@angular/build@22.1.2(9f8834ba6c8fdff652d4ac7537fafa50)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2201.2(chokidar@5.0.0) @@ -18301,7 +18277,7 @@ snapshots: beasties: 0.4.3 browserslist: 4.28.7 esbuild: 0.28.1 - https-proxy-agent: 9.1.0 + https-proxy-agent: 9.1.0(supports-color@7.2.0) jsonc-parser: 3.3.1 listr2: 10.2.2 magic-string: 1.0.0 @@ -31174,7 +31150,7 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy-middleware@4.2.0: + http-proxy-middleware@4.2.0(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) httpxy: 0.5.5 @@ -31229,7 +31205,7 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@9.1.0: + https-proxy-agent@9.1.0(supports-color@7.2.0): dependencies: agent-base: 9.0.0 debug: 4.4.3(supports-color@7.2.0) @@ -34730,7 +34706,7 @@ snapshots: postcss: 8.5.23 rollup-plugin-dts: 6.4.1(rollup@4.59.0)(typescript@5.8.3) rxjs: 7.8.2 - sass: 1.99.0 + sass: 1.101.0 tinyglobby: 0.2.17 tslib: 2.8.1 typescript: 5.8.3 @@ -37795,14 +37771,8 @@ snapshots: transitivePeerDependencies: - supports-color - systemjs-plugin-babel@0.0.25: {} - - systemjs-plugin-css@0.1.37: {} - systemjs-plugin-json@0.3.0: {} - systemjs-plugin-text@0.0.11: {} - systemjs@0.19.41: dependencies: when: 3.7.8