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/package.json b/package.json index e9149f4128e5..fb3772676525 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/node": "catalog:tools", "@types/shelljs": "0.8.15", "axe-core": "catalog:", + "chokidar": "5.0.0", "@types/yargs": "17.0.35", "devextreme-internal-tools": "catalog:tools", "devextreme-metadata": "workspace:*", diff --git a/packages/devextreme-scss/package.json b/packages/devextreme-scss/package.json index d4f56ce31229..dc5bb1b2b5db 100644 --- a/packages/devextreme-scss/package.json +++ b/packages/devextreme-scss/package.json @@ -3,7 +3,6 @@ "type": "module", "devDependencies": { "autoprefixer": "10.5.0", - "chokidar": "5.0.0", "clean-css": "5.3.3", "opentype.js": "1.3.4", "postcss": "8.5.10", diff --git a/packages/devextreme/build/gulp/bundler-config.js b/packages/devextreme/build/gulp/bundler-config.js index 22d073510759..f151e3cabfc6 100644 --- a/packages/devextreme/build/gulp/bundler-config.js +++ b/packages/devextreme/build/gulp/bundler-config.js @@ -1,45 +1,8 @@ 'use strict'; const gulp = require('gulp'); -const replace = require('gulp-replace'); -const concat = require('gulp-concat'); -const rename = require('gulp-rename'); -const header = require('gulp-header'); -const eol = require('gulp-eol'); +const shell = require('gulp-shell'); -const context = require('./context.js'); -const headerPipes = require('./header-pipes.js'); -const { packageDir } = require('./utils'); - -const BUNDLE_CONFIG_SOURCES = [ - 'build/bundle-templates/modules/parts/core.js', - 'build/bundle-templates/modules/parts/data.js', - 'build/bundle-templates/modules/parts/widgets-base.js', - 'build/bundle-templates/modules/parts/widgets-web.js', - 'build/bundle-templates/modules/parts/viz.js', - 'build/bundle-templates/modules/parts/aspnet.js' -]; - -gulp.task('bundler-config', function() { - return gulp.src(BUNDLE_CONFIG_SOURCES) - .pipe(replace(/[^]*BUNDLER_PARTS.*?$([^]*)^.*?BUNDLER_PARTS_END[^]*/gm, '$1')) - .pipe(concat('dx.custom.js')) - .pipe(header('/* Comment lines below for the widgets you don\'t require and run "devextreme-bundler" in this directory, then include dx.custom.js in your project */')) - .pipe(headerPipes.useStrict()) - .pipe(replace(/require *\( *["']..\/..\//g, 'require(\'')) - .pipe(replace(/^[ ]{4}/gm, '')) - .pipe(replace(/^[\n\r]{2,}/gm, '\n\n')) - .pipe(eol()) - .pipe(gulp.dest('build/bundle-templates')) - .pipe(rename('dx.custom.config.js')) - .pipe(replace(/require *\( *["']..\//g, 'require(\'devextreme/')) - .pipe(gulp.dest(`${context.RESULT_NPM_PATH}/${packageDir}/bundles`)); -}); - -gulp.task('bundler-config-watch', function() { - return gulp - .watch(BUNDLE_CONFIG_SOURCES, gulp.series('bundler-config')) - .on('ready', () => console.log( - 'bundler-config task is watching for changes...' - )); -}); +gulp.task('bundler-config', shell.task( + 'pnpm nx build:devextreme-bundler-config devextreme && pnpm nx build:devextreme-bundler-config devextreme -c prod' +)); diff --git a/packages/devextreme/build/gulp/js-bundles.js b/packages/devextreme/build/gulp/js-bundles.js deleted file mode 100644 index a25e4119390b..000000000000 --- a/packages/devextreme/build/gulp/js-bundles.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -const gulp = require('gulp'); -const gulpIf = require('gulp-if'); -const lazyPipe = require('lazypipe'); -const named = require('vinyl-named'); -const notify = require('gulp-notify'); -const path = require('path'); -const plumber = require('gulp-plumber'); -const shell = require('gulp-shell'); -const webpack = require('webpack'); -const webpackStream = require('webpack-stream'); - -const ctx = require('./context.js'); -const headerPipes = require('./header-pipes.js'); -const webpackConfig = require('../../webpack.config.js'); -const env = require('./env-variables.js'); -const namedDebug = lazyPipe() - .pipe(named, (file) => path.basename(file.path, path.extname(file.path)) + '.debug'); - -const BUNDLES = [ - '/bundles/dx.ai-integration.js', - '/bundles/dx.all.js', - '/bundles/dx.web.js', - '/bundles/dx.viz.js' -]; - -const DEBUG_BUNDLES = BUNDLES.concat([ '/bundles/dx.custom.js' ]); - -const processBundles = (bundles, pathPrefix) => bundles.map((bundle) => pathPrefix + bundle); -const muteWebPack = () => undefined; -const getWebpackConfig = () => { - const plugins = []; - const isInternalBuild = env.BUILD_INTERNAL_PACKAGE || env.BUILD_TEST_INTERNAL_PACKAGE; - - if (isInternalBuild) { - plugins.push(new webpack.NormalModuleReplacementPlugin(/(.*)\/license_validation/, resource => { - resource.request = resource.request.replace('license_validation', 'license_validation_internal'); - })); - } - - return Object.assign(webpackConfig, { plugins }); -}; - -gulp.task('js-bundles-prod', shell.task( - ctx.uglify - ? 'pnpm nx run devextreme:bundle:prod -c production' - : 'pnpm nx run devextreme:bundle:prod' -)); - -function prepareDebugMeta(watch) { - const debugConfig = Object.assign({ watch }, getWebpackConfig()); - const bundlesPath = ctx.TRANSPILED_PROD_RENOVATION_PATH; - - const bundles = processBundles(DEBUG_BUNDLES, bundlesPath); - - debugConfig.output = Object.assign({}, webpackConfig.output); - debugConfig.output['pathinfo'] = true; - debugConfig.mode = watch ? 'development' : 'production'; - debugConfig.optimization.minimize = false; - - if(!ctx.uglify) { - debugConfig.devtool = 'eval-source-map'; - } - - return { debugConfig, bundles }; -} - -function createDebugBundlesStream(watch, displayName) { - const { debugConfig, bundles } = prepareDebugMeta(watch); - const destination = ctx.RESULT_JS_PATH; - - const task = () => gulp.src(bundles) - .pipe(namedDebug()) - .pipe(gulpIf(watch, plumber({ - errorHandler: notify.onError('Error: <%= error.message %>') - .bind() // bind call is necessary to prevent firing 'end' event in notify.onError implementation - }))) - .pipe(webpackStream(debugConfig, webpack, muteWebPack)) - .pipe(headerPipes.useStrict()) - .pipe(headerPipes.bangLicense()) - .pipe(gulp.dest(destination)); - - task.displayName = `${displayName}-worker`; - - return task; -} - -gulp.task('js-bundles-debug', shell.task( - ctx.uglify - ? 'pnpm nx run devextreme:bundle:debug -c production' - : 'pnpm nx run devextreme:bundle:debug' -)); - -gulp.task('js-bundles-watch', gulp.parallel( - createDebugBundlesStream(true, 'js-bundles-watch') -)); diff --git a/packages/devextreme/build/gulp/localization.js b/packages/devextreme/build/gulp/localization.js index ecf67b940fc5..3f43b1c65bf7 100644 --- a/packages/devextreme/build/gulp/localization.js +++ b/packages/devextreme/build/gulp/localization.js @@ -1,50 +1,8 @@ 'use strict'; const gulp = require('gulp'); -const path = require('path'); const shell = require('gulp-shell'); -const through = require('through2'); -const fs = require('fs'); - -const DEFAULT_LOCALE = 'en'; -const DICTIONARY_SOURCE_FOLDER = 'js/localization/messages'; gulp.task('localization', shell.task('pnpm nx build:localization devextreme')); -gulp.task('generate-community-locales', () => { - const defaultFile = fs.readFileSync(path.join(DICTIONARY_SOURCE_FOLDER, DEFAULT_LOCALE + '.json')).toString(); - const defaultDictionaryKeys = Object.keys(JSON.parse(defaultFile)[DEFAULT_LOCALE]); - - return gulp - .src([ - 'js/localization/messages/*.json', - '!js/localization/messages/en.json' - ]) - .pipe(through.obj(function(file, encoding, callback) { - const parsedFile = JSON.parse(file.contents.toString(encoding)); - - const [locale] = Object.keys(parsedFile); - const dictionary = parsedFile[locale]; - - let newFile = defaultFile.replace(`"${DEFAULT_LOCALE}"`, `"${locale}"`); - - defaultDictionaryKeys.forEach((key) => { - let replaceValue = null; - // eslint-disable-next-line no-prototype-builtins - if(dictionary.hasOwnProperty(key)) { - const val = dictionary[key]; - if(!val.includes('TODO')) { - replaceValue = val.replace(/"/g, '\\"'); - } - } - - if(replaceValue != null) { - newFile = newFile.replace(new RegExp(`"${key}":.*"(,)?`), `"${key}": "${replaceValue}"$1`); - } - }); - - file.contents = Buffer.from(newFile, encoding); - callback(null, file); - })) - .pipe(gulp.dest(DICTIONARY_SOURCE_FOLDER)); -}); +gulp.task('generate-community-locales', shell.task('pnpm nx build:community-localization devextreme')); diff --git a/packages/devextreme/build/gulp/systemjs.js b/packages/devextreme/build/gulp/systemjs.js deleted file mode 100644 index fdc2e33ede7a..000000000000 --- a/packages/devextreme/build/gulp/systemjs.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -const path = require('path'); -const { spawn } = require('child_process'); -const gulp = require('gulp'); - -const root = path.resolve(__dirname, '../..'); - -gulp.task('transpile-systemjs-modules', (callback) => { - spawn('node', [path.resolve(root, 'testing/systemjs-builder.js'), '--transpile=modules'], { stdio: 'inherit' }) - .on('error', callback) - .on('close', code => code ? callback(new Error(code)) : callback()); -}); - -gulp.task('transpile-systemjs-testing', (callback) => { - spawn('node', [path.resolve(root, 'testing/systemjs-builder.js'), '--transpile=testing'], { stdio: 'inherit' }) - .on('error', callback) - .on('close', code => code ? callback(new Error(code)) : callback()); -}); - -gulp.task('transpile-systemjs-css', (callback) => { - spawn('node', [path.resolve(root, 'testing/systemjs-builder.js'), '--transpile=css'], { stdio: 'inherit' }) - .on('error', callback) - .on('close', code => code ? callback(new Error(code)) : callback()); -}); - -gulp.task('transpile-systemjs-js-vendors', (callback) => { - spawn('node', [path.resolve(root, 'testing/systemjs-builder.js'), '--transpile=js-vendors'], { stdio: 'inherit' }) - .on('error', callback) - .on('close', code => code ? callback(new Error(code)) : callback()); -}); - -gulp.task('transpile-systemjs', gulp.parallel( - 'transpile-systemjs-modules', - 'transpile-systemjs-testing', - 'transpile-systemjs-css', - 'transpile-systemjs-js-vendors' -)); 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 d865ef7676a5..99f1884dd65e 100644 --- a/packages/devextreme/gulpfile.js +++ b/packages/devextreme/gulpfile.js @@ -12,9 +12,19 @@ gulp.task('clean', shell.task('pnpm nx clean:artifacts devextreme')); require('./build/gulp/bundler-config'); require('./build/gulp/transpile'); -require('./build/gulp/js-bundles'); require('./build/gulp/localization'); -require('./build/gulp/systemjs'); + +gulp.task('js-bundles-prod', shell.task( + context.uglify + ? 'pnpm nx run devextreme:bundle:prod -c production' + : 'pnpm nx run devextreme:bundle:prod' +)); + +gulp.task('js-bundles-debug', shell.task( + context.uglify + ? 'pnpm nx run devextreme:bundle:debug -c production' + : 'pnpm nx run devextreme:bundle:debug' +)); function getTranspileConfig() { if(env.TEST_CI) { @@ -104,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) { @@ -116,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'); @@ -125,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'); } @@ -134,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('node ./testing/launch')); - -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 14883042adfa..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", @@ -190,10 +192,9 @@ "build-dist": "cross-env BUILD_ESM_PACKAGE=true gulp default --uglify", "build:modular": "cd ./playground/modular && webpack", "build:modular:watch": "cd ./playground/modular && webpack --watch", - "build:community-localization": "gulp generate-community-locales", - "build:systemjs": "gulp transpile-systemjs", - "dev": "cross-env DEVEXTREME_TEST_CI=true gulp dev", - "dev:watch": "cross-env DEVEXTREME_TEST_CI=true gulp dev-watch", + "build:community-localization": "pnpm nx build:community-localization devextreme", + "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 df4511af3456..2e6aa846b001 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -50,6 +50,14 @@ "{projectRoot}/js/__internal/core/localization/cldr-data" ] }, + "build:community-localization": { + "cache": false, + "executor": "devextreme-nx-infra-plugin:generate-community-locales", + "options": { + "messagesDir": "./js/localization/messages", + "defaultLocale": "en" + } + }, "build:localization": { "cache": true, "executor": "nx:run-commands", @@ -144,6 +152,54 @@ "{projectRoot}/artifacts/npm/devextreme-internal/bundles/dx.custom.config.js" ] }, + "build:devextreme-bundler-config:watch": { + "executor": "devextreme-nx-infra-plugin:concatenate-files", + "cache": false, + "options": { + "sourceFiles": [ + "./build/bundle-templates/modules/parts/core.js", + "./build/bundle-templates/modules/parts/data.js", + "./build/bundle-templates/modules/parts/widgets-base.js", + "./build/bundle-templates/modules/parts/widgets-web.js", + "./build/bundle-templates/modules/parts/viz.js", + "./build/bundle-templates/modules/parts/aspnet.js" + ], + "outputFile": "./build/bundle-templates/dx.custom.js", + "extractPattern": "[^]*BUNDLER_PARTS.*?$([^]*)^.*?BUNDLER_PARTS_END[^]*", + "extractPatternFlags": "gm", + "header": "\"use strict\";\n\n/* Comment lines below for the widgets you don't require and run \"devextreme-bundler\" in this directory, then include dx.custom.js in your project */\n\n", + "transforms": [ + { + "find": "require *\\( *[\"']..\\/\\.\\.\\/", + "replace": "require('" + }, + { + "find": "^[ ]{4}", + "replace": "", + "flags": "gm" + }, + { + "find": "\\n{3,}", + "replace": "\n\n", + "flags": "g" + } + ], + "additionalPasses": [ + { + "sourceFiles": ["./build/bundle-templates/dx.custom.js"], + "outputFile": "./artifacts/npm/devextreme/bundles/dx.custom.config.js", + "transforms": [ + { + "find": "require *\\( *[\"']\\.\\.\\/", + "replace": "require('devextreme/" + } + ] + } + ], + "watch": true, + "watchPaths": ["./build/bundle-templates/modules/parts/**/*.js"] + } + }, "clean:dist-ts": { "executor": "devextreme-nx-infra-plugin:clean", "options": { @@ -181,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": { @@ -212,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": { @@ -285,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": { @@ -451,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": { @@ -571,6 +712,34 @@ "{projectRoot}/artifacts/js/dx.{all,web,viz,ai-integration,custom}.debug.js" ] }, + "bundle:watch": { + "executor": "devextreme-nx-infra-plugin:bundle", + "options": { + "watch": true, + "entries": [ + "bundles/dx.all.js", + "bundles/dx.web.js", + "bundles/dx.viz.js", + "bundles/dx.ai-integration.js", + "bundles/dx.custom.js" + ], + "sourceDir": "./artifacts/transpiled-renovation-npm", + "outDir": "./artifacts/js", + "mode": "debug", + "webpackConfigPath": "./webpack.config.js", + "applyLicenseHeaders": { + "prependAfterLicense": "\"use strict\";\n\n", + "separator": "", + "includePatterns": ["dx.*.debug.js"] + } + }, + "configurations": { + "production": { + "sourceMap": false + } + }, + "cache": false + }, "bundle:prod": { "executor": "nx:run-commands", "options": { @@ -1558,9 +1727,16 @@ "cache": true }, "build:systemjs": { - "executor": "nx:run-script", + "executor": "nx:run-commands", "options": { - "script": "build:systemjs" + "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" @@ -1589,7 +1765,6 @@ "{projectRoot}/testing/helpers/**/*", "{projectRoot}/testing/tests/**/*", "{projectRoot}/testing/systemjs-builder.js", - "{projectRoot}/build/gulp/systemjs.js", "{workspaceRoot}/pnpm-lock.yaml" ], "outputs": [ @@ -1601,13 +1776,45 @@ "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", + "cache": false, + "options": { + "command": "node ./testing/launch", + "cwd": "{projectRoot}" + } }, "lint": { "executor": "nx:noop", 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 6d34aebaf1c1..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,9 +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')`. | +| 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/executors.json b/packages/nx-infra-plugin/executors.json index 1be9f2301656..aa8b5c56283a 100644 --- a/packages/nx-infra-plugin/executors.json +++ b/packages/nx-infra-plugin/executors.json @@ -60,6 +60,11 @@ "schema": "./src/executors/localization/schema.json", "description": "Generate localization message files and TypeScript CLDR data modules" }, + "generate-community-locales": { + "implementation": "./src/executors/generate-community-locales/executor", + "schema": "./src/executors/generate-community-locales/schema.json", + "description": "Normalize community locale message files against the default locale dictionary" + }, "concatenate-files": { "implementation": "./src/executors/concatenate-files/executor", "schema": "./src/executors/concatenate-files/schema.json", 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/bundle/bundle.impl.ts b/packages/nx-infra-plugin/src/executors/bundle/bundle.impl.ts index f22714cf467c..3c2e8824512d 100644 --- a/packages/nx-infra-plugin/src/executors/bundle/bundle.impl.ts +++ b/packages/nx-infra-plugin/src/executors/bundle/bundle.impl.ts @@ -51,6 +51,7 @@ function createWebpackConfig( mode: 'debug' | 'production', projectRoot: string, sourceMap: boolean, + watch: boolean, ): Configuration { const config: Configuration = { ...baseConfig, @@ -68,6 +69,10 @@ function createWebpackConfig( minimize: false, }; + if (watch) { + config.mode = 'development'; + } + if (mode === 'debug') { config.output = { ...(config.output || {}), @@ -98,6 +103,19 @@ function createWebpackConfig( return config; } +function collectWebpackErrorMessages(stats: Stats): string { + const info = stats.toJson({ errors: true }); + return (info.errors || []).map((entry) => entry.message).join('\n'); +} + +function logWebpackWarnings(stats: Stats): void { + if (!stats.hasWarnings()) { + return; + } + const info = stats.toJson({ warnings: true }); + (info.warnings || []).forEach((warning) => logger.warn(warning.message)); +} + function runWebpack(webpack: typeof import('webpack'), config: Configuration): Promise { return new Promise((resolve, reject) => { webpack(config, (err, stats) => { @@ -110,9 +128,7 @@ function runWebpack(webpack: typeof import('webpack'), config: Configuration): P return; } if (stats.hasErrors()) { - const info = stats.toJson({ errors: true }); - const errorMessages = (info.errors || []).map((entry) => entry.message).join('\n'); - reject(new Error(errorMessages)); + reject(new Error(collectWebpackErrorMessages(stats))); return; } resolve(stats); @@ -120,6 +136,88 @@ function runWebpack(webpack: typeof import('webpack'), config: Configuration): P }); } +async function runWebpackWatch( + webpack: typeof import('webpack'), + config: Configuration, + onSuccess: (stats: Stats) => Promise, +): Promise { + await new Promise((resolve) => { + const compiler = webpack(config); + let watching: ReturnType | undefined; + let isClosing = false; + + let postBuildChain = Promise.resolve(); + + const removeSignalHandlers = (): void => { + process.removeListener('SIGINT', stopWatching); + process.removeListener('SIGTERM', stopWatching); + }; + + const finishShutdown = (): void => { + void postBuildChain.finally(() => { + resolve(); + }); + }; + + const stopWatching = (): void => { + if (isClosing) { + return; + } + isClosing = true; + removeSignalHandlers(); + + if (!watching) { + finishShutdown(); + return; + } + watching.close(() => { + setTimeout(finishShutdown, 100); + }); + }; + + watching = compiler.watch( + { + aggregateTimeout: 200, + ignored: /node_modules/, + ...(process.platform === 'win32' ? { poll: 1000 } : {}), + }, + (err, stats) => { + if (isClosing) { + return; + } + if (err) { + logger.error(err.message); + return; + } + if (!stats) { + return; + } + if (stats.hasErrors()) { + logger.error(collectWebpackErrorMessages(stats)); + return; + } + + logWebpackWarnings(stats); + + postBuildChain = postBuildChain + .then(() => onSuccess(stats)) + .then(() => { + logger.info('bundle watch: rebuild complete'); + }) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error); + logger.error(`bundle watch post-build failed: ${message}`); + }); + }, + ); + + logger.info('bundle watch mode is watching for changes...'); + + process.once('SIGINT', stopWatching); + process.once('SIGTERM', stopWatching); + }); +} + function loadWebpackConfig(resolvedConfigPath: string): Configuration { if (!fs.existsSync(resolvedConfigPath)) { throw new Error(ERROR_MESSAGES.WEBPACK_CONFIG_NOT_FOUND(resolvedConfigPath)); @@ -146,6 +244,7 @@ interface ResolvedBundle { resolvedOutDir: string; mode: 'debug' | 'production'; sourceMap: boolean; + watch: boolean; webpack: typeof import('webpack'); baseConfig: Configuration; licenseHeaders?: ResolvedLicenseHeaders; @@ -195,6 +294,7 @@ export default createExecutor({ mode, webpackConfigPath = './webpack.config.js', sourceMap = true, + watch = false, applyLicenseHeaders, } = options; @@ -217,6 +317,7 @@ export default createExecutor({ resolvedOutDir, mode, sourceMap, + watch, webpack, baseConfig, licenseHeaders, @@ -232,19 +333,31 @@ export default createExecutor({ resolved.mode, resolved.projectRoot, resolved.sourceMap, + resolved.watch, ); logger.verbose(`Bundling ${resolved.entries.length} entries in ${resolved.mode} mode`); logger.verbose(`Source: ${resolved.resolvedSourceDir}`); logger.verbose(`Output: ${resolved.resolvedOutDir}`); + const applyHeadersIfNeeded = async (): Promise => { + if (resolved.licenseHeaders) { + await applyLicenseHeadersStep(resolved.resolvedOutDir, resolved.licenseHeaders); + } + }; + + if (resolved.watch) { + await runWebpackWatch(resolved.webpack, config, async (stats) => { + const assets = Object.keys(stats.compilation.assets); + logger.verbose(`Produced ${assets.length} bundle(s): ${assets.join(', ')}`); + await applyHeadersIfNeeded(); + }); + return; + } + try { const stats = await runWebpack(resolved.webpack, config); - - if (stats.hasWarnings()) { - const info = stats.toJson({ warnings: true }); - (info.warnings || []).forEach((warning) => logger.warn(warning.message)); - } + logWebpackWarnings(stats); const assets = Object.keys(stats.compilation.assets); logger.verbose(`Produced ${assets.length} bundle(s): ${assets.join(', ')}`); @@ -253,8 +366,6 @@ export default createExecutor({ throw new Error(ERROR_MESSAGES.WEBPACK_ERROR(message)); } - if (resolved.licenseHeaders) { - await applyLicenseHeadersStep(resolved.resolvedOutDir, resolved.licenseHeaders); - } + await applyHeadersIfNeeded(); }, }); diff --git a/packages/nx-infra-plugin/src/executors/bundle/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/bundle/executor.e2e.spec.ts index 3a7a05d7c419..d5fe5952213a 100644 --- a/packages/nx-infra-plugin/src/executors/bundle/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/bundle/executor.e2e.spec.ts @@ -135,4 +135,46 @@ describe('BundleExecutor E2E', () => { expect(bundleContent).toMatch(/^\/\*!/); expect(bundleContent).toContain('DevExtreme (dx.all.js)'); }, 60000); + + async function waitUntil( + predicate: () => boolean | Promise, + timeoutMs = 30000, + intervalMs = 200, + ): Promise { + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + if (await predicate()) { + return; + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + + throw new Error(`Timed out after ${timeoutMs}ms`); + } + + it('should produce bundles on initial watch compilation', async () => { + const outputPath = path.join(projectDir, 'artifacts', 'js', 'dx.all.debug.js'); + + const options: BundleExecutorSchema = { + entries: ['bundles/dx.all.js'], + sourceDir: './artifacts/transpiled-renovation-npm', + outDir: './artifacts/js', + mode: 'debug', + webpackConfigPath: './webpack.config.js', + watch: true, + }; + + const watchPromise = executor(options, context); + + await waitUntil(() => fs.existsSync(outputPath)); + expect(await readFileText(outputPath)).toContain('Hello,'); + + setImmediate(() => { + process.emit('SIGINT'); + }); + + const result = await watchPromise; + expect(result.success).toBe(true); + }, 60000); }); diff --git a/packages/nx-infra-plugin/src/executors/bundle/schema.json b/packages/nx-infra-plugin/src/executors/bundle/schema.json index a94a4bd268c1..8e8a82be1bb7 100644 --- a/packages/nx-infra-plugin/src/executors/bundle/schema.json +++ b/packages/nx-infra-plugin/src/executors/bundle/schema.json @@ -37,6 +37,11 @@ "description": "Enable eval-source-map devtool in debug mode", "default": true }, + "watch": { + "type": "boolean", + "description": "Keep webpack running in watch mode until SIGINT/SIGTERM", + "default": false + }, "applyLicenseHeaders": { "type": "object", "description": "Optional post-step that prepends license headers to bundle output files", diff --git a/packages/nx-infra-plugin/src/executors/bundle/schema.ts b/packages/nx-infra-plugin/src/executors/bundle/schema.ts index 0f22e11c1209..6037d4d12890 100644 --- a/packages/nx-infra-plugin/src/executors/bundle/schema.ts +++ b/packages/nx-infra-plugin/src/executors/bundle/schema.ts @@ -18,5 +18,6 @@ export interface BundleExecutorSchema { mode: 'debug' | 'production'; webpackConfigPath?: string; sourceMap?: boolean; + watch?: boolean; applyLicenseHeaders?: BundleLicenseHeadersOption; } 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 425cf52c2e3e..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 @@ -5,7 +5,8 @@ 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 { ConcatenateFilesExecutorSchema } from './schema'; +import { watchWithChokidar } from '../../utils/watch'; +import { ConcatenateFilesExecutorSchema, ConcatenatePass } from './schema'; const ERROR_SOURCE_FILES_EMPTY = 'sourceFiles must contain at least one file'; const ERROR_NO_FILES_RESOLVED = 'No source files found after resolving patterns'; @@ -133,44 +134,84 @@ async function resolveSourceFiles(sourceFiles: string[], projectRoot: string): P return resolved; } +// 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. +async function runPass(projectRoot: string, pass: ConcatenatePass): Promise { + if (!pass.sourceFiles?.length) { + throw new Error(ERROR_SOURCE_FILES_EMPTY); + } + + const resolvedFiles = await resolveSourceFiles(pass.sourceFiles, projectRoot); + if (resolvedFiles.length === 0) { + throw new Error(ERROR_NO_FILES_RESOLVED); + } + + const outputPath = path.resolve(projectRoot, pass.outputFile); + logger.verbose(`Concatenating ${resolvedFiles.length} files...`); + await concatToFile(outputPath, { + sourceFiles: resolvedFiles, + header: pass.header, + footer: pass.footer, + extractPattern: pass.extractPattern, + extractPatternFlags: pass.extractPatternFlags, + transforms: pass.transforms, + normalizeLineEndings: pass.normalizeLineEndings, + separator: pass.separator, + }); + logger.verbose(`Created: ${path.relative(projectRoot, outputPath)}`); +} + +async function runAllPasses( + projectRoot: string, + options: ConcatenateFilesExecutorSchema, +): Promise { + await runPass(projectRoot, options); + for (const pass of options.additionalPasses ?? []) { + await runPass(projectRoot, pass); + } +} + +async function runWatchBuild( + projectRoot: string, + options: ConcatenateFilesExecutorSchema, +): Promise { + // chokidar v4+ dropped glob support, so expand watch patterns to concrete files upfront. + const watchTargets = options.watchPaths?.length ? options.watchPaths : options.sourceFiles; + const watchFiles = await resolveSourceFiles(watchTargets, projectRoot); + if (watchFiles.length === 0) { + throw new Error('No watch files found after resolving watchPaths'); + } + await runAllPasses(projectRoot, options); + + await watchWithChokidar({ + projectRoot, + watchTargets: watchFiles, + label: 'concatenate-files watch', + onRebuild: () => runAllPasses(projectRoot, options), + }); +} + interface ResolvedConcatenate { projectRoot: string; - resolvedFiles: string[]; - outputPath: string; options: ConcatenateFilesExecutorSchema; } export default createExecutor({ name: 'ConcatenateFiles', - resolve: async (options, { projectRoot }) => { + resolve: (options, { projectRoot }) => { if (!options.sourceFiles?.length) { throw new Error(ERROR_SOURCE_FILES_EMPTY); } - const resolvedFiles = await resolveSourceFiles(options.sourceFiles, projectRoot); - if (resolvedFiles.length === 0) { - throw new Error(ERROR_NO_FILES_RESOLVED); + return { projectRoot, options }; + }, + run: async ({ projectRoot, options }) => { + if (options.watch) { + await runWatchBuild(projectRoot, options); + return; } - return { - projectRoot, - resolvedFiles, - outputPath: path.resolve(projectRoot, options.outputFile), - options, - }; - }, - run: async ({ projectRoot, resolvedFiles, outputPath, options }) => { - logger.verbose(`Concatenating ${resolvedFiles.length} files...`); - await concatToFile(outputPath, { - sourceFiles: resolvedFiles, - header: options.header, - footer: options.footer, - extractPattern: options.extractPattern, - extractPatternFlags: options.extractPatternFlags, - transforms: options.transforms, - normalizeLineEndings: options.normalizeLineEndings, - separator: options.separator, - }); - logger.verbose(`Created: ${path.relative(projectRoot, outputPath)}`); + await runAllPasses(projectRoot, options); }, }); diff --git a/packages/nx-infra-plugin/src/executors/concatenate-files/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/concatenate-files/executor.e2e.spec.ts index 7e34cbdd15fb..ddebe8a4bdad 100644 --- a/packages/nx-infra-plugin/src/executors/concatenate-files/executor.e2e.spec.ts +++ b/packages/nx-infra-plugin/src/executors/concatenate-files/executor.e2e.spec.ts @@ -5,6 +5,46 @@ import { ConcatenateFilesExecutorSchema } from './schema'; import { createTempDir, cleanupTempDir, createMockContext } from '../../utils/test-utils'; import { writeFileText, readFileText } from '../../utils'; +type WatchHandler = (event: string, file: string) => void; + +function getWatchHandler(): WatchHandler | undefined { + return (globalThis as unknown as { __concatWatchHandler?: WatchHandler }).__concatWatchHandler; +} + +// Mock chokidar so the executor's projectRequire('chokidar') resolves; the mock captures +// the change 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.__concatWatchHandler = 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'); +} + describe('ConcatenateFilesExecutor E2E', () => { let tempDir: string; let context = createMockContext(); @@ -89,4 +129,92 @@ describe('ConcatenateFilesExecutor E2E', () => { expect(versionPos).toBeLessThan(requirePos); expect(requirePos).toBeLessThan(exportPos); }); + + it('runs additionalPasses after the primary output', async () => { + await writeFileText(path.join(projectDir, 'a.js'), 'AAA'); + await writeFileText(path.join(projectDir, 'b.js'), 'BBB'); + + const options: ConcatenateFilesExecutorSchema = { + sourceFiles: ['./a.js', './b.js'], + outputFile: './out/primary.js', + separator: '\n', + additionalPasses: [ + { + sourceFiles: ['./out/primary.js'], + outputFile: './out/derived.js', + transforms: [{ find: 'A', replace: 'X', flags: 'g' }], + }, + ], + }; + + const result = await executor(options, context); + expect(result.success).toBe(true); + + expect(await readFileText(path.join(projectDir, 'out', 'primary.js'))).toBe('AAA\nBBB'); + // derived pass reads the primary output and applies its own transform + expect(await readFileText(path.join(projectDir, 'out', 'derived.js'))).toBe('XXX\nBBB'); + }); + + it('watch mode rebuilds all passes on change', async () => { + writeChokidarMock(projectDir); + await writeFileText(path.join(projectDir, 'a.js'), 'AAA'); + await writeFileText(path.join(projectDir, 'b.js'), 'BBB'); + + const options: ConcatenateFilesExecutorSchema = { + sourceFiles: ['./a.js', './b.js'], + outputFile: './out/primary.js', + separator: '\n', + additionalPasses: [{ sourceFiles: ['./out/primary.js'], outputFile: './out/derived.js' }], + watch: true, + watchPaths: ['./*.js'], + }; + + const primaryPath = path.join(projectDir, 'out', 'primary.js'); + const derivedPath = path.join(projectDir, 'out', 'derived.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(primaryPath) && fs.existsSync(derivedPath)); + expect(await readFileText(primaryPath)).toBe('AAA\nBBB'); + expect(await readFileText(derivedPath)).toBe('AAA\nBBB'); + expect(typeof getWatchHandler()).toBe('function'); + + await writeFileText(path.join(projectDir, 'a.js'), 'CCC'); + getWatchHandler()?.('change', path.join(projectDir, 'a.js')); + + await waitFor(async () => (await readFileText(primaryPath)) === 'CCC\nBBB'); + expect(await readFileText(derivedPath)).toBe('CCC\nBBB'); + + stopWatch?.(); + stopWatch = undefined; + + expect((await run).success).toBe(true); + run = undefined; + } finally { + // Ensure the long-running watch executor is stopped even if an assertion fails above. + stopWatch?.(); + if (run) { + await run; + } + (process as unknown as { once: typeof process.once }).once = originalOnce; + delete (globalThis as unknown as { __concatWatchHandler?: WatchHandler }) + .__concatWatchHandler; + } + }); }); diff --git a/packages/nx-infra-plugin/src/executors/concatenate-files/schema.json b/packages/nx-infra-plugin/src/executors/concatenate-files/schema.json index 878245ad338a..1af73dd7f4c5 100644 --- a/packages/nx-infra-plugin/src/executors/concatenate-files/schema.json +++ b/packages/nx-infra-plugin/src/executors/concatenate-files/schema.json @@ -52,7 +52,53 @@ "type": "string", "description": "Separator between concatenated files (default: '\\n')", "default": "\n" + }, + "additionalPasses": { + "type": "array", + "description": "Extra concatenation passes run after the primary output, in order (e.g. a derived output built from the primary). Each pass is resolved at run time.", + "items": { "$ref": "#/definitions/pass" } + }, + "watch": { + "type": "boolean", + "description": "Watch the source files and rebuild all passes on changes.", + "default": false + }, + "watchPaths": { + "type": "array", + "description": "Glob patterns to watch in watch mode (defaults to sourceFiles when omitted).", + "items": { "type": "string" } } }, - "required": ["sourceFiles", "outputFile"] + "required": ["sourceFiles", "outputFile"], + "definitions": { + "pass": { + "type": "object", + "properties": { + "sourceFiles": { + "type": "array", + "items": { "type": "string" } + }, + "outputFile": { "type": "string" }, + "extractPattern": { "type": "string" }, + "extractPatternFlags": { "type": "string" }, + "header": { "type": "string" }, + "footer": { "type": "string" }, + "transforms": { + "type": "array", + "items": { + "type": "object", + "properties": { + "find": { "type": "string" }, + "replace": { "type": "string" }, + "flags": { "type": "string" } + }, + "required": ["find", "replace"] + } + }, + "normalizeLineEndings": { "type": "boolean" }, + "separator": { "type": "string" } + }, + "required": ["sourceFiles", "outputFile"] + } + } } diff --git a/packages/nx-infra-plugin/src/executors/concatenate-files/schema.ts b/packages/nx-infra-plugin/src/executors/concatenate-files/schema.ts index 4e22150e5d5b..6519badf220d 100644 --- a/packages/nx-infra-plugin/src/executors/concatenate-files/schema.ts +++ b/packages/nx-infra-plugin/src/executors/concatenate-files/schema.ts @@ -4,7 +4,7 @@ export interface TransformRule { flags?: string; } -export interface ConcatenateFilesExecutorSchema { +export interface ConcatenatePass { sourceFiles: string[]; outputFile: string; extractPattern?: string; @@ -15,3 +15,9 @@ export interface ConcatenateFilesExecutorSchema { normalizeLineEndings?: boolean; separator?: string; } + +export interface ConcatenateFilesExecutorSchema extends ConcatenatePass { + additionalPasses?: ConcatenatePass[]; + watch?: boolean; + watchPaths?: string[]; +} diff --git a/packages/nx-infra-plugin/src/executors/generate-community-locales/executor.e2e.spec.ts b/packages/nx-infra-plugin/src/executors/generate-community-locales/executor.e2e.spec.ts new file mode 100644 index 000000000000..57da42b1749f --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/generate-community-locales/executor.e2e.spec.ts @@ -0,0 +1,108 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import executor from './executor'; +import { GenerateCommunityLocalesExecutorSchema } from './schema'; +import { + writeFileText, + readFileText, + cleanupTempDir, + createTempDir, + createMockContext, +} from '../../utils'; + +const PROJECT_SUBPATH = ['packages', 'test-lib'] as const; + +const EN_JSON = `{ + "en": { + "Yes": "Yes", + "No": "No", + "Cancel": "Cancel", + "Quote": "Say \\"hi\\"", + "Loading": "Loading..." + } +} +`; + +const FR_JSON_INPUT = `{ + "fr": { + "Yes": "Oui", + "No": "TODO: Non", + "Quote": "Dire \\"salut\\"", + "Loading": "Chargement...", + "Extra": "Extra" + } +} +`; + +const EXPECTED_FR = `{ + "fr": { + "Yes": "Oui", + "No": "No", + "Cancel": "Cancel", + "Quote": "Dire \\"salut\\"", + "Loading": "Chargement..." + } +} +`; + +interface Fixture { + projectDir: string; + messagesDir: string; +} + +async function createFixture(tempDir: string): Promise { + const projectDir = path.join(tempDir, ...PROJECT_SUBPATH); + const messagesDir = path.join(projectDir, 'js', 'localization', 'messages'); + + fs.mkdirSync(messagesDir, { recursive: true }); + + await writeFileText(path.join(messagesDir, 'en.json'), EN_JSON); + await writeFileText(path.join(messagesDir, 'fr.json'), FR_JSON_INPUT); + + return { projectDir, messagesDir }; +} + +describe('GenerateCommunityLocalesExecutor E2E', () => { + let tempDir: string; + let context = createMockContext(); + let fixture: Fixture; + + beforeEach(async () => { + tempDir = createTempDir('nx-community-locales-e2e-'); + context = createMockContext({ root: tempDir }); + fixture = await createFixture(tempDir); + }); + + afterEach(() => { + cleanupTempDir(tempDir); + }); + + it('normalizes community locale files against the default locale', async () => { + const options: GenerateCommunityLocalesExecutorSchema = { + messagesDir: './js/localization/messages', + defaultLocale: 'en', + }; + + const result = await executor(options, context); + + expect(result.success).toBe(true); + + const frContent = await readFileText(path.join(fixture.messagesDir, 'fr.json')); + expect(frContent).toBe(EXPECTED_FR); + }); + + it('leaves the default locale file untouched', async () => { + const result = await executor({}, context); + + expect(result.success).toBe(true); + + const enContent = await readFileText(path.join(fixture.messagesDir, 'en.json')); + expect(enContent).toBe(EN_JSON); + }); + + it('fails when the messages directory is missing', async () => { + const result = await executor({ messagesDir: './js/localization/does-not-exist' }, context); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/nx-infra-plugin/src/executors/generate-community-locales/executor.ts b/packages/nx-infra-plugin/src/executors/generate-community-locales/executor.ts new file mode 100644 index 000000000000..76ffddcce1f7 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/generate-community-locales/executor.ts @@ -0,0 +1 @@ +export { default } from './generate-community-locales.impl'; diff --git a/packages/nx-infra-plugin/src/executors/generate-community-locales/generate-community-locales.impl.ts b/packages/nx-infra-plugin/src/executors/generate-community-locales/generate-community-locales.impl.ts new file mode 100644 index 000000000000..afb550e72f70 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/generate-community-locales/generate-community-locales.impl.ts @@ -0,0 +1,104 @@ +import { logger } from '@nx/devkit'; +import * as path from 'path'; +import * as fs from 'fs'; +import { createExecutor } from '../../utils/create-executor'; +import { readFileText, writeFileText } from '../../utils/file-operations'; +import { discoverFiles } from '../../utils/glob-discovery'; +import { GenerateCommunityLocalesExecutorSchema } from './schema'; + +const DEFAULT_MESSAGES_DIR = './js/localization/messages'; +const DEFAULT_LOCALE = 'en'; +const TODO_MARKER = 'TODO'; + +const ERROR_MESSAGES = { + MESSAGES_DIR_NOT_FOUND: (directory: string) => `Messages directory not found: ${directory}`, + DEFAULT_LOCALE_NOT_FOUND: (filePath: string) => `Default locale file not found: ${filePath}`, +} as const; + +interface LocaleDictionary { + [key: string]: string; +} + +function normalizeLocaleFile( + defaultFile: string, + defaultDictionaryKeys: string[], + defaultLocale: string, + fileContents: string, +): string { + const parsedFile = JSON.parse(fileContents) as Record; + + const [locale] = Object.keys(parsedFile); + const dictionary = parsedFile[locale]; + + let newFile = defaultFile.replace(`"${defaultLocale}"`, `"${locale}"`); + + defaultDictionaryKeys.forEach((key) => { + let replaceValue: string | null = null; + if (Object.prototype.hasOwnProperty.call(dictionary, key)) { + const val = dictionary[key]; + if (!val.includes(TODO_MARKER)) { + replaceValue = val.replace(/"/g, '\\"'); + } + } + + if (replaceValue != null) { + newFile = newFile.replace(new RegExp(`"${key}":.*"(,)?`), `"${key}": "${replaceValue}"$1`); + } + }); + + return newFile; +} + +interface ResolvedGenerateCommunityLocales { + messagesDir: string; + defaultLocale: string; +} + +export default createExecutor< + GenerateCommunityLocalesExecutorSchema, + ResolvedGenerateCommunityLocales +>({ + name: 'Generate Community Locales', + resolve: (options, { projectRoot }) => ({ + messagesDir: path.join(projectRoot, options.messagesDir || DEFAULT_MESSAGES_DIR), + defaultLocale: options.defaultLocale || DEFAULT_LOCALE, + }), + run: async ({ messagesDir, defaultLocale }) => { + if (!fs.existsSync(messagesDir)) { + throw new Error(ERROR_MESSAGES.MESSAGES_DIR_NOT_FOUND(messagesDir)); + } + + const defaultFilePath = path.join(messagesDir, `${defaultLocale}.json`); + if (!fs.existsSync(defaultFilePath)) { + throw new Error(ERROR_MESSAGES.DEFAULT_LOCALE_NOT_FOUND(defaultFilePath)); + } + + const defaultFile = await readFileText(defaultFilePath); + const defaultDictionaryKeys = Object.keys( + (JSON.parse(defaultFile) as Record)[defaultLocale], + ); + + const localeFiles = await discoverFiles({ + cwd: messagesDir, + includePatterns: ['*.json'], + excludePatterns: [`${defaultLocale}.json`], + }); + + logger.verbose(`Normalizing ${localeFiles.length} community locale files...`); + + await Promise.all( + localeFiles.map(async (filePath) => { + const fileContents = await readFileText(filePath); + const newFile = normalizeLocaleFile( + defaultFile, + defaultDictionaryKeys, + defaultLocale, + fileContents, + ); + await writeFileText(filePath, newFile); + }), + ); + + logger.verbose(`Community locale files normalized in ${messagesDir}`); + }, +}); diff --git a/packages/nx-infra-plugin/src/executors/generate-community-locales/schema.json b/packages/nx-infra-plugin/src/executors/generate-community-locales/schema.json new file mode 100644 index 000000000000..8fc0c2bee210 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/generate-community-locales/schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/schema", + "title": "Generate Community Locales Executor Schema", + "description": "Normalize community locale message files against the default locale dictionary", + "type": "object", + "properties": { + "messagesDir": { + "type": "string", + "description": "Directory containing locale message JSON files (e.g., en.json, de.json)", + "default": "./js/localization/messages" + }, + "defaultLocale": { + "type": "string", + "description": "Locale used as the canonical source of keys, ordering and formatting", + "default": "en" + } + } +} diff --git a/packages/nx-infra-plugin/src/executors/generate-community-locales/schema.ts b/packages/nx-infra-plugin/src/executors/generate-community-locales/schema.ts new file mode 100644 index 000000000000..5366cd0c8a84 --- /dev/null +++ b/packages/nx-infra-plugin/src/executors/generate-community-locales/schema.ts @@ -0,0 +1,4 @@ +export interface GenerateCommunityLocalesExecutorSchema { + messagesDir?: string; + defaultLocale?: string; +} 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 0e6836447ceb..f8f5f5211aa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,9 @@ importers: axe-core: specifier: 'catalog:' version: 4.11.3 + chokidar: + specifier: 5.0.0 + version: 5.0.0 devextreme-internal-tools: specifier: catalog:tools version: 22.0.0 @@ -1405,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) @@ -2005,9 +2014,6 @@ importers: autoprefixer: specifier: 10.5.0 version: 10.5.0(postcss@8.5.10) - chokidar: - specifier: 5.0.0 - version: 5.0.0 clean-css: specifier: 5.3.3 version: 5.3.3 @@ -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