Skip to content

Commit 283094c

Browse files
Migrate gulp js-bundles-watch to NX (#34308)
Signed-off-by: Aliullov Vlad <91639107+GoodDayForSurf@users.noreply.github.com> Co-authored-by: Andrey Vorobev <738482+vorobey@users.noreply.github.com>
1 parent 2dd0777 commit 283094c

8 files changed

Lines changed: 219 additions & 109 deletions

File tree

packages/devextreme/build/gulp/js-bundles.js

Lines changed: 0 additions & 97 deletions
This file was deleted.

packages/devextreme/gulpfile.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,26 @@ gulp.task('clean', shell.task('pnpm nx clean:artifacts devextreme'));
1212

1313
require('./build/gulp/bundler-config');
1414
require('./build/gulp/transpile');
15-
require('./build/gulp/js-bundles');
1615
require('./build/gulp/localization');
1716

17+
gulp.task('js-bundles-prod', shell.task(
18+
context.uglify
19+
? 'pnpm nx run devextreme:bundle:prod -c production'
20+
: 'pnpm nx run devextreme:bundle:prod'
21+
));
22+
23+
gulp.task('js-bundles-debug', shell.task(
24+
context.uglify
25+
? 'pnpm nx run devextreme:bundle:debug -c production'
26+
: 'pnpm nx run devextreme:bundle:debug'
27+
));
28+
29+
gulp.task('js-bundles-watch', shell.task(
30+
context.uglify
31+
? 'pnpm nx run devextreme:bundle:watch -c production'
32+
: 'pnpm nx run devextreme:bundle:watch'
33+
));
34+
1835
function getTranspileConfig() {
1936
if(env.TEST_CI) {
2037
return 'ci';

packages/devextreme/project.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,34 @@
627627
"{projectRoot}/artifacts/js/dx.{all,web,viz,ai-integration,custom}.debug.js"
628628
]
629629
},
630+
"bundle:watch": {
631+
"executor": "devextreme-nx-infra-plugin:bundle",
632+
"options": {
633+
"watch": true,
634+
"entries": [
635+
"bundles/dx.all.js",
636+
"bundles/dx.web.js",
637+
"bundles/dx.viz.js",
638+
"bundles/dx.ai-integration.js",
639+
"bundles/dx.custom.js"
640+
],
641+
"sourceDir": "./artifacts/transpiled-renovation-npm",
642+
"outDir": "./artifacts/js",
643+
"mode": "debug",
644+
"webpackConfigPath": "./webpack.config.js",
645+
"applyLicenseHeaders": {
646+
"prependAfterLicense": "\"use strict\";\n\n",
647+
"separator": "",
648+
"includePatterns": ["dx.*.debug.js"]
649+
}
650+
},
651+
"configurations": {
652+
"production": {
653+
"sourceMap": false
654+
}
655+
},
656+
"cache": false
657+
},
630658
"bundle:prod": {
631659
"executor": "nx:run-commands",
632660
"options": {

packages/nx-infra-plugin/AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ Gulp tasks that have been migrated to Nx executor targets (the gulp task is now
7676
| `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')`. |
7777
| `test-env` | `test-env` | Uses `nx:run-commands` to wrap the existing `node ./testing/launch` script (compiles the QUnit runner, starts the test server on port 20060, opens the browser). `cache: false`, no outputs (long-running server). `cwd` is `{projectRoot}`. The gulp task delegates via `shell.task('pnpm nx test-env devextreme')`. Still a `dev-watch` member, so the gulp registration stays as a delegate; full removal is deferred until `dev-watch` is migrated. |
7878
| `transpile-systemjs` | `build:systemjs` | Uses `nx:run-commands` (no custom executor — same precedent as `test-env`) to run `node testing/systemjs-builder.js --transpile=<mode>` 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. |
79+
| `js-bundles-watch` | `bundle:watch` | Webpack watch via `devextreme-nx-infra-plugin:bundle` with `watch: true`. Does not run `compress:bundles`. Gulp delegates via `shell.task('pnpm nx run devextreme:bundle:watch')`. |
80+
| `js-bundles-prod` | `bundle:prod` | Already delegated via `shell.task('pnpm nx run devextreme:bundle:prod ...')`. |
81+
| `js-bundles-debug` | `bundle:debug` | Already delegated via `shell.task('pnpm nx run devextreme:bundle:debug ...')`. |
7982

8083
When migrating additional gulp tasks, follow the same pattern:
8184

packages/nx-infra-plugin/src/executors/bundle/bundle.impl.ts

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function createWebpackConfig(
5151
mode: 'debug' | 'production',
5252
projectRoot: string,
5353
sourceMap: boolean,
54+
watch: boolean,
5455
): Configuration {
5556
const config: Configuration = {
5657
...baseConfig,
@@ -68,6 +69,10 @@ function createWebpackConfig(
6869
minimize: false,
6970
};
7071

72+
if (watch) {
73+
config.mode = 'development';
74+
}
75+
7176
if (mode === 'debug') {
7277
config.output = {
7378
...(config.output || {}),
@@ -98,6 +103,19 @@ function createWebpackConfig(
98103
return config;
99104
}
100105

106+
function collectWebpackErrorMessages(stats: Stats): string {
107+
const info = stats.toJson({ errors: true });
108+
return (info.errors || []).map((entry) => entry.message).join('\n');
109+
}
110+
111+
function logWebpackWarnings(stats: Stats): void {
112+
if (!stats.hasWarnings()) {
113+
return;
114+
}
115+
const info = stats.toJson({ warnings: true });
116+
(info.warnings || []).forEach((warning) => logger.warn(warning.message));
117+
}
118+
101119
function runWebpack(webpack: typeof import('webpack'), config: Configuration): Promise<Stats> {
102120
return new Promise((resolve, reject) => {
103121
webpack(config, (err, stats) => {
@@ -110,16 +128,96 @@ function runWebpack(webpack: typeof import('webpack'), config: Configuration): P
110128
return;
111129
}
112130
if (stats.hasErrors()) {
113-
const info = stats.toJson({ errors: true });
114-
const errorMessages = (info.errors || []).map((entry) => entry.message).join('\n');
115-
reject(new Error(errorMessages));
131+
reject(new Error(collectWebpackErrorMessages(stats)));
116132
return;
117133
}
118134
resolve(stats);
119135
});
120136
});
121137
}
122138

139+
async function runWebpackWatch(
140+
webpack: typeof import('webpack'),
141+
config: Configuration,
142+
onSuccess: (stats: Stats) => Promise<void>,
143+
): Promise<void> {
144+
await new Promise<void>((resolve) => {
145+
const compiler = webpack(config);
146+
let watching: ReturnType<typeof compiler.watch> | undefined;
147+
let isClosing = false;
148+
149+
let postBuildChain = Promise.resolve();
150+
151+
const removeSignalHandlers = (): void => {
152+
process.removeListener('SIGINT', stopWatching);
153+
process.removeListener('SIGTERM', stopWatching);
154+
};
155+
156+
const finishShutdown = (): void => {
157+
void postBuildChain.finally(() => {
158+
resolve();
159+
});
160+
};
161+
162+
const stopWatching = (): void => {
163+
if (isClosing) {
164+
return;
165+
}
166+
isClosing = true;
167+
removeSignalHandlers();
168+
169+
if (!watching) {
170+
finishShutdown();
171+
return;
172+
}
173+
watching.close(() => {
174+
setTimeout(finishShutdown, 100);
175+
});
176+
};
177+
178+
watching = compiler.watch(
179+
{
180+
aggregateTimeout: 200,
181+
ignored: /node_modules/,
182+
...(process.platform === 'win32' ? { poll: 1000 } : {}),
183+
},
184+
(err, stats) => {
185+
if (isClosing) {
186+
return;
187+
}
188+
if (err) {
189+
logger.error(err.message);
190+
return;
191+
}
192+
if (!stats) {
193+
return;
194+
}
195+
if (stats.hasErrors()) {
196+
logger.error(collectWebpackErrorMessages(stats));
197+
return;
198+
}
199+
200+
logWebpackWarnings(stats);
201+
202+
postBuildChain = postBuildChain
203+
.then(() => onSuccess(stats))
204+
.then(() => {
205+
logger.info('bundle watch: rebuild complete');
206+
})
207+
.catch((error) => {
208+
const message = error instanceof Error ? error.message : String(error);
209+
logger.error(`bundle watch post-build failed: ${message}`);
210+
});
211+
},
212+
);
213+
214+
logger.info('bundle watch mode is watching for changes...');
215+
216+
process.once('SIGINT', stopWatching);
217+
process.once('SIGTERM', stopWatching);
218+
});
219+
}
220+
123221
function loadWebpackConfig(resolvedConfigPath: string): Configuration {
124222
if (!fs.existsSync(resolvedConfigPath)) {
125223
throw new Error(ERROR_MESSAGES.WEBPACK_CONFIG_NOT_FOUND(resolvedConfigPath));
@@ -146,6 +244,7 @@ interface ResolvedBundle {
146244
resolvedOutDir: string;
147245
mode: 'debug' | 'production';
148246
sourceMap: boolean;
247+
watch: boolean;
149248
webpack: typeof import('webpack');
150249
baseConfig: Configuration;
151250
licenseHeaders?: ResolvedLicenseHeaders;
@@ -195,6 +294,7 @@ export default createExecutor<BundleExecutorSchema, ResolvedBundle>({
195294
mode,
196295
webpackConfigPath = './webpack.config.js',
197296
sourceMap = true,
297+
watch = false,
198298
applyLicenseHeaders,
199299
} = options;
200300

@@ -217,6 +317,7 @@ export default createExecutor<BundleExecutorSchema, ResolvedBundle>({
217317
resolvedOutDir,
218318
mode,
219319
sourceMap,
320+
watch,
220321
webpack,
221322
baseConfig,
222323
licenseHeaders,
@@ -232,19 +333,31 @@ export default createExecutor<BundleExecutorSchema, ResolvedBundle>({
232333
resolved.mode,
233334
resolved.projectRoot,
234335
resolved.sourceMap,
336+
resolved.watch,
235337
);
236338

237339
logger.verbose(`Bundling ${resolved.entries.length} entries in ${resolved.mode} mode`);
238340
logger.verbose(`Source: ${resolved.resolvedSourceDir}`);
239341
logger.verbose(`Output: ${resolved.resolvedOutDir}`);
240342

343+
const applyHeadersIfNeeded = async (): Promise<void> => {
344+
if (resolved.licenseHeaders) {
345+
await applyLicenseHeadersStep(resolved.resolvedOutDir, resolved.licenseHeaders);
346+
}
347+
};
348+
349+
if (resolved.watch) {
350+
await runWebpackWatch(resolved.webpack, config, async (stats) => {
351+
const assets = Object.keys(stats.compilation.assets);
352+
logger.verbose(`Produced ${assets.length} bundle(s): ${assets.join(', ')}`);
353+
await applyHeadersIfNeeded();
354+
});
355+
return;
356+
}
357+
241358
try {
242359
const stats = await runWebpack(resolved.webpack, config);
243-
244-
if (stats.hasWarnings()) {
245-
const info = stats.toJson({ warnings: true });
246-
(info.warnings || []).forEach((warning) => logger.warn(warning.message));
247-
}
360+
logWebpackWarnings(stats);
248361

249362
const assets = Object.keys(stats.compilation.assets);
250363
logger.verbose(`Produced ${assets.length} bundle(s): ${assets.join(', ')}`);
@@ -253,8 +366,6 @@ export default createExecutor<BundleExecutorSchema, ResolvedBundle>({
253366
throw new Error(ERROR_MESSAGES.WEBPACK_ERROR(message));
254367
}
255368

256-
if (resolved.licenseHeaders) {
257-
await applyLicenseHeadersStep(resolved.resolvedOutDir, resolved.licenseHeaders);
258-
}
369+
await applyHeadersIfNeeded();
259370
},
260371
});

0 commit comments

Comments
 (0)