diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bf621fa4a7b3..fc4f51710653 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -147,6 +147,10 @@ pnpm run all:build # full production build pnpm nx build:dev devextreme # single package, dev mode pnpm nx build devextreme -c=testing # CI testing configuration pnpm nx build:transpile devextreme # transpile only (babel-transform / build-typescript) +pnpm nx build:transpile:watch devextreme # transpile in watch mode (incremental JS + TS rebuild) +pnpm nx transpile:tests devextreme # transpile testing/**/*.js in place (replaces gulp transpile-tests) +pnpm nx dev devextreme # build:dev, then start all dev watches (replaces gulp dev) +pnpm nx dev-watch devextreme # start all dev watches in parallel, skipping the initial build pnpm nx bundle:debug devextreme # debug bundle (Webpack via nx-infra-plugin) pnpm nx bundle:prod devextreme # production bundle pnpm nx build:localization devextreme # localization files only @@ -197,7 +201,7 @@ For the full executor catalogue, conventions, and refactoring guidance, see @pac ## Build Pipeline -clean (`devextreme-nx-infra-plugin:clean` preserving CSS and npm metadata) → localization → component generation (Renovation) → transpile (`babel-transform` for JS, `build-typescript` for TS) → bundle (Webpack via `devextreme-nx-infra-plugin:bundle`, debug + prod targets) → TypeScript declarations → SCSS compile (`devextreme-scss`) → npm package preparation. Task orchestration goes through Nx; cross-package builds (`all:build-dev`, `all:build`) live in the `workflows` package. The `gulpfile.js` clean task is a thin delegate to `pnpm nx clean:artifacts devextreme`. Pre-commit hook runs `lint-staged` (stylelint + eslint --quiet). +clean (`devextreme-nx-infra-plugin:clean` preserving CSS and npm metadata) → localization → component generation (Renovation) → transpile (`babel-transform` for JS, `build-typescript` for TS) → bundle (Webpack via `devextreme-nx-infra-plugin:bundle`, debug + prod targets) → TypeScript declarations → SCSS compile (`devextreme-scss`) → npm package preparation. Task orchestration goes through Nx; cross-package builds (`all:build-dev`, `all:build`) live in the `workflows` package. The `gulpfile.js` clean task is a thin delegate to `pnpm nx clean:artifacts devextreme`. Pre-commit hook runs `lint-staged` (stylelint + eslint --quiet). The developer watch workflow (`pnpm run dev` / `dev:watch`) is now native composite Nx targets (`dev`, `dev-watch`) rather than gulp orchestrators — see the "Migrated gulp tasks" table in @packages/nx-infra-plugin/AGENTS.md. ## Conventions diff --git a/packages/devextreme/build/gulp/bundler-config.js b/packages/devextreme/build/gulp/bundler-config.js index 2f95b3e1b0eb..f151e3cabfc6 100644 --- a/packages/devextreme/build/gulp/bundler-config.js +++ b/packages/devextreme/build/gulp/bundler-config.js @@ -6,5 +6,3 @@ const shell = require('gulp-shell'); gulp.task('bundler-config', shell.task( 'pnpm nx build:devextreme-bundler-config devextreme && pnpm nx build:devextreme-bundler-config devextreme -c prod' )); - -gulp.task('bundler-config-watch', shell.task('pnpm nx build:devextreme-bundler-config:watch devextreme')); diff --git a/packages/devextreme/build/gulp/transpile-config.js b/packages/devextreme/build/gulp/transpile-config.js index 9bee0fbd6141..ebf153007cc7 100644 --- a/packages/devextreme/build/gulp/transpile-config.js +++ b/packages/devextreme/build/gulp/transpile-config.js @@ -3,7 +3,6 @@ const common = { plugins: [ ['babel-plugin-inferno', { 'imports': true }], - ['@babel/plugin-transform-object-rest-spread', { loose: true }], ], ignore: ['**/*.json'], }; diff --git a/packages/devextreme/build/gulp/transpile.js b/packages/devextreme/build/gulp/transpile.js index 67a75ca37d59..b90727ca2382 100644 --- a/packages/devextreme/build/gulp/transpile.js +++ b/packages/devextreme/build/gulp/transpile.js @@ -1,84 +1,6 @@ 'use strict'; -const babel = require('gulp-babel'); const gulp = require('gulp'); -const notify = require('gulp-notify'); -const path = require('path'); -const plumber = require('gulp-plumber'); -const replace = require('gulp-replace'); -const watch = require('gulp-watch'); +const shell = require('gulp-shell'); -const ctx = require('./context.js'); -const testsConfig = require('../../testing/tests.babelrc.json'); -const transpileConfig = require('./transpile-config'); -const createTsCompiler = require('./typescript/compiler'); - -const REMOVE_DEBUG_REGEXP = /\/{2,}\s{0,}#DEBUG[\s\S]*?\/{2,}\s{0,}#ENDDEBUG/g; - -const src = [ - 'js/**/*.*', - '!js/**/*.d.ts', - '!js/**/*.{tsx,ts}', - '!js/__internal/**/*.*', -]; - -const TS_OUTPUT_BASE_DIR = 'artifacts/dist_ts'; -const TS_COMPILER_CONFIG = { - baseAbsPath: path.resolve(__dirname, '../..'), - relativePath: { - tsconfig: 'js/__internal/tsconfig.json', - alias: 'js', - dist: TS_OUTPUT_BASE_DIR, - }, - tsBaseDirName: '__internal', - messages: { - createDirErr: 'Cannot create directory', - createFileErr: 'Cannot create file', - compilationFailed: 'TS Compilation failed', - }, -}; - -const watchJsTask = () => { - const watchTask = watch(src) - .on('ready', () => console.log('transpile JS is watching for changes...')) - .pipe(plumber({ - errorHandler: notify - .onError('Error: <%= error.message %>') - .bind() // bind call is necessary to prevent firing 'end' event in notify.onError implementation - })); - watchTask - .pipe(babel(transpileConfig.cjs)) - .pipe(gulp.dest(ctx.TRANSPILED_PATH)); - watchTask - .pipe(replace(REMOVE_DEBUG_REGEXP, '')) - .pipe(babel(transpileConfig.cjs)) - .pipe(gulp.dest(ctx.TRANSPILED_PROD_RENOVATION_PATH)); - return watchTask; -}; -watchJsTask.displayName = 'transpile JS watch'; - -const watchTsTask = async() => { - const compiler = await createTsCompiler(TS_COMPILER_CONFIG); - const tsWatch = compiler.watchTs(); - - tsWatch - .pipe(plumber({ - errorHandler: notify - .onError('Error: <%= error.message %>') - .bind() // bind call is necessary to prevent firing 'end' event in notify.onError implementation - })) - .pipe(babel(transpileConfig.tsCjs)) - .pipe(gulp.dest(ctx.TRANSPILED_PATH)) - .pipe(replace(REMOVE_DEBUG_REGEXP, '')) - .pipe(gulp.dest(ctx.TRANSPILED_PROD_RENOVATION_PATH)); -}; -watchTsTask.displayName = 'transpile TS watch'; - -gulp.task('transpile-watch', gulp.parallel(watchJsTask, watchTsTask)); - -gulp.task('transpile-tests', gulp.series('bundler-config', () => - gulp - .src(['testing/**/*.js']) - .pipe(babel(testsConfig)) - .pipe(gulp.dest('testing')) -)); +gulp.task('transpile-tests', shell.task('pnpm nx transpile:tests devextreme')); diff --git a/packages/devextreme/gulpfile.js b/packages/devextreme/gulpfile.js index 61d0b09ba9d3..99f1884dd65e 100644 --- a/packages/devextreme/gulpfile.js +++ b/packages/devextreme/gulpfile.js @@ -26,12 +26,6 @@ gulp.task('js-bundles-debug', shell.task( : 'pnpm nx run devextreme:bundle:debug' )); -gulp.task('js-bundles-watch', shell.task( - context.uglify - ? 'pnpm nx run devextreme:bundle:watch -c production' - : 'pnpm nx run devextreme:bundle:watch' -)); - function getTranspileConfig() { if(env.TEST_CI) { return 'ci'; @@ -120,9 +114,9 @@ function createMiscBatch() { return gulp.parallel(tasks); } -function createMainBatch(dev) { +function createMainBatch() { const tasks = []; - if(!dev && !env.BUILD_TESTCAFE) { + if(!env.BUILD_TESTCAFE) { tasks.push('js-bundles-debug'); } if(!env.TEST_CI || env.BUILD_TESTCAFE) { @@ -132,8 +126,8 @@ function createMainBatch(dev) { return (callback) => multiProcess(tasks, callback, true); } -function createDefaultBatch(dev) { - const tasks = dev ? [] : ['clean']; +function createDefaultBatch() { + const tasks = ['clean']; tasks.push('localization'); tasks.push('transpile'); @@ -141,8 +135,8 @@ function createDefaultBatch(dev) { tasks.push('state-manager-optimize'); } - tasks.push(dev && !env.BUILD_TESTCAFE ? 'main-batch-dev' : 'main-batch'); - if(!env.TEST_CI && !dev && !env.BUILD_TESTCAFE) { + tasks.push('main-batch'); + if(!env.TEST_CI && !env.BUILD_TESTCAFE) { tasks.push('npm'); tasks.push('check-license-notices'); } @@ -150,22 +144,6 @@ function createDefaultBatch(dev) { } gulp.task('misc-batch', createMiscBatch()); -gulp.task('main-batch', createMainBatch(false)); -gulp.task('main-batch-dev', createMainBatch(true)); +gulp.task('main-batch', createMainBatch()); gulp.task('default', createDefaultBatch()); -gulp.task('default-dev', createDefaultBatch(true)); - -gulp.task('test-env', shell.task('pnpm nx test-env devextreme')); - -gulp.task('dev-watch', gulp.parallel( - 'transpile-watch', - 'bundler-config-watch', - 'js-bundles-watch', - 'test-env' -)); - -gulp.task('dev', gulp.series( - 'default-dev', - 'dev-watch' -)); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index e76325929330..831bd363a0e2 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -73,6 +73,8 @@ "@babel/eslint-parser": "catalog:", "@babel/plugin-proposal-decorators": "7.29.7", "@babel/plugin-transform-modules-commonjs": "7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "7.29.7", + "@babel/plugin-transform-optional-chaining": "7.29.7", "@babel/plugin-transform-runtime": "7.29.7", "@babel/plugin-transform-typescript": "7.29.7", "@babel/preset-env": "7.29.7", @@ -191,8 +193,8 @@ "build:modular": "cd ./playground/modular && webpack", "build:modular:watch": "cd ./playground/modular && webpack --watch", "build:community-localization": "pnpm nx build:community-localization devextreme", - "dev": "cross-env DEVEXTREME_TEST_CI=true gulp dev", - "dev:watch": "cross-env DEVEXTREME_TEST_CI=true gulp dev-watch", + "dev": "cross-env DEVEXTREME_TEST_CI=true nx run devextreme:dev", + "dev:watch": "cross-env DEVEXTREME_TEST_CI=true nx run devextreme:dev-watch", "dev:playground": "vite", "transpile-tests": "gulp transpile-tests", "update-ts-reexports": "dx-tools generate-reexports --sources ./js --exclude \"((dialog|export|list_light|notify|overlay|palette|set_template_engine|splitter_control|themes|themes_callback|track_bar|utils|validation_engine|validation_message)[.d.ts])\" --compiler-options \"{ \\\"typeRoots\\\": [] }\"", diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index 23364f0e6bb3..2e6aa846b001 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -237,6 +237,18 @@ "{projectRoot}/artifacts/dist_ts" ] }, + "build:ts:internal:watch": { + "executor": "devextreme-nx-infra-plugin:build-typescript", + "cache": false, + "options": { + "srcPattern": "./js/__internal/**/*.{ts,tsx}", + "tsconfig": "./js/__internal/tsconfig.json", + "outDir": "./artifacts/dist_ts", + "resolvePaths": true, + "resolvePathsBaseDir": "./js", + "watch": true + } + }, "build:cjs": { "executor": "devextreme-nx-infra-plugin:babel-transform", "options": { @@ -268,6 +280,31 @@ "{projectRoot}/artifacts/transpiled-renovation-npm" ] }, + "build:cjs:watch": { + "executor": "devextreme-nx-infra-plugin:babel-transform", + "cache": false, + "options": { + "babelConfigPath": "./build/gulp/transpile-config.js", + "configKey": "cjs", + "sourcePattern": "./js/**/*.{js,jsx}", + "excludePatterns": [ + "./js/**/*.d.ts", + "./js/__internal/**/*" + ], + "outDir": "./artifacts/transpiled", + "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": { + "production": { + "outDir": "./artifacts/transpiled-renovation-npm", + "removeDebug": true + } + } + }, "build:npm:esm": { "executor": "devextreme-nx-infra-plugin:babel-transform", "options": { @@ -341,6 +378,26 @@ "{projectRoot}/artifacts/transpiled-renovation-npm/__internal" ] }, + "build:cjs:internal:watch": { + "executor": "devextreme-nx-infra-plugin:babel-transform", + "cache": false, + "options": { + "babelConfigPath": "./build/gulp/transpile-config.js", + "configKey": "tsCjs", + "sourcePattern": "./artifacts/dist_ts/__internal/**/*.{js,jsx}", + "outDir": "./artifacts/transpiled/__internal", + "renameExtensions": { + ".jsx": ".js" + }, + "watch": true + }, + "configurations": { + "production": { + "outDir": "./artifacts/transpiled-renovation-npm/__internal", + "removeDebug": true + } + } + }, "build:npm:esm:internal": { "executor": "devextreme-nx-infra-plugin:babel-transform", "options": { @@ -507,6 +564,34 @@ } } }, + "build:transpile:watch": { + "executor": "nx:run-commands", + "cache": false, + "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" + ], + "cwd": "{projectRoot}", + "parallel": true + }, + "dependsOn": ["build:ts:internal"] + }, + "transpile:tests": { + "executor": "devextreme-nx-infra-plugin:babel-transform", + "cache": false, + "options": { + "babelConfigPath": "./testing/tests.babelrc.json", + "sourcePattern": "./testing/**/*.js", + "outDir": "./testing" + }, + "dependsOn": [ + "build:devextreme-bundler-config" + ] + }, "state-manager:optimize": { "executor": "devextreme-nx-infra-plugin:state-manager-optimize", "options": { @@ -1691,13 +1776,37 @@ "cache": true }, "dev": { - "executor": "nx:run-script", - "options": { - "script": "dev" - }, + "executor": "nx:run-commands", + "cache": false, "dependsOn": [ - "^build" - ] + "build:dev" + ], + "options": { + "cwd": "{projectRoot}", + "command": "pnpm nx dev-watch devextreme" + } + }, + "dev-watch": { + "executor": "nx:run-commands", + "cache": false, + "options": { + "cwd": "{projectRoot}", + "parallel": true, + "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" + ] + } + }, + "test-env": { + "executor": "nx:run-commands", + "cache": false, + "options": { + "command": "node ./testing/launch", + "cwd": "{projectRoot}" + } }, "test-env": { "executor": "nx:run-commands", diff --git a/packages/devextreme/testing/tests.babelrc.json b/packages/devextreme/testing/tests.babelrc.json index 6ac0a3547bd5..bfeb03396f13 100644 --- a/packages/devextreme/testing/tests.babelrc.json +++ b/packages/devextreme/testing/tests.babelrc.json @@ -1,7 +1,7 @@ { "presets": [["@babel/preset-env", { "modules": false }]], "plugins": [ - ["transform-es2015-modules-commonjs", { "strict": false, "allowTopLevelThis": true }, "add-module-exports"], + ["@babel/plugin-transform-modules-commonjs", { "strict": false, "allowTopLevelThis": true }, "add-module-exports"], "@babel/plugin-transform-nullish-coalescing-operator", "@babel/plugin-transform-optional-chaining" ], "ignore": ["**/*.json"] diff --git a/packages/nx-infra-plugin/AGENTS.md b/packages/nx-infra-plugin/AGENTS.md index 9982e1dc517f..0a70d0d439d2 100644 --- a/packages/nx-infra-plugin/AGENTS.md +++ b/packages/nx-infra-plugin/AGENTS.md @@ -35,6 +35,7 @@ Each cross-executor concern (license banner, glob-aware copy, file concatenation - Throw inside `resolve` and `run`; the wrapper converts to `{ success: false }`. - Keep the default export shape `PromiseExecutor`. Tests import `from './executor'`. - Use `logger.verbose(...)` from `@nx/devkit` for diagnostic output in executors. Never use `console.log` or `logger.info` for routine progress messages — they pollute every run; `logger.verbose` surfaces only when callers pass `--verbose`. +- For long-running `watch` executors, reuse `src/utils/watch.ts` (`loadChokidar` + `watchWithChokidar`): it owns chokidar resolution from the project root, the debounced non-reentrant rebuild loop, and SIGINT/SIGTERM shutdown. Do not reimplement a watch loop. TypeScript watch is the exception — it uses `ts.createWatchProgram` directly (own file watching) rather than chokidar. ## Constraints @@ -69,16 +70,20 @@ Each behavior is owned by exactly ONE executor's canonical tests; consumers must Gulp tasks that have been migrated to Nx executor targets (the gulp task is now a thin `shell.task` delegate): -| 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`. The gulp task delegates via `shell.task('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 gulp `bundler-config` chain. `cache: false`. The non-watch gulp `bundler-config` now also delegates to the existing `build:devextreme-bundler-config` target (default + `-c prod`). File deletion is deferred until `dev-watch` is migrated. | -| `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). The gulp task delegates via `shell.task('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}`. The gulp task delegates via `shell.task('pnpm nx test-env devextreme')`. Still a `dev-watch` member, so the gulp registration stays as a delegate; full removal is deferred until `dev-watch` is migrated. | -| `transpile-systemjs` | `build:systemjs` | Uses `nx:run-commands` (no custom executor — same precedent as `test-env`) to run `node testing/systemjs-builder.js --transpile=` for each mode (`modules`, `testing`, `css`, `js-vendors`) with `parallel: true` and `cwd: {projectRoot}`, mirroring the old `gulp.parallel`. The transform logic stays entirely in `testing/systemjs-builder.js` (untouched, guaranteeing byte parity); the devextreme-specific script path + mode list live in `project.json`, not in the plugin. `cache: true`, keeps the existing `inputs`/`outputs`/`dependsOn: [build:dev]`. Not a `dev-watch` member, so `build/gulp/systemjs.js`, its `gulpfile.js` require, and the `build:systemjs` npm script were deleted outright (no delegate). CI runs `pnpm exec nx build:systemjs` directly. | -| `js-bundles-watch` | `bundle:watch` | Webpack watch via `devextreme-nx-infra-plugin:bundle` with `watch: true`. Does not run `compress:bundles`. Gulp delegates via `shell.task('pnpm nx run devextreme:bundle:watch')`. | -| `js-bundles-prod` | `bundle:prod` | Already delegated via `shell.task('pnpm nx run devextreme:bundle:prod ...')`. | -| `js-bundles-debug` | `bundle:debug` | Already delegated via `shell.task('pnpm nx run devextreme:bundle:debug ...')`. | +| 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`. The gulp task delegates via `shell.task('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 gulp `bundler-config` chain. `cache: false`. The non-watch gulp `bundler-config` still delegates to the existing `build:devextreme-bundler-config` target (default + `-c prod`). The `bundler-config-watch` gulp task itself was deleted outright (no delegate) now that `dev-watch` is a native Nx target. | +| `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). The gulp task delegates via `shell.task('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}`. The `test-env` gulp task was deleted outright (no delegate) now that `dev-watch` is a native Nx target. | +| `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 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. The `transpile-watch` gulp task was deleted outright (no delegate) now that `dev-watch` is a native Nx target. | +| `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 gulp `series('bundler-config', …)` prerequisite. `cache: false` (in-place source transform, not a cached artifact). The gulp task delegates via `shell.task('pnpm nx transpile:tests devextreme')`. | +| `transpile-systemjs` | `build:systemjs` | Uses `nx:run-commands` (no custom executor — same precedent as `test-env`) to run `node testing/systemjs-builder.js --transpile=` for each mode (`modules`, `testing`, `css`, `js-vendors`) with `parallel: true` and `cwd: {projectRoot}`, mirroring the old `gulp.parallel`. The transform logic stays entirely in `testing/systemjs-builder.js` (untouched, guaranteeing byte parity); the devextreme-specific script path + mode list live in `project.json`, not in the plugin. `cache: true`, keeps the existing `inputs`/`outputs`/`dependsOn: [build:dev]`. Not a `dev-watch` member, so `build/gulp/systemjs.js`, its `gulpfile.js` require, and the `build:systemjs` npm script were deleted outright (no delegate). CI runs `pnpm exec nx build:systemjs` directly. | +| `js-bundles-watch` | `bundle:watch` | Webpack watch via `devextreme-nx-infra-plugin:bundle` with `watch: true`. Does not run `compress:bundles`. The `js-bundles-watch` gulp task was deleted outright (no delegate) now that `dev-watch` is a native Nx target. | +| `js-bundles-prod` | `bundle:prod` | Already delegated via `shell.task('pnpm nx run devextreme:bundle:prod ...')`. | +| `js-bundles-debug` | `bundle:debug` | Already delegated via `shell.task('pnpm nx run devextreme:bundle:debug ...')`. | +| `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')`. The `dev-watch` gulp task was deleted outright (no delegate). | +| `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 the existing `build:dev` target 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. The `dev`, `default-dev`, and `main-batch-dev` gulp tasks were deleted outright (no delegate); `createDefaultBatch`/`createMainBatch` in `gulpfile.js` dropped their now-dead `dev` parameter since `default` is their only remaining caller. | When migrating additional gulp tasks, follow the same pattern: diff --git a/packages/nx-infra-plugin/src/executors/babel-transform/babel-transform.impl.ts b/packages/nx-infra-plugin/src/executors/babel-transform/babel-transform.impl.ts index 588c25184816..8953370bf9e1 100644 --- a/packages/nx-infra-plugin/src/executors/babel-transform/babel-transform.impl.ts +++ b/packages/nx-infra-plugin/src/executors/babel-transform/babel-transform.impl.ts @@ -3,10 +3,12 @@ import * as fs from 'fs-extra'; import { stat } from 'fs/promises'; import * as babel from '@babel/core'; import { glob } from 'glob'; +import { minimatch } from 'minimatch'; import { logger } from '@nx/devkit'; import { createExecutor } from '../../utils/create-executor'; import { toPosixPath } from '../../utils/path-resolver'; import { copyFile } from '../../utils/file-operations'; +import { watchWithChokidar } from '../../utils/watch'; import { copyDirectory } from '../copy-files/copy-files.impl'; import { stripDebug } from '../compress/compress.impl'; import { BabelTransformAsset, BabelTransformExecutorSchema } from './schema'; @@ -17,7 +19,7 @@ const ERROR_ASSET_NOT_FOUND = (source: string) => `Asset source not found: ${sou function loadBabelConfig( projectRoot: string, configPath: string, - configKey: string, + configKey?: string, ): babel.TransformOptions { const fullConfigPath = path.join(projectRoot, configPath); @@ -27,6 +29,10 @@ function loadBabelConfig( const config = require(fullConfigPath); + if (!configKey) { + return config; + } + if (!config[configKey]) { const availableKeys = Object.keys(config).join(', '); throw new Error(`Config key '${configKey}' not found. Available: ${availableKeys}`); @@ -45,6 +51,17 @@ function applyExtensionRenames(filePath: string, renameExtensions: Record= 0 + ? cleanPattern.substring(0, globIndex).replace(/\/$/, '') + : cleanPattern.split('/')[0]; + return path.join(projectRoot, patternBase); +} + async function transformFile( filePath: string, projectRoot: string, @@ -69,13 +86,7 @@ async function transformFile( throw new Error(`Babel returned no code for ${filePath}`); } - const cleanPattern = sourcePattern.replace(/^\.\//, ''); - const globIndex = cleanPattern.search(/\*+/); - const patternBase = - globIndex > 0 - ? cleanPattern.substring(0, globIndex).replace(/\/$/, '') - : cleanPattern.split('/')[0]; - const sourceBase = path.join(projectRoot, patternBase); + const sourceBase = resolveSourceBase(projectRoot, sourcePattern); const relativePath = path.relative(sourceBase, filePath); const renamedRelativePath = applyExtensionRenames(relativePath, renameExtensions); @@ -116,6 +127,7 @@ interface ResolvedBabelTransform { globPattern: string; excludePatterns: string[]; resolvedAssets: ResolvedBabelTransformAsset[]; + watchDir: string; } function resolveAssets( @@ -130,6 +142,98 @@ function resolveAssets( })); } +function transformOne( + resolved: ResolvedBabelTransform, + options: BabelTransformExecutorSchema, + filePath: string, +): Promise { + return transformFile( + filePath, + resolved.projectRoot, + options.outDir, + options.sourcePattern, + resolved.babelConfig, + resolved.removeDebug, + resolved.renameExtensions, + ); +} + +async function copyResolvedAssets(resolved: ResolvedBabelTransform, outDir: string): Promise { + if (resolved.resolvedAssets.length === 0) { + return; + } + logger.verbose(`Copying ${resolved.resolvedAssets.length} asset entries to ${outDir}`); + for (const asset of resolved.resolvedAssets) { + await copyResolvedAsset(asset); + } +} + +async function runFullTransform( + resolved: ResolvedBabelTransform, + options: BabelTransformExecutorSchema, +): Promise { + const sourceFiles = await glob(resolved.globPattern, { + absolute: true, + ignore: resolved.excludePatterns, + }); + + if (sourceFiles.length === 0) { + logger.warn(ERROR_NO_FILES_MATCHED); + throw new Error(ERROR_NO_FILES_MATCHED); + } + + logger.verbose( + `Transforming ${sourceFiles.length} files with config '${options.configKey ?? 'default'}'...`, + ); + if (resolved.removeDebug) { + logger.verbose('Debug blocks will be removed (production mode)'); + } + + await Promise.all(sourceFiles.map((file) => transformOne(resolved, options, file))); + + logger.verbose(`Successfully transformed ${sourceFiles.length} files to ${options.outDir}`); + + await copyResolvedAssets(resolved, options.outDir); +} + +// Deletions are ignored on purpose — gulp-watch left stale output in place too. +const WATCHED_EVENTS = new Set(['add', 'change']); + +function isRelevantSource( + resolved: ResolvedBabelTransform, + event: string, + filePath: string, +): boolean { + if (!WATCHED_EVENTS.has(event)) { + return false; + } + const posixPath = toPosixPath(filePath); + if (!minimatch(posixPath, resolved.globPattern)) { + return false; + } + return !resolved.excludePatterns.some((pattern) => minimatch(posixPath, pattern)); +} + +async function runWatch( + resolved: ResolvedBabelTransform, + options: BabelTransformExecutorSchema, +): Promise { + // dist_ts may not exist yet if the TS watcher hasn't emitted anything — chokidar needs the dir to attach. + await fs.ensureDir(resolved.watchDir); + await copyResolvedAssets(resolved, options.outDir); + + await watchWithChokidar({ + projectRoot: resolved.projectRoot, + watchTargets: resolved.watchDir, + label: `babel-transform watch (${options.outDir})`, + eventFilter: (event, filePath) => isRelevantSource(resolved, event, filePath), + onRebuild: async (events) => { + const files = [...new Set(events.map((e) => e.filePath))]; + await Promise.all(files.map((file) => transformOne(resolved, options, file))); + }, + }); +} + export default createExecutor({ name: 'BabelTransform', resolve: (options, { projectRoot }) => { @@ -147,6 +251,7 @@ export default createExecutor { - const sourceFiles = await glob(resolved.globPattern, { - absolute: true, - ignore: resolved.excludePatterns, - }); - - if (sourceFiles.length === 0) { - logger.warn(ERROR_NO_FILES_MATCHED); - throw new Error(ERROR_NO_FILES_MATCHED); + if (options.watch) { + await runWatch(resolved, options); + return; } - logger.verbose( - `Transforming ${sourceFiles.length} files with config '${options.configKey}'...`, - ); - if (resolved.removeDebug) { - logger.verbose('Debug blocks will be removed (production mode)'); - } - - await Promise.all( - sourceFiles.map((file) => - transformFile( - file, - resolved.projectRoot, - options.outDir, - options.sourcePattern, - resolved.babelConfig, - resolved.removeDebug, - resolved.renameExtensions, - ), - ), - ); - - logger.verbose(`Successfully transformed ${sourceFiles.length} files to ${options.outDir}`); - - if (resolved.resolvedAssets.length > 0) { - logger.verbose( - `Copying ${resolved.resolvedAssets.length} asset entries to ${options.outDir}`, - ); - for (const asset of resolved.resolvedAssets) { - await copyResolvedAsset(asset); - } - } + await runFullTransform(resolved, options); }, }); diff --git a/packages/nx-infra-plugin/src/executors/babel-transform/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/babel-transform/executor.e2e.spec.ts index 36186b5a2de4..bcb605b1d4d0 100644 --- a/packages/nx-infra-plugin/src/executors/babel-transform/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/babel-transform/executor.e2e.spec.ts @@ -12,6 +12,46 @@ import { writeFileText, readFileText } from '../../utils'; const WORKSPACE_ROOT = findWorkspaceRoot(); +type WatchHandler = (event: string, file: string) => void; + +function getWatchHandler(): WatchHandler | undefined { + return (globalThis as unknown as { __babelWatchHandler?: WatchHandler }).__babelWatchHandler; +} + +// Mock chokidar so the shared watch util's projectRequire('chokidar') resolves; the mock +// captures the 'all' handler on globalThis so the test can drive a rebuild deterministically. +function writeChokidarMock(projectRoot: string): void { + const chokidarDir = path.join(projectRoot, 'node_modules', 'chokidar'); + fs.mkdirSync(chokidarDir, { recursive: true }); + fs.writeFileSync(path.join(chokidarDir, 'package.json'), JSON.stringify({ main: 'index.js' })); + fs.writeFileSync( + path.join(chokidarDir, 'index.js'), + [ + 'module.exports = {', + ' watch: function watch() {', + ' return {', + ' on: function on(event, handler) { globalThis.__babelWatchHandler = handler; return this; },', + ' close: function close() { return Promise.resolve(); },', + ' };', + ' },', + '};', + '', + ].join('\n'), + ); +} + +async function waitFor( + predicate: () => Promise | boolean, + timeoutMs = 3000, +): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (await predicate()) return; + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw new Error('waitFor timed out'); +} + const BABEL_CONFIG = ` 'use strict'; @@ -263,3 +303,113 @@ module.exports = { }, 30000); }); }); + +describe('BabelTransformExecutor flat config + watch', () => { + let tempDir: string; + let context = createMockContext(); + let projectDir: string; + let savedCwd: string; + + const FLAT_CONFIG = JSON.stringify({ comments: false }); + + beforeEach(async () => { + savedCwd = process.cwd(); + tempDir = createTempDir('nx-babel-watch-e2e-'); + context = createMockContext({ root: tempDir }); + projectDir = path.join(tempDir, 'packages', 'test-lib'); + fs.mkdirSync(projectDir, { recursive: true }); + await writeFileText( + path.join(projectDir, 'package.json'), + JSON.stringify({ name: 'test-lib' }), + ); + process.chdir(projectDir); + + // Flat (keyless) babel config, mirroring testing/tests.babelrc.json usage. + await writeFileText(path.join(projectDir, 'babel.flat.json'), FLAT_CONFIG); + + const jsDir = path.join(projectDir, 'js'); + fs.mkdirSync(jsDir, { recursive: true }); + await writeFileText( + path.join(jsDir, 'a.js'), + 'const a = 1; // comment-a\nmodule.exports = a;\n', + ); + await writeFileText( + path.join(jsDir, 'b.js'), + 'const b = 2; // comment-b\nmodule.exports = b;\n', + ); + }); + + afterEach(() => { + process.chdir(savedCwd); + cleanupTempDir(tempDir); + delete (globalThis as unknown as { __babelWatchHandler?: WatchHandler }).__babelWatchHandler; + }); + + it('uses the whole module as the babel config when configKey is omitted', async () => { + const options: BabelTransformExecutorSchema = { + babelConfigPath: './babel.flat.json', + sourcePattern: './js/**/*.js', + outDir: './out', + }; + + const result = await executor(options, context); + expect(result.success).toBe(true); + + const aContent = await readFileText(path.join(projectDir, 'out', 'a.js')); + expect(aContent).not.toContain('comment-a'); + }, 30000); + + it('watch mode re-transforms only the changed file', async () => { + writeChokidarMock(projectDir); + + const options: BabelTransformExecutorSchema = { + babelConfigPath: './babel.flat.json', + sourcePattern: './js/**/*.js', + outDir: './out', + watch: true, + }; + + const aOut = path.join(projectDir, 'out', 'a.js'); + const bOut = path.join(projectDir, 'out', 'b.js'); + + const originalOnce = process.once; + let stopWatch: (() => void) | undefined; + let run: Promise<{ success: boolean }> | undefined; + + (process as unknown as { once: typeof process.once }).once = (( + event: string, + handler: () => void, + ) => { + if (event === 'SIGINT' || event === 'SIGTERM') { + stopWatch = handler; + return process; + } + return originalOnce.call(process, event, handler as never); + }) as typeof process.once; + + try { + run = executor(options, context); + + await waitFor(() => typeof getWatchHandler() === 'function'); + + // Only a.js changes; b.js must stay untouched (file-level incremental). + getWatchHandler()?.('change', path.join(projectDir, 'js', 'a.js')); + + await waitFor(() => fs.existsSync(aOut)); + expect(await readFileText(aOut)).not.toContain('comment-a'); + expect(fs.existsSync(bOut)).toBe(false); + + stopWatch?.(); + stopWatch = undefined; + + expect((await run).success).toBe(true); + run = undefined; + } finally { + stopWatch?.(); + if (run) { + await run; + } + (process as unknown as { once: typeof process.once }).once = originalOnce; + } + }, 30000); +}); diff --git a/packages/nx-infra-plugin/src/executors/babel-transform/schema.json b/packages/nx-infra-plugin/src/executors/babel-transform/schema.json index 79f2256d12e7..76254ae3bc96 100644 --- a/packages/nx-infra-plugin/src/executors/babel-transform/schema.json +++ b/packages/nx-infra-plugin/src/executors/babel-transform/schema.json @@ -10,7 +10,7 @@ }, "configKey": { "type": "string", - "description": "Key in the config object to use (e.g., 'cjs', 'tsCjs', 'esm')" + "description": "Key in the config object to use (e.g., 'cjs', 'tsCjs', 'esm'). Omit to use the loaded module as the config directly (e.g., a flat .babelrc.json)." }, "sourcePattern": { "type": "string", @@ -59,11 +59,15 @@ "to" ] } + }, + "watch": { + "type": "boolean", + "default": false, + "description": "Watch the source pattern and re-transform files as they change (file-level incremental rebuild)." } }, "required": [ "babelConfigPath", - "configKey", "sourcePattern", "outDir" ] diff --git a/packages/nx-infra-plugin/src/executors/babel-transform/schema.ts b/packages/nx-infra-plugin/src/executors/babel-transform/schema.ts index c940ca84a383..00c63dced7c2 100644 --- a/packages/nx-infra-plugin/src/executors/babel-transform/schema.ts +++ b/packages/nx-infra-plugin/src/executors/babel-transform/schema.ts @@ -5,11 +5,12 @@ export interface BabelTransformAsset { export interface BabelTransformExecutorSchema { babelConfigPath: string; - configKey: string; + configKey?: string; sourcePattern: string; excludePatterns?: string[]; outDir: string; removeDebug?: boolean; renameExtensions?: Record; copyAssets?: BabelTransformAsset[]; + watch?: boolean; } diff --git a/packages/nx-infra-plugin/src/executors/build-typescript/build-typescript.impl.ts b/packages/nx-infra-plugin/src/executors/build-typescript/build-typescript.impl.ts index 8b9d31ba0ce3..4db1d4c087b9 100644 --- a/packages/nx-infra-plugin/src/executors/build-typescript/build-typescript.impl.ts +++ b/packages/nx-infra-plugin/src/executors/build-typescript/build-typescript.impl.ts @@ -239,6 +239,62 @@ function emitStandard(program: ts.Program): EmitProgramResult { return { success: true }; } +// Same tsc-alias resolution as the one-shot build, applied per incremental re-emit. +async function runWatch(config: ResolvedConfig): Promise { + const aliasTranspileFunc = + config.resolvePaths && config.resolvePathsBaseDir + ? await createAliasTranspileFunc(config.tsconfigPath, config.resolvePathsBaseDir) + : undefined; + + await ensureDir(config.outDir); + + const reportDiagnostic = (diagnostic: ts.Diagnostic): void => { + formatDiagnostics([diagnostic]).forEach((message) => logger.error(message)); + }; + const reportWatchStatus = (diagnostic: ts.Diagnostic): void => { + logger.verbose(ts.flattenDiagnosticMessageText(diagnostic.messageText, NEWLINE_CHAR)); + }; + + const compilerOptions: ts.CompilerOptions = { + outDir: config.outDir, + ...(config.resolvePaths ? {} : { paths: {} }), + }; + + const host = ts.createWatchCompilerHost( + config.tsconfigPath, + compilerOptions, + ts.sys, + ts.createSemanticDiagnosticsBuilderProgram, + reportDiagnostic, + reportWatchStatus, + ); + + const emitWriteFile: ts.WriteFileCallback = (filePath, data, writeByteOrderMark) => { + let content = data; + if (aliasTranspileFunc && config.resolvePathsBaseDir) { + const normalizedFilePath = replacePathPrefix( + filePath, + config.outDir, + config.resolvePathsBaseDir, + ); + content = aliasTranspileFunc(normalizedFilePath, data); + } + ts.sys.writeFile(filePath, content, writeByteOrderMark); + }; + (host as { writeFile?: ts.WriteFileCallback }).writeFile = emitWriteFile; + const watchProgram = ts.createWatchProgram(host); + logger.verbose(`TypeScript watch is emitting to ${config.outDir}...`); + + await new Promise((resolve) => { + const stop = (): void => { + watchProgram.close(); + resolve(); + }; + process.once('SIGINT', stop); + process.once('SIGTERM', stop); + }); +} + interface ResolvedBuildTypescript { config: ResolvedConfig; } @@ -250,7 +306,12 @@ export default createExecutor { + run: async ({ config }, options) => { + if (options.watch) { + await runWatch(config); + return; + } + const { content: tsconfigContent, compilerOptions } = await loadTsConfig(config.tsconfigPath); compilerOptions.outDir = config.outDir; await ensureDir(config.outDir); diff --git a/packages/nx-infra-plugin/src/executors/build-typescript/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/build-typescript/executor.e2e.spec.ts index 46f72350a0f9..a7db0e3e2460 100644 --- a/packages/nx-infra-plugin/src/executors/build-typescript/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/build-typescript/executor.e2e.spec.ts @@ -172,4 +172,63 @@ describe('BuildTypescriptExecutor E2E', () => { expect(result.success).toBe(false); }); }); + + describe('watch mode', () => { + async function waitFor(predicate: () => boolean, timeoutMs = 20000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (predicate()) return; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + throw new Error('waitFor timed out'); + } + + it('emits to outDir with alias resolution and stops on SIGINT', async () => { + const options: BuildTypescriptExecutorSchema = { + module: 'esm', + tsconfig: './tsconfig.esm.json', + outDir: './npm/esm-watch', + resolvePaths: true, + resolvePathsBaseDir: './src', + watch: true, + }; + + const outIndex = path.join(projectDir, 'npm', 'esm-watch', 'index.js'); + + const originalOnce = process.once; + let stopWatch: (() => void) | undefined; + let run: Promise<{ success: boolean }> | undefined; + + (process as unknown as { once: typeof process.once }).once = (( + event: string, + handler: () => void, + ) => { + if (event === 'SIGINT' || event === 'SIGTERM') { + stopWatch = handler; + return process; + } + return originalOnce.call(process, event, handler as never); + }) as typeof process.once; + + try { + run = executor(options, context); + + await waitFor(() => fs.existsSync(outIndex)); + const indexContent = await readFileText(outIndex); + expect(indexContent).not.toContain('@lib/utils'); + + stopWatch?.(); + stopWatch = undefined; + + expect((await run).success).toBe(true); + run = undefined; + } finally { + stopWatch?.(); + if (run) { + await run; + } + (process as unknown as { once: typeof process.once }).once = originalOnce; + } + }, 30000); + }); }); diff --git a/packages/nx-infra-plugin/src/executors/build-typescript/schema.json b/packages/nx-infra-plugin/src/executors/build-typescript/schema.json index 41c9036fcdf4..b77bb1964ee8 100644 --- a/packages/nx-infra-plugin/src/executors/build-typescript/schema.json +++ b/packages/nx-infra-plugin/src/executors/build-typescript/schema.json @@ -43,6 +43,11 @@ "resolvePathsBaseDir": { "type": "string", "description": "Base directory for path alias resolution (used as aliasRoot for tsc-alias). Relative to project root. Required when resolvePaths is true. Example: './js' for DevExtreme's __internal TypeScript compilation." + }, + "watch": { + "type": "boolean", + "default": false, + "description": "Watch the sources via the TypeScript incremental watch program and re-emit changed files to outDir." } } } diff --git a/packages/nx-infra-plugin/src/executors/build-typescript/schema.ts b/packages/nx-infra-plugin/src/executors/build-typescript/schema.ts index 1fa42424af72..7c3b2e10ccfe 100644 --- a/packages/nx-infra-plugin/src/executors/build-typescript/schema.ts +++ b/packages/nx-infra-plugin/src/executors/build-typescript/schema.ts @@ -6,4 +6,5 @@ export interface BuildTypescriptExecutorSchema { outDir?: string; resolvePaths?: boolean; resolvePathsBaseDir?: string; + watch?: boolean; } diff --git a/packages/nx-infra-plugin/src/executors/concatenate-files/concatenate-files.impl.ts b/packages/nx-infra-plugin/src/executors/concatenate-files/concatenate-files.impl.ts index 04dbab539e5e..a3a110ff0c9c 100644 --- a/packages/nx-infra-plugin/src/executors/concatenate-files/concatenate-files.impl.ts +++ b/packages/nx-infra-plugin/src/executors/concatenate-files/concatenate-files.impl.ts @@ -1,11 +1,11 @@ import * as path from 'path'; -import { createRequire } from 'module'; import { logger } from '@nx/devkit'; import { glob } from 'glob'; import { createExecutor } from '../../utils/create-executor'; import { toPosixPath } from '../../utils/path-resolver'; import { containsGlobPattern } from '../../utils/common'; import { exists, normalizeEol, readFileText, writeFileText } from '../../utils/file-operations'; +import { watchWithChokidar } from '../../utils/watch'; import { ConcatenateFilesExecutorSchema, ConcatenatePass } from './schema'; const ERROR_SOURCE_FILES_EMPTY = 'sourceFiles must contain at least one file'; @@ -134,8 +134,6 @@ async function resolveSourceFiles(sourceFiles: string[], projectRoot: string): P return resolved; } -const WATCH_DEBOUNCE_MS = 200; - // Sources are resolved per pass at run time (not once in `resolve`) so that an // additional pass can consume a file produced by an earlier pass, and so rebuilds // re-resolve any glob patterns on each run. @@ -174,27 +172,6 @@ async function runAllPasses( } } -interface ChokidarWatcher { - on: (event: string, handler: (...args: unknown[]) => void) => unknown; - close: () => Promise | void; -} - -interface Chokidar { - watch: (paths: string | string[], options?: Record) => ChokidarWatcher; -} - -function loadChokidar(projectRoot: string): Chokidar { - const projectRequire = createRequire(path.join(projectRoot, 'package.json')); - try { - return projectRequire('chokidar') as Chokidar; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error( - `concatenate-files watch mode requires 'chokidar' to be installed in the project (${projectRoot}): ${message}`, - ); - } -} - async function runWatchBuild( projectRoot: string, options: ConcatenateFilesExecutorSchema, @@ -206,58 +183,12 @@ async function runWatchBuild( throw new Error('No watch files found after resolving watchPaths'); } await runAllPasses(projectRoot, options); - logger.info('concatenate-files watch mode is watching for changes...'); - - await new Promise((resolve) => { - let timer: NodeJS.Timeout | undefined; - let busy = false; - let pending = false; - - const runRebuild = async (): Promise => { - if (busy) { - pending = true; - return; - } - - busy = true; - try { - await runAllPasses(projectRoot, options); - logger.info('concatenate-files watch: rebuild complete'); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - logger.error(`concatenate-files watch rebuild failed: ${message}`); - } finally { - busy = false; - if (pending) { - pending = false; - void runRebuild(); - } - } - }; - - const scheduleRebuild = () => { - if (timer) { - clearTimeout(timer); - } - - timer = setTimeout(() => { - void runRebuild(); - }, WATCH_DEBOUNCE_MS); - }; - - const chokidar = loadChokidar(projectRoot); - const watcher = chokidar.watch(watchFiles, { ignoreInitial: true }); - watcher.on('all', scheduleRebuild); - - const stopWatcher = () => { - if (timer) { - clearTimeout(timer); - } - void Promise.resolve(watcher.close()).finally(resolve); - }; - process.once('SIGINT', stopWatcher); - process.once('SIGTERM', stopWatcher); + await watchWithChokidar({ + projectRoot, + watchTargets: watchFiles, + label: 'concatenate-files watch', + onRebuild: () => runAllPasses(projectRoot, options), }); } diff --git a/packages/nx-infra-plugin/src/utils/index.ts b/packages/nx-infra-plugin/src/utils/index.ts index 5ea9b84eaedd..7e25468436f9 100644 --- a/packages/nx-infra-plugin/src/utils/index.ts +++ b/packages/nx-infra-plugin/src/utils/index.ts @@ -6,3 +6,4 @@ export * from './common'; export * from './test-utils'; export * from './create-executor'; export * from './glob-discovery'; +export * from './watch'; diff --git a/packages/nx-infra-plugin/src/utils/watch.ts b/packages/nx-infra-plugin/src/utils/watch.ts new file mode 100644 index 000000000000..8fd3b7e46dc0 --- /dev/null +++ b/packages/nx-infra-plugin/src/utils/watch.ts @@ -0,0 +1,124 @@ +import * as path from 'path'; +import { createRequire } from 'module'; +import { logger } from '@nx/devkit'; + +export const DEFAULT_WATCH_DEBOUNCE_MS = 200; + +export interface ChokidarWatcher { + on: (event: string, handler: (...args: unknown[]) => void) => unknown; + close: () => Promise | void; +} + +export interface Chokidar { + watch: (paths: string | string[], options?: Record) => ChokidarWatcher; +} + +// chokidar is a root devDependency, not a dependency of this plugin (and not the same +// chokidar gulp-watch bundles internally) — resolve it from the project root at runtime. +export function loadChokidar(projectRoot: string): Chokidar { + const projectRequire = createRequire(path.join(projectRoot, 'package.json')); + try { + return projectRequire('chokidar') as Chokidar; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `watch mode requires 'chokidar' to be installed in the project (${projectRoot}): ${message}`, + ); + } +} + +export interface WatchEvent { + event: string; + filePath: string; +} + +export interface WatchWithChokidarOptions { + projectRoot: string; + /** Concrete files or directories to watch (chokidar v4 has no glob support). */ + watchTargets: string | string[]; + /** Label used in watch log messages. */ + label: string; + /** Rebuild callback invoked with the batch of events; debounced while idle, queued to run again immediately once a running rebuild finishes. */ + onRebuild: (events: WatchEvent[]) => Promise | void; + /** Optional filter deciding whether a raw chokidar event is relevant. */ + eventFilter?: (event: string, filePath: string) => boolean; + debounceMs?: number; + chokidarOptions?: Record; +} + +// Resolves on SIGINT/SIGTERM. Doesn't run an initial build — that's on the caller. +export async function watchWithChokidar(options: WatchWithChokidarOptions): Promise { + const { + projectRoot, + watchTargets, + label, + onRebuild, + eventFilter, + debounceMs = DEFAULT_WATCH_DEBOUNCE_MS, + chokidarOptions, + } = options; + + await new Promise((resolve) => { + let timer: NodeJS.Timeout | undefined; + let busy = false; + let pending = false; + let batch: WatchEvent[] = []; + + const runRebuild = async (): Promise => { + if (busy) { + pending = true; + return; + } + + const events = batch; + batch = []; + busy = true; + try { + await onRebuild(events); + logger.verbose(`${label}: rebuild complete`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error(`${label} rebuild failed: ${message}`); + } finally { + busy = false; + if (pending) { + pending = false; + void runRebuild(); + } + } + }; + + const scheduleRebuild = (): void => { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + void runRebuild(); + }, debounceMs); + }; + + const chokidar = loadChokidar(projectRoot); + const watcher = chokidar.watch(watchTargets, { ignoreInitial: true, ...chokidarOptions }); + + watcher.on('all', (...args: unknown[]) => { + const [event, filePath] = args as [string, string]; + if (eventFilter && !eventFilter(event, filePath)) { + return; + } + batch.push({ event, filePath }); + scheduleRebuild(); + }); + + logger.verbose(`${label} is watching for changes...`); + + const stopWatcher = (): void => { + if (timer) { + clearTimeout(timer); + } + void Promise.resolve(watcher.close()).finally(resolve); + }; + + process.once('SIGINT', stopWatcher); + process.once('SIGTERM', stopWatcher); + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eddaab0adc53..f8f5f5211aa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1408,6 +1408,12 @@ importers: '@babel/plugin-transform-modules-commonjs': specifier: 7.29.7 version: 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': + specifier: 7.29.7 + version: 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': + specifier: 7.29.7 + version: 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-runtime': specifier: 7.29.7 version: 7.29.7(@babel/core@7.29.7) @@ -3031,10 +3037,6 @@ packages: peerDependencies: '@babel/core': ^7.29.1 - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} - engines: {node: '>=6.9.0'} - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} engines: {node: '>=6.9.0'} @@ -3667,12 +3669,6 @@ packages: peerDependencies: '@babel/core': ^7.29.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': - resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} engines: {node: '>=6.9.0'} @@ -3727,12 +3723,6 @@ packages: peerDependencies: '@babel/core': ^7.29.1 - '@babel/plugin-transform-optional-chaining@7.28.6': - resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.1 - '@babel/plugin-transform-optional-chaining@7.29.7': resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==} engines: {node: '>=6.9.0'} @@ -18637,7 +18627,7 @@ snapshots: '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: @@ -18792,13 +18782,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': dependencies: '@babel/traverse': 7.29.7 @@ -18846,7 +18829,7 @@ snapshots: '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -18862,7 +18845,7 @@ snapshots: '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7)': dependencies: @@ -18872,7 +18855,7 @@ snapshots: '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7)': dependencies: @@ -18890,9 +18873,9 @@ snapshots: '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -18908,7 +18891,7 @@ snapshots: '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -18972,7 +18955,7 @@ snapshots: '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': dependencies: @@ -18982,7 +18965,7 @@ snapshots: '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19002,7 +18985,7 @@ snapshots: '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19058,12 +19041,12 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19127,7 +19110,7 @@ snapshots: '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19137,7 +19120,7 @@ snapshots: '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19148,7 +19131,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color @@ -19164,7 +19147,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color @@ -19182,7 +19165,7 @@ snapshots: '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.7) '@babel/traverse': 7.29.7 transitivePeerDependencies: @@ -19203,7 +19186,7 @@ snapshots: '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/template': 7.29.7 '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7)': @@ -19215,7 +19198,7 @@ snapshots: '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -19232,7 +19215,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19243,7 +19226,7 @@ snapshots: '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19254,7 +19237,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19265,7 +19248,7 @@ snapshots: '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19275,7 +19258,7 @@ snapshots: '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -19291,7 +19274,7 @@ snapshots: '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19301,7 +19284,7 @@ snapshots: '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19317,8 +19300,8 @@ snapshots: '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color @@ -19334,7 +19317,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -19351,7 +19334,7 @@ snapshots: '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19361,7 +19344,7 @@ snapshots: '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19371,7 +19354,7 @@ snapshots: '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19381,7 +19364,7 @@ snapshots: '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19392,7 +19375,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color @@ -19424,7 +19407,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.29.7 transitivePeerDependencies: @@ -19444,7 +19427,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color @@ -19460,7 +19443,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19471,18 +19454,13 @@ snapshots: '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -19491,7 +19469,7 @@ snapshots: '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19523,7 +19501,7 @@ snapshots: '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -19539,21 +19517,13 @@ snapshots: '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -19565,7 +19535,7 @@ snapshots: '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19576,7 +19546,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color @@ -19593,7 +19563,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color @@ -19609,7 +19579,7 @@ snapshots: '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19658,7 +19628,7 @@ snapshots: '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19669,7 +19639,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19680,7 +19650,7 @@ snapshots: '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19738,7 +19708,7 @@ snapshots: '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19748,8 +19718,8 @@ snapshots: '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color @@ -19764,7 +19734,7 @@ snapshots: '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19774,7 +19744,7 @@ snapshots: '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19784,7 +19754,7 @@ snapshots: '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19805,7 +19775,7 @@ snapshots: '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19816,7 +19786,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19828,7 +19798,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19840,7 +19810,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7)': dependencies: @@ -19969,12 +19939,12 @@ snapshots: '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.7) '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.7) '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.7) '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.7) '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.7) '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.7) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.7) '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.7) '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.7) @@ -20164,7 +20134,7 @@ snapshots: '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/types': 7.29.7 esutils: 2.0.3