@rs-x/cli 2.0.0-next.2 → 2.0.0-next.21

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 (66) hide show
  1. package/README.md +5 -0
  2. package/bin/rsx.cjs +2868 -595
  3. package/package.json +5 -1
  4. package/{rs-x-vscode-extension-2.0.0-next.2.vsix → rs-x-vscode-extension-2.0.0-next.21.vsix} +0 -0
  5. package/scripts/prepare-local-rsx-packages.sh +20 -0
  6. package/scripts/verify-rsx-cli-mutations.sh +296 -0
  7. package/scripts/verify-rsx-projects.sh +220 -0
  8. package/scripts/verify-rsx-setup.sh +190 -0
  9. package/templates/angular-demo/README.md +115 -0
  10. package/templates/angular-demo/src/app/app.component.css +97 -0
  11. package/templates/angular-demo/src/app/app.component.html +58 -0
  12. package/templates/angular-demo/src/app/app.component.ts +52 -0
  13. package/templates/angular-demo/src/app/virtual-table/row-data.ts +35 -0
  14. package/templates/angular-demo/src/app/virtual-table/row-model.ts +45 -0
  15. package/templates/angular-demo/src/app/virtual-table/virtual-table-data.service.ts +136 -0
  16. package/templates/angular-demo/src/app/virtual-table/virtual-table-model.ts +224 -0
  17. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.css +174 -0
  18. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.html +50 -0
  19. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.ts +83 -0
  20. package/templates/angular-demo/src/index.html +11 -0
  21. package/templates/angular-demo/src/main.ts +16 -0
  22. package/templates/angular-demo/src/styles.css +261 -0
  23. package/templates/next-demo/README.md +26 -0
  24. package/templates/next-demo/app/globals.css +431 -0
  25. package/templates/next-demo/app/layout.tsx +22 -0
  26. package/templates/next-demo/app/page.tsx +5 -0
  27. package/templates/next-demo/components/demo-app.tsx +114 -0
  28. package/templates/next-demo/components/virtual-table-row.tsx +40 -0
  29. package/templates/next-demo/components/virtual-table-shell.tsx +86 -0
  30. package/templates/next-demo/hooks/use-virtual-table-controller.ts +26 -0
  31. package/templates/next-demo/hooks/use-virtual-table-viewport.ts +41 -0
  32. package/templates/next-demo/lib/row-data.ts +35 -0
  33. package/templates/next-demo/lib/row-model.ts +45 -0
  34. package/templates/next-demo/lib/rsx-bootstrap.ts +46 -0
  35. package/templates/next-demo/lib/virtual-table-controller.ts +259 -0
  36. package/templates/next-demo/lib/virtual-table-data.service.ts +132 -0
  37. package/templates/react-demo/README.md +113 -0
  38. package/templates/react-demo/index.html +12 -0
  39. package/templates/react-demo/src/app/app.tsx +87 -0
  40. package/templates/react-demo/src/app/hooks/use-virtual-table-controller.ts +24 -0
  41. package/templates/react-demo/src/app/hooks/use-virtual-table-viewport.ts +39 -0
  42. package/templates/react-demo/src/app/virtual-table/row-data.ts +35 -0
  43. package/templates/react-demo/src/app/virtual-table/row-model.ts +45 -0
  44. package/templates/react-demo/src/app/virtual-table/virtual-table-controller.ts +259 -0
  45. package/templates/react-demo/src/app/virtual-table/virtual-table-data.service.ts +132 -0
  46. package/templates/react-demo/src/app/virtual-table/virtual-table-row.tsx +38 -0
  47. package/templates/react-demo/src/app/virtual-table/virtual-table-shell.tsx +84 -0
  48. package/templates/react-demo/src/main.tsx +24 -0
  49. package/templates/react-demo/src/rsx-bootstrap.ts +48 -0
  50. package/templates/react-demo/src/styles.css +422 -0
  51. package/templates/react-demo/tsconfig.json +17 -0
  52. package/templates/react-demo/vite.config.ts +6 -0
  53. package/templates/vue-demo/README.md +27 -0
  54. package/templates/vue-demo/src/App.vue +89 -0
  55. package/templates/vue-demo/src/components/VirtualTableRow.vue +33 -0
  56. package/templates/vue-demo/src/components/VirtualTableShell.vue +71 -0
  57. package/templates/vue-demo/src/composables/use-virtual-table-controller.ts +33 -0
  58. package/templates/vue-demo/src/composables/use-virtual-table-viewport.ts +40 -0
  59. package/templates/vue-demo/src/env.d.ts +10 -0
  60. package/templates/vue-demo/src/lib/row-data.ts +35 -0
  61. package/templates/vue-demo/src/lib/row-model.ts +45 -0
  62. package/templates/vue-demo/src/lib/rsx-bootstrap.ts +46 -0
  63. package/templates/vue-demo/src/lib/virtual-table-controller.ts +259 -0
  64. package/templates/vue-demo/src/lib/virtual-table-data.service.ts +132 -0
  65. package/templates/vue-demo/src/main.ts +13 -0
  66. package/templates/vue-demo/src/style.css +440 -0
package/bin/rsx.cjs CHANGED
@@ -5,8 +5,40 @@ const path = require('node:path');
5
5
  const readline = require('node:readline/promises');
6
6
  const { spawnSync } = require('node:child_process');
7
7
 
8
- const CLI_VERSION = '0.2.0';
8
+ const CLI_VERSION = (() => {
9
+ try {
10
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
11
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
12
+ return packageJson.version ?? '0.0.0';
13
+ } catch {
14
+ return '0.0.0';
15
+ }
16
+ })();
9
17
  const VS_CODE_EXTENSION_ID = 'rs-x.rs-x-vscode-extension';
18
+ const ANGULAR_DEMO_TEMPLATE_DIR = path.join(
19
+ __dirname,
20
+ '..',
21
+ 'templates',
22
+ 'angular-demo',
23
+ );
24
+ const REACT_DEMO_TEMPLATE_DIR = path.join(
25
+ __dirname,
26
+ '..',
27
+ 'templates',
28
+ 'react-demo',
29
+ );
30
+ const VUE_DEMO_TEMPLATE_DIR = path.join(
31
+ __dirname,
32
+ '..',
33
+ 'templates',
34
+ 'vue-demo',
35
+ );
36
+ const NEXT_DEMO_TEMPLATE_DIR = path.join(
37
+ __dirname,
38
+ '..',
39
+ 'templates',
40
+ 'next-demo',
41
+ );
10
42
  const RUNTIME_PACKAGES = [
11
43
  '@rs-x/core',
12
44
  '@rs-x/state-manager',
@@ -119,7 +151,7 @@ function parseArgs(argv) {
119
151
  }
120
152
 
121
153
  function run(command, args, options = {}) {
122
- const { dryRun, cwd = process.cwd() } = options;
154
+ const { dryRun, cwd = process.cwd(), env } = options;
123
155
  const printable = [command, ...args].join(' ');
124
156
 
125
157
  if (dryRun) {
@@ -129,6 +161,7 @@ function run(command, args, options = {}) {
129
161
 
130
162
  const result = spawnSync(command, args, {
131
163
  cwd,
164
+ env: env ? { ...process.env, ...env } : process.env,
132
165
  stdio: 'inherit',
133
166
  });
134
167
 
@@ -178,6 +211,22 @@ function detectPackageManager(explicitPm) {
178
211
  return 'npm';
179
212
  }
180
213
 
214
+ function resolveCliPackageManager(projectRoot, explicitPm) {
215
+ if (explicitPm) {
216
+ return explicitPm;
217
+ }
218
+
219
+ const cliConfig = resolveRsxCliConfig(projectRoot);
220
+ if (
221
+ typeof cliConfig.packageManager === 'string' &&
222
+ ['pnpm', 'npm', 'yarn', 'bun'].includes(cliConfig.packageManager)
223
+ ) {
224
+ return cliConfig.packageManager;
225
+ }
226
+
227
+ return detectPackageManager(undefined);
228
+ }
229
+
181
230
  function applyTagToPackages(packages, tag) {
182
231
  return packages.map((pkg) => {
183
232
  const lastAt = pkg.lastIndexOf('@');
@@ -191,11 +240,68 @@ function applyTagToPackages(packages, tag) {
191
240
  }
192
241
 
193
242
  function resolveInstallTag(flags) {
194
- return parseBooleanFlag(flags.next, false) ? 'next' : undefined;
243
+ if (parseBooleanFlag(flags.next, false)) {
244
+ return 'next';
245
+ }
246
+
247
+ if (CLI_VERSION.includes('-')) {
248
+ return 'next';
249
+ }
250
+
251
+ const checkoutRoot = findRepoRoot(__dirname);
252
+ if (!checkoutRoot) {
253
+ return undefined;
254
+ }
255
+
256
+ const branchResult = spawnSync('git', ['branch', '--show-current'], {
257
+ cwd: checkoutRoot,
258
+ encoding: 'utf8',
259
+ });
260
+ const branch = branchResult.status === 0 ? branchResult.stdout.trim() : '';
261
+ if (branch && branch !== 'main') {
262
+ return 'next';
263
+ }
264
+
265
+ return undefined;
266
+ }
267
+
268
+ function resolveConfiguredInstallTag(projectRoot, flags) {
269
+ const explicitTag = resolveInstallTag(flags);
270
+ if (explicitTag) {
271
+ return explicitTag;
272
+ }
273
+
274
+ const cliConfig = resolveRsxCliConfig(projectRoot);
275
+ if (
276
+ typeof cliConfig.installTag === 'string' &&
277
+ ['latest', 'next'].includes(cliConfig.installTag)
278
+ ) {
279
+ return cliConfig.installTag;
280
+ }
281
+
282
+ return undefined;
283
+ }
284
+
285
+ function resolveCliVerifyFlag(projectRoot, flags, sectionName) {
286
+ if (flags.verify !== undefined) {
287
+ return parseBooleanFlag(flags.verify, false);
288
+ }
289
+
290
+ const cliConfig = resolveRsxCliConfig(projectRoot);
291
+ const sectionConfig = cliConfig[sectionName];
292
+ if (
293
+ typeof sectionConfig === 'object' &&
294
+ sectionConfig &&
295
+ typeof sectionConfig.verify === 'boolean'
296
+ ) {
297
+ return sectionConfig.verify;
298
+ }
299
+
300
+ return false;
195
301
  }
196
302
 
197
303
  function installPackages(pm, packages, options = {}) {
198
- const { dev = false, dryRun = false, label = 'packages', tag } = options;
304
+ const { dev = false, dryRun = false, label = 'packages', tag, cwd } = options;
199
305
  const resolvedPackages = tag ? applyTagToPackages(packages, tag) : packages;
200
306
  const argsByPm = {
201
307
  pnpm: dev
@@ -220,24 +326,74 @@ function installPackages(pm, packages, options = {}) {
220
326
 
221
327
  const tagInfo = tag ? ` (tag: ${tag})` : '';
222
328
  logInfo(`Installing ${label} with ${pm}${tagInfo}...`);
223
- run(pm, installArgs, { dryRun });
329
+ run(pm, installArgs, { dryRun, cwd });
224
330
  logOk(`Installed ${label}.`);
225
331
  }
226
332
 
227
- function installRuntimePackages(pm, dryRun, tag) {
228
- installPackages(pm, RUNTIME_PACKAGES, {
333
+ function resolveLocalRsxSpecs(projectRoot, flags, options = {}) {
334
+ const tarballsDir =
335
+ typeof flags?.['tarballs-dir'] === 'string'
336
+ ? path.resolve(projectRoot, flags['tarballs-dir'])
337
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
338
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
339
+ ? path.resolve(projectRoot, process.env.RSX_TARBALLS_DIR)
340
+ : null;
341
+ const workspaceRoot = findRepoRoot(projectRoot);
342
+ return resolveProjectRsxSpecs(
343
+ projectRoot,
344
+ workspaceRoot,
345
+ tarballsDir,
346
+ options,
347
+ );
348
+ }
349
+
350
+ function installResolvedPackages(pm, packageNames, options = {}) {
351
+ const {
352
+ dryRun = false,
353
+ label = 'packages',
354
+ tag,
355
+ cwd,
356
+ specs,
357
+ dev = false,
358
+ } = options;
359
+ const resolvedPackages = packageNames.map((packageName) => {
360
+ const spec = specs?.[packageName];
361
+ return spec ? `${packageName}@${spec}` : packageName;
362
+ });
363
+
364
+ installPackages(pm, resolvedPackages, {
365
+ dev,
366
+ dryRun,
367
+ label,
368
+ tag: specs ? undefined : tag,
369
+ cwd,
370
+ });
371
+ }
372
+
373
+ function installRuntimePackages(pm, dryRun, tag, projectRoot, flags) {
374
+ const specs = resolveLocalRsxSpecs(projectRoot ?? process.cwd(), flags, {
375
+ tag,
376
+ });
377
+ installResolvedPackages(pm, RUNTIME_PACKAGES, {
229
378
  dev: false,
230
379
  dryRun,
231
380
  tag,
381
+ specs,
382
+ cwd: projectRoot,
232
383
  label: 'runtime RS-X packages',
233
384
  });
234
385
  }
235
386
 
236
- function installCompilerPackages(pm, dryRun, tag) {
237
- installPackages(pm, COMPILER_PACKAGES, {
387
+ function installCompilerPackages(pm, dryRun, tag, projectRoot, flags) {
388
+ const specs = resolveLocalRsxSpecs(projectRoot ?? process.cwd(), flags, {
389
+ tag,
390
+ });
391
+ installResolvedPackages(pm, COMPILER_PACKAGES, {
238
392
  dev: true,
239
393
  dryRun,
240
394
  tag,
395
+ specs,
396
+ cwd: projectRoot,
241
397
  label: 'compiler tooling',
242
398
  });
243
399
  }
@@ -262,14 +418,50 @@ function installVsCodeExtension(flags) {
262
418
  return;
263
419
  }
264
420
 
265
- const args = ['--install-extension', VS_CODE_EXTENSION_ID];
421
+ installBundledVsix(dryRun, force);
422
+ }
423
+
424
+ function resolveBundledVsix() {
425
+ const packageRoot = path.resolve(__dirname, '..');
426
+ const candidates = fs
427
+ .readdirSync(packageRoot)
428
+ .filter((name) => /^rs-x-vscode-extension-.*\.vsix$/u.test(name))
429
+ .map((name) => path.join(packageRoot, name));
430
+
431
+ if (candidates.length === 0) {
432
+ return null;
433
+ }
434
+
435
+ const latest = candidates
436
+ .map((fullPath) => ({
437
+ fullPath,
438
+ mtimeMs: fs.statSync(fullPath).mtimeMs,
439
+ }))
440
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
441
+
442
+ return latest?.fullPath ?? null;
443
+ }
444
+
445
+ function installBundledVsix(dryRun, force) {
446
+ const bundledVsix = resolveBundledVsix();
447
+ if (!bundledVsix) {
448
+ logWarn(
449
+ 'No bundled VSIX found in @rs-x/cli. Skipping VS Code extension install.',
450
+ );
451
+ logInfo(
452
+ 'If you are developing in the rs-x repo, use `rsx install vscode --local` instead.',
453
+ );
454
+ return;
455
+ }
456
+
457
+ const args = ['--install-extension', bundledVsix];
266
458
  if (force) {
267
459
  args.push('--force');
268
460
  }
269
461
 
270
- logInfo(`Installing ${VS_CODE_EXTENSION_ID} from VS Code marketplace...`);
462
+ logInfo(`Installing bundled VSIX from ${bundledVsix}...`);
271
463
  run('code', args, { dryRun });
272
- logOk('VS Code extension installed.');
464
+ logOk('VS Code extension installed from bundled VSIX.');
273
465
  }
274
466
 
275
467
  function installLocalVsix(dryRun, force) {
@@ -343,6 +535,13 @@ function findRepoRoot(startDir) {
343
535
  function runDoctor() {
344
536
  const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
345
537
  const hasCode = hasCommand('code');
538
+ const availablePackageManagers = ['pnpm', 'npm', 'yarn', 'bun'].filter((pm) =>
539
+ hasCommand(pm),
540
+ );
541
+ const packageRoot = findNearestPackageRoot(process.cwd());
542
+ const duplicateRsxPackages = packageRoot
543
+ ? findDuplicateRsxPackages(packageRoot)
544
+ : [];
346
545
  const checks = [
347
546
  {
348
547
  name: 'Node.js >= 20',
@@ -356,12 +555,25 @@ function runDoctor() {
356
555
  },
357
556
  {
358
557
  name: 'Package manager (pnpm/npm/yarn/bun)',
359
- ok:
360
- hasCommand('pnpm') ||
361
- hasCommand('npm') ||
362
- hasCommand('yarn') ||
363
- hasCommand('bun'),
364
- details: 'required for compiler package installation',
558
+ ok: availablePackageManagers.length > 0,
559
+ details:
560
+ availablePackageManagers.length > 0
561
+ ? `available: ${availablePackageManagers.join(', ')}`
562
+ : 'required for compiler package installation',
563
+ },
564
+ {
565
+ name: 'Duplicate @rs-x package versions',
566
+ ok: duplicateRsxPackages.length === 0,
567
+ details: packageRoot
568
+ ? duplicateRsxPackages.length === 0
569
+ ? `not detected in ${path.relative(process.cwd(), packageRoot) || '.'}`
570
+ : duplicateRsxPackages
571
+ .map(
572
+ ({ name, versions }) =>
573
+ `${name} (${Array.from(versions).join(', ')})`,
574
+ )
575
+ .join('; ')
576
+ : 'no package.json found in current directory tree',
365
577
  },
366
578
  ];
367
579
 
@@ -369,6 +581,149 @@ function runDoctor() {
369
581
  const tag = check.ok ? '[OK]' : '[WARN]';
370
582
  console.log(`${tag} ${check.name} - ${check.details}`);
371
583
  }
584
+
585
+ const failingChecks = checks.filter((check) => !check.ok);
586
+ if (failingChecks.length > 0) {
587
+ console.log('');
588
+ console.log('Suggested next steps:');
589
+ for (const check of failingChecks) {
590
+ if (check.name === 'Node.js >= 20') {
591
+ console.log(
592
+ ' - Install Node.js 20 or newer before running `rsx setup` or `rsx project`.',
593
+ );
594
+ } else if (check.name === 'VS Code CLI (code)') {
595
+ console.log(
596
+ ' - Install the VS Code shell command or use `rsx install vscode --force` later.',
597
+ );
598
+ } else if (check.name === 'Package manager (pnpm/npm/yarn/bun)') {
599
+ console.log(
600
+ ' - Install npm, pnpm, yarn, or bun so the CLI can add RS-X packages.',
601
+ );
602
+ } else if (check.name === 'Duplicate @rs-x package versions') {
603
+ console.log(
604
+ ' - Reinstall dependencies so all `@rs-x/*` packages resolve to a single version, then rerun `rsx doctor`.',
605
+ );
606
+ console.log(
607
+ ' - If you are linking local packages, remove nested `node_modules` inside linked RS-X packages.',
608
+ );
609
+ }
610
+ }
611
+ return;
612
+ }
613
+
614
+ console.log('');
615
+ console.log('Suggested next steps:');
616
+ console.log(
617
+ ' - Run `rsx project <framework>` to scaffold a starter, or `rsx setup` inside an existing app.',
618
+ );
619
+ console.log(' - Use `rsx add` to create your first expression file.');
620
+ }
621
+
622
+ function findNearestPackageRoot(startDirectory) {
623
+ let currentDirectory = path.resolve(startDirectory);
624
+ while (true) {
625
+ if (fs.existsSync(path.join(currentDirectory, 'package.json'))) {
626
+ return currentDirectory;
627
+ }
628
+ const parentDirectory = path.dirname(currentDirectory);
629
+ if (parentDirectory === currentDirectory) {
630
+ return null;
631
+ }
632
+ currentDirectory = parentDirectory;
633
+ }
634
+ }
635
+
636
+ function readRsxPackageVersionsFromNodeModules(
637
+ nodeModulesDirectory,
638
+ versionsByPackage,
639
+ ) {
640
+ const scopeDirectory = path.join(nodeModulesDirectory, '@rs-x');
641
+ if (!fs.existsSync(scopeDirectory)) {
642
+ return [];
643
+ }
644
+
645
+ const packageDirectories = fs
646
+ .readdirSync(scopeDirectory, { withFileTypes: true })
647
+ .filter((entry) => entry.isDirectory())
648
+ .map((entry) => path.join(scopeDirectory, entry.name));
649
+
650
+ for (const packageDirectory of packageDirectories) {
651
+ const packageJsonPath = path.join(packageDirectory, 'package.json');
652
+ if (!fs.existsSync(packageJsonPath)) {
653
+ continue;
654
+ }
655
+
656
+ try {
657
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
658
+ const packageName = packageJson.name;
659
+ const packageVersion = packageJson.version;
660
+ if (
661
+ typeof packageName === 'string' &&
662
+ typeof packageVersion === 'string'
663
+ ) {
664
+ if (!versionsByPackage.has(packageName)) {
665
+ versionsByPackage.set(packageName, new Set());
666
+ }
667
+ versionsByPackage.get(packageName).add(packageVersion);
668
+ }
669
+ } catch {
670
+ // Ignore malformed package.json files during doctor output.
671
+ }
672
+ }
673
+
674
+ return packageDirectories;
675
+ }
676
+
677
+ function collectNestedRsxPackageVersions(
678
+ packageDirectory,
679
+ versionsByPackage,
680
+ depth,
681
+ ) {
682
+ if (depth <= 0) {
683
+ return;
684
+ }
685
+
686
+ const nestedNodeModulesDirectory = path.join(
687
+ packageDirectory,
688
+ 'node_modules',
689
+ );
690
+ if (!fs.existsSync(nestedNodeModulesDirectory)) {
691
+ return;
692
+ }
693
+
694
+ const nestedPackageDirectories = readRsxPackageVersionsFromNodeModules(
695
+ nestedNodeModulesDirectory,
696
+ versionsByPackage,
697
+ );
698
+
699
+ for (const nestedPackageDirectory of nestedPackageDirectories) {
700
+ collectNestedRsxPackageVersions(
701
+ nestedPackageDirectory,
702
+ versionsByPackage,
703
+ depth - 1,
704
+ );
705
+ }
706
+ }
707
+
708
+ function findDuplicateRsxPackages(projectRoot) {
709
+ const rootNodeModulesDirectory = path.join(projectRoot, 'node_modules');
710
+ if (!fs.existsSync(rootNodeModulesDirectory)) {
711
+ return [];
712
+ }
713
+
714
+ const versionsByPackage = new Map();
715
+ const rootPackageDirectories = readRsxPackageVersionsFromNodeModules(
716
+ rootNodeModulesDirectory,
717
+ versionsByPackage,
718
+ );
719
+
720
+ for (const packageDirectory of rootPackageDirectories) {
721
+ collectNestedRsxPackageVersions(packageDirectory, versionsByPackage, 3);
722
+ }
723
+
724
+ return Array.from(versionsByPackage.entries())
725
+ .filter(([, versions]) => versions.size > 1)
726
+ .map(([name, versions]) => ({ name, versions }));
372
727
  }
373
728
 
374
729
  function isValidTsIdentifier(input) {
@@ -446,104 +801,540 @@ function stripTsLikeExtension(fileName) {
446
801
  return fileName.replace(/\.[cm]?[jt]sx?$/u, '');
447
802
  }
448
803
 
449
- function createModelTemplate() {
804
+ function inferModelKeysFromExpression(expressionSource) {
805
+ const matches = expressionSource.matchAll(
806
+ /(?<![\w$.])([A-Za-z_$][A-Za-z0-9_$]*)(?![\w$])/gu,
807
+ );
808
+ const identifiers = [];
809
+ const ignoredIdentifiers = new Set([
810
+ 'undefined',
811
+ 'null',
812
+ 'true',
813
+ 'false',
814
+ 'Math',
815
+ 'Date',
816
+ 'Number',
817
+ 'String',
818
+ 'Boolean',
819
+ 'Array',
820
+ 'Object',
821
+ 'JSON',
822
+ 'console',
823
+ ]);
824
+
825
+ for (const match of matches) {
826
+ const identifier = match[1];
827
+ if (
828
+ TS_RESERVED_WORDS.has(identifier) ||
829
+ ignoredIdentifiers.has(identifier)
830
+ ) {
831
+ continue;
832
+ }
833
+ if (!identifiers.includes(identifier)) {
834
+ identifiers.push(identifier);
835
+ }
836
+ }
837
+
838
+ return identifiers;
839
+ }
840
+
841
+ function createModelTemplate(expressionSource) {
842
+ const modelKeys = inferModelKeysFromExpression(expressionSource);
843
+ const modelBody =
844
+ modelKeys.length > 0
845
+ ? modelKeys.map((key) => ` ${key}: 1,`).join('\n')
846
+ : ' a: 1,';
847
+
450
848
  return `export const model = {
451
- a: 1,
849
+ ${modelBody}
452
850
  };
453
851
  `;
454
852
  }
455
853
 
854
+ function createInlineExpressionTemplate(expressionName, expressionSource) {
855
+ return `import { rsx } from '@rs-x/expression-parser';
856
+
857
+ ${createModelTemplate(expressionSource).trim()}
858
+
859
+ export const ${expressionName} = rsx(${JSON.stringify(expressionSource)})(model);
860
+ `;
861
+ }
862
+
863
+ function createInlineExpressionAppendTemplate(
864
+ expressionName,
865
+ expressionSource,
866
+ fileContent,
867
+ ) {
868
+ const hasRsxImport = fileContent.includes("from '@rs-x/expression-parser'");
869
+ const hasModelExport = /\bexport\s+const\s+model\s*=/u.test(fileContent);
870
+ const sections = [];
871
+
872
+ if (!hasRsxImport) {
873
+ sections.push("import { rsx } from '@rs-x/expression-parser';");
874
+ }
875
+
876
+ if (!hasModelExport) {
877
+ sections.push(createModelTemplate(expressionSource).trim());
878
+ }
879
+
880
+ sections.push(
881
+ `export const ${expressionName} = rsx(${JSON.stringify(expressionSource)})(model);`,
882
+ );
883
+ return `\n${sections.join('\n\n')}\n`;
884
+ }
885
+
456
886
  function createExpressionTemplate(
457
887
  expressionName,
888
+ expressionSource,
458
889
  modelImportPath,
459
890
  modelExportName,
460
891
  ) {
461
892
  return `import { rsx } from '@rs-x/expression-parser';
462
893
  import { ${modelExportName} } from '${modelImportPath}';
463
894
 
464
- export const ${expressionName} = rsx('a')(${modelExportName});
895
+ export const ${expressionName} = rsx(${JSON.stringify(expressionSource)})(${modelExportName});
465
896
  `;
466
897
  }
467
898
 
468
- async function askForIdentifierWithDefault(rl, prompt, defaultValue) {
469
- while (true) {
470
- const answer = (await rl.question(prompt)).trim();
471
- if (!answer) {
472
- return defaultValue;
899
+ function findExpressionFiles(searchRoot) {
900
+ const results = [];
901
+ const expressionPattern =
902
+ /\b(?:export\s+)?const\s+[A-Za-z_$][\w$]*\s*=\s*rsx\(/u;
903
+
904
+ function visit(currentPath) {
905
+ if (!fs.existsSync(currentPath)) {
906
+ return;
473
907
  }
474
908
 
475
- if (isValidTsIdentifier(answer)) {
476
- return answer;
909
+ const stat = fs.statSync(currentPath);
910
+ if (stat.isDirectory()) {
911
+ const baseName = path.basename(currentPath);
912
+ if (
913
+ baseName === 'node_modules' ||
914
+ baseName === 'dist' ||
915
+ baseName === 'build' ||
916
+ baseName === '.git' ||
917
+ baseName === '.next' ||
918
+ baseName === 'coverage' ||
919
+ baseName === '.tests' ||
920
+ baseName === 'tmp'
921
+ ) {
922
+ return;
923
+ }
924
+
925
+ for (const entry of fs.readdirSync(currentPath)) {
926
+ visit(path.join(currentPath, entry));
927
+ }
928
+ return;
477
929
  }
478
930
 
479
- logWarn(`"${answer}" is not a valid TypeScript identifier.`);
931
+ if (!/\.[cm]?[jt]sx?$/u.test(currentPath)) {
932
+ return;
933
+ }
934
+
935
+ const content = fs.readFileSync(currentPath, 'utf8');
936
+ if (
937
+ content.includes("from '@rs-x/expression-parser'") &&
938
+ expressionPattern.test(content)
939
+ ) {
940
+ results.push(currentPath);
941
+ }
480
942
  }
943
+
944
+ visit(searchRoot);
945
+ return results.sort((left, right) => left.localeCompare(right));
481
946
  }
482
947
 
483
- async function runAdd() {
484
- const rl = readline.createInterface({
485
- input: process.stdin,
486
- output: process.stdout,
948
+ function resolveRsxCliAddConfig(projectRoot) {
949
+ const cliConfig = resolveRsxCliConfig(projectRoot);
950
+ const addConfig = cliConfig.add ?? {};
951
+ const defaultSearchRoots = ['src', 'app', 'expressions'];
952
+ const configuredSearchRoots = Array.isArray(addConfig.searchRoots)
953
+ ? addConfig.searchRoots.filter(
954
+ (value) => typeof value === 'string' && value.trim() !== '',
955
+ )
956
+ : defaultSearchRoots;
957
+ const defaultDirectory =
958
+ typeof addConfig.defaultDirectory === 'string' &&
959
+ addConfig.defaultDirectory.trim() !== ''
960
+ ? addConfig.defaultDirectory.trim()
961
+ : 'src/expressions';
962
+
963
+ return {
964
+ defaultDirectory,
965
+ searchRoots: configuredSearchRoots,
966
+ };
967
+ }
968
+
969
+ function filterPreferredExpressionFiles(candidates, projectRoot, addConfig) {
970
+ const preferredRootPrefixes = addConfig.searchRoots
971
+ .map((rootPath) =>
972
+ path.isAbsolute(rootPath) ? rootPath : path.join(projectRoot, rootPath),
973
+ )
974
+ .map((value) => `${value.replace(/\\/gu, '/')}/`);
975
+
976
+ const preferredCandidates = candidates.filter((candidate) => {
977
+ const normalizedCandidate = candidate.replace(/\\/gu, '/');
978
+ return preferredRootPrefixes.some((prefix) =>
979
+ normalizedCandidate.startsWith(prefix),
980
+ );
487
981
  });
488
982
 
489
- try {
490
- const expressionName = await askUntilValidIdentifier(rl);
983
+ return preferredCandidates.length > 0 ? preferredCandidates : candidates;
984
+ }
491
985
 
492
- const kebabAnswer = await rl.question('Use kebab-case file name? [Y/n]: ');
493
- const useKebabCase = normalizeYesNo(kebabAnswer, true);
986
+ function rankExpressionFiles(candidates, resolvedDirectory, projectRoot) {
987
+ const sourceRootPatterns = ['/src/', '/app/', '/expressions/'];
494
988
 
495
- const directoryInput = await askUntilNonEmpty(
496
- rl,
497
- 'Directory path (relative or absolute): ',
498
- );
499
- const resolvedDirectory = path.isAbsolute(directoryInput)
500
- ? directoryInput
501
- : path.resolve(process.cwd(), directoryInput);
989
+ function score(filePath) {
990
+ const normalized = filePath.replace(/\\/gu, '/');
991
+ let value = 0;
502
992
 
503
- const baseFileName = useKebabCase
504
- ? toKebabCase(expressionName)
505
- : expressionName;
506
- const expressionFileName = ensureTsExtension(baseFileName);
507
- const expressionFileBase = stripTsLikeExtension(expressionFileName);
508
- const modelFileName = `${expressionFileBase}.model.ts`;
509
- const expressionPath = path.join(resolvedDirectory, expressionFileName);
510
- const modelPath = path.join(resolvedDirectory, modelFileName);
511
- const useExistingModelAnswer = await rl.question(
512
- 'Use existing model file? [y/N]: ',
513
- );
514
- const useExistingModel = normalizeYesNo(useExistingModelAnswer, false);
993
+ if (normalized.startsWith(resolvedDirectory.replace(/\\/gu, '/'))) {
994
+ value += 1000;
995
+ }
515
996
 
516
- if (
517
- fs.existsSync(expressionPath) ||
518
- (!useExistingModel && fs.existsSync(modelPath))
519
- ) {
520
- const overwriteAnswer = await rl.question(
521
- `One or more target files already exist. Overwrite? [y/N]: `,
522
- );
523
- const shouldOverwrite = normalizeYesNo(overwriteAnswer, false);
524
- if (!shouldOverwrite) {
525
- logInfo('Cancelled. Existing file was not modified.');
526
- return;
527
- }
997
+ if (sourceRootPatterns.some((pattern) => normalized.includes(pattern))) {
998
+ value += 100;
528
999
  }
529
1000
 
530
- fs.mkdirSync(resolvedDirectory, { recursive: true });
531
- let modelImportPath = `./${expressionFileBase}.model`;
532
- let modelExportName = 'model';
1001
+ const relativeSegments = path
1002
+ .relative(projectRoot, filePath)
1003
+ .replace(/\\/gu, '/')
1004
+ .split('/').length;
1005
+ value -= relativeSegments;
1006
+ return value;
1007
+ }
533
1008
 
534
- if (useExistingModel) {
535
- const existingModelPathInput = await askUntilNonEmpty(
536
- rl,
537
- 'Existing model file path (relative to output dir or absolute): ',
538
- );
539
- const resolvedExistingModelPath = path.isAbsolute(existingModelPathInput)
540
- ? existingModelPathInput
541
- : path.resolve(resolvedDirectory, existingModelPathInput);
1009
+ return [...candidates].sort((left, right) => {
1010
+ const scoreDifference = score(right) - score(left);
1011
+ if (scoreDifference !== 0) {
1012
+ return scoreDifference;
1013
+ }
1014
+ return left.localeCompare(right);
1015
+ });
1016
+ }
542
1017
 
543
- if (!fs.existsSync(resolvedExistingModelPath)) {
544
- logError(`Model file not found: ${resolvedExistingModelPath}`);
545
- return;
546
- }
1018
+ async function askForExistingExpressionFile(rl, resolvedDirectory) {
1019
+ const projectRoot = process.cwd();
1020
+ const addConfig = resolveRsxCliAddConfig(projectRoot);
1021
+ const directoryCandidates = filterPreferredExpressionFiles(
1022
+ findExpressionFiles(resolvedDirectory),
1023
+ projectRoot,
1024
+ addConfig,
1025
+ );
1026
+ const fallbackCandidates =
1027
+ directoryCandidates.length > 0
1028
+ ? []
1029
+ : rankExpressionFiles(
1030
+ filterPreferredExpressionFiles(
1031
+ findExpressionFiles(projectRoot),
1032
+ projectRoot,
1033
+ addConfig,
1034
+ ),
1035
+ resolvedDirectory,
1036
+ projectRoot,
1037
+ );
1038
+ const candidates =
1039
+ directoryCandidates.length > 0 ? directoryCandidates : fallbackCandidates;
1040
+
1041
+ if (candidates.length > 0) {
1042
+ console.log(
1043
+ directoryCandidates.length > 0
1044
+ ? 'Existing expression files in the selected directory:'
1045
+ : 'Existing expression files in the current project:',
1046
+ );
1047
+ candidates.forEach((candidate, index) => {
1048
+ console.log(
1049
+ ` ${index + 1}. ${path.relative(process.cwd(), candidate).replace(/\\/gu, '/')}`,
1050
+ );
1051
+ });
1052
+ console.log(' 0. Enter a custom path');
1053
+ }
1054
+
1055
+ while (true) {
1056
+ const answer = (
1057
+ await rl.question(
1058
+ candidates.length > 0
1059
+ ? 'Choose a file number or enter a file path: '
1060
+ : 'Existing file path (relative to output dir or absolute): ',
1061
+ )
1062
+ ).trim();
1063
+
1064
+ if (!answer && candidates.length > 0) {
1065
+ logWarn('Please choose a file or enter a path.');
1066
+ continue;
1067
+ }
1068
+
1069
+ if (candidates.length > 0 && /^\d+$/u.test(answer)) {
1070
+ const selectedIndex = Number.parseInt(answer, 10);
1071
+ if (selectedIndex === 0) {
1072
+ const customPath = await askUntilNonEmpty(
1073
+ rl,
1074
+ 'Existing file path (relative to output dir or absolute): ',
1075
+ );
1076
+ const resolvedPath = path.isAbsolute(customPath)
1077
+ ? customPath
1078
+ : path.resolve(resolvedDirectory, customPath);
1079
+
1080
+ if (!fs.existsSync(resolvedPath)) {
1081
+ logWarn(`File not found: ${resolvedPath}`);
1082
+ continue;
1083
+ }
1084
+
1085
+ return resolvedPath;
1086
+ }
1087
+
1088
+ if (selectedIndex >= 1 && selectedIndex <= candidates.length) {
1089
+ return candidates[selectedIndex - 1];
1090
+ }
1091
+
1092
+ logWarn(`"${answer}" is not a valid file number.`);
1093
+ continue;
1094
+ }
1095
+
1096
+ const resolvedPath = path.isAbsolute(answer)
1097
+ ? answer
1098
+ : path.resolve(resolvedDirectory, answer);
1099
+
1100
+ if (!fs.existsSync(resolvedPath)) {
1101
+ logWarn(`File not found: ${resolvedPath}`);
1102
+ continue;
1103
+ }
1104
+
1105
+ return resolvedPath;
1106
+ }
1107
+ }
1108
+
1109
+ async function askForIdentifierWithDefault(rl, prompt, defaultValue) {
1110
+ while (true) {
1111
+ const answer = (await rl.question(prompt)).trim();
1112
+ if (!answer) {
1113
+ return defaultValue;
1114
+ }
1115
+
1116
+ if (isValidTsIdentifier(answer)) {
1117
+ return answer;
1118
+ }
1119
+
1120
+ logWarn(`"${answer}" is not a valid TypeScript identifier.`);
1121
+ }
1122
+ }
1123
+
1124
+ async function askForExpressionSource(rl) {
1125
+ while (true) {
1126
+ const answer = (
1127
+ await rl.question("Initial expression string ['a']: ")
1128
+ ).trim();
1129
+
1130
+ if (!answer) {
1131
+ return 'a';
1132
+ }
1133
+
1134
+ return answer;
1135
+ }
1136
+ }
1137
+
1138
+ async function askForDirectoryPath(rl, defaultDirectory) {
1139
+ while (true) {
1140
+ const prompt = defaultDirectory
1141
+ ? `Directory path (relative or absolute) [${defaultDirectory}]: `
1142
+ : 'Directory path (relative or absolute): ';
1143
+ const answer = (await rl.question(prompt)).trim();
1144
+
1145
+ if (answer) {
1146
+ return answer;
1147
+ }
1148
+
1149
+ if (defaultDirectory) {
1150
+ return defaultDirectory;
1151
+ }
1152
+
1153
+ logWarn('Please enter a directory path.');
1154
+ }
1155
+ }
1156
+
1157
+ async function askForAddMode(rl) {
1158
+ console.log('Choose add mode:');
1159
+ console.log(
1160
+ ' 1. Create new expression file (model in same file) [Recommended]',
1161
+ );
1162
+ console.log(' 2. Create new expression file with separate model');
1163
+ console.log(' 3. Update an existing expression file');
1164
+
1165
+ while (true) {
1166
+ const answer = (await rl.question('Add mode [1]: ')).trim();
1167
+
1168
+ if (!answer || answer === '1') {
1169
+ return 'create-inline';
1170
+ }
1171
+ if (answer === '2') {
1172
+ return 'create-separate';
1173
+ }
1174
+ if (answer === '3') {
1175
+ return 'update-existing';
1176
+ }
1177
+
1178
+ logWarn(`"${answer}" is not a valid add mode. Use 1, 2, or 3.`);
1179
+ }
1180
+ }
1181
+
1182
+ async function runAdd() {
1183
+ const rl = readline.createInterface({
1184
+ input: process.stdin,
1185
+ output: process.stdout,
1186
+ });
1187
+
1188
+ try {
1189
+ const projectRoot = process.cwd();
1190
+ const addConfig = resolveRsxCliAddConfig(projectRoot);
1191
+ const expressionName = await askUntilValidIdentifier(rl);
1192
+ const expressionSource = await askForExpressionSource(rl);
1193
+
1194
+ const kebabAnswer = await rl.question('Use kebab-case file name? [Y/n]: ');
1195
+ const useKebabCase = normalizeYesNo(kebabAnswer, true);
1196
+
1197
+ const directoryInput = await askForDirectoryPath(
1198
+ rl,
1199
+ addConfig.defaultDirectory,
1200
+ );
1201
+ const resolvedDirectory = path.isAbsolute(directoryInput)
1202
+ ? directoryInput
1203
+ : path.resolve(projectRoot, directoryInput);
1204
+
1205
+ const baseFileName = useKebabCase
1206
+ ? toKebabCase(expressionName)
1207
+ : expressionName;
1208
+ const expressionFileName = ensureTsExtension(baseFileName);
1209
+ const expressionFileBase = stripTsLikeExtension(expressionFileName);
1210
+ const modelFileName = `${expressionFileBase}.model.ts`;
1211
+ const expressionPath = path.join(resolvedDirectory, expressionFileName);
1212
+ const modelPath = path.join(resolvedDirectory, modelFileName);
1213
+ const addMode = await askForAddMode(rl);
1214
+ const updateExistingFile = addMode === 'update-existing';
1215
+ const keepModelInSameFile =
1216
+ addMode === 'create-inline'
1217
+ ? true
1218
+ : addMode === 'create-separate'
1219
+ ? false
1220
+ : normalizeYesNo(
1221
+ await rl.question('Keep model in the same file? [Y/n]: '),
1222
+ true,
1223
+ );
1224
+
1225
+ if (updateExistingFile) {
1226
+ const resolvedExistingFilePath = await askForExistingExpressionFile(
1227
+ rl,
1228
+ resolvedDirectory,
1229
+ );
1230
+ const existingFileContent = fs.readFileSync(
1231
+ resolvedExistingFilePath,
1232
+ 'utf8',
1233
+ );
1234
+ let appendContent;
1235
+
1236
+ if (keepModelInSameFile) {
1237
+ appendContent = createInlineExpressionAppendTemplate(
1238
+ expressionName,
1239
+ expressionSource,
1240
+ existingFileContent,
1241
+ );
1242
+ } else {
1243
+ const useExistingModelAnswer = await rl.question(
1244
+ 'Use existing model file? [y/N]: ',
1245
+ );
1246
+ const useExistingModel = normalizeYesNo(useExistingModelAnswer, false);
1247
+ let modelImportPath = './model';
1248
+ let modelExportName = 'model';
1249
+
1250
+ if (useExistingModel) {
1251
+ const existingModelPathInput = await askUntilNonEmpty(
1252
+ rl,
1253
+ 'Existing model file path (relative to output dir or absolute): ',
1254
+ );
1255
+ const resolvedExistingModelPath = path.isAbsolute(
1256
+ existingModelPathInput,
1257
+ )
1258
+ ? existingModelPathInput
1259
+ : path.resolve(resolvedDirectory, existingModelPathInput);
1260
+
1261
+ if (!fs.existsSync(resolvedExistingModelPath)) {
1262
+ logError(`Model file not found: ${resolvedExistingModelPath}`);
1263
+ return;
1264
+ }
1265
+
1266
+ modelImportPath = toModuleImportPath(
1267
+ resolvedExistingFilePath,
1268
+ resolvedExistingModelPath,
1269
+ );
1270
+ modelExportName = await askForIdentifierWithDefault(
1271
+ rl,
1272
+ 'Model export name [model]: ',
1273
+ 'model',
1274
+ );
1275
+ }
1276
+
1277
+ appendContent = `\n${createExpressionTemplate(
1278
+ expressionName,
1279
+ expressionSource,
1280
+ modelImportPath,
1281
+ modelExportName,
1282
+ )}`;
1283
+ }
1284
+
1285
+ fs.appendFileSync(resolvedExistingFilePath, appendContent, 'utf8');
1286
+ logOk(`Updated ${resolvedExistingFilePath}`);
1287
+ return;
1288
+ }
1289
+
1290
+ const useExistingModel = !keepModelInSameFile
1291
+ ? normalizeYesNo(
1292
+ await rl.question('Use existing model file? [y/N]: '),
1293
+ false,
1294
+ )
1295
+ : false;
1296
+
1297
+ if (
1298
+ fs.existsSync(expressionPath) ||
1299
+ (!keepModelInSameFile && !useExistingModel && fs.existsSync(modelPath))
1300
+ ) {
1301
+ const overwriteAnswer = await rl.question(
1302
+ `One or more target files already exist. Overwrite? [y/N]: `,
1303
+ );
1304
+ const shouldOverwrite = normalizeYesNo(overwriteAnswer, false);
1305
+ if (!shouldOverwrite) {
1306
+ logInfo('Cancelled. Existing file was not modified.');
1307
+ return;
1308
+ }
1309
+ }
1310
+
1311
+ fs.mkdirSync(resolvedDirectory, { recursive: true });
1312
+ let modelImportPath = `./${expressionFileBase}.model`;
1313
+ let modelExportName = 'model';
1314
+
1315
+ if (keepModelInSameFile) {
1316
+ fs.writeFileSync(
1317
+ expressionPath,
1318
+ createInlineExpressionTemplate(expressionName, expressionSource),
1319
+ 'utf8',
1320
+ );
1321
+ logOk(`Created ${expressionPath}`);
1322
+ return;
1323
+ }
1324
+
1325
+ if (useExistingModel) {
1326
+ const existingModelPathInput = await askUntilNonEmpty(
1327
+ rl,
1328
+ 'Existing model file path (relative to output dir or absolute): ',
1329
+ );
1330
+ const resolvedExistingModelPath = path.isAbsolute(existingModelPathInput)
1331
+ ? existingModelPathInput
1332
+ : path.resolve(resolvedDirectory, existingModelPathInput);
1333
+
1334
+ if (!fs.existsSync(resolvedExistingModelPath)) {
1335
+ logError(`Model file not found: ${resolvedExistingModelPath}`);
1336
+ return;
1337
+ }
547
1338
 
548
1339
  modelImportPath = toModuleImportPath(
549
1340
  expressionPath,
@@ -555,7 +1346,11 @@ async function runAdd() {
555
1346
  'model',
556
1347
  );
557
1348
  } else {
558
- fs.writeFileSync(modelPath, createModelTemplate(), 'utf8');
1349
+ fs.writeFileSync(
1350
+ modelPath,
1351
+ createModelTemplate(expressionSource),
1352
+ 'utf8',
1353
+ );
559
1354
  logOk(`Created ${modelPath}`);
560
1355
  }
561
1356
 
@@ -563,6 +1358,7 @@ async function runAdd() {
563
1358
  expressionPath,
564
1359
  createExpressionTemplate(
565
1360
  expressionName,
1361
+ expressionSource,
566
1362
  modelImportPath,
567
1363
  modelExportName,
568
1364
  ),
@@ -585,87 +1381,301 @@ function writeFileWithDryRun(filePath, content, dryRun) {
585
1381
  fs.writeFileSync(filePath, content, 'utf8');
586
1382
  }
587
1383
 
588
- function toFileDependencySpec(fromDir, targetPath) {
589
- const relative = path.relative(fromDir, targetPath).replace(/\\/gu, '/');
590
- const normalized = relative.startsWith('.') ? relative : `./${relative}`;
591
- return `file:${normalized}`;
592
- }
593
-
594
- function findLatestTarball(packageDir, packageSlug) {
595
- if (!fs.existsSync(packageDir)) {
596
- return null;
597
- }
1384
+ function stripJsonComments(content) {
1385
+ let result = '';
1386
+ let inString = false;
1387
+ let stringDelimiter = '"';
1388
+ let inLineComment = false;
1389
+ let inBlockComment = false;
1390
+
1391
+ for (let index = 0; index < content.length; index += 1) {
1392
+ const current = content[index];
1393
+ const next = content[index + 1];
1394
+
1395
+ if (inLineComment) {
1396
+ if (current === '\n') {
1397
+ inLineComment = false;
1398
+ result += current;
1399
+ }
1400
+ continue;
1401
+ }
598
1402
 
599
- const candidates = [];
600
- const stack = [packageDir];
1403
+ if (inBlockComment) {
1404
+ if (current === '*' && next === '/') {
1405
+ inBlockComment = false;
1406
+ index += 1;
1407
+ }
1408
+ continue;
1409
+ }
601
1410
 
602
- while (stack.length > 0) {
603
- const currentDir = stack.pop();
604
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
605
- for (const entry of entries) {
606
- const fullPath = path.join(currentDir, entry.name);
607
- if (entry.isDirectory()) {
608
- stack.push(fullPath);
1411
+ if (inString) {
1412
+ result += current;
1413
+ if (current === '\\') {
1414
+ index += 1;
1415
+ if (index < content.length) {
1416
+ result += content[index];
1417
+ }
609
1418
  continue;
610
1419
  }
611
-
612
- if (
613
- entry.isFile() &&
614
- entry.name.startsWith(`${packageSlug}-`) &&
615
- entry.name.endsWith('.tgz')
616
- ) {
617
- candidates.push(fullPath);
1420
+ if (current === stringDelimiter) {
1421
+ inString = false;
618
1422
  }
1423
+ continue;
1424
+ }
1425
+
1426
+ if (current === '"' || current === "'") {
1427
+ inString = true;
1428
+ stringDelimiter = current;
1429
+ result += current;
1430
+ continue;
1431
+ }
1432
+
1433
+ if (current === '/' && next === '/') {
1434
+ inLineComment = true;
1435
+ index += 1;
1436
+ continue;
1437
+ }
1438
+
1439
+ if (current === '/' && next === '*') {
1440
+ inBlockComment = true;
1441
+ index += 1;
1442
+ continue;
619
1443
  }
1444
+
1445
+ result += current;
620
1446
  }
621
1447
 
622
- candidates.sort();
623
- if (candidates.length === 0) {
624
- return null;
1448
+ return result;
1449
+ }
1450
+
1451
+ function parseJsonc(content) {
1452
+ return JSON.parse(stripJsonComments(content.replace(/^\uFEFF/u, '')));
1453
+ }
1454
+
1455
+ function copyPathWithDryRun(sourcePath, targetPath, dryRun) {
1456
+ if (dryRun) {
1457
+ logInfo(`[dry-run] copy ${sourcePath} -> ${targetPath}`);
1458
+ return;
625
1459
  }
626
1460
 
627
- return candidates[candidates.length - 1];
1461
+ const stat = fs.statSync(sourcePath);
1462
+ if (stat.isDirectory()) {
1463
+ fs.mkdirSync(targetPath, { recursive: true });
1464
+ for (const entry of fs.readdirSync(sourcePath, { withFileTypes: true })) {
1465
+ copyPathWithDryRun(
1466
+ path.join(sourcePath, entry.name),
1467
+ path.join(targetPath, entry.name),
1468
+ false,
1469
+ );
1470
+ }
1471
+ return;
1472
+ }
1473
+
1474
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1475
+ fs.copyFileSync(sourcePath, targetPath);
628
1476
  }
629
1477
 
630
- function resolveProjectRsxSpecs(
631
- projectRoot,
632
- workspaceRoot,
633
- tarballsDir,
634
- options = {},
635
- ) {
636
- const includeAngularPackage = Boolean(options.includeAngularPackage);
637
- const versionSpec = options.tag ? options.tag : RSX_PACKAGE_VERSION;
638
- const defaults = {
639
- '@rs-x/core': versionSpec,
640
- '@rs-x/state-manager': versionSpec,
641
- '@rs-x/expression-parser': versionSpec,
642
- '@rs-x/compiler': versionSpec,
643
- '@rs-x/typescript-plugin': versionSpec,
644
- ...(includeAngularPackage ? { '@rs-x/angular': versionSpec } : {}),
645
- '@rs-x/cli': null,
646
- };
1478
+ function removeFileOrDirectoryWithDryRun(targetPath, dryRun) {
1479
+ if (!fs.existsSync(targetPath)) {
1480
+ return;
1481
+ }
647
1482
 
648
- const tarballSlugs = {
649
- '@rs-x/core': 'rs-x-core',
650
- '@rs-x/state-manager': 'rs-x-state-manager',
651
- '@rs-x/expression-parser': 'rs-x-expression-parser',
652
- '@rs-x/compiler': 'rs-x-compiler',
653
- '@rs-x/typescript-plugin': 'rs-x-typescript-plugin',
654
- ...(includeAngularPackage ? { '@rs-x/angular': 'rs-x-angular' } : {}),
655
- '@rs-x/cli': 'rs-x-cli',
656
- };
1483
+ if (dryRun) {
1484
+ logInfo(`[dry-run] remove ${targetPath}`);
1485
+ return;
1486
+ }
657
1487
 
658
- if (tarballsDir) {
659
- const specs = { ...defaults };
660
- const packageDirBySlug = {
661
- 'rs-x-core': path.join(tarballsDir, 'rs-x-core'),
662
- 'rs-x-state-manager': path.join(tarballsDir, 'rs-x-state-manager'),
663
- 'rs-x-expression-parser': path.join(
664
- tarballsDir,
665
- 'rs-x-expression-parser',
666
- ),
667
- 'rs-x-compiler': path.join(tarballsDir, 'rs-x-compiler'),
668
- 'rs-x-typescript-plugin': path.join(
1488
+ fs.rmSync(targetPath, { recursive: true, force: true });
1489
+ }
1490
+
1491
+ function resolveProjectTsConfig(projectRoot) {
1492
+ const appTsConfigPath = path.join(projectRoot, 'tsconfig.app.json');
1493
+ if (fs.existsSync(appTsConfigPath)) {
1494
+ return appTsConfigPath;
1495
+ }
1496
+
1497
+ return path.join(projectRoot, 'tsconfig.json');
1498
+ }
1499
+
1500
+ function resolveAngularProjectTsConfig(projectRoot) {
1501
+ return resolveProjectTsConfig(projectRoot);
1502
+ }
1503
+
1504
+ function upsertTypescriptPluginInTsConfig(configPath, dryRun) {
1505
+ if (!fs.existsSync(configPath)) {
1506
+ logWarn(`TypeScript config not found: ${configPath}`);
1507
+ return;
1508
+ }
1509
+
1510
+ const tsConfig = parseJsonc(fs.readFileSync(configPath, 'utf8'));
1511
+ const compilerOptions = tsConfig.compilerOptions ?? {};
1512
+ const plugins = Array.isArray(compilerOptions.plugins)
1513
+ ? compilerOptions.plugins
1514
+ : [];
1515
+
1516
+ if (
1517
+ !plugins.some(
1518
+ (plugin) =>
1519
+ plugin &&
1520
+ typeof plugin === 'object' &&
1521
+ plugin.name === '@rs-x/typescript-plugin',
1522
+ )
1523
+ ) {
1524
+ plugins.push({ name: '@rs-x/typescript-plugin' });
1525
+ }
1526
+
1527
+ compilerOptions.plugins = plugins;
1528
+ tsConfig.compilerOptions = compilerOptions;
1529
+
1530
+ if (dryRun) {
1531
+ logInfo(`[dry-run] patch ${configPath}`);
1532
+ return;
1533
+ }
1534
+
1535
+ fs.writeFileSync(
1536
+ configPath,
1537
+ `${JSON.stringify(tsConfig, null, 2)}\n`,
1538
+ 'utf8',
1539
+ );
1540
+ }
1541
+
1542
+ function ensureTsConfigIncludePattern(configPath, pattern, dryRun) {
1543
+ if (!fs.existsSync(configPath)) {
1544
+ return;
1545
+ }
1546
+
1547
+ const tsConfig = parseJsonc(fs.readFileSync(configPath, 'utf8'));
1548
+ const include = Array.isArray(tsConfig.include) ? tsConfig.include : [];
1549
+ if (!include.includes(pattern)) {
1550
+ include.push(pattern);
1551
+ }
1552
+ tsConfig.include = include;
1553
+
1554
+ if (dryRun) {
1555
+ logInfo(`[dry-run] patch ${configPath}`);
1556
+ return;
1557
+ }
1558
+
1559
+ fs.writeFileSync(
1560
+ configPath,
1561
+ `${JSON.stringify(tsConfig, null, 2)}\n`,
1562
+ 'utf8',
1563
+ );
1564
+ }
1565
+
1566
+ function ensureVueEnvTypes(projectRoot, dryRun) {
1567
+ const envTypesPath = path.join(projectRoot, 'src', 'vite-env.d.ts');
1568
+ const envTypesSource = `/// <reference types="vite/client" />
1569
+
1570
+ declare module '*.vue' {
1571
+ import type { DefineComponent } from 'vue';
1572
+
1573
+ const component: DefineComponent<{}, {}, any>;
1574
+ export default component;
1575
+ }
1576
+ `;
1577
+
1578
+ if (fs.existsSync(envTypesPath)) {
1579
+ return;
1580
+ }
1581
+
1582
+ if (dryRun) {
1583
+ logInfo(`[dry-run] create ${envTypesPath}`);
1584
+ return;
1585
+ }
1586
+
1587
+ fs.mkdirSync(path.dirname(envTypesPath), { recursive: true });
1588
+ fs.writeFileSync(envTypesPath, envTypesSource, 'utf8');
1589
+ logOk(`Created ${envTypesPath}`);
1590
+ }
1591
+
1592
+ function toFileDependencySpec(fromDir, targetPath) {
1593
+ const relative = path.relative(fromDir, targetPath).replace(/\\/gu, '/');
1594
+ const normalized = relative.startsWith('.') ? relative : `./${relative}`;
1595
+ return `file:${normalized}`;
1596
+ }
1597
+
1598
+ function findLatestTarball(packageDir, packageSlug) {
1599
+ if (!fs.existsSync(packageDir)) {
1600
+ return null;
1601
+ }
1602
+
1603
+ const candidates = [];
1604
+ const stack = [packageDir];
1605
+
1606
+ while (stack.length > 0) {
1607
+ const currentDir = stack.pop();
1608
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
1609
+ for (const entry of entries) {
1610
+ const fullPath = path.join(currentDir, entry.name);
1611
+ if (entry.isDirectory()) {
1612
+ stack.push(fullPath);
1613
+ continue;
1614
+ }
1615
+
1616
+ if (
1617
+ entry.isFile() &&
1618
+ entry.name.startsWith(`${packageSlug}-`) &&
1619
+ entry.name.endsWith('.tgz')
1620
+ ) {
1621
+ candidates.push(fullPath);
1622
+ }
1623
+ }
1624
+ }
1625
+
1626
+ candidates.sort();
1627
+ if (candidates.length === 0) {
1628
+ return null;
1629
+ }
1630
+
1631
+ return candidates[candidates.length - 1];
1632
+ }
1633
+
1634
+ function resolveProjectRsxSpecs(
1635
+ projectRoot,
1636
+ workspaceRoot,
1637
+ tarballsDir,
1638
+ options = {},
1639
+ ) {
1640
+ const includeAngularPackage = Boolean(options.includeAngularPackage);
1641
+ const includeReactPackage = Boolean(options.includeReactPackage);
1642
+ const includeVuePackage = Boolean(options.includeVuePackage);
1643
+ const versionSpec = options.tag ? options.tag : RSX_PACKAGE_VERSION;
1644
+ const defaults = {
1645
+ '@rs-x/core': versionSpec,
1646
+ '@rs-x/state-manager': versionSpec,
1647
+ '@rs-x/expression-parser': versionSpec,
1648
+ '@rs-x/compiler': versionSpec,
1649
+ '@rs-x/typescript-plugin': versionSpec,
1650
+ ...(includeAngularPackage ? { '@rs-x/angular': versionSpec } : {}),
1651
+ ...(includeReactPackage ? { '@rs-x/react': versionSpec } : {}),
1652
+ ...(includeVuePackage ? { '@rs-x/vue': versionSpec } : {}),
1653
+ '@rs-x/cli': versionSpec,
1654
+ };
1655
+
1656
+ const tarballSlugs = {
1657
+ '@rs-x/core': 'rs-x-core',
1658
+ '@rs-x/state-manager': 'rs-x-state-manager',
1659
+ '@rs-x/expression-parser': 'rs-x-expression-parser',
1660
+ '@rs-x/compiler': 'rs-x-compiler',
1661
+ '@rs-x/typescript-plugin': 'rs-x-typescript-plugin',
1662
+ ...(includeAngularPackage ? { '@rs-x/angular': 'rs-x-angular' } : {}),
1663
+ ...(includeReactPackage ? { '@rs-x/react': 'rs-x-react' } : {}),
1664
+ ...(includeVuePackage ? { '@rs-x/vue': 'rs-x-vue' } : {}),
1665
+ '@rs-x/cli': 'rs-x-cli',
1666
+ };
1667
+
1668
+ if (tarballsDir) {
1669
+ const specs = { ...defaults };
1670
+ const packageDirBySlug = {
1671
+ 'rs-x-core': path.join(tarballsDir, 'rs-x-core'),
1672
+ 'rs-x-state-manager': path.join(tarballsDir, 'rs-x-state-manager'),
1673
+ 'rs-x-expression-parser': path.join(
1674
+ tarballsDir,
1675
+ 'rs-x-expression-parser',
1676
+ ),
1677
+ 'rs-x-compiler': path.join(tarballsDir, 'rs-x-compiler'),
1678
+ 'rs-x-typescript-plugin': path.join(
669
1679
  tarballsDir,
670
1680
  'rs-x-typescript-plugin',
671
1681
  ),
@@ -674,6 +1684,16 @@ function resolveProjectRsxSpecs(
674
1684
  'rs-x-angular': path.join(tarballsDir, 'rs-x-angular'),
675
1685
  }
676
1686
  : {}),
1687
+ ...(includeReactPackage
1688
+ ? {
1689
+ 'rs-x-react': path.join(tarballsDir, 'rs-x-react'),
1690
+ }
1691
+ : {}),
1692
+ ...(includeVuePackage
1693
+ ? {
1694
+ 'rs-x-vue': path.join(tarballsDir, 'rs-x-vue'),
1695
+ }
1696
+ : {}),
677
1697
  'rs-x-cli': path.join(tarballsDir, 'rs-x-cli'),
678
1698
  };
679
1699
 
@@ -711,10 +1731,21 @@ function resolveProjectRsxSpecs(
711
1731
  ),
712
1732
  ...(includeAngularPackage
713
1733
  ? {
714
- '@rs-x/angular': path.join(
715
- workspaceRoot,
716
- 'rs-x-angular/projects/rsx',
717
- ),
1734
+ '@rs-x/angular': fs.existsSync(
1735
+ path.join(workspaceRoot, 'rs-x-angular/dist/rsx'),
1736
+ )
1737
+ ? path.join(workspaceRoot, 'rs-x-angular/dist/rsx')
1738
+ : path.join(workspaceRoot, 'rs-x-angular/projects/rsx'),
1739
+ }
1740
+ : {}),
1741
+ ...(includeReactPackage
1742
+ ? {
1743
+ '@rs-x/react': path.join(workspaceRoot, 'rs-x-react'),
1744
+ }
1745
+ : {}),
1746
+ ...(includeVuePackage
1747
+ ? {
1748
+ '@rs-x/vue': path.join(workspaceRoot, 'rs-x-vue'),
718
1749
  }
719
1750
  : {}),
720
1751
  '@rs-x/cli': path.join(workspaceRoot, 'rs-x-cli'),
@@ -777,6 +1808,22 @@ function createProjectPackageJson(projectName, rsxSpecs) {
777
1808
  );
778
1809
  }
779
1810
 
1811
+ function resolveProjectRoot(projectName, flags) {
1812
+ const parentDir =
1813
+ typeof flags?.['project-parent-dir'] === 'string'
1814
+ ? flags['project-parent-dir']
1815
+ : typeof process.env.RSX_PROJECT_PARENT_DIR === 'string' &&
1816
+ process.env.RSX_PROJECT_PARENT_DIR.trim().length > 0
1817
+ ? process.env.RSX_PROJECT_PARENT_DIR
1818
+ : null;
1819
+
1820
+ if (parentDir) {
1821
+ return path.resolve(parentDir, projectName);
1822
+ }
1823
+
1824
+ return path.resolve(process.cwd(), projectName);
1825
+ }
1826
+
780
1827
  function createProjectTsConfig() {
781
1828
  return (
782
1829
  JSON.stringify(
@@ -945,8 +1992,9 @@ function applyVueRsxTemplate(projectRoot, dryRun) {
945
1992
  async function runProject(flags) {
946
1993
  const dryRun = Boolean(flags['dry-run']);
947
1994
  const skipInstall = Boolean(flags['skip-install']);
948
- const pm = detectPackageManager(flags.pm);
949
- const tag = resolveInstallTag(flags);
1995
+ const invocationRoot = process.cwd();
1996
+ const pm = resolveCliPackageManager(invocationRoot, flags.pm);
1997
+ const tag = resolveConfiguredInstallTag(invocationRoot, flags);
950
1998
  let projectName = typeof flags.name === 'string' ? flags.name.trim() : '';
951
1999
 
952
2000
  if (!projectName) {
@@ -961,7 +2009,7 @@ async function runProject(flags) {
961
2009
  }
962
2010
  }
963
2011
 
964
- const projectRoot = path.resolve(process.cwd(), projectName);
2012
+ const projectRoot = resolveProjectRoot(projectName, flags);
965
2013
  const tarballsDir =
966
2014
  typeof flags['tarballs-dir'] === 'string'
967
2015
  ? path.resolve(process.cwd(), flags['tarballs-dir'])
@@ -1051,135 +2099,676 @@ export const sampleExpression = rsx('a + b')(model);
1051
2099
  `import { sampleExpression } from './expressions/sample.expression';
1052
2100
  import { initRsx } from './rsx-bootstrap';
1053
2101
 
1054
- async function main(): Promise<void> {
1055
- await initRsx();
1056
- console.log('RS-X sample expression initialized:', Boolean(sampleExpression));
1057
- }
2102
+ async function main(): Promise<void> {
2103
+ await initRsx();
2104
+ console.log('RS-X sample expression initialized:', Boolean(sampleExpression));
2105
+ }
2106
+
2107
+ void main();
2108
+ `,
2109
+ dryRun,
2110
+ );
2111
+
2112
+ if (!skipInstall) {
2113
+ const installArgsByPm = {
2114
+ pnpm: ['install'],
2115
+ npm: ['install'],
2116
+ yarn: ['install'],
2117
+ bun: ['install'],
2118
+ };
2119
+ const installArgs = installArgsByPm[pm];
2120
+ if (!installArgs) {
2121
+ logError(`Unsupported package manager: ${pm}`);
2122
+ process.exit(1);
2123
+ }
2124
+
2125
+ logInfo(`Installing dependencies with ${pm}...`);
2126
+ run(pm, installArgs, { dryRun, cwd: projectRoot });
2127
+ logOk('Dependencies installed.');
2128
+ } else {
2129
+ logInfo('Skipping dependency install (--skip-install).');
2130
+ }
2131
+
2132
+ logOk(`Created RS-X project: ${projectRoot}`);
2133
+ logInfo('Next steps:');
2134
+ console.log(` cd ${projectName}`);
2135
+ if (skipInstall) {
2136
+ console.log(' npm install');
2137
+ }
2138
+ console.log(' npm run build');
2139
+ console.log(' npm run start');
2140
+ }
2141
+
2142
+ async function resolveProjectName(nameFromFlags, fallbackName) {
2143
+ const fromFlags =
2144
+ typeof nameFromFlags === 'string' ? nameFromFlags.trim() : '';
2145
+ if (fromFlags.length > 0) {
2146
+ return fromFlags;
2147
+ }
2148
+
2149
+ const fromFallback =
2150
+ typeof fallbackName === 'string' ? fallbackName.trim() : '';
2151
+ if (fromFallback.length > 0) {
2152
+ return fromFallback;
2153
+ }
2154
+
2155
+ const rl = readline.createInterface({
2156
+ input: process.stdin,
2157
+ output: process.stdout,
2158
+ });
2159
+ try {
2160
+ return await askUntilNonEmpty(rl, 'Project name: ');
2161
+ } finally {
2162
+ rl.close();
2163
+ }
2164
+ }
2165
+
2166
+ function scaffoldProjectTemplate(
2167
+ template,
2168
+ projectName,
2169
+ projectRoot,
2170
+ pm,
2171
+ flags,
2172
+ ) {
2173
+ const dryRun = Boolean(flags['dry-run']);
2174
+ const skipInstall = Boolean(flags['skip-install']);
2175
+ const scaffoldCwd = path.dirname(projectRoot);
2176
+ const scaffoldProjectArg = `./${projectName}`;
2177
+ const scaffoldEnv = {
2178
+ INIT_CWD: scaffoldCwd,
2179
+ npm_config_local_prefix: scaffoldCwd,
2180
+ npm_prefix: scaffoldCwd,
2181
+ PWD: scaffoldCwd,
2182
+ CI: 'true',
2183
+ };
2184
+
2185
+ if (template === 'angular') {
2186
+ const args = [
2187
+ '-y',
2188
+ '@angular/cli@latest',
2189
+ 'new',
2190
+ projectName,
2191
+ '--directory',
2192
+ scaffoldProjectArg,
2193
+ '--defaults',
2194
+ '--standalone',
2195
+ '--routing',
2196
+ '--style',
2197
+ 'css',
2198
+ '--skip-git',
2199
+ ];
2200
+ if (skipInstall) {
2201
+ args.push('--skip-install');
2202
+ }
2203
+ run('npx', args, { dryRun, cwd: scaffoldCwd, env: scaffoldEnv });
2204
+ return;
2205
+ }
2206
+
2207
+ if (template === 'react') {
2208
+ run(
2209
+ 'npx',
2210
+ [
2211
+ 'create-vite@latest',
2212
+ scaffoldProjectArg,
2213
+ '--no-interactive',
2214
+ '--template',
2215
+ 'react-ts',
2216
+ ],
2217
+ {
2218
+ dryRun,
2219
+ cwd: scaffoldCwd,
2220
+ env: scaffoldEnv,
2221
+ },
2222
+ );
2223
+ return;
2224
+ }
2225
+
2226
+ if (template === 'vuejs') {
2227
+ run(
2228
+ 'npx',
2229
+ [
2230
+ 'create-vite@latest',
2231
+ scaffoldProjectArg,
2232
+ '--no-interactive',
2233
+ '--template',
2234
+ 'vue-ts',
2235
+ ],
2236
+ {
2237
+ dryRun,
2238
+ cwd: scaffoldCwd,
2239
+ env: scaffoldEnv,
2240
+ },
2241
+ );
2242
+ return;
2243
+ }
2244
+
2245
+ if (template === 'nextjs') {
2246
+ const packageManagerFlagByPm = {
2247
+ npm: '--use-npm',
2248
+ pnpm: '--use-pnpm',
2249
+ yarn: '--use-yarn',
2250
+ bun: '--use-bun',
2251
+ };
2252
+ const args = [
2253
+ 'create-next-app@latest',
2254
+ scaffoldProjectArg,
2255
+ '--yes',
2256
+ '--ts',
2257
+ '--app',
2258
+ '--eslint',
2259
+ '--import-alias',
2260
+ '@/*',
2261
+ packageManagerFlagByPm[pm] ?? '--use-npm',
2262
+ ];
2263
+ if (skipInstall) {
2264
+ args.push('--skip-install');
2265
+ }
2266
+ run('npx', args, { dryRun, cwd: scaffoldCwd, env: scaffoldEnv });
2267
+ return;
2268
+ }
2269
+
2270
+ logError(`Unknown project template: ${template}`);
2271
+ process.exit(1);
2272
+ }
2273
+
2274
+ function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
2275
+ const dryRun = Boolean(flags['dry-run']);
2276
+ ensureRsxConfigFile(projectRoot, 'angular', dryRun);
2277
+ const tag = resolveInstallTag(flags);
2278
+ const tarballsDir =
2279
+ typeof flags['tarballs-dir'] === 'string'
2280
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
2281
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
2282
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
2283
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
2284
+ : null;
2285
+ const workspaceRoot = findRepoRoot(projectRoot);
2286
+ const rsxSpecs = resolveProjectRsxSpecs(
2287
+ projectRoot,
2288
+ workspaceRoot,
2289
+ tarballsDir,
2290
+ { tag, includeAngularPackage: true },
2291
+ );
2292
+
2293
+ const templateFiles = ['README.md', 'src'];
2294
+ for (const entry of templateFiles) {
2295
+ copyPathWithDryRun(
2296
+ path.join(ANGULAR_DEMO_TEMPLATE_DIR, entry),
2297
+ path.join(projectRoot, entry),
2298
+ dryRun,
2299
+ );
2300
+ }
2301
+
2302
+ const staleAngularFiles = [
2303
+ path.join(projectRoot, 'src/app/app.ts'),
2304
+ path.join(projectRoot, 'src/app/app.spec.ts'),
2305
+ path.join(projectRoot, 'src/app/app.html'),
2306
+ path.join(projectRoot, 'src/app/app.css'),
2307
+ path.join(projectRoot, 'src/app/app.routes.ts'),
2308
+ path.join(projectRoot, 'src/app/app.config.ts'),
2309
+ ];
2310
+ for (const stalePath of staleAngularFiles) {
2311
+ removeFileOrDirectoryWithDryRun(stalePath, dryRun);
2312
+ }
2313
+
2314
+ const readmePath = path.join(projectRoot, 'README.md');
2315
+ if (fs.existsSync(readmePath)) {
2316
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
2317
+ const nextReadme = readmeSource.replace(
2318
+ /^#\s+rsx-angular-example/mu,
2319
+ `# ${projectName}`,
2320
+ );
2321
+ if (dryRun) {
2322
+ logInfo(`[dry-run] patch ${readmePath}`);
2323
+ } else {
2324
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
2325
+ }
2326
+ }
2327
+
2328
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2329
+ if (!fs.existsSync(packageJsonPath)) {
2330
+ logError(
2331
+ `package.json not found in generated Angular app: ${packageJsonPath}`,
2332
+ );
2333
+ process.exit(1);
2334
+ }
2335
+
2336
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2337
+ const angularTsConfigPath = resolveAngularProjectTsConfig(projectRoot);
2338
+ const angularTsConfigRelative = path
2339
+ .relative(projectRoot, angularTsConfigPath)
2340
+ .replace(/\\/gu, '/');
2341
+ packageJson.name = projectName;
2342
+ packageJson.private = true;
2343
+ packageJson.version = '0.1.0';
2344
+ packageJson.scripts = {
2345
+ 'build:rsx': `rsx build --project ${angularTsConfigRelative} --no-emit --prod`,
2346
+ 'typecheck:rsx': `rsx typecheck --project ${angularTsConfigRelative}`,
2347
+ prebuild: 'npm run build:rsx',
2348
+ start: 'npm run build:rsx && ng serve',
2349
+ build: 'ng build',
2350
+ };
2351
+ packageJson.dependencies = {
2352
+ ...(packageJson.dependencies ?? {}),
2353
+ '@rs-x/angular': rsxSpecs['@rs-x/angular'],
2354
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
2355
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
2356
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
2357
+ };
2358
+ packageJson.devDependencies = {
2359
+ ...(packageJson.devDependencies ?? {}),
2360
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
2361
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
2362
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
2363
+ };
2364
+
2365
+ if (dryRun) {
2366
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
2367
+ } else {
2368
+ fs.writeFileSync(
2369
+ packageJsonPath,
2370
+ `${JSON.stringify(packageJson, null, 2)}\n`,
2371
+ 'utf8',
2372
+ );
2373
+ }
2374
+
2375
+ const angularJsonPath = path.join(projectRoot, 'angular.json');
2376
+ if (!fs.existsSync(angularJsonPath)) {
2377
+ logError(
2378
+ `angular.json not found in generated Angular app: ${angularJsonPath}`,
2379
+ );
2380
+ process.exit(1);
2381
+ }
2382
+
2383
+ const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
2384
+ const projects = angularJson.projects ?? {};
2385
+ const [angularProjectName] = Object.keys(projects);
2386
+ if (!angularProjectName) {
2387
+ logError('Generated angular.json does not define any projects.');
2388
+ process.exit(1);
2389
+ }
2390
+
2391
+ const angularProject = projects[angularProjectName];
2392
+ const architect = angularProject.architect ?? angularProject.targets;
2393
+ const build = architect?.build;
2394
+ if (!build) {
2395
+ logError('Generated Angular project is missing a build target.');
2396
+ process.exit(1);
2397
+ }
2398
+
2399
+ const buildOptions = build.options ?? {};
2400
+ const styles = Array.isArray(buildOptions.styles) ? buildOptions.styles : [];
2401
+ if (!styles.includes('src/styles.css')) {
2402
+ styles.push('src/styles.css');
2403
+ }
2404
+ buildOptions.styles = styles;
2405
+ buildOptions.preserveSymlinks = true;
2406
+
2407
+ const registrationFile =
2408
+ 'src/rsx-generated/rsx-aot-registration.generated.ts';
2409
+ let polyfills = buildOptions.polyfills;
2410
+ if (typeof polyfills === 'string') {
2411
+ polyfills = [polyfills];
2412
+ } else if (!Array.isArray(polyfills)) {
2413
+ polyfills = [];
2414
+ }
2415
+ if (!polyfills.includes(registrationFile)) {
2416
+ polyfills.push(registrationFile);
2417
+ }
2418
+ buildOptions.polyfills = polyfills;
2419
+ build.options = buildOptions;
2420
+
2421
+ if (build.configurations?.production?.budgets) {
2422
+ delete build.configurations.production.budgets;
2423
+ }
2424
+
2425
+ if (dryRun) {
2426
+ logInfo(`[dry-run] patch ${angularJsonPath}`);
2427
+ } else {
2428
+ fs.writeFileSync(
2429
+ angularJsonPath,
2430
+ `${JSON.stringify(angularJson, null, 2)}\n`,
2431
+ 'utf8',
2432
+ );
2433
+ }
2434
+
2435
+ if (!Boolean(flags['skip-install'])) {
2436
+ logInfo(`Refreshing ${pm} dependencies for the RS-X Angular starter...`);
2437
+ run(pm, ['install'], { dryRun });
2438
+ logOk('Angular starter dependencies are up to date.');
2439
+ }
2440
+ }
2441
+
2442
+ function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
2443
+ const dryRun = Boolean(flags['dry-run']);
2444
+ ensureRsxConfigFile(projectRoot, 'react', dryRun);
2445
+ const tag = resolveInstallTag(flags);
2446
+ const tarballsDir =
2447
+ typeof flags['tarballs-dir'] === 'string'
2448
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
2449
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
2450
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
2451
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
2452
+ : null;
2453
+ const workspaceRoot = findRepoRoot(projectRoot);
2454
+ const rsxSpecs = resolveProjectRsxSpecs(
2455
+ projectRoot,
2456
+ workspaceRoot,
2457
+ tarballsDir,
2458
+ { tag, includeReactPackage: true },
2459
+ );
2460
+
2461
+ const templateFiles = [
2462
+ 'README.md',
2463
+ 'index.html',
2464
+ 'src',
2465
+ 'tsconfig.json',
2466
+ 'vite.config.ts',
2467
+ ];
2468
+ for (const entry of templateFiles) {
2469
+ copyPathWithDryRun(
2470
+ path.join(REACT_DEMO_TEMPLATE_DIR, entry),
2471
+ path.join(projectRoot, entry),
2472
+ dryRun,
2473
+ );
2474
+ }
2475
+
2476
+ const staleReactFiles = [
2477
+ path.join(projectRoot, 'src/App.tsx'),
2478
+ path.join(projectRoot, 'src/App.css'),
2479
+ path.join(projectRoot, 'src/index.css'),
2480
+ path.join(projectRoot, 'src/vite-env.d.ts'),
2481
+ path.join(projectRoot, 'src/assets'),
2482
+ path.join(projectRoot, 'public'),
2483
+ path.join(projectRoot, 'eslint.config.js'),
2484
+ path.join(projectRoot, 'eslint.config.ts'),
2485
+ path.join(projectRoot, 'tsconfig.app.json'),
2486
+ path.join(projectRoot, 'tsconfig.node.json'),
2487
+ ];
2488
+ for (const stalePath of staleReactFiles) {
2489
+ removeFileOrDirectoryWithDryRun(stalePath, dryRun);
2490
+ }
2491
+
2492
+ const readmePath = path.join(projectRoot, 'README.md');
2493
+ if (fs.existsSync(readmePath)) {
2494
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
2495
+ const nextReadme = readmeSource.replace(
2496
+ /^#\s+rsx-react-example/mu,
2497
+ `# ${projectName}`,
2498
+ );
2499
+ if (dryRun) {
2500
+ logInfo(`[dry-run] patch ${readmePath}`);
2501
+ } else {
2502
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
2503
+ }
2504
+ }
2505
+
2506
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2507
+ if (!fs.existsSync(packageJsonPath)) {
2508
+ logError(
2509
+ `package.json not found in generated React app: ${packageJsonPath}`,
2510
+ );
2511
+ process.exit(1);
2512
+ }
2513
+
2514
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2515
+ packageJson.name = projectName;
2516
+ packageJson.private = true;
2517
+ packageJson.version = '0.1.0';
2518
+ packageJson.type = 'module';
2519
+ packageJson.scripts = {
2520
+ 'build:rsx': 'rsx build --project tsconfig.json --no-emit --prod',
2521
+ dev: 'npm run build:rsx && vite',
2522
+ build: 'npm run build:rsx && vite build',
2523
+ preview: 'vite preview',
2524
+ };
2525
+ packageJson.dependencies = {
2526
+ react: packageJson.dependencies?.react ?? '^19.2.4',
2527
+ 'react-dom': packageJson.dependencies?.['react-dom'] ?? '^19.2.4',
2528
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
2529
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
2530
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
2531
+ '@rs-x/react': rsxSpecs['@rs-x/react'],
2532
+ };
2533
+ packageJson.devDependencies = {
2534
+ typescript: packageJson.devDependencies?.typescript ?? '^5.9.3',
2535
+ vite: packageJson.devDependencies?.vite ?? '^7.3.1',
2536
+ '@vitejs/plugin-react':
2537
+ packageJson.devDependencies?.['@vitejs/plugin-react'] ?? '^5.1.4',
2538
+ '@types/react': packageJson.devDependencies?.['@types/react'] ?? '^19.2.2',
2539
+ '@types/react-dom':
2540
+ packageJson.devDependencies?.['@types/react-dom'] ?? '^19.2.2',
2541
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
2542
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
2543
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
2544
+ };
2545
+
2546
+ if (dryRun) {
2547
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
2548
+ } else {
2549
+ fs.writeFileSync(
2550
+ packageJsonPath,
2551
+ `${JSON.stringify(packageJson, null, 2)}\n`,
2552
+ 'utf8',
2553
+ );
2554
+ }
2555
+
2556
+ const tsConfigPath = path.join(projectRoot, 'tsconfig.json');
2557
+ if (fs.existsSync(tsConfigPath)) {
2558
+ upsertTypescriptPluginInTsConfig(tsConfigPath, dryRun);
2559
+ }
2560
+
2561
+ if (!Boolean(flags['skip-install'])) {
2562
+ logInfo(`Refreshing ${pm} dependencies for the RS-X React starter...`);
2563
+ run(pm, ['install'], { dryRun });
2564
+ logOk('React starter dependencies are up to date.');
2565
+ }
2566
+ }
2567
+
2568
+ function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
2569
+ const dryRun = Boolean(flags['dry-run']);
2570
+ ensureRsxConfigFile(projectRoot, 'vuejs', dryRun);
2571
+ const tag = resolveInstallTag(flags);
2572
+ const tarballsDir =
2573
+ typeof flags['tarballs-dir'] === 'string'
2574
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
2575
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
2576
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
2577
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
2578
+ : null;
2579
+ const workspaceRoot = findRepoRoot(projectRoot);
2580
+ const rsxSpecs = resolveProjectRsxSpecs(
2581
+ projectRoot,
2582
+ workspaceRoot,
2583
+ tarballsDir,
2584
+ { tag, includeVuePackage: true },
2585
+ );
2586
+
2587
+ const templateFiles = ['README.md', 'src'];
2588
+ for (const entry of templateFiles) {
2589
+ copyPathWithDryRun(
2590
+ path.join(VUE_DEMO_TEMPLATE_DIR, entry),
2591
+ path.join(projectRoot, entry),
2592
+ dryRun,
2593
+ );
2594
+ }
1058
2595
 
1059
- void main();
1060
- `,
1061
- dryRun,
1062
- );
2596
+ const staleVueFiles = [
2597
+ path.join(projectRoot, 'public'),
2598
+ path.join(projectRoot, 'src/components/HelloWorld.vue'),
2599
+ path.join(projectRoot, 'src/assets'),
2600
+ ];
2601
+ for (const stalePath of staleVueFiles) {
2602
+ removeFileOrDirectoryWithDryRun(stalePath, dryRun);
2603
+ }
1063
2604
 
1064
- if (!skipInstall) {
1065
- const installArgsByPm = {
1066
- pnpm: ['install'],
1067
- npm: ['install'],
1068
- yarn: ['install'],
1069
- bun: ['install'],
1070
- };
1071
- const installArgs = installArgsByPm[pm];
1072
- if (!installArgs) {
1073
- logError(`Unsupported package manager: ${pm}`);
1074
- process.exit(1);
2605
+ const readmePath = path.join(projectRoot, 'README.md');
2606
+ if (fs.existsSync(readmePath)) {
2607
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
2608
+ const nextReadme = readmeSource.replace(
2609
+ /^#\s+rsx-vue-example/mu,
2610
+ `# ${projectName}`,
2611
+ );
2612
+ if (dryRun) {
2613
+ logInfo(`[dry-run] patch ${readmePath}`);
2614
+ } else {
2615
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
1075
2616
  }
1076
-
1077
- logInfo(`Installing dependencies with ${pm}...`);
1078
- run(pm, installArgs, { dryRun, cwd: projectRoot });
1079
- logOk('Dependencies installed.');
1080
- } else {
1081
- logInfo('Skipping dependency install (--skip-install).');
1082
2617
  }
1083
2618
 
1084
- logOk(`Created RS-X project: ${projectRoot}`);
1085
- logInfo('Next steps:');
1086
- console.log(` cd ${projectName}`);
1087
- if (skipInstall) {
1088
- console.log(' npm install');
2619
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2620
+ if (!fs.existsSync(packageJsonPath)) {
2621
+ logError(`package.json not found in generated Vue app: ${packageJsonPath}`);
2622
+ process.exit(1);
1089
2623
  }
1090
- console.log(' npm run build');
1091
- console.log(' npm run start');
1092
- }
1093
2624
 
1094
- async function resolveProjectName(nameFromFlags, fallbackName) {
1095
- const fromFlags =
1096
- typeof nameFromFlags === 'string' ? nameFromFlags.trim() : '';
1097
- if (fromFlags.length > 0) {
1098
- return fromFlags;
2625
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2626
+ packageJson.name = projectName;
2627
+ packageJson.private = true;
2628
+ packageJson.version = '0.1.0';
2629
+ packageJson.type = 'module';
2630
+ packageJson.scripts = {
2631
+ 'build:rsx': 'rsx build --project tsconfig.app.json --no-emit --prod',
2632
+ 'typecheck:rsx': 'rsx typecheck --project tsconfig.app.json',
2633
+ dev: 'npm run build:rsx && vite',
2634
+ build: 'npm run build:rsx && vue-tsc -b && vite build',
2635
+ preview: 'vite preview',
2636
+ };
2637
+ packageJson.dependencies = {
2638
+ vue: packageJson.dependencies?.vue ?? '^3.5.30',
2639
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
2640
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
2641
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
2642
+ '@rs-x/vue': rsxSpecs['@rs-x/vue'],
2643
+ };
2644
+ packageJson.devDependencies = {
2645
+ ...(packageJson.devDependencies ?? {}),
2646
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
2647
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
2648
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
2649
+ };
2650
+
2651
+ if (dryRun) {
2652
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
2653
+ } else {
2654
+ fs.writeFileSync(
2655
+ packageJsonPath,
2656
+ `${JSON.stringify(packageJson, null, 2)}\n`,
2657
+ 'utf8',
2658
+ );
1099
2659
  }
1100
2660
 
1101
- const fromFallback =
1102
- typeof fallbackName === 'string' ? fallbackName.trim() : '';
1103
- if (fromFallback.length > 0) {
1104
- return fromFallback;
2661
+ const tsConfigAppPath = path.join(projectRoot, 'tsconfig.app.json');
2662
+ if (fs.existsSync(tsConfigAppPath)) {
2663
+ upsertTypescriptPluginInTsConfig(tsConfigAppPath, dryRun);
2664
+ ensureTsConfigIncludePattern(tsConfigAppPath, 'src/**/*.d.ts', dryRun);
1105
2665
  }
1106
2666
 
1107
- const rl = readline.createInterface({
1108
- input: process.stdin,
1109
- output: process.stdout,
1110
- });
1111
- try {
1112
- return await askUntilNonEmpty(rl, 'Project name: ');
1113
- } finally {
1114
- rl.close();
2667
+ if (!Boolean(flags['skip-install'])) {
2668
+ logInfo(`Refreshing ${pm} dependencies for the RS-X Vue starter...`);
2669
+ run(pm, ['install'], { dryRun });
2670
+ logOk('Vue starter dependencies are up to date.');
1115
2671
  }
1116
2672
  }
1117
2673
 
1118
- function scaffoldProjectTemplate(template, projectName, pm, flags) {
2674
+ function applyNextDemoStarter(projectRoot, projectName, pm, flags) {
1119
2675
  const dryRun = Boolean(flags['dry-run']);
1120
- const skipInstall = Boolean(flags['skip-install']);
2676
+ ensureRsxConfigFile(projectRoot, 'nextjs', dryRun);
2677
+ const tag = resolveInstallTag(flags);
2678
+ const tarballsDir =
2679
+ typeof flags['tarballs-dir'] === 'string'
2680
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
2681
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
2682
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
2683
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
2684
+ : null;
2685
+ const workspaceRoot = findRepoRoot(projectRoot);
2686
+ const rsxSpecs = resolveProjectRsxSpecs(
2687
+ projectRoot,
2688
+ workspaceRoot,
2689
+ tarballsDir,
2690
+ { tag, includeReactPackage: true },
2691
+ );
1121
2692
 
1122
- if (template === 'angular') {
1123
- const args = [
1124
- '-y',
1125
- '@angular/cli@latest',
1126
- 'new',
1127
- projectName,
1128
- '--defaults',
1129
- '--standalone',
1130
- '--routing',
1131
- '--style',
1132
- 'css',
1133
- '--skip-git',
1134
- ];
1135
- if (skipInstall) {
1136
- args.push('--skip-install');
2693
+ const templateFiles = ['README.md', 'app', 'components', 'hooks', 'lib'];
2694
+ for (const entry of templateFiles) {
2695
+ copyPathWithDryRun(
2696
+ path.join(NEXT_DEMO_TEMPLATE_DIR, entry),
2697
+ path.join(projectRoot, entry),
2698
+ dryRun,
2699
+ );
2700
+ }
2701
+
2702
+ const readmePath = path.join(projectRoot, 'README.md');
2703
+ if (fs.existsSync(readmePath)) {
2704
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
2705
+ const nextReadme = readmeSource.replace(
2706
+ /^#\s+rsx-next-example/mu,
2707
+ `# ${projectName}`,
2708
+ );
2709
+ if (dryRun) {
2710
+ logInfo(`[dry-run] patch ${readmePath}`);
2711
+ } else {
2712
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
1137
2713
  }
1138
- run('npx', args, { dryRun });
1139
- return;
1140
2714
  }
1141
2715
 
1142
- if (template === 'react') {
1143
- run('npx', ['create-vite@latest', projectName, '--template', 'react-ts'], {
1144
- dryRun,
1145
- });
1146
- return;
2716
+ const publicDir = path.join(projectRoot, 'public');
2717
+ removeFileOrDirectoryWithDryRun(publicDir, dryRun);
2718
+
2719
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2720
+ if (!fs.existsSync(packageJsonPath)) {
2721
+ logError(
2722
+ `package.json not found in generated Next.js app: ${packageJsonPath}`,
2723
+ );
2724
+ process.exit(1);
1147
2725
  }
1148
2726
 
1149
- if (template === 'vuejs') {
1150
- run('npx', ['create-vite@latest', projectName, '--template', 'vue-ts'], {
1151
- dryRun,
1152
- });
1153
- return;
2727
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2728
+ packageJson.name = projectName;
2729
+ packageJson.private = true;
2730
+ packageJson.version = '0.1.0';
2731
+ packageJson.scripts = {
2732
+ ...packageJson.scripts,
2733
+ 'build:rsx': 'rsx build --project tsconfig.json --no-emit --prod',
2734
+ dev: 'npm run build:rsx && next dev',
2735
+ build: 'npm run build:rsx && next build',
2736
+ start: 'next start',
2737
+ };
2738
+ packageJson.dependencies = {
2739
+ ...(packageJson.dependencies ?? {}),
2740
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
2741
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
2742
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
2743
+ '@rs-x/react': rsxSpecs['@rs-x/react'],
2744
+ };
2745
+ packageJson.devDependencies = {
2746
+ ...(packageJson.devDependencies ?? {}),
2747
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
2748
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
2749
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
2750
+ };
2751
+
2752
+ if (dryRun) {
2753
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
2754
+ } else {
2755
+ fs.writeFileSync(
2756
+ packageJsonPath,
2757
+ `${JSON.stringify(packageJson, null, 2)}\n`,
2758
+ 'utf8',
2759
+ );
1154
2760
  }
1155
2761
 
1156
- if (template === 'nextjs') {
1157
- const packageManagerFlagByPm = {
1158
- npm: '--use-npm',
1159
- pnpm: '--use-pnpm',
1160
- yarn: '--use-yarn',
1161
- bun: '--use-bun',
1162
- };
1163
- const args = [
1164
- 'create-next-app@latest',
1165
- projectName,
1166
- '--yes',
1167
- '--ts',
1168
- '--app',
1169
- '--eslint',
1170
- '--import-alias',
1171
- '@/*',
1172
- packageManagerFlagByPm[pm] ?? '--use-npm',
1173
- ];
1174
- if (skipInstall) {
1175
- args.push('--skip-install');
1176
- }
1177
- run('npx', args, { dryRun });
1178
- return;
2762
+ const tsConfigPath = path.join(projectRoot, 'tsconfig.json');
2763
+ if (fs.existsSync(tsConfigPath)) {
2764
+ upsertTypescriptPluginInTsConfig(tsConfigPath, dryRun);
1179
2765
  }
1180
2766
 
1181
- logError(`Unknown project template: ${template}`);
1182
- process.exit(1);
2767
+ if (!Boolean(flags['skip-install'])) {
2768
+ logInfo(`Refreshing ${pm} dependencies for the RS-X Next.js starter...`);
2769
+ run(pm, ['install'], { dryRun });
2770
+ logOk('Next.js starter dependencies are up to date.');
2771
+ }
1183
2772
  }
1184
2773
 
1185
2774
  async function runProjectWithTemplate(template, flags) {
@@ -1196,15 +2785,22 @@ async function runProjectWithTemplate(template, flags) {
1196
2785
  return;
1197
2786
  }
1198
2787
 
1199
- const pm = detectPackageManager(flags.pm);
2788
+ const invocationRoot = process.cwd();
2789
+ const pm = resolveCliPackageManager(invocationRoot, flags.pm);
1200
2790
  const projectName = await resolveProjectName(flags.name, flags._nameHint);
1201
- const projectRoot = path.resolve(process.cwd(), projectName);
2791
+ const projectRoot = resolveProjectRoot(projectName, flags);
1202
2792
  if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
1203
2793
  logError(`Target directory is not empty: ${projectRoot}`);
1204
2794
  process.exit(1);
1205
2795
  }
1206
2796
 
1207
- scaffoldProjectTemplate(normalizedTemplate, projectName, pm, flags);
2797
+ scaffoldProjectTemplate(
2798
+ normalizedTemplate,
2799
+ projectName,
2800
+ projectRoot,
2801
+ pm,
2802
+ flags,
2803
+ );
1208
2804
  const dryRun = Boolean(flags['dry-run']);
1209
2805
  if (dryRun) {
1210
2806
  logInfo(`[dry-run] setup RS-X in ${projectRoot}`);
@@ -1213,40 +2809,221 @@ async function runProjectWithTemplate(template, flags) {
1213
2809
 
1214
2810
  withWorkingDirectory(projectRoot, () => {
1215
2811
  if (normalizedTemplate === 'angular') {
1216
- runSetupAngular(flags);
2812
+ applyAngularDemoStarter(projectRoot, projectName, pm, flags);
1217
2813
  return;
1218
2814
  }
1219
2815
  if (normalizedTemplate === 'react') {
1220
- runSetupReact({
1221
- ...flags,
1222
- entry: flags.entry ?? 'src/main.tsx',
1223
- });
2816
+ applyReactDemoStarter(projectRoot, projectName, pm, flags);
1224
2817
  return;
1225
2818
  }
1226
2819
  if (normalizedTemplate === 'nextjs') {
1227
- runSetupNext(flags);
2820
+ applyNextDemoStarter(projectRoot, projectName, pm, flags);
1228
2821
  return;
1229
2822
  }
1230
2823
  if (normalizedTemplate === 'vuejs') {
1231
- runSetupVue({
1232
- ...flags,
1233
- entry: flags.entry ?? 'src/main.ts',
1234
- });
1235
- applyVueRsxTemplate(projectRoot, dryRun);
2824
+ applyVueDemoStarter(projectRoot, projectName, pm, flags);
2825
+ }
2826
+ });
2827
+
2828
+ verifyGeneratedProject(projectRoot, normalizedTemplate);
2829
+ if (resolveCliVerifyFlag(invocationRoot, flags, 'project')) {
2830
+ logInfo('Re-running starter verification (--verify)...');
2831
+ verifyGeneratedProject(projectRoot, normalizedTemplate);
2832
+ }
2833
+
2834
+ logOk(`Created RS-X ${normalizedTemplate} project: ${projectRoot}`);
2835
+ logInfo('Next steps:');
2836
+ console.log(` cd ${projectName}`);
2837
+ if (Boolean(flags['skip-install'])) {
2838
+ console.log(` ${pm} install`);
2839
+ }
2840
+ if (normalizedTemplate === 'angular') {
2841
+ console.log(` ${pm} run start`);
2842
+ } else {
2843
+ console.log(` ${pm} run dev`);
2844
+ }
2845
+ }
2846
+
2847
+ function verifyGeneratedProject(projectRoot, template) {
2848
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2849
+ if (!fs.existsSync(packageJsonPath)) {
2850
+ logError(`Generated project is missing package.json: ${packageJsonPath}`);
2851
+ process.exit(1);
2852
+ }
2853
+
2854
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2855
+ const scripts = packageJson.scripts ?? {};
2856
+ const dependencies = {
2857
+ ...(packageJson.dependencies ?? {}),
2858
+ ...(packageJson.devDependencies ?? {}),
2859
+ };
2860
+
2861
+ const expectationsByTemplate = {
2862
+ angular: {
2863
+ scripts: ['build:rsx', 'start'],
2864
+ dependencies: [
2865
+ '@rs-x/angular',
2866
+ '@rs-x/compiler',
2867
+ '@rs-x/typescript-plugin',
2868
+ ],
2869
+ files: [
2870
+ 'src/main.ts',
2871
+ 'src/app/app.component.ts',
2872
+ 'src/app/virtual-table/virtual-table.component.ts',
2873
+ ],
2874
+ },
2875
+ react: {
2876
+ scripts: ['build:rsx', 'dev', 'build'],
2877
+ dependencies: [
2878
+ '@rs-x/react',
2879
+ '@rs-x/compiler',
2880
+ '@rs-x/typescript-plugin',
2881
+ ],
2882
+ files: [
2883
+ 'src/main.tsx',
2884
+ 'src/rsx-bootstrap.ts',
2885
+ 'src/app/app.tsx',
2886
+ 'src/app/virtual-table/virtual-table-shell.tsx',
2887
+ ],
2888
+ },
2889
+ vuejs: {
2890
+ scripts: ['build:rsx', 'dev', 'build'],
2891
+ dependencies: ['@rs-x/vue', '@rs-x/compiler', '@rs-x/typescript-plugin'],
2892
+ files: [
2893
+ 'src/main.ts',
2894
+ 'src/App.vue',
2895
+ 'src/lib/rsx-bootstrap.ts',
2896
+ 'src/components/VirtualTableShell.vue',
2897
+ ],
2898
+ },
2899
+ nextjs: {
2900
+ scripts: ['build:rsx', 'dev', 'build'],
2901
+ dependencies: [
2902
+ '@rs-x/react',
2903
+ '@rs-x/compiler',
2904
+ '@rs-x/typescript-plugin',
2905
+ ],
2906
+ files: [
2907
+ 'app/layout.tsx',
2908
+ 'app/page.tsx',
2909
+ 'components/demo-app.tsx',
2910
+ 'lib/rsx-bootstrap.ts',
2911
+ ],
2912
+ },
2913
+ };
2914
+
2915
+ const expected = expectationsByTemplate[template];
2916
+ if (!expected) {
2917
+ return;
2918
+ }
2919
+
2920
+ for (const scriptName of expected.scripts) {
2921
+ if (
2922
+ typeof scripts[scriptName] !== 'string' ||
2923
+ scripts[scriptName].trim() === ''
2924
+ ) {
2925
+ logError(
2926
+ `Generated ${template} project is missing script "${scriptName}" in package.json.`,
2927
+ );
2928
+ process.exit(1);
2929
+ }
2930
+ }
2931
+
2932
+ for (const dependencyName of expected.dependencies) {
2933
+ if (typeof dependencies[dependencyName] !== 'string') {
2934
+ logError(
2935
+ `Generated ${template} project is missing dependency "${dependencyName}" in package.json.`,
2936
+ );
2937
+ process.exit(1);
2938
+ }
2939
+ }
2940
+
2941
+ for (const relativeFilePath of expected.files) {
2942
+ const absoluteFilePath = path.join(projectRoot, relativeFilePath);
2943
+ if (!fs.existsSync(absoluteFilePath)) {
2944
+ logError(
2945
+ `Generated ${template} project is missing expected file: ${absoluteFilePath}`,
2946
+ );
2947
+ process.exit(1);
2948
+ }
2949
+ }
2950
+
2951
+ const rsxConfigPath = path.join(projectRoot, 'rsx.config.json');
2952
+ if (!fs.existsSync(rsxConfigPath)) {
2953
+ logError(
2954
+ `Generated ${template} project is missing expected file: ${rsxConfigPath}`,
2955
+ );
2956
+ process.exit(1);
2957
+ }
2958
+
2959
+ logOk(`Verified generated ${template} project structure.`);
2960
+ }
2961
+
2962
+ function verifySetupOutput(projectRoot, template) {
2963
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2964
+ if (!fs.existsSync(packageJsonPath)) {
2965
+ logError(`Project is missing package.json: ${packageJsonPath}`);
2966
+ process.exit(1);
2967
+ }
2968
+
2969
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2970
+ const scripts = packageJson.scripts ?? {};
2971
+
2972
+ const setupExpectations = {
2973
+ react: {
2974
+ scripts: ['build:rsx', 'typecheck:rsx', 'dev', 'build'],
2975
+ files: ['vite.config.ts', 'src/rsx-bootstrap.ts'],
2976
+ },
2977
+ vuejs: {
2978
+ scripts: ['build:rsx', 'typecheck:rsx', 'dev', 'build'],
2979
+ files: ['vite.config.ts', 'src/rsx-bootstrap.ts'],
2980
+ },
2981
+ next: {
2982
+ scripts: ['build:rsx', 'typecheck:rsx', 'dev', 'build'],
2983
+ files: [
2984
+ 'next.config.js',
2985
+ 'rsx-webpack-loader.cjs',
2986
+ 'app/rsx-bootstrap.ts',
2987
+ ],
2988
+ },
2989
+ angular: {
2990
+ scripts: ['build:rsx', 'typecheck:rsx', 'prebuild', 'start'],
2991
+ files: ['src/main.ts', 'angular.json'],
2992
+ },
2993
+ };
2994
+
2995
+ const expected = setupExpectations[template];
2996
+ if (!expected) {
2997
+ return;
2998
+ }
2999
+
3000
+ for (const scriptName of expected.scripts) {
3001
+ if (
3002
+ typeof scripts[scriptName] !== 'string' ||
3003
+ scripts[scriptName].trim() === ''
3004
+ ) {
3005
+ logError(
3006
+ `Setup output is missing script "${scriptName}" in package.json.`,
3007
+ );
3008
+ process.exit(1);
1236
3009
  }
1237
- });
3010
+ }
1238
3011
 
1239
- logOk(`Created RS-X ${normalizedTemplate} project: ${projectRoot}`);
1240
- logInfo('Next steps:');
1241
- console.log(` cd ${projectName}`);
1242
- if (Boolean(flags['skip-install'])) {
1243
- console.log(` ${pm} install`);
3012
+ for (const relativeFilePath of expected.files) {
3013
+ const absoluteFilePath = path.join(projectRoot, relativeFilePath);
3014
+ if (!fs.existsSync(absoluteFilePath)) {
3015
+ logError(`Setup output is missing expected file: ${absoluteFilePath}`);
3016
+ process.exit(1);
3017
+ }
1244
3018
  }
1245
- if (normalizedTemplate === 'angular') {
1246
- console.log(` ${pm} run start`);
1247
- } else {
1248
- console.log(` ${pm} run dev`);
3019
+
3020
+ const rsxConfigPath = path.join(projectRoot, 'rsx.config.json');
3021
+ if (!fs.existsSync(rsxConfigPath)) {
3022
+ logError(`Setup output is missing expected file: ${rsxConfigPath}`);
3023
+ process.exit(1);
1249
3024
  }
3025
+
3026
+ logOk(`Verified ${template} setup output.`);
1250
3027
  }
1251
3028
 
1252
3029
  function detectProjectContext(projectRoot) {
@@ -1507,12 +3284,12 @@ function ensureNextGateFile(gateFile, bootstrapFile, dryRun) {
1507
3284
  const content = useTypeScript
1508
3285
  ? `'use client';
1509
3286
 
1510
- import { type ReactNode, useEffect, useState } from 'react';
3287
+ import { type ReactElement, type ReactNode, useEffect, useState } from 'react';
1511
3288
 
1512
3289
  import { initRsx } from '${importPath}';
1513
3290
 
1514
3291
  // Generated by rsx init
1515
- export function RsxBootstrapGate(props: { children: ReactNode }): JSX.Element | null {
3292
+ export function RsxBootstrapGate(props: { children: ReactNode }): ReactElement | null {
1516
3293
  const [ready, setReady] = useState(false);
1517
3294
 
1518
3295
  useEffect(() => {
@@ -1726,15 +3503,14 @@ function patchEntryFileForRsx(entryFile, bootstrapFile, context, dryRun) {
1726
3503
 
1727
3504
  function runInit(flags) {
1728
3505
  const dryRun = Boolean(flags['dry-run']);
1729
- const skipVscode = Boolean(flags['skip-vscode']);
1730
3506
  const skipInstall = Boolean(flags['skip-install']);
1731
- const pm = detectPackageManager(flags.pm);
1732
- const tag = resolveInstallTag(flags);
1733
3507
  const projectRoot = process.cwd();
3508
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
3509
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
1734
3510
 
1735
3511
  if (!skipInstall) {
1736
- installRuntimePackages(pm, dryRun, tag);
1737
- installCompilerPackages(pm, dryRun, tag);
3512
+ installRuntimePackages(pm, dryRun, tag, projectRoot, flags);
3513
+ installCompilerPackages(pm, dryRun, tag, projectRoot, flags);
1738
3514
  } else {
1739
3515
  logInfo('Skipping package installation (--skip-install).');
1740
3516
  }
@@ -1788,13 +3564,179 @@ function runInit(flags) {
1788
3564
  }
1789
3565
  }
1790
3566
 
1791
- if (!skipVscode) {
1792
- installVsCodeExtension(flags);
1793
- }
3567
+ ensureRsxConfigFile(projectRoot, effectiveContext, dryRun);
1794
3568
 
1795
3569
  logOk('RS-X init completed.');
1796
3570
  }
1797
3571
 
3572
+ function ensureAngularProvidersInEntry(entryFile, dryRun) {
3573
+ if (!fs.existsSync(entryFile)) {
3574
+ return false;
3575
+ }
3576
+
3577
+ const original = fs.readFileSync(entryFile, 'utf8');
3578
+
3579
+ if (!original.includes('bootstrapApplication(')) {
3580
+ logWarn(
3581
+ `Could not automatically patch Angular providers in ${entryFile}. Expected bootstrapApplication(...).`,
3582
+ );
3583
+ logInfo(
3584
+ "Manual setup: import { providexRsx } from '@rs-x/angular' and add providers: [...providexRsx()] to bootstrapApplication(...).",
3585
+ );
3586
+ return false;
3587
+ }
3588
+
3589
+ const bootstrapCallMatch = original.match(
3590
+ /bootstrapApplication\(\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*|\{[\s\S]*?\}))?\s*\)/mu,
3591
+ );
3592
+ const componentIdentifier = bootstrapCallMatch?.[1] ?? null;
3593
+ const configArgument = bootstrapCallMatch?.[2] ?? null;
3594
+ const configImportIdentifier =
3595
+ configArgument && /^[A-Za-z_$][\w$]*$/u.test(configArgument)
3596
+ ? configArgument
3597
+ : original.includes('appConfig')
3598
+ ? 'appConfig'
3599
+ : null;
3600
+
3601
+ const staticImportPattern = new RegExp(
3602
+ String.raw`^\s*import\s+\{\s*([^}]*)\b${componentIdentifier ?? ''}\b([^}]*)\}\s+from\s+['"]([^'"]+)['"];\s*$`,
3603
+ 'mu',
3604
+ );
3605
+ const componentImportMatch = componentIdentifier
3606
+ ? original.match(staticImportPattern)
3607
+ : null;
3608
+ const dynamicComponentImportMatch = componentIdentifier
3609
+ ? original.match(
3610
+ new RegExp(
3611
+ String.raw`const\s+\{\s*${componentIdentifier}\s*\}\s*=\s*await\s+import\('([^']+)'\);`,
3612
+ 'mu',
3613
+ ),
3614
+ )
3615
+ : null;
3616
+ const componentImportPath =
3617
+ componentImportMatch?.[3] ?? dynamicComponentImportMatch?.[1] ?? null;
3618
+
3619
+ const configImportMatch = configImportIdentifier
3620
+ ? original.match(
3621
+ new RegExp(
3622
+ String.raw`^\s*import\s+\{\s*([^}]*)\b${configImportIdentifier}\b([^}]*)\}\s+from\s+['"]([^'"]+)['"];\s*$`,
3623
+ 'mu',
3624
+ ),
3625
+ )
3626
+ : null;
3627
+ const configImportPath = configImportMatch?.[3] ?? null;
3628
+
3629
+ const canPreloadBeforeComponentImport =
3630
+ componentIdentifier !== null && componentImportPath !== null;
3631
+
3632
+ if (canPreloadBeforeComponentImport) {
3633
+ const importLines = original
3634
+ .split('\n')
3635
+ .filter((line) => /^\s*import\s+/u.test(line))
3636
+ .filter((line) => !line.match(staticImportPattern))
3637
+ .filter((line) =>
3638
+ configImportMatch
3639
+ ? !line.match(
3640
+ new RegExp(
3641
+ String.raw`^\s*import\s+\{\s*([^}]*)\b${configImportIdentifier}\b([^}]*)\}\s+from\s+['"][^'"]+['"];\s*$`,
3642
+ 'u',
3643
+ ),
3644
+ )
3645
+ : true,
3646
+ )
3647
+ .filter(
3648
+ (line) =>
3649
+ !line.includes("import { providexRsx } from '@rs-x/angular';"),
3650
+ )
3651
+ .filter(
3652
+ (line) =>
3653
+ !line.includes("import { InjectionContainer } from '@rs-x/core';"),
3654
+ )
3655
+ .filter(
3656
+ (line) =>
3657
+ !line.includes(
3658
+ "import { RsXExpressionParserModule } from '@rs-x/expression-parser';",
3659
+ ),
3660
+ );
3661
+
3662
+ const bootstrapConfigExpression =
3663
+ configImportPath && configImportIdentifier
3664
+ ? `const [{ ${configImportIdentifier} }, { ${componentIdentifier} }] = await Promise.all([\n import('${configImportPath}'),\n import('${componentImportPath}'),\n ]);\n\n await bootstrapApplication(${componentIdentifier}, {\n ...${configImportIdentifier},\n providers: [...(${configImportIdentifier}.providers ?? []), ...providexRsx()],\n });`
3665
+ : `const { ${componentIdentifier} } = await import('${componentImportPath}');\n\n await bootstrapApplication(${componentIdentifier}, {\n providers: [...providexRsx()],\n });`;
3666
+
3667
+ const rewritten = `${importLines.join('\n')}
3668
+ import { providexRsx } from '@rs-x/angular';
3669
+ import { InjectionContainer } from '@rs-x/core';
3670
+ import { RsXExpressionParserModule } from '@rs-x/expression-parser';
3671
+
3672
+ const bootstrap = async (): Promise<void> => {
3673
+ await InjectionContainer.load(RsXExpressionParserModule);
3674
+ ${bootstrapConfigExpression}
3675
+ };
3676
+
3677
+ void bootstrap().catch((error) => {
3678
+ console.error(error);
3679
+ });
3680
+ `;
3681
+
3682
+ if (dryRun) {
3683
+ logInfo(`[dry-run] patch ${entryFile} (providexRsx + preload)`);
3684
+ return true;
3685
+ }
3686
+
3687
+ fs.writeFileSync(entryFile, rewritten, 'utf8');
3688
+ logOk(`Patched ${entryFile} to preload RS-X and include providexRsx.`);
3689
+ return true;
3690
+ }
3691
+
3692
+ const sourceWithImport = injectImport(
3693
+ original,
3694
+ "import { providexRsx } from '@rs-x/angular';",
3695
+ );
3696
+
3697
+ let updated = sourceWithImport;
3698
+ if (/bootstrapApplication\([\s\S]*?,\s*appConfig\s*\)/mu.test(updated)) {
3699
+ updated = updated.replace(
3700
+ /bootstrapApplication\(([\s\S]*?),\s*appConfig\s*\)/mu,
3701
+ 'bootstrapApplication($1, {\n ...appConfig,\n providers: [...(appConfig.providers ?? []), ...providexRsx()],\n})',
3702
+ );
3703
+ } else if (
3704
+ /bootstrapApplication\([\s\S]*?,\s*\{[\s\S]*?providers\s*:/mu.test(updated)
3705
+ ) {
3706
+ updated = updated.replace(
3707
+ /providers\s*:\s*\[/mu,
3708
+ 'providers: [...providexRsx(), ',
3709
+ );
3710
+ } else if (/bootstrapApplication\([\s\S]*?,\s*\{/mu.test(updated)) {
3711
+ updated = updated.replace(
3712
+ /bootstrapApplication\(([\s\S]*?),\s*\{/mu,
3713
+ 'bootstrapApplication($1, {\n providers: [...providexRsx()],',
3714
+ );
3715
+ } else {
3716
+ updated = updated.replace(
3717
+ /bootstrapApplication\(([\s\S]*?)\)\s*(?:\.catch\([\s\S]*?\))?\s*;/mu,
3718
+ 'bootstrapApplication($1, {\n providers: [...providexRsx()],\n}).catch((error) => {\n console.error(error);\n});',
3719
+ );
3720
+ }
3721
+
3722
+ if (updated === sourceWithImport) {
3723
+ logWarn(`Could not automatically inject providexRsx into ${entryFile}.`);
3724
+ logInfo(
3725
+ "Manual setup: import { providexRsx } from '@rs-x/angular' and add providers: [...providexRsx()] to bootstrapApplication(...).",
3726
+ );
3727
+ return false;
3728
+ }
3729
+
3730
+ if (dryRun) {
3731
+ logInfo(`[dry-run] patch ${entryFile} (providexRsx)`);
3732
+ return true;
3733
+ }
3734
+
3735
+ fs.writeFileSync(entryFile, updated, 'utf8');
3736
+ logOk(`Patched ${entryFile} to include providexRsx.`);
3737
+ return true;
3738
+ }
3739
+
1798
3740
  function upsertScriptInPackageJson(
1799
3741
  projectRoot,
1800
3742
  scriptName,
@@ -1906,109 +3848,6 @@ module.exports = function rsxWebpackLoader(source) {
1906
3848
  }
1907
3849
 
1908
3850
  function wireRsxVitePlugin(projectRoot, dryRun) {
1909
- const pluginFile = path.join(projectRoot, 'rsx-vite-plugin.mjs');
1910
- const pluginSource = `import path from 'node:path';
1911
-
1912
- import ts from 'typescript';
1913
-
1914
- import { createExpressionCachePreloadTransformer } from '@rs-x/compiler';
1915
-
1916
- function normalizeFileName(fileName) {
1917
- return path.resolve(fileName).replace(/\\\\/gu, '/');
1918
- }
1919
-
1920
- function buildTransformedSourceMap(tsconfigPath) {
1921
- const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
1922
- if (configFile.error) {
1923
- return new Map();
1924
- }
1925
-
1926
- const parsed = ts.parseJsonConfigFileContent(
1927
- configFile.config,
1928
- ts.sys,
1929
- path.dirname(tsconfigPath),
1930
- undefined,
1931
- tsconfigPath,
1932
- );
1933
- if (parsed.errors.length > 0) {
1934
- return new Map();
1935
- }
1936
-
1937
- const program = ts.createProgram({
1938
- rootNames: parsed.fileNames,
1939
- options: parsed.options,
1940
- });
1941
- const transformer = createExpressionCachePreloadTransformer(program);
1942
- const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
1943
- const transformedByFile = new Map();
1944
-
1945
- for (const sourceFile of program.getSourceFiles()) {
1946
- if (sourceFile.isDeclarationFile) {
1947
- continue;
1948
- }
1949
-
1950
- if (sourceFile.fileName.includes('/node_modules/')) {
1951
- continue;
1952
- }
1953
-
1954
- const transformed = ts.transform(sourceFile, [transformer]);
1955
- const transformedSource = transformed.transformed[0];
1956
- const transformedText = printer.printFile(transformedSource);
1957
- transformed.dispose();
1958
-
1959
- transformedByFile.set(normalizeFileName(sourceFile.fileName), transformedText);
1960
- }
1961
-
1962
- return transformedByFile;
1963
- }
1964
-
1965
- export function rsxVitePlugin(tsconfigPath = 'tsconfig.json') {
1966
- let transformedByFile = new Map();
1967
- let resolvedTsConfigPath = '';
1968
-
1969
- const refresh = () => {
1970
- transformedByFile = buildTransformedSourceMap(resolvedTsConfigPath);
1971
- };
1972
-
1973
- return {
1974
- name: 'rsx-vite-transform',
1975
- enforce: 'pre',
1976
- configResolved(config) {
1977
- resolvedTsConfigPath = normalizeFileName(path.resolve(config.root, tsconfigPath));
1978
- refresh();
1979
- },
1980
- buildStart() {
1981
- if (!resolvedTsConfigPath) {
1982
- resolvedTsConfigPath = normalizeFileName(path.resolve(process.cwd(), tsconfigPath));
1983
- }
1984
- refresh();
1985
- },
1986
- handleHotUpdate() {
1987
- refresh();
1988
- },
1989
- transform(_code, id) {
1990
- const normalizedId = normalizeFileName(id.split('?')[0]);
1991
- const transformed = transformedByFile.get(normalizedId);
1992
- if (!transformed) {
1993
- return null;
1994
- }
1995
-
1996
- return {
1997
- code: transformed,
1998
- map: null,
1999
- };
2000
- },
2001
- };
2002
- }
2003
- `;
2004
-
2005
- if (dryRun) {
2006
- logInfo(`[dry-run] create ${pluginFile}`);
2007
- } else {
2008
- fs.writeFileSync(pluginFile, pluginSource, 'utf8');
2009
- logOk(`Created ${pluginFile}`);
2010
- }
2011
-
2012
3851
  const viteConfigCandidates = [
2013
3852
  'vite.config.ts',
2014
3853
  'vite.config.mts',
@@ -2018,61 +3857,47 @@ export function rsxVitePlugin(tsconfigPath = 'tsconfig.json') {
2018
3857
  const viteConfigPath = viteConfigCandidates.find((candidate) =>
2019
3858
  fs.existsSync(candidate),
2020
3859
  );
3860
+ const stalePluginFiles = [
3861
+ path.join(projectRoot, 'rsx-vite-plugin.ts'),
3862
+ path.join(projectRoot, 'rsx-vite-plugin.mjs'),
3863
+ path.join(projectRoot, 'rsx-vite-plugin.d.ts'),
3864
+ ];
3865
+
2021
3866
  if (!viteConfigPath) {
2022
- logWarn(
2023
- 'No vite.config.[ts|mts|js|mjs] found. RS-X Vite plugin file was created, but config patch was skipped.',
2024
- );
2025
- logInfo(
2026
- "Add it manually: import { rsxVitePlugin } from './rsx-vite-plugin.mjs' and include rsxVitePlugin() in plugins.",
2027
- );
3867
+ for (const staleFile of stalePluginFiles) {
3868
+ removeFileOrDirectoryWithDryRun(staleFile, dryRun);
3869
+ }
2028
3870
  return;
2029
3871
  }
2030
3872
 
2031
3873
  const original = fs.readFileSync(viteConfigPath, 'utf8');
2032
- if (original.includes('rsxVitePlugin(')) {
2033
- logInfo(`Vite config already includes RS-X plugin: ${viteConfigPath}`);
2034
- return;
2035
- }
2036
-
2037
- let updated = original;
2038
- const importStatement =
2039
- "import { rsxVitePlugin } from './rsx-vite-plugin.mjs';";
2040
- if (!updated.includes(importStatement)) {
2041
- const lines = updated.split('\n');
2042
- let insertAt = 0;
2043
- while (
2044
- insertAt < lines.length &&
2045
- lines[insertAt].trim().startsWith('import ')
2046
- ) {
2047
- insertAt += 1;
3874
+ const updated = original
3875
+ .replace(
3876
+ /import\s+\{\s*rsxVitePlugin\s*\}\s+from\s+['"]\.\/rsx-vite-plugin(?:\.mjs)?['"];\n?/gu,
3877
+ '',
3878
+ )
3879
+ .replace(/rsxVitePlugin\(\)\s*,\s*/gu, '')
3880
+ .replace(/,\s*rsxVitePlugin\(\)/gu, '')
3881
+ .replace(/\[\s*rsxVitePlugin\(\)\s*\]/gu, '[]');
3882
+
3883
+ if (updated !== original) {
3884
+ if (dryRun) {
3885
+ logInfo(
3886
+ `[dry-run] patch ${viteConfigPath} (remove legacy RS-X Vite plugin)`,
3887
+ );
3888
+ } else {
3889
+ fs.writeFileSync(viteConfigPath, updated, 'utf8');
3890
+ logOk(`Patched ${viteConfigPath} (removed legacy RS-X Vite plugin).`);
2048
3891
  }
2049
- lines.splice(insertAt, 0, importStatement);
2050
- updated = lines.join('\n');
2051
- }
2052
-
2053
- if (/plugins\s*:\s*\[/u.test(updated)) {
2054
- updated = updated.replace(
2055
- /plugins\s*:\s*\[/u,
2056
- 'plugins: [rsxVitePlugin(), ',
2057
- );
2058
- } else if (/defineConfig\s*\(\s*\{/u.test(updated)) {
2059
- updated = updated.replace(
2060
- /defineConfig\s*\(\s*\{/u,
2061
- 'defineConfig({\n plugins: [rsxVitePlugin()],',
2062
- );
2063
3892
  } else {
2064
- logWarn(`Could not patch Vite config automatically: ${viteConfigPath}`);
2065
- logInfo('Add `rsxVitePlugin()` to your Vite plugins manually.');
2066
- return;
3893
+ logInfo(
3894
+ `Vite config already uses the default plugin list: ${viteConfigPath}`,
3895
+ );
2067
3896
  }
2068
3897
 
2069
- if (dryRun) {
2070
- logInfo(`[dry-run] patch ${viteConfigPath}`);
2071
- return;
3898
+ for (const staleFile of stalePluginFiles) {
3899
+ removeFileOrDirectoryWithDryRun(staleFile, dryRun);
2072
3900
  }
2073
-
2074
- fs.writeFileSync(viteConfigPath, updated, 'utf8');
2075
- logOk(`Patched ${viteConfigPath} with RS-X Vite plugin.`);
2076
3901
  }
2077
3902
 
2078
3903
  function wireRsxNextWebpack(projectRoot, dryRun) {
@@ -2154,96 +3979,15 @@ ${patchBlock}
2154
3979
  logOk(`Patched ${nextConfigJs} with RS-X webpack loader.`);
2155
3980
  }
2156
3981
 
2157
- function wireRsxAngularWebpack(projectRoot, dryRun) {
2158
- const angularJsonPath = path.join(projectRoot, 'angular.json');
2159
- if (!fs.existsSync(angularJsonPath)) {
2160
- logWarn('angular.json not found. Skipping Angular build integration.');
2161
- return;
2162
- }
2163
-
2164
- createRsxWebpackLoaderFile(projectRoot, dryRun);
2165
-
2166
- const webpackConfigPath = path.join(projectRoot, 'rsx-angular-webpack.cjs');
2167
- const webpackConfigSource = `const path = require('node:path');
2168
-
2169
- module.exports = {
2170
- module: {
2171
- rules: [
2172
- {
2173
- test: /\\.[jt]sx?$/u,
2174
- exclude: /node_modules/u,
2175
- use: [
2176
- {
2177
- loader: path.resolve(__dirname, './rsx-webpack-loader.cjs'),
2178
- },
2179
- ],
2180
- },
2181
- ],
2182
- },
2183
- };
2184
- `;
2185
-
2186
- if (dryRun) {
2187
- logInfo(`[dry-run] create ${webpackConfigPath}`);
2188
- } else {
2189
- fs.writeFileSync(webpackConfigPath, webpackConfigSource, 'utf8');
2190
- logOk(`Created ${webpackConfigPath}`);
2191
- }
2192
-
2193
- const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
2194
- const projects = angularJson.projects ?? {};
2195
- const projectNames = Object.keys(projects);
2196
- if (projectNames.length === 0) {
2197
- logWarn('No Angular projects found in angular.json.');
2198
- return;
2199
- }
2200
-
2201
- const patchPath = 'rsx-angular-webpack.cjs';
2202
- for (const projectName of projectNames) {
2203
- const project = projects[projectName];
2204
- const architect = project.architect ?? project.targets;
2205
- if (!architect?.build) {
2206
- continue;
2207
- }
2208
-
2209
- const build = architect.build;
2210
- if (build.builder !== '@angular-builders/custom-webpack:browser') {
2211
- build.builder = '@angular-builders/custom-webpack:browser';
2212
- }
2213
- build.options = build.options ?? {};
2214
- build.options.customWebpackConfig = build.options.customWebpackConfig ?? {};
2215
- build.options.customWebpackConfig.path = patchPath;
2216
-
2217
- if (architect.serve) {
2218
- const serve = architect.serve;
2219
- if (serve.builder !== '@angular-builders/custom-webpack:dev-server') {
2220
- serve.builder = '@angular-builders/custom-webpack:dev-server';
2221
- }
2222
- serve.options = serve.options ?? {};
2223
- serve.options.buildTarget =
2224
- serve.options.buildTarget ?? `${projectName}:build`;
2225
- serve.options.browserTarget =
2226
- serve.options.browserTarget ?? `${projectName}:build`;
2227
- }
2228
- }
2229
-
2230
- if (dryRun) {
2231
- logInfo(`[dry-run] patch ${angularJsonPath}`);
2232
- } else {
2233
- fs.writeFileSync(
2234
- angularJsonPath,
2235
- `${JSON.stringify(angularJson, null, 2)}\n`,
2236
- 'utf8',
2237
- );
2238
- logOk(`Patched ${angularJsonPath} for RS-X Angular webpack integration.`);
2239
- }
2240
- }
2241
-
2242
3982
  function runSetupReact(flags) {
2243
3983
  const dryRun = Boolean(flags['dry-run']);
2244
- const pm = detectPackageManager(flags.pm);
2245
- const tag = resolveInstallTag(flags);
2246
3984
  const projectRoot = process.cwd();
3985
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
3986
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
3987
+ const reactTsConfigPath = resolveProjectTsConfig(projectRoot);
3988
+ const reactTsConfigRelative = path
3989
+ .relative(projectRoot, reactTsConfigPath)
3990
+ .replace(/\\/gu, '/');
2247
3991
  const packageJsonPath = path.join(projectRoot, 'package.json');
2248
3992
  if (!fs.existsSync(packageJsonPath)) {
2249
3993
  logError(`package.json not found in ${projectRoot}`);
@@ -2266,124 +4010,318 @@ function runSetupReact(flags) {
2266
4010
  'skip-vscode': true,
2267
4011
  });
2268
4012
  if (!Boolean(flags['skip-install'])) {
2269
- installPackages(pm, ['@rs-x/react'], {
4013
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
4014
+ tag,
4015
+ includeReactPackage: true,
4016
+ });
4017
+ installResolvedPackages(pm, ['@rs-x/react'], {
2270
4018
  dev: false,
2271
4019
  dryRun,
2272
4020
  tag,
4021
+ specs,
4022
+ cwd: projectRoot,
2273
4023
  label: 'RS-X React bindings',
2274
4024
  });
4025
+ installResolvedPackages(pm, ['@rs-x/cli'], {
4026
+ dev: true,
4027
+ dryRun,
4028
+ tag,
4029
+ specs,
4030
+ cwd: projectRoot,
4031
+ label: 'RS-X CLI',
4032
+ });
2275
4033
  } else {
2276
4034
  logInfo('Skipping RS-X React bindings install (--skip-install).');
2277
4035
  }
4036
+ ensureRsxConfigFile(projectRoot, 'react', dryRun);
4037
+ upsertScriptInPackageJson(
4038
+ projectRoot,
4039
+ 'build:rsx',
4040
+ `rsx build --project ${reactTsConfigRelative} --no-emit --prod`,
4041
+ dryRun,
4042
+ );
4043
+ upsertScriptInPackageJson(
4044
+ projectRoot,
4045
+ 'typecheck:rsx',
4046
+ `rsx typecheck --project ${reactTsConfigRelative}`,
4047
+ dryRun,
4048
+ );
4049
+ upsertScriptInPackageJson(
4050
+ projectRoot,
4051
+ 'dev',
4052
+ 'npm run build:rsx && vite',
4053
+ dryRun,
4054
+ );
4055
+ upsertScriptInPackageJson(
4056
+ projectRoot,
4057
+ 'build',
4058
+ 'npm run build:rsx && vite build',
4059
+ dryRun,
4060
+ );
2278
4061
  wireRsxVitePlugin(projectRoot, dryRun);
2279
- if (!Boolean(flags['skip-vscode'])) {
2280
- installVsCodeExtension(flags);
4062
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4063
+ verifySetupOutput(projectRoot, 'react');
2281
4064
  }
2282
4065
  logOk('RS-X React setup completed.');
2283
4066
  }
2284
4067
 
2285
4068
  function runSetupNext(flags) {
2286
4069
  const dryRun = Boolean(flags['dry-run']);
2287
- const pm = detectPackageManager(flags.pm);
2288
- const tag = resolveInstallTag(flags);
4070
+ const projectRoot = process.cwd();
4071
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
4072
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
4073
+ const nextTsConfigPath = resolveProjectTsConfig(projectRoot);
4074
+ const nextTsConfigRelative = path
4075
+ .relative(projectRoot, nextTsConfigPath)
4076
+ .replace(/\\/gu, '/');
2289
4077
  runInit({
2290
4078
  ...flags,
2291
4079
  'skip-vscode': true,
2292
4080
  });
2293
4081
  if (!Boolean(flags['skip-install'])) {
2294
- installPackages(pm, ['@rs-x/react'], {
4082
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
4083
+ tag,
4084
+ includeReactPackage: true,
4085
+ });
4086
+ installResolvedPackages(pm, ['@rs-x/react'], {
2295
4087
  dev: false,
2296
4088
  dryRun,
2297
4089
  tag,
4090
+ specs,
4091
+ cwd: projectRoot,
2298
4092
  label: 'RS-X React bindings',
2299
4093
  });
2300
4094
  } else {
2301
4095
  logInfo('Skipping RS-X React bindings install (--skip-install).');
2302
4096
  }
2303
- wireRsxNextWebpack(process.cwd(), dryRun);
2304
- if (!Boolean(flags['skip-vscode'])) {
2305
- installVsCodeExtension(flags);
4097
+ ensureRsxConfigFile(projectRoot, 'next', dryRun);
4098
+ upsertScriptInPackageJson(
4099
+ projectRoot,
4100
+ 'build:rsx',
4101
+ `rsx build --project ${nextTsConfigRelative} --no-emit --prod`,
4102
+ dryRun,
4103
+ );
4104
+ upsertScriptInPackageJson(
4105
+ projectRoot,
4106
+ 'typecheck:rsx',
4107
+ `rsx typecheck --project ${nextTsConfigRelative}`,
4108
+ dryRun,
4109
+ );
4110
+ upsertScriptInPackageJson(
4111
+ projectRoot,
4112
+ 'dev',
4113
+ 'npm run build:rsx && next dev',
4114
+ dryRun,
4115
+ );
4116
+ upsertScriptInPackageJson(
4117
+ projectRoot,
4118
+ 'build',
4119
+ 'npm run build:rsx && next build',
4120
+ dryRun,
4121
+ );
4122
+ wireRsxNextWebpack(projectRoot, dryRun);
4123
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4124
+ verifySetupOutput(projectRoot, 'next');
2306
4125
  }
2307
4126
  logOk('RS-X Next.js setup completed.');
2308
4127
  }
2309
4128
 
2310
4129
  function runSetupVue(flags) {
2311
4130
  const dryRun = Boolean(flags['dry-run']);
2312
- const pm = detectPackageManager(flags.pm);
2313
- const tag = resolveInstallTag(flags);
4131
+ const projectRoot = process.cwd();
4132
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
4133
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
2314
4134
  runInit({
2315
4135
  ...flags,
2316
4136
  'skip-vscode': true,
2317
4137
  });
2318
4138
  if (!Boolean(flags['skip-install'])) {
2319
- installPackages(pm, ['@rs-x/vue'], {
4139
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
4140
+ tag,
4141
+ includeVuePackage: true,
4142
+ });
4143
+ installResolvedPackages(pm, ['@rs-x/vue'], {
2320
4144
  dev: false,
2321
4145
  dryRun,
2322
4146
  tag,
4147
+ specs,
4148
+ cwd: projectRoot,
2323
4149
  label: 'RS-X Vue bindings',
2324
4150
  });
4151
+ installResolvedPackages(pm, ['@rs-x/cli'], {
4152
+ dev: true,
4153
+ dryRun,
4154
+ tag,
4155
+ specs,
4156
+ cwd: projectRoot,
4157
+ label: 'RS-X CLI',
4158
+ });
2325
4159
  } else {
2326
4160
  logInfo('Skipping RS-X Vue bindings install (--skip-install).');
2327
4161
  }
2328
- wireRsxVitePlugin(process.cwd(), dryRun);
2329
- if (!Boolean(flags['skip-vscode'])) {
2330
- installVsCodeExtension(flags);
4162
+ ensureRsxConfigFile(projectRoot, 'vuejs', dryRun);
4163
+ upsertScriptInPackageJson(
4164
+ projectRoot,
4165
+ 'build:rsx',
4166
+ 'rsx build --project tsconfig.app.json --no-emit --prod',
4167
+ dryRun,
4168
+ );
4169
+ upsertScriptInPackageJson(
4170
+ projectRoot,
4171
+ 'typecheck:rsx',
4172
+ 'rsx typecheck --project tsconfig.app.json',
4173
+ dryRun,
4174
+ );
4175
+ upsertScriptInPackageJson(
4176
+ projectRoot,
4177
+ 'dev',
4178
+ 'npm run build:rsx && vite',
4179
+ dryRun,
4180
+ );
4181
+ upsertScriptInPackageJson(
4182
+ projectRoot,
4183
+ 'build',
4184
+ 'npm run build:rsx && vue-tsc -b && vite build',
4185
+ dryRun,
4186
+ );
4187
+ const vueTsConfigPath = path.join(projectRoot, 'tsconfig.app.json');
4188
+ upsertTypescriptPluginInTsConfig(vueTsConfigPath, dryRun);
4189
+ ensureTsConfigIncludePattern(vueTsConfigPath, 'src/**/*.d.ts', dryRun);
4190
+ ensureVueEnvTypes(projectRoot, dryRun);
4191
+ wireRsxVitePlugin(projectRoot, dryRun);
4192
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4193
+ verifySetupOutput(projectRoot, 'vuejs');
2331
4194
  }
2332
4195
  logOk('RS-X Vue setup completed.');
2333
4196
  }
2334
4197
 
2335
4198
  function runSetupAngular(flags) {
2336
4199
  const dryRun = Boolean(flags['dry-run']);
2337
- const pm = detectPackageManager(flags.pm);
2338
- const tag = resolveInstallTag(flags);
2339
-
2340
- runInit({
2341
- ...flags,
2342
- 'skip-vscode': true,
2343
- });
4200
+ const projectRoot = process.cwd();
4201
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
4202
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
4203
+ const angularTsConfigPath = resolveAngularProjectTsConfig(projectRoot);
4204
+ const angularTsConfigRelative = path
4205
+ .relative(projectRoot, angularTsConfigPath)
4206
+ .replace(/\\/gu, '/');
2344
4207
 
2345
4208
  if (!Boolean(flags['skip-install'])) {
2346
- installPackages(pm, ['@rs-x/angular'], {
4209
+ installRuntimePackages(pm, dryRun, tag, projectRoot, flags);
4210
+ installCompilerPackages(pm, dryRun, tag, projectRoot, flags);
4211
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
4212
+ tag,
4213
+ includeAngularPackage: true,
4214
+ });
4215
+ installResolvedPackages(pm, ['@rs-x/angular'], {
2347
4216
  dev: false,
2348
4217
  dryRun,
2349
4218
  tag,
4219
+ specs,
4220
+ cwd: projectRoot,
2350
4221
  label: 'RS-X Angular bindings',
2351
4222
  });
2352
- installPackages(pm, ['@angular-builders/custom-webpack'], {
4223
+ installResolvedPackages(pm, ['@rs-x/cli'], {
2353
4224
  dev: true,
2354
4225
  dryRun,
2355
- label: 'Angular custom webpack builder',
4226
+ tag,
4227
+ specs,
4228
+ cwd: projectRoot,
4229
+ label: 'RS-X CLI',
2356
4230
  });
2357
4231
  } else {
4232
+ logInfo('Skipping package installation (--skip-install).');
4233
+ }
4234
+
4235
+ const entryFile = resolveEntryFile(projectRoot, 'angular', flags.entry);
4236
+ if (entryFile) {
4237
+ logInfo(`Using Angular entry file: ${entryFile}`);
4238
+ ensureAngularProvidersInEntry(entryFile, dryRun);
4239
+ } else {
4240
+ logWarn('Could not detect an Angular entry file automatically.');
2358
4241
  logInfo(
2359
- 'Skipping Angular custom webpack builder install (--skip-install).',
4242
+ 'Manual setup: add providexRsx() to bootstrapApplication(...) in your main entry file.',
2360
4243
  );
2361
4244
  }
2362
4245
 
2363
- wireRsxAngularWebpack(process.cwd(), dryRun);
4246
+ ensureRsxConfigFile(projectRoot, 'angular', dryRun);
4247
+
2364
4248
  upsertScriptInPackageJson(
2365
- process.cwd(),
4249
+ projectRoot,
2366
4250
  'build:rsx',
2367
- 'rsx build --project tsconfig.json',
4251
+ `rsx build --project ${angularTsConfigRelative} --no-emit --prod`,
2368
4252
  dryRun,
2369
4253
  );
2370
4254
  upsertScriptInPackageJson(
2371
- process.cwd(),
4255
+ projectRoot,
2372
4256
  'typecheck:rsx',
2373
- 'rsx typecheck --project tsconfig.json',
4257
+ `rsx typecheck --project ${angularTsConfigRelative}`,
4258
+ dryRun,
4259
+ );
4260
+ upsertScriptInPackageJson(
4261
+ projectRoot,
4262
+ 'prebuild',
4263
+ 'npm run build:rsx',
4264
+ dryRun,
4265
+ );
4266
+ upsertScriptInPackageJson(
4267
+ projectRoot,
4268
+ 'start',
4269
+ 'npm run build:rsx && ng serve',
2374
4270
  dryRun,
2375
4271
  );
2376
4272
 
2377
- if (!Boolean(flags['skip-vscode'])) {
2378
- installVsCodeExtension(flags);
4273
+ const rsxRegistrationFile = path.join(
4274
+ projectRoot,
4275
+ 'src/rsx-generated/rsx-aot-registration.generated.ts',
4276
+ );
4277
+ const angularJsonPath = path.join(projectRoot, 'angular.json');
4278
+ if (fs.existsSync(angularJsonPath)) {
4279
+ const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
4280
+ const projects = angularJson.projects ?? {};
4281
+ for (const projectConfig of Object.values(projects)) {
4282
+ const buildOptions = projectConfig?.architect?.build?.options;
4283
+ if (buildOptions && typeof buildOptions === 'object') {
4284
+ buildOptions.preserveSymlinks = true;
4285
+ }
4286
+ if (
4287
+ projectConfig?.architect?.build?.configurations?.production?.budgets
4288
+ ) {
4289
+ delete projectConfig.architect.build.configurations.production.budgets;
4290
+ }
4291
+ }
4292
+ if (dryRun) {
4293
+ logInfo(
4294
+ `[dry-run] patch ${angularJsonPath} (preserveSymlinks, production budgets)`,
4295
+ );
4296
+ } else {
4297
+ fs.writeFileSync(
4298
+ angularJsonPath,
4299
+ `${JSON.stringify(angularJson, null, 2)}\n`,
4300
+ 'utf8',
4301
+ );
4302
+ logOk(
4303
+ `Patched ${angularJsonPath} (preserveSymlinks, production budgets).`,
4304
+ );
4305
+ }
4306
+ }
4307
+ ensureAngularPolyfillsContainsFile({
4308
+ projectRoot,
4309
+ configPath: angularTsConfigPath,
4310
+ filePath: rsxRegistrationFile,
4311
+ dryRun,
4312
+ });
4313
+
4314
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4315
+ verifySetupOutput(projectRoot, 'angular');
2379
4316
  }
4317
+
2380
4318
  logOk('RS-X Angular setup completed.');
2381
4319
  }
2382
4320
 
2383
4321
  function runSetupAuto(flags) {
2384
4322
  const projectRoot = process.cwd();
2385
4323
  const context = detectProjectContext(projectRoot);
2386
- const tag = resolveInstallTag(flags);
4324
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
2387
4325
 
2388
4326
  if (context === 'react') {
2389
4327
  logInfo('Auto-detected framework: react');
@@ -2410,10 +4348,21 @@ function runSetupAuto(flags) {
2410
4348
  }
2411
4349
 
2412
4350
  logInfo('No framework-specific setup detected; running generic setup.');
2413
- const pm = detectPackageManager(flags.pm);
2414
- installRuntimePackages(pm, Boolean(flags['dry-run']), tag);
2415
- installCompilerPackages(pm, Boolean(flags['dry-run']), tag);
2416
- installVsCodeExtension(flags);
4351
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
4352
+ installRuntimePackages(
4353
+ pm,
4354
+ Boolean(flags['dry-run']),
4355
+ tag,
4356
+ projectRoot,
4357
+ flags,
4358
+ );
4359
+ installCompilerPackages(
4360
+ pm,
4361
+ Boolean(flags['dry-run']),
4362
+ tag,
4363
+ projectRoot,
4364
+ flags,
4365
+ );
2417
4366
  }
2418
4367
 
2419
4368
  function resolveProjectModule(projectRoot, moduleName) {
@@ -2430,8 +4379,13 @@ function runBuild(flags) {
2430
4379
  const dryRun = Boolean(flags['dry-run']);
2431
4380
  const noEmit = Boolean(flags['no-emit']);
2432
4381
  const prodMode = parseBooleanFlag(flags.prod, false);
4382
+ const invocationConfig = resolveRsxBuildConfig(invocationRoot);
2433
4383
  const projectArg =
2434
- typeof flags.project === 'string' ? flags.project : 'tsconfig.json';
4384
+ typeof flags.project === 'string'
4385
+ ? flags.project
4386
+ : typeof invocationConfig.tsconfig === 'string'
4387
+ ? invocationConfig.tsconfig
4388
+ : 'tsconfig.json';
2435
4389
  const configPath = path.resolve(invocationRoot, projectArg);
2436
4390
  const projectRoot = path.dirname(configPath);
2437
4391
  const context = detectProjectContext(projectRoot);
@@ -2530,7 +4484,9 @@ function runBuild(flags) {
2530
4484
  const outDirOverride =
2531
4485
  typeof flags['out-dir'] === 'string'
2532
4486
  ? path.resolve(projectRoot, flags['out-dir'])
2533
- : null;
4487
+ : typeof rsxBuildConfig.outDir === 'string'
4488
+ ? path.resolve(projectRoot, rsxBuildConfig.outDir)
4489
+ : null;
2534
4490
  const outDir =
2535
4491
  outDirOverride ??
2536
4492
  parsedConfig.options.outDir ??
@@ -2978,9 +4934,14 @@ function ensureAngularPolyfillsContainsFile({
2978
4934
  });
2979
4935
 
2980
4936
  const selectedEntries = targetEntries.length > 0 ? targetEntries : entries;
2981
- const polyfillsPath = path
4937
+ const polyfillsRelativePath = path
2982
4938
  .relative(projectRoot, filePath)
2983
4939
  .replace(/\\/g, '/');
4940
+ const polyfillsPath =
4941
+ polyfillsRelativePath.startsWith('./') ||
4942
+ polyfillsRelativePath.startsWith('../')
4943
+ ? polyfillsRelativePath
4944
+ : `./${polyfillsRelativePath}`;
2984
4945
 
2985
4946
  let changed = false;
2986
4947
  const isRsxAotRegistrationEntry = (entry) =>
@@ -3047,20 +5008,286 @@ function ensureAngularPolyfillsContainsFile({
3047
5008
  logOk(`Updated angular.json to inject RS-X AOT runtime registration.`);
3048
5009
  }
3049
5010
 
3050
- function resolveRsxBuildConfig(projectRoot) {
3051
- const packageJsonPath = path.join(projectRoot, 'package.json');
3052
- if (!fs.existsSync(packageJsonPath)) {
3053
- return {};
5011
+ function readJsonFileIfPresent(filePath) {
5012
+ if (!fs.existsSync(filePath)) {
5013
+ return null;
3054
5014
  }
3055
5015
 
3056
5016
  try {
3057
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
3058
- const rsxConfig = packageJson.rsx ?? {};
3059
- const buildConfig = rsxConfig.build ?? {};
3060
- return typeof buildConfig === 'object' && buildConfig ? buildConfig : {};
5017
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
3061
5018
  } catch {
3062
- return {};
5019
+ return null;
5020
+ }
5021
+ }
5022
+
5023
+ function validateRsxConfigShape(config, filePath) {
5024
+ if (typeof config !== 'object' || !config || Array.isArray(config)) {
5025
+ logError(
5026
+ `Invalid RS-X config in ${filePath}: expected a JSON object at the top level.`,
5027
+ );
5028
+ process.exit(1);
5029
+ }
5030
+
5031
+ const build = config.build;
5032
+ if (build !== undefined) {
5033
+ if (typeof build !== 'object' || !build || Array.isArray(build)) {
5034
+ logError(
5035
+ `Invalid RS-X config in ${filePath}: "build" must be an object.`,
5036
+ );
5037
+ process.exit(1);
5038
+ }
5039
+
5040
+ const stringKeys = ['preparseFile', 'compiledFile', 'registrationFile'];
5041
+ const extraStringKeys = ['tsconfig', 'outDir'];
5042
+ for (const key of extraStringKeys) {
5043
+ if (build[key] !== undefined && typeof build[key] !== 'string') {
5044
+ logError(
5045
+ `Invalid RS-X config in ${filePath}: "build.${key}" must be a string.`,
5046
+ );
5047
+ process.exit(1);
5048
+ }
5049
+ }
5050
+ for (const key of stringKeys) {
5051
+ if (build[key] !== undefined && typeof build[key] !== 'string') {
5052
+ logError(
5053
+ `Invalid RS-X config in ${filePath}: "build.${key}" must be a string.`,
5054
+ );
5055
+ process.exit(1);
5056
+ }
5057
+ }
5058
+
5059
+ const booleanKeys = ['preparse', 'compiled', 'compiledResolvedEvaluator'];
5060
+ for (const key of booleanKeys) {
5061
+ if (build[key] !== undefined && typeof build[key] !== 'boolean') {
5062
+ logError(
5063
+ `Invalid RS-X config in ${filePath}: "build.${key}" must be a boolean.`,
5064
+ );
5065
+ process.exit(1);
5066
+ }
5067
+ }
5068
+ }
5069
+
5070
+ const cli = config.cli;
5071
+ if (cli !== undefined) {
5072
+ if (typeof cli !== 'object' || !cli || Array.isArray(cli)) {
5073
+ logError(`Invalid RS-X config in ${filePath}: "cli" must be an object.`);
5074
+ process.exit(1);
5075
+ }
5076
+
5077
+ const cliStringKeys = ['packageManager', 'installTag'];
5078
+ for (const key of cliStringKeys) {
5079
+ if (cli[key] !== undefined && typeof cli[key] !== 'string') {
5080
+ logError(
5081
+ `Invalid RS-X config in ${filePath}: "cli.${key}" must be a string.`,
5082
+ );
5083
+ process.exit(1);
5084
+ }
5085
+ }
5086
+
5087
+ if (
5088
+ cli.packageManager !== undefined &&
5089
+ !['pnpm', 'npm', 'yarn', 'bun'].includes(cli.packageManager)
5090
+ ) {
5091
+ logError(
5092
+ `Invalid RS-X config in ${filePath}: "cli.packageManager" must be one of pnpm, npm, yarn, or bun.`,
5093
+ );
5094
+ process.exit(1);
5095
+ }
5096
+
5097
+ if (
5098
+ cli.installTag !== undefined &&
5099
+ !['latest', 'next'].includes(cli.installTag)
5100
+ ) {
5101
+ logError(
5102
+ `Invalid RS-X config in ${filePath}: "cli.installTag" must be "latest" or "next".`,
5103
+ );
5104
+ process.exit(1);
5105
+ }
5106
+
5107
+ const cliBooleanSections = ['setup', 'project'];
5108
+ for (const key of cliBooleanSections) {
5109
+ if (cli[key] !== undefined) {
5110
+ if (
5111
+ typeof cli[key] !== 'object' ||
5112
+ !cli[key] ||
5113
+ Array.isArray(cli[key])
5114
+ ) {
5115
+ logError(
5116
+ `Invalid RS-X config in ${filePath}: "cli.${key}" must be an object.`,
5117
+ );
5118
+ process.exit(1);
5119
+ }
5120
+ if (
5121
+ cli[key].verify !== undefined &&
5122
+ typeof cli[key].verify !== 'boolean'
5123
+ ) {
5124
+ logError(
5125
+ `Invalid RS-X config in ${filePath}: "cli.${key}.verify" must be a boolean.`,
5126
+ );
5127
+ process.exit(1);
5128
+ }
5129
+ }
5130
+ }
5131
+
5132
+ const add = cli.add;
5133
+ if (add !== undefined) {
5134
+ if (typeof add !== 'object' || !add || Array.isArray(add)) {
5135
+ logError(
5136
+ `Invalid RS-X config in ${filePath}: "cli.add" must be an object.`,
5137
+ );
5138
+ process.exit(1);
5139
+ }
5140
+
5141
+ if (
5142
+ add.defaultDirectory !== undefined &&
5143
+ typeof add.defaultDirectory !== 'string'
5144
+ ) {
5145
+ logError(
5146
+ `Invalid RS-X config in ${filePath}: "cli.add.defaultDirectory" must be a string.`,
5147
+ );
5148
+ process.exit(1);
5149
+ }
5150
+
5151
+ if (add.searchRoots !== undefined) {
5152
+ if (
5153
+ !Array.isArray(add.searchRoots) ||
5154
+ add.searchRoots.some((entry) => typeof entry !== 'string')
5155
+ ) {
5156
+ logError(
5157
+ `Invalid RS-X config in ${filePath}: "cli.add.searchRoots" must be an array of strings.`,
5158
+ );
5159
+ process.exit(1);
5160
+ }
5161
+ }
5162
+ }
5163
+ }
5164
+ }
5165
+
5166
+ function mergeRsxConfig(baseConfig, overrideConfig) {
5167
+ const base = typeof baseConfig === 'object' && baseConfig ? baseConfig : {};
5168
+ const override =
5169
+ typeof overrideConfig === 'object' && overrideConfig ? overrideConfig : {};
5170
+
5171
+ return {
5172
+ ...base,
5173
+ ...override,
5174
+ build: {
5175
+ ...(typeof base.build === 'object' && base.build ? base.build : {}),
5176
+ ...(typeof override.build === 'object' && override.build
5177
+ ? override.build
5178
+ : {}),
5179
+ },
5180
+ cli: {
5181
+ ...(typeof base.cli === 'object' && base.cli ? base.cli : {}),
5182
+ ...(typeof override.cli === 'object' && override.cli ? override.cli : {}),
5183
+ add: {
5184
+ ...(typeof base.cli?.add === 'object' && base.cli?.add
5185
+ ? base.cli.add
5186
+ : {}),
5187
+ ...(typeof override.cli?.add === 'object' && override.cli?.add
5188
+ ? override.cli.add
5189
+ : {}),
5190
+ },
5191
+ },
5192
+ };
5193
+ }
5194
+
5195
+ function resolveRsxProjectConfig(projectRoot) {
5196
+ const fileConfigPath = path.join(projectRoot, 'rsx.config.json');
5197
+ const fileRsxConfig = readJsonFileIfPresent(fileConfigPath) ?? {};
5198
+ validateRsxConfigShape(fileRsxConfig, fileConfigPath);
5199
+ return mergeRsxConfig({}, fileRsxConfig);
5200
+ }
5201
+
5202
+ function defaultCliAddConfigForTemplate(template) {
5203
+ if (template === 'next' || template === 'nextjs') {
5204
+ return {
5205
+ defaultDirectory: 'app/expressions',
5206
+ searchRoots: ['app', 'src', 'expressions'],
5207
+ };
5208
+ }
5209
+
5210
+ return {
5211
+ defaultDirectory: 'src/expressions',
5212
+ searchRoots: ['src', 'app', 'expressions'],
5213
+ };
5214
+ }
5215
+
5216
+ function defaultCliConfigForTemplate(template) {
5217
+ return {
5218
+ packageManager: 'npm',
5219
+ installTag: 'next',
5220
+ setup: {
5221
+ verify: false,
5222
+ },
5223
+ project: {
5224
+ verify: false,
5225
+ },
5226
+ add: defaultCliAddConfigForTemplate(template),
5227
+ };
5228
+ }
5229
+
5230
+ function defaultRsxBuildConfigForTemplate(template) {
5231
+ if (template === 'next' || template === 'nextjs') {
5232
+ return {
5233
+ preparse: true,
5234
+ preparseFile: 'app/rsx-generated/rsx-aot-preparsed.generated.ts',
5235
+ compiled: true,
5236
+ compiledFile: 'app/rsx-generated/rsx-aot-compiled.generated.ts',
5237
+ registrationFile: 'app/rsx-generated/rsx-aot-registration.generated.ts',
5238
+ compiledResolvedEvaluator: false,
5239
+ };
5240
+ }
5241
+
5242
+ return {
5243
+ preparse: true,
5244
+ preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
5245
+ compiled: true,
5246
+ compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
5247
+ registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
5248
+ compiledResolvedEvaluator: false,
5249
+ };
5250
+ }
5251
+
5252
+ function ensureRsxConfigFile(projectRoot, template, dryRun) {
5253
+ const configPath = path.join(projectRoot, 'rsx.config.json');
5254
+ const defaultConfig = {
5255
+ build: defaultRsxBuildConfigForTemplate(template),
5256
+ cli: defaultCliConfigForTemplate(template),
5257
+ };
5258
+
5259
+ const existingConfig = readJsonFileIfPresent(configPath);
5260
+ const nextConfig = mergeRsxConfig(defaultConfig, existingConfig ?? {});
5261
+
5262
+ if (
5263
+ existingConfig &&
5264
+ JSON.stringify(existingConfig) === JSON.stringify(nextConfig)
5265
+ ) {
5266
+ return false;
5267
+ }
5268
+
5269
+ if (dryRun) {
5270
+ logInfo(`[dry-run] ${existingConfig ? 'patch' : 'create'} ${configPath}`);
5271
+ return true;
3063
5272
  }
5273
+
5274
+ fs.writeFileSync(
5275
+ configPath,
5276
+ `${JSON.stringify(nextConfig, null, 2)}\n`,
5277
+ 'utf8',
5278
+ );
5279
+ logOk(`${existingConfig ? 'Patched' : 'Created'} ${configPath}`);
5280
+ return true;
5281
+ }
5282
+
5283
+ function resolveRsxBuildConfig(projectRoot) {
5284
+ const buildConfig = resolveRsxProjectConfig(projectRoot).build ?? {};
5285
+ return typeof buildConfig === 'object' && buildConfig ? buildConfig : {};
5286
+ }
5287
+
5288
+ function resolveRsxCliConfig(projectRoot) {
5289
+ const cliConfig = resolveRsxProjectConfig(projectRoot).cli ?? {};
5290
+ return typeof cliConfig === 'object' && cliConfig ? cliConfig : {};
3064
5291
  }
3065
5292
 
3066
5293
  function parseBooleanFlag(value, defaultValue) {
@@ -3121,7 +5348,7 @@ function printGeneralHelp() {
3121
5348
  console.log(
3122
5349
  ' typecheck Type-check project + RS-X semantic checks',
3123
5350
  );
3124
- console.log(' version | -v Print CLI version');
5351
+ console.log(' version | v | -v Print CLI version');
3125
5352
  console.log('');
3126
5353
  console.log('Help Aliases:');
3127
5354
  console.log(' rsx -h');
@@ -3155,14 +5382,28 @@ function printAddHelp() {
3155
5382
  console.log(
3156
5383
  ' - Prompts for expression export name (must be valid TS identifier)',
3157
5384
  );
5385
+ console.log(" - Prompts for the initial expression string (default: 'a')");
5386
+ console.log(
5387
+ ' - Seeds the generated model with top-level identifiers found in that expression when possible',
5388
+ );
3158
5389
  console.log(
3159
5390
  ' - Prompts whether file name should be kebab-case (default: yes)',
3160
5391
  );
3161
5392
  console.log(' - Prompts for output directory (relative or absolute)');
3162
- console.log(' - Prompts whether to reuse an existing model file');
3163
- console.log(' - Creates <name>.ts and optionally creates <name>.model.ts');
3164
5393
  console.log(
3165
- ' - Expression file imports selected model and exports rsx expression',
5394
+ ' - Prompts for add mode: new one-file, new separate-model, or update existing',
5395
+ );
5396
+ console.log(
5397
+ ' - Defaults to keeping the model in the same file as the expression',
5398
+ );
5399
+ console.log(
5400
+ ' - If you choose an existing file, shows a list of files that already contain RS-X expressions',
5401
+ );
5402
+ console.log(
5403
+ ' - Can still create or reuse a separate model file when you opt out of same-file mode',
5404
+ );
5405
+ console.log(
5406
+ ' - Respects rsx.config.json (`cli.add`) for add defaults and file discovery',
3166
5407
  );
3167
5408
  }
3168
5409
 
@@ -3201,7 +5442,7 @@ function printInstallHelp(target) {
3201
5442
  function printSetupHelp() {
3202
5443
  console.log('Usage:');
3203
5444
  console.log(
3204
- ' rsx setup [--pm <pnpm|npm|yarn|bun>] [--next] [--force] [--local] [--dry-run]',
5445
+ ' rsx setup [--pm <pnpm|npm|yarn|bun>] [--next] [--verify] [--force] [--local] [--dry-run]',
3205
5446
  );
3206
5447
  console.log('');
3207
5448
  console.log('What it does:');
@@ -3210,12 +5451,17 @@ function printSetupHelp() {
3210
5451
  );
3211
5452
  console.log(' - Installs runtime packages');
3212
5453
  console.log(' - Installs compiler tooling packages');
3213
- console.log(' - Installs VS Code extension');
5454
+ console.log(' - Writes rsx.build config plus build/typecheck scripts');
5455
+ console.log(' - Creates rsx.config.json with CLI defaults you can override');
3214
5456
  console.log(' - Applies framework-specific transform/build integration');
5457
+ console.log(' - Does not install the VS Code extension automatically');
3215
5458
  console.log('');
3216
5459
  console.log('Options:');
3217
5460
  console.log(' --pm Explicit package manager');
3218
5461
  console.log(' --next Install prerelease versions (dist-tag next)');
5462
+ console.log(
5463
+ ' --verify Validate the resulting setup output before returning',
5464
+ );
3219
5465
  console.log(' --force Reinstall extension if already installed');
3220
5466
  console.log(' --local Build/install local VSIX from repo workspace');
3221
5467
  console.log(' --dry-run Print commands without executing them');
@@ -3234,14 +5480,17 @@ function printInitHelp() {
3234
5480
  console.log(
3235
5481
  ' - Detects project context and wires RS-X bootstrap in entry file',
3236
5482
  );
3237
- console.log(' - Installs VS Code extension (unless --skip-vscode)');
5483
+ console.log(' - Creates rsx.config.json with CLI defaults you can override');
5484
+ console.log(' - Does not install the VS Code extension automatically');
3238
5485
  console.log('');
3239
5486
  console.log('Options:');
3240
5487
  console.log(' --pm Explicit package manager');
3241
5488
  console.log(' --entry Explicit application entry file');
3242
5489
  console.log(' --next Install prerelease versions (dist-tag next)');
3243
5490
  console.log(' --skip-install Skip npm/pnpm/yarn/bun package installation');
3244
- console.log(' --skip-vscode Skip VS Code extension installation');
5491
+ console.log(
5492
+ ' --skip-vscode Accepted for compatibility; VS Code is not auto-installed',
5493
+ );
3245
5494
  console.log(' --force Reinstall extension if already installed');
3246
5495
  console.log(' --local Build/install local VSIX from repo workspace');
3247
5496
  console.log(' --dry-run Print commands without executing them');
@@ -3250,21 +5499,30 @@ function printInitHelp() {
3250
5499
  function printProjectHelp() {
3251
5500
  console.log('Usage:');
3252
5501
  console.log(
3253
- ' rsx project [angular|vuejs|react|nextjs|nodejs] [--name <project-name>] [--pm <pnpm|npm|yarn|bun>] [--next] [--template <angular|vuejs|react|nextjs|nodejs>] [--tarballs-dir <path>] [--skip-install] [--skip-vscode] [--dry-run]',
5502
+ ' rsx project [angular|vuejs|react|nextjs|nodejs] [--name <project-name>] [--pm <pnpm|npm|yarn|bun>] [--next] [--template <angular|vuejs|react|nextjs|nodejs>] [--tarballs-dir <path>] [--skip-install] [--skip-vscode] [--verify] [--dry-run]',
3254
5503
  );
3255
5504
  console.log('');
3256
5505
  console.log('What it does:');
3257
5506
  console.log(' - Creates a new project folder');
3258
5507
  console.log(' - Supports templates: angular, vuejs, react, nextjs, nodejs');
5508
+ console.log(
5509
+ ' - Angular generates the RS-X virtual-table demo starter on top of the latest Angular scaffold',
5510
+ );
3259
5511
  console.log(' - Scaffolds framework app and wires RS-X bootstrap/setup');
3260
5512
  console.log(' - Writes package.json with RS-X dependencies');
5513
+ console.log(
5514
+ ' - Creates rsx.config.json with starter CLI defaults you can override',
5515
+ );
3261
5516
  console.log(
3262
5517
  ' - Adds tsconfig + TypeScript plugin config for editor support',
3263
5518
  );
3264
- console.log(' - For Angular template: also installs @rs-x/angular');
5519
+ console.log(
5520
+ ' - For Angular template: uses the latest Angular CLI scaffold, then applies the RS-X demo starter',
5521
+ );
3265
5522
  console.log(' - For React/Next templates: also installs @rs-x/react');
3266
5523
  console.log(' - For Vue template: also installs @rs-x/vue');
3267
5524
  console.log(' - Installs dependencies (unless --skip-install)');
5525
+ console.log(' - Verifies the generated starter before reporting success');
3268
5526
  console.log('');
3269
5527
  console.log('Options:');
3270
5528
  console.log(' --name Project folder/package name');
@@ -3278,7 +5536,12 @@ function printProjectHelp() {
3278
5536
  );
3279
5537
  console.log(' (or set RSX_TARBALLS_DIR env var)');
3280
5538
  console.log(' --skip-install Skip dependency installation');
3281
- console.log(' --skip-vscode Skip VS Code extension installation');
5539
+ console.log(
5540
+ ' --skip-vscode Accepted for compatibility; VS Code is not auto-installed',
5541
+ );
5542
+ console.log(
5543
+ ' --verify Re-run starter structure checks explicitly after generation',
5544
+ );
3282
5545
  console.log(' --dry-run Print actions without writing files');
3283
5546
  }
3284
5547
 
@@ -3338,6 +5601,7 @@ function printTypecheckHelp() {
3338
5601
  function printVersionHelp() {
3339
5602
  console.log('Usage:');
3340
5603
  console.log(' rsx version');
5604
+ console.log(' rsx v');
3341
5605
  console.log(' rsx -v');
3342
5606
  console.log(' rsx -version');
3343
5607
  console.log(' rsx --version');
@@ -3349,6 +5613,7 @@ function isHelpToken(value) {
3349
5613
 
3350
5614
  function isVersionToken(value) {
3351
5615
  return (
5616
+ value === 'v' ||
3352
5617
  value === '-v' ||
3353
5618
  value === '--version' ||
3354
5619
  value === '-version' ||
@@ -3409,6 +5674,7 @@ function printHelpFor(command, target) {
3409
5674
  }
3410
5675
 
3411
5676
  if (
5677
+ command === 'v' ||
3412
5678
  command === 'version' ||
3413
5679
  command === '-v' ||
3414
5680
  command === '--version' ||
@@ -3493,9 +5759,16 @@ function main() {
3493
5759
  }
3494
5760
 
3495
5761
  if (command === 'install' && target === 'compiler') {
3496
- const pm = detectPackageManager(flags.pm);
3497
- const tag = resolveInstallTag(flags);
3498
- installCompilerPackages(pm, Boolean(flags['dry-run']), tag);
5762
+ const projectRoot = process.cwd();
5763
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
5764
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
5765
+ installCompilerPackages(
5766
+ pm,
5767
+ Boolean(flags['dry-run']),
5768
+ tag,
5769
+ projectRoot,
5770
+ flags,
5771
+ );
3499
5772
  return;
3500
5773
  }
3501
5774