@eui/cli 22.0.0-alpha.6 → 22.0.0-alpha.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +357 -88
  2. package/bin/eui-cli.js +26 -0
  3. package/bin/scripts/migrate.js +5 -0
  4. package/lib/app-utils/build.js +26 -0
  5. package/lib/app-utils/migrate.js +610 -0
  6. package/lib/app-utils/serve.js +32 -0
  7. package/lib/build.js +28 -7
  8. package/lib/cli.js +16 -2
  9. package/lib/config.js +10 -1
  10. package/lib/skeletons/_angular/base/mock/app/routes/{index.js → index.cjs} +1 -1
  11. package/lib/skeletons/_angular/base/mock/app/routes/{user_routes.js → user_routes.cjs} +1 -1
  12. package/lib/skeletons/_angular/base/mock/{server.js → server.cjs} +1 -1
  13. package/lib/skeletons/_angular/base/package.json +4 -3
  14. package/lib/skeletons/_angular/base-mobile/package.json +5 -4
  15. package/lib/skeletons/_angular/options/eds/src/app/app.component.html +35 -0
  16. package/lib/skeletons/_angular/options/eds/src/app/app.component.spec.ts +56 -0
  17. package/lib/skeletons/_angular/options/eds/src/app/app.component.ts +44 -0
  18. package/lib/skeletons/_angular/options/eds/src/index.html +21 -0
  19. package/lib/skeletons/_angular/options/pnpm/package.json +98 -48
  20. package/lib/skeletons/_react/base/README.md +65 -0
  21. package/lib/skeletons/_react/base/eslint.config.mjs +17 -0
  22. package/lib/skeletons/_react/base/gitignore_TO_REPLACE +34 -0
  23. package/lib/skeletons/_react/base/index.html +13 -0
  24. package/lib/skeletons/_react/base/mock/app/models/user.cjs +6 -0
  25. package/lib/skeletons/_react/base/mock/app/routes/index.cjs +5 -0
  26. package/lib/skeletons/_react/base/mock/app/routes/user_routes.cjs +44 -0
  27. package/lib/skeletons/_react/base/mock/db/db.json +10 -0
  28. package/lib/skeletons/_react/base/mock/server.cjs +23 -0
  29. package/lib/skeletons/_react/base/package.json +29 -0
  30. package/lib/skeletons/_react/base/public/.gitkeep +0 -0
  31. package/lib/skeletons/_react/base/src/App.tsx +73 -0
  32. package/lib/skeletons/_react/base/src/main.tsx +30 -0
  33. package/lib/skeletons/_react/base/src/pages/about.tsx +27 -0
  34. package/lib/skeletons/_react/base/src/pages/home.tsx +50 -0
  35. package/lib/skeletons/_react/base/src/routes.ts +18 -0
  36. package/lib/skeletons/_react/base/src/styles.css +37 -0
  37. package/lib/skeletons/_react/base/src/vite-env.d.ts +1 -0
  38. package/lib/skeletons/_react/base/tsconfig.app.json +18 -0
  39. package/lib/skeletons/_react/base/tsconfig.json +7 -0
  40. package/lib/skeletons/_react/base/tsconfig.node.json +15 -0
  41. package/lib/skeletons/_react/base/vite.config.ts +77 -0
  42. package/lib/skeletons/_react/options/pnpm/package.json +46 -0
  43. package/lib/utils.js +26 -2
  44. package/package.json +1 -1
  45. package/sonar-project.properties +0 -14
  46. /package/lib/skeletons/_angular/base/mock/app/models/{user.js → user.cjs} +0 -0
@@ -0,0 +1,610 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const childProcess = require('child_process');
5
+ const execFileSync = childProcess.execFileSync;
6
+ const { MultiSelect } = require('enquirer');
7
+
8
+ const utils = require('../utils');
9
+
10
+ /**
11
+ * Installs graceful cancellation handlers for the interactive enquirer prompts.
12
+ *
13
+ * Without these, pressing Ctrl+C mid-prompt makes enquirer call into an already-closed readline
14
+ * interface and Node throws an uncaught `ERR_USE_AFTER_CLOSE` with a stack trace. This mirrors the
15
+ * handling in `lib/cli.js` (the `new` command): swallow that specific readline error and treat
16
+ * SIGINT as a clean "cancelled by user" exit. Idempotent — safe to call more than once.
17
+ */
18
+ let cancelHandlersInstalled = false;
19
+ const installCancelHandlers = () => {
20
+ if (cancelHandlersInstalled) {
21
+ return;
22
+ }
23
+ cancelHandlersInstalled = true;
24
+
25
+ // enquirer can reach into a closed readline on Ctrl+C; swallow only that specific error.
26
+ process.on('uncaughtException', (err) => {
27
+ if (err && err.code === 'ERR_USE_AFTER_CLOSE') {
28
+ process.exit(130); // 128 + SIGINT(2)
29
+ }
30
+ throw err;
31
+ });
32
+
33
+ // Ctrl+C: exit cleanly with the conventional 130 code.
34
+ process.on('SIGINT', () => {
35
+ console.log('\n\nOperation cancelled by user');
36
+ process.exit(130);
37
+ });
38
+ };
39
+
40
+ /**
41
+ * Reads the set of schematic names a package actually exposes, as installed in the target
42
+ * application's `node_modules` (resolved from `process.cwd()`).
43
+ *
44
+ * The available schematics depend on the exact installed version of the package — e.g. Angular 21
45
+ * has no `service-migration`, Angular 22 does; a given `@eui/core` build may or may not ship
46
+ * `migrate-ecl`. Introspecting the installed collection lets the command self-adapt instead of
47
+ * relying on a hardcoded, version-specific list.
48
+ *
49
+ * @param {string} pkg - Package name, e.g. `@angular/core` or `@eui/core`.
50
+ * @returns {Set<string>|null} Set of schematic names, or null if the collection can't be resolved.
51
+ */
52
+ const loadInstalledSchematics = (pkg) => {
53
+ try {
54
+ // Resolve the package.json from the APP's node_modules (not the CLI's own).
55
+ const pkgJsonPath = require.resolve(`${pkg}/package.json`, { paths: [process.cwd()] });
56
+ const pkgJson = require(pkgJsonPath);
57
+ const collectionRel = pkgJson.schematics;
58
+ if (!collectionRel) {
59
+ return null; // package exposes no schematics collection
60
+ }
61
+ const collectionPath = path.resolve(path.dirname(pkgJsonPath), collectionRel);
62
+ const collection = require(collectionPath);
63
+ return new Set(Object.keys(collection.schematics || {}));
64
+ } catch (e) {
65
+ utils.logDebugTrace('migrate.js', 'loadInstalledSchematics', e);
66
+ return null;
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Reads the installed major version of a package from the target application's `node_modules`.
72
+ *
73
+ * @param {string} pkg - Package name, e.g. `@angular/core`.
74
+ * @returns {number|null} The major version number, or null if it can't be resolved/parsed.
75
+ */
76
+ const getInstalledMajor = (pkg) => {
77
+ try {
78
+ const pkgJsonPath = require.resolve(`${pkg}/package.json`, { paths: [process.cwd()] });
79
+ const version = require(pkgJsonPath).version;
80
+ const major = parseInt(String(version).split('.')[0], 10);
81
+ return Number.isNaN(major) ? null : major;
82
+ } catch (e) {
83
+ utils.logDebugTrace('migrate.js', 'getInstalledMajor', e);
84
+ return null;
85
+ }
86
+ };
87
+
88
+ // The command's Angular migration group targets this Angular major only.
89
+ const SUPPORTED_ANGULAR_MAJOR = 22;
90
+
91
+ /**
92
+ * Returns a copy of GROUPS filtered to the schematics actually present in each group's installed
93
+ * package collection. A group whose package can't be resolved is kept as-is (best effort — let
94
+ * `ng` report the real error). A group with an `allSchematic` that isn't installed falls back to
95
+ * running its individual members. Schematics not found in the installed collection are dropped and
96
+ * reported, so a version mismatch warns-and-skips instead of hard-failing.
97
+ *
98
+ * @returns {{groups: typeof GROUPS, skipped: {group:string, name:string}[]}} Filtered groups and
99
+ * the list of schematics that were skipped because they are not installed.
100
+ */
101
+ const resolveAvailableGroups = () => {
102
+ const skipped = [];
103
+ const groups = GROUPS.map((group) => {
104
+ const available = loadInstalledSchematics(group.package);
105
+
106
+ // Can't introspect (package/collection not resolvable): keep the group unchanged.
107
+ if (available === null) {
108
+ return { ...group };
109
+ }
110
+
111
+ const schematics = group.schematics.filter((s) => {
112
+ if (available.has(s.name)) {
113
+ return true;
114
+ }
115
+ skipped.push({ group: group.key, name: s.name });
116
+ return false;
117
+ });
118
+
119
+ // Drop the aggregate if the installed collection doesn't have it (fall back to members).
120
+ const allSchematic = group.allSchematic && available.has(group.allSchematic)
121
+ ? group.allSchematic
122
+ : null;
123
+
124
+ return { ...group, schematics, allSchematic };
125
+ });
126
+
127
+ return { groups, skipped };
128
+ };
129
+
130
+ /**
131
+ * Resolves the Angular CLI entry script to run, from the target application's installation.
132
+ *
133
+ * Install layouts differ: the package's bin may be `bin/ng` or `bin/ng.js` depending on the
134
+ * Angular version; `@angular/cli` may be hoisted (monorepo / pnpm); and the convenience shim lives
135
+ * at `node_modules/.bin/ng`. We resolve, in order:
136
+ * 1. the real entry from `@angular/cli`'s own `bin.ng` field (via require.resolve from the app) —
137
+ * the most reliable, and a plain `.js` we can run with `node` on every OS;
138
+ * 2. common fixed locations: `@angular/cli/bin/ng.js`, `@angular/cli/bin/ng`;
139
+ * 3. the `node_modules/.bin/ng` shim as a last resort.
140
+ *
141
+ * @returns {string|null} Absolute path to the Angular CLI script, or null if none is found.
142
+ */
143
+ const resolveNgBin = () => {
144
+ const cwd = process.cwd();
145
+
146
+ // 1) Resolve via the package's own bin field (handles ng vs ng.js, and hoisting).
147
+ try {
148
+ const pkgJsonPath = require.resolve('@angular/cli/package.json', { paths: [cwd] });
149
+ const pkgJson = require(pkgJsonPath);
150
+ const binRel = typeof pkgJson.bin === 'string' ? pkgJson.bin : (pkgJson.bin && pkgJson.bin.ng);
151
+ if (binRel) {
152
+ const binAbs = path.resolve(path.dirname(pkgJsonPath), binRel);
153
+ if (utils.isFileExists(binAbs)) {
154
+ return binAbs;
155
+ }
156
+ }
157
+ } catch (e) {
158
+ utils.logDebugTrace('migrate.js', 'resolveNgBin', e);
159
+ }
160
+
161
+ // 2) & 3) Fall back to well-known locations relative to the app's node_modules.
162
+ const candidates = [
163
+ path.resolve(cwd, 'node_modules', '@angular', 'cli', 'bin', 'ng.js'),
164
+ path.resolve(cwd, 'node_modules', '@angular', 'cli', 'bin', 'ng'),
165
+ path.resolve(cwd, 'node_modules', '.bin', 'ng'),
166
+ ];
167
+ return candidates.find((c) => utils.isFileExists(c)) || null;
168
+ };
169
+
170
+ /**
171
+ * `eui-cli migrate` — run migration schematics on a standalone eUI application.
172
+ *
173
+ * A standalone eUI application (one that is NOT part of the MyWorkplace / CSDR ecosystem, where
174
+ * the csdr-engine runs these schematics automatically) is responsible for running its own
175
+ * migrations. This command executes the migration schematics directly via `ng generate`
176
+ * (NOT `ng update`, which only runs the version-gated subset tied to a package upgrade), so the
177
+ * developer can run — or re-run — any migration on demand.
178
+ *
179
+ * Migrations are organised into three groups. Each group lists the individual schematics it can
180
+ * run; when a whole group is selected without drilling into it, its `allSchematic` (an aggregate
181
+ * that runs every member in sequence) is executed when one exists, otherwise all members are run.
182
+ *
183
+ * - angular : the official Angular v22 migrations (https://v22.angular.dev/reference/migrations),
184
+ * invoked as `ng generate @angular/core:<name>`. Only v22 schematics are supported.
185
+ * - eui : the eUI breaking-change / modernisation schematics from `@eui/core`'s collection,
186
+ * invoked as `ng generate @eui/core:<name>`. The `migrate-all` aggregate runs them
187
+ * all in sequence.
188
+ * - ecl : the eUI-ECL template migration, `ng generate @eui/core:migrate-ecl`.
189
+ *
190
+ * Group order is angular → eui → ecl so framework-level migrations run before the eUI/ECL ones
191
+ * that build on them.
192
+ */
193
+ const GROUPS = [
194
+ {
195
+ key: 'angular',
196
+ label: 'Angular v22 migrations (v22.angular.dev/reference/migrations)',
197
+ package: '@angular/core',
198
+ // Angular v22 migration schematics only (https://v22.angular.dev/reference/migrations), each
199
+ // present in the v22 @angular/core schematics collection. No single "run all" aggregate exists
200
+ // for Angular migrations; running the whole group runs each member schematic in order.
201
+ allSchematic: null,
202
+ schematics: [
203
+ { name: 'standalone-migration', description: 'Standalone components' },
204
+ { name: 'control-flow-migration', description: 'Built-in control flow syntax (@if/@for/@switch)' },
205
+ { name: 'inject-migration', description: 'Constructor DI to the inject() function' },
206
+ { name: 'route-lazy-loading-migration', description: 'Convert eager component routes to lazy-loaded' },
207
+ { name: 'signal-input-migration', description: '@Input() fields to the signal input() API' },
208
+ { name: 'output-migration', description: '@Output() events to the output() function' },
209
+ { name: 'signal-queries-migration', description: 'Decorator queries to signal queries' },
210
+ { name: 'cleanup-unused-imports', description: 'Remove unused imports' },
211
+ { name: 'self-closing-tags-migration', description: 'Use self-closing tags where possible' },
212
+ { name: 'common-to-standalone-migration', description: 'CommonModule to individual standalone imports' },
213
+ { name: 'ngclass-to-class-migration', description: 'NgClass to class bindings' },
214
+ { name: 'ngstyle-to-style-migration', description: 'NgStyle to style bindings' },
215
+ { name: 'router-testing-module-migration', description: 'RouterTestingModule to RouterModule in TestBed' },
216
+ { name: 'service-migration', description: 'Eligible @Injectable classes to @Service' },
217
+ ],
218
+ },
219
+ {
220
+ key: 'eui',
221
+ label: 'eUI migrations (@eui/core breaking changes & modernisation)',
222
+ package: '@eui/core',
223
+ // `migrate-all` runs every eUI migration schematic in sequence — the default for the group.
224
+ allSchematic: 'migrate-all',
225
+ schematics: [
226
+ { name: 'migrate', description: 'Icons, modules and breaking-change replacements' },
227
+ { name: 'icon-migrate', description: 'Rename deprecated eUI icon names' },
228
+ { name: 'migrate-eui-tabs', description: 'eui-tabs label/content projection to header/body' },
229
+ { name: 'migrate-to-standalone', description: 'Deprecated eUI modules to standalone imports' },
230
+ { name: 'migrate-eui-alert', description: 'Remove deprecated eui-alert inputs' },
231
+ { name: 'migrate-eui-progress-circle', description: 'Rename deprecated eui-progress-circle inputs' },
232
+ { name: 'migrate-eui-popover', description: 'Remove deprecated eui-popover type input' },
233
+ { name: 'migrate-eui-icon-toggle', description: 'Rename eui-icon-toggle iconSet to iconSvgName' },
234
+ { name: 'migrate-eui-icon-svg', description: 'Rename removed eui-icon-svg inputs' },
235
+ { name: 'migrate-eui-fieldset', description: 'Rename eui-fieldset iconSvgType to iconSvgName' },
236
+ { name: 'migrate-eui-avatar', description: 'Remove deprecated eui-avatar isFlat input' },
237
+ { name: 'migrate-eui-editor', description: 'Rename eui-editor onEditorChanged to contentChange' },
238
+ { name: 'migrate-eui-discussion-thread', description: 'Remove trackByFn [trackBy] from eui-discussion-thread' },
239
+ { name: 'migrate-eui-button', description: 'Rename euiButtonCall to euiCTAButton' },
240
+ { name: 'migrate-eui-accent', description: 'Replace euiAccent directive with euiPrimary' },
241
+ { name: 'migrate-eui-toolbar-menu', description: 'eui-toolbar-menu to eui-toolbar-mega-menu' },
242
+ { name: 'migrate-eui-table', description: 'eui-table breaking changes' },
243
+ { name: 'migrate-eui-chip-list', description: 'eui-chip-list breaking changes' },
244
+ { name: 'migrate-eui-chip', description: 'Remove deprecated eui-chip isSquared input' },
245
+ { name: 'add-eui-imports', description: 'Ensure correct eUI imports for template selectors' },
246
+ { name: 'fix-no-multiple-empty-lines', description: 'Collapse multiple consecutive empty lines' },
247
+ ],
248
+ },
249
+ {
250
+ key: 'ecl',
251
+ label: 'ECL migration (eUI-ECL templates 19.x to 21.x)',
252
+ package: '@eui/core',
253
+ // ECL has a single schematic; running the group runs it directly.
254
+ allSchematic: 'migrate-ecl',
255
+ schematics: [
256
+ { name: 'migrate-ecl', description: 'Migrate eUI-ECL templates (selector/output renames + manual-action warnings)' },
257
+ ],
258
+ },
259
+ ];
260
+
261
+ // The "active" groups used for the current run. Defaults to the full static GROUPS; `run()`
262
+ // replaces it with the set filtered to what is actually installed in the target app. Keeping it
263
+ // module-level lets the parse/prompt/build helpers resolve against the same filtered data.
264
+ let activeGroups = GROUPS;
265
+ const setActiveGroups = (groups) => { activeGroups = groups; };
266
+
267
+ const allGroupKeys = () => activeGroups.map((g) => g.key);
268
+ const groupByKey = (key) => activeGroups.find((g) => g.key === key);
269
+
270
+ /**
271
+ * Parses a comma/space separated list argument into a lowercased, de-duplicated array.
272
+ *
273
+ * @param {string|boolean|undefined} value - Raw CLI value.
274
+ * @returns {string[]|null} The parsed tokens, or null when the argument was not provided.
275
+ */
276
+ const parseListArg = (value) => {
277
+ if (value === undefined || value === true || value === '') {
278
+ return null;
279
+ }
280
+ const tokens = String(value)
281
+ .split(/[,\s]+/)
282
+ .map((t) => t.trim().toLowerCase())
283
+ .filter(Boolean);
284
+ // de-duplicate, preserve first-seen order
285
+ return [...new Set(tokens)];
286
+ };
287
+
288
+ /**
289
+ * Resolves the groups requested via `--groups` (or legacy `--schematics`). Validates against the
290
+ * known group keys and exits on an unknown value so a typo never silently skips a migration.
291
+ *
292
+ * @param {string|boolean|undefined} groupsArg - Raw `--groups` / `--schematics` value.
293
+ * @returns {string[]|null} Group keys in canonical order, or null when not provided.
294
+ */
295
+ const parseGroupsArg = (groupsArg) => {
296
+ const requested = parseListArg(groupsArg);
297
+ if (requested === null) {
298
+ return null;
299
+ }
300
+ const unknown = requested.filter((k) => !allGroupKeys().includes(k));
301
+ if (unknown.length > 0) {
302
+ utils.logError(`Unknown migration group(s): ${unknown.join(', ')}`);
303
+ utils.logError(`Available groups: ${allGroupKeys().join(', ')}`);
304
+ process.exit(1);
305
+ }
306
+ return allGroupKeys().filter((k) => requested.includes(k));
307
+ };
308
+
309
+ /**
310
+ * Resolves specific schematic names requested via `--only`, validating each against the schematics
311
+ * of the (already-resolved) selected groups. Exits on an unknown schematic name.
312
+ *
313
+ * @param {string|boolean|undefined} onlyArg - Raw `--only` value.
314
+ * @param {string[]} selectedGroupKeys - Groups the schematics must belong to.
315
+ * @returns {string[]|null} Requested schematic names (validated), or null when not provided.
316
+ */
317
+ const parseOnlyArg = (onlyArg, selectedGroupKeys) => {
318
+ const requested = parseListArg(onlyArg);
319
+ if (requested === null) {
320
+ return null;
321
+ }
322
+ const available = new Set(
323
+ selectedGroupKeys
324
+ .map(groupByKey)
325
+ .flatMap((g) => g.schematics.map((s) => s.name.toLowerCase())),
326
+ );
327
+ const unknown = requested.filter((n) => !available.has(n));
328
+ if (unknown.length > 0) {
329
+ utils.logError(`Unknown schematic(s) for the selected group(s): ${unknown.join(', ')}`);
330
+ utils.logError(`Available schematics: ${[...available].join(', ')}`);
331
+ process.exit(1);
332
+ }
333
+ return requested;
334
+ };
335
+
336
+ /**
337
+ * Builds the ordered list of commands to execute from a per-group selection map.
338
+ *
339
+ * Resolution rules per group (key present in `selection`):
340
+ * - value `null` (whole group): run its `allSchematic` aggregate when one exists (e.g. eUI
341
+ * `migrate-all`), else every member schematic in canonical order.
342
+ * - value `string[]` (specific selection): run exactly those members, as individual
343
+ * `ng generate <package>:<name>` invocations, in canonical order.
344
+ *
345
+ * @param {Object.<string, (string[]|null)>} selection - Per-group selection map. Only groups
346
+ * present as keys are considered; `null` means whole group, an array means specific names.
347
+ * @returns {{group:string, package:string, schematic:string, label:string}[]} Ordered tasks.
348
+ */
349
+ const buildTasks = (selection) => {
350
+ const tasks = [];
351
+
352
+ for (const key of allGroupKeys()) {
353
+ if (!(key in selection)) {
354
+ continue;
355
+ }
356
+ const group = groupByKey(key);
357
+ const picked = selection[key]; // null => whole group, array => specific member names
358
+
359
+ // Whole group and an aggregate exists (e.g. eUI migrate-all) => run the aggregate.
360
+ if (picked === null && group.allSchematic) {
361
+ tasks.push({
362
+ group: group.key,
363
+ package: group.package,
364
+ schematic: group.allSchematic,
365
+ label: `${group.label} — all (${group.allSchematic})`,
366
+ });
367
+ continue;
368
+ }
369
+
370
+ // Whole group with no aggregate (e.g. Angular) => run every member in order.
371
+ // Specific selection => run exactly the picked members (keep canonical order).
372
+ const pickedSet = picked === null ? null : new Set(picked.map((n) => n.toLowerCase()));
373
+ const members = pickedSet
374
+ ? group.schematics.filter((s) => pickedSet.has(s.name.toLowerCase()))
375
+ : group.schematics;
376
+
377
+ for (const member of members) {
378
+ tasks.push({
379
+ group: group.key,
380
+ package: group.package,
381
+ schematic: member.name,
382
+ label: `${group.key}: ${member.name} — ${member.description}`,
383
+ });
384
+ }
385
+ }
386
+
387
+ return tasks;
388
+ };
389
+
390
+ /**
391
+ * Interactive flow (two stages):
392
+ * 1. Pick the migration groups (all pre-selected).
393
+ * 2. For each selected multi-schematic group, a pre-checked checklist of its individual
394
+ * schematics — pressing Enter keeps them all, or toggle to run a subset.
395
+ * Single-schematic groups (e.g. ecl) skip stage 2 since there is nothing to choose.
396
+ *
397
+ * @returns {Promise<Object.<string, (string[]|null)>>} Per-group selection map: value is `null`
398
+ * when the whole group is kept (so aggregates are used), or an array of chosen schematic names.
399
+ */
400
+ const promptSelection = async () => {
401
+ const groupPrompt = new MultiSelect({
402
+ name: 'groups',
403
+ message: 'Select the migration groups to run (all selected by default) :',
404
+ hint: '(space to toggle, enter to confirm)',
405
+ choices: activeGroups.map((g) => ({ name: g.key, message: g.label, value: g.key })),
406
+ initial: allGroupKeys(),
407
+ });
408
+ const chosenGroups = await groupPrompt.run();
409
+ const groupKeys = allGroupKeys().filter((k) => chosenGroups.includes(k));
410
+
411
+ const selection = {};
412
+
413
+ for (const key of groupKeys) {
414
+ const group = groupByKey(key);
415
+
416
+ // Single-schematic group (e.g. ecl): nothing to drill into — keep the whole group.
417
+ if (group.schematics.length <= 1) {
418
+ selection[key] = null;
419
+ continue;
420
+ }
421
+
422
+ // Stage 2: pre-checked checklist of the group's individual schematics.
423
+ const schematicPrompt = new MultiSelect({
424
+ name: `schematics-${key}`,
425
+ message: `Select "${key}" schematics to run (all selected by default) :`,
426
+ hint: '(space to toggle, enter to confirm)',
427
+ choices: group.schematics.map((s) => ({ name: s.name, message: `${s.name} — ${s.description}`, value: s.name })),
428
+ initial: group.schematics.map((s) => s.name),
429
+ });
430
+ const chosen = await schematicPrompt.run();
431
+ const chosenNames = group.schematics.filter((s) => chosen.includes(s.name)).map((s) => s.name);
432
+
433
+ // If the user kept every schematic, treat it as "whole group" so an aggregate (if any) is used.
434
+ if (chosenNames.length === group.schematics.length) {
435
+ selection[key] = null;
436
+ } else {
437
+ selection[key] = chosenNames;
438
+ }
439
+ }
440
+
441
+ return selection;
442
+ };
443
+
444
+ /**
445
+ * Runs the selected migration schematics via `ng generate`.
446
+ *
447
+ * @param {Object} args - Arguments object (retrieved via utils.getArgs()).
448
+ * @param {string} [args.groups] - Comma/space separated groups (angular, eui, ecl). Alias:
449
+ * `--schematics`. When omitted (and `--only` is also omitted) the command prompts interactively
450
+ * with all groups pre-selected.
451
+ * @param {string} [args.only] - Comma/space separated specific schematic names to run (across the
452
+ * selected groups). Runs non-interactively (skips the schematic checklist prompts).
453
+ * @param {boolean} [args.dryRun] - Pass `--dry-run` to each `ng generate` (no files written).
454
+ * @param {boolean} [args.force] - Pass `--force` to each `ng generate` (allow a dirty tree).
455
+ * @param {string} [args.path] - Restrict schematics to a sub-path (passed as `--path=` when the
456
+ * schematic supports it; eUI schematics default to `./src`).
457
+ * @returns {Promise<void>} Resolves when all selected migrations have run.
458
+ */
459
+ module.exports.run = async () => {
460
+ const args = utils.getArgs();
461
+ // `--schematics` kept as a backward-compatible alias for `--groups`.
462
+ const groupsArg = args.groups !== undefined ? args.groups : args.schematics;
463
+ const { only, dryRun, force } = args;
464
+ const scopePath = args.path;
465
+
466
+ utils.logBigTitle('eUI Migrate');
467
+
468
+ // Migration runs Angular/eUI schematics (`ng generate`). It requires an Angular workspace in the
469
+ // current directory; a standalone Vite React eUI app, or any folder without an `angular.json`,
470
+ // has nothing to migrate this way.
471
+ if (!utils.isFileExists(path.join(process.cwd(), 'angular.json'))) {
472
+ utils.logError('The migrate command is only available for Angular-based eUI applications.');
473
+ utils.logError('No angular.json found in the current directory.');
474
+ process.exit(1);
475
+ }
476
+
477
+ // The Angular migration group supports Angular v22 schematics only. Warn (don't hard-fail) when
478
+ // the installed @angular/core is a different major: the runtime filter below drops any schematic
479
+ // not present in the installed collection anyway, so eUI/ECL migrations can still proceed.
480
+ const angularMajor = getInstalledMajor('@angular/core');
481
+ if (angularMajor !== null && angularMajor !== SUPPORTED_ANGULAR_MAJOR) {
482
+ utils.logWarning(`The Angular migration group targets Angular v${SUPPORTED_ANGULAR_MAJOR} only; detected v${angularMajor}.`);
483
+ utils.logWarning('Angular schematics not present in the installed @angular/core will be skipped.');
484
+ }
485
+
486
+ // Filter the known schematics down to what the app's installed package collections expose, so a
487
+ // schematic missing from the installed @angular/core or @eui/core is skipped (with a warning)
488
+ // rather than failing the run.
489
+ const { groups: available, skipped } = resolveAvailableGroups();
490
+ setActiveGroups(available);
491
+
492
+ if (skipped.length > 0) {
493
+ utils.logWarning(`Skipping ${skipped.length} schematic(s) not available in the installed packages:`);
494
+ skipped.forEach((s) => utils.logWarning(` - ${s.group}: ${s.name}`));
495
+ }
496
+
497
+ // A group whose every schematic was filtered out has nothing to run.
498
+ const runnableGroupKeys = available.filter((g) => g.schematics.length > 0).map((g) => g.key);
499
+ if (runnableGroupKeys.length === 0) {
500
+ utils.logWarning('No migration schematics are available in the installed packages, nothing to migrate.');
501
+ return;
502
+ }
503
+
504
+ // Resolve the selection into a per-group map: { groupKey: null | string[] }
505
+ // null => whole group (aggregate when available), array => specific schematic names.
506
+ let groupKeys = parseGroupsArg(groupsArg);
507
+ const interactive = groupKeys === null && only === undefined;
508
+ let selection;
509
+
510
+ if (interactive) {
511
+ installCancelHandlers();
512
+ try {
513
+ selection = await promptSelection();
514
+ } catch (e) {
515
+ // enquirer rejects with '' or an error whose message is 'canceled' when the user aborts
516
+ // (Esc / Ctrl+C). Treat that as a clean cancellation rather than a crash.
517
+ if (e === '' || e === undefined || (e && e.message === 'canceled')) {
518
+ console.log('\n\nOperation cancelled by user');
519
+ return;
520
+ }
521
+ throw e;
522
+ }
523
+ groupKeys = Object.keys(selection);
524
+ } else {
525
+ // Non-interactive: default to all groups when --groups is absent but --only is present.
526
+ if (groupKeys === null) {
527
+ groupKeys = [...allGroupKeys()];
528
+ }
529
+ const onlyNames = parseOnlyArg(only, groupKeys);
530
+ selection = {};
531
+ for (const key of groupKeys) {
532
+ if (onlyNames === null) {
533
+ // Whole group.
534
+ selection[key] = null;
535
+ } else {
536
+ // Keep only the requested names that belong to this group (canonical order).
537
+ const names = groupByKey(key)
538
+ .schematics.filter((s) => onlyNames.includes(s.name.toLowerCase()))
539
+ .map((s) => s.name);
540
+ selection[key] = names.length ? names : [];
541
+ }
542
+ }
543
+ }
544
+
545
+ if (!groupKeys || groupKeys.length === 0) {
546
+ utils.logWarning('No migration groups selected, nothing to migrate.');
547
+ return;
548
+ }
549
+
550
+ const tasks = buildTasks(selection);
551
+
552
+ if (tasks.length === 0) {
553
+ utils.logWarning('No schematics resolved from the selection, nothing to migrate.');
554
+ return;
555
+ }
556
+
557
+ utils.logInfo(`Groups : ${groupKeys.join(', ')}`);
558
+ utils.logInfo(`Schematics to run (${tasks.length}) :`);
559
+ tasks.forEach((t) => utils.logInfo(` - ng generate ${t.package}:${t.schematic}`));
560
+
561
+ // Resolve the app's own Angular CLI entry script (handles bin/ng vs bin/ng.js, hoisting and the
562
+ // node_modules/.bin/ng shim).
563
+ const ng = resolveNgBin();
564
+
565
+ // The schematics run through the app's own Angular CLI. If dependencies were never installed,
566
+ // no `ng` binary is present — fail early with an actionable message instead of a cryptic ENOENT
567
+ // from the spawned process.
568
+ if (!ng) {
569
+ utils.logError('Angular CLI not found in this project.');
570
+ utils.logError(`Looked under : ${path.resolve(process.cwd(), 'node_modules')} (@angular/cli and .bin/ng)`);
571
+ utils.logError('Install the project dependencies first, then re-run the migration:');
572
+ utils.logError(' npm install');
573
+ utils.logError(' eui-cli migrate');
574
+ process.exit(1);
575
+ }
576
+
577
+ const commonFlags = [];
578
+ if (dryRun) commonFlags.push('--dry-run');
579
+ if (force) commonFlags.push('--force');
580
+ if (scopePath) commonFlags.push(`--path=${scopePath}`);
581
+
582
+ for (const task of tasks) {
583
+ utils.logTitle(`Running ${task.package}:${task.schematic}`);
584
+ utils.logInfo(task.label);
585
+
586
+ // Build an argv array and run it WITHOUT a shell (execFileSync), so a project path containing
587
+ // spaces or other shell-special characters (e.g. ".../ease-portal-ui-new 3/...") is passed
588
+ // literally and never word-split. `ng` is a Node script, so it is invoked via `node <ng>`.
589
+ const ngArgs = [ng, 'generate', `${task.package}:${task.schematic}`, ...commonFlags];
590
+
591
+ utils.logInfo(`running : ng generate ${task.package}:${task.schematic} ${commonFlags.join(' ')}`.trim());
592
+
593
+ try {
594
+ execFileSync(process.execPath, ngArgs, { cwd: process.cwd(), stdio: 'inherit' });
595
+ utils.logSuccess();
596
+ } catch (e) {
597
+ utils.logError(`Migration failed for ${task.package}:${task.schematic}.`);
598
+ utils.logErrorTrace('migrate.js', 'run', e);
599
+ process.exit(1);
600
+ }
601
+ }
602
+
603
+ utils.logSuccess('All selected migrations completed.');
604
+ };
605
+
606
+ // Exposed for testing / potential programmatic use.
607
+ module.exports.GROUPS = GROUPS;
608
+ module.exports.parseGroupsArg = parseGroupsArg;
609
+ module.exports.parseOnlyArg = parseOnlyArg;
610
+ module.exports.buildTasks = buildTasks;
@@ -32,6 +32,11 @@ module.exports.run = () => {
32
32
 
33
33
  let { configuration, baseHref, servePath, host, proxyConfig, maxSpaceSize, port, disableHostCheck, open, debug, configEnvTarget, noLiveReload, project } = utils.getArgs();
34
34
 
35
+ // Vite React app — serve with the Vite dev server instead of the Angular CLI.
36
+ if (utils.detectAppTool() === 'vite') {
37
+ return serveVite({ port, host, open });
38
+ }
39
+
35
40
  const ng = path.resolve(process.cwd(), 'node_modules', '@angular', 'cli', 'bin', 'ng');
36
41
 
37
42
  let prjName = 'app';
@@ -93,3 +98,30 @@ module.exports.run = () => {
93
98
 
94
99
  return execSync(`node ${args.join(' ')}`, { cwd: process.cwd(), stdio: 'inherit' });
95
100
  }
101
+
102
+ /**
103
+ * Serves a standalone Vite React app via the local `vite` binary. Maps the shared serve options
104
+ * (port / host / open) onto Vite's flags so the `eui-cli serve-app` surface stays the same for
105
+ * both Angular and React apps.
106
+ */
107
+ const serveVite = ({ port, host, open }) => {
108
+ utils.logTitle('Serving Vite React application...');
109
+
110
+ const args = ['vite'];
111
+
112
+ if (port) {
113
+ args.push(`--port=${port}`);
114
+ }
115
+ if (host) {
116
+ args.push(`--host=${host}`);
117
+ }
118
+ if (open === 'false') {
119
+ args.push('--no-open');
120
+ } else {
121
+ args.push('--open');
122
+ }
123
+
124
+ utils.logInfo(`vite serve with args ${args.join(' ')}`);
125
+
126
+ return execSync(`npx ${args.join(' ')}`, { cwd: process.cwd(), stdio: 'inherit' });
127
+ };