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

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 +1373 -334
  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.20.vsix} +0 -0
  5. package/scripts/prepare-local-rsx-packages.sh +20 -0
  6. package/scripts/verify-rsx-cli-mutations.sh +258 -0
  7. package/scripts/verify-rsx-projects.sh +134 -0
  8. package/scripts/verify-rsx-setup.sh +186 -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
 
@@ -191,11 +224,33 @@ function applyTagToPackages(packages, tag) {
191
224
  }
192
225
 
193
226
  function resolveInstallTag(flags) {
194
- return parseBooleanFlag(flags.next, false) ? 'next' : undefined;
227
+ if (parseBooleanFlag(flags.next, false)) {
228
+ return 'next';
229
+ }
230
+
231
+ if (CLI_VERSION.includes('-')) {
232
+ return 'next';
233
+ }
234
+
235
+ const checkoutRoot = findRepoRoot(__dirname);
236
+ if (!checkoutRoot) {
237
+ return undefined;
238
+ }
239
+
240
+ const branchResult = spawnSync('git', ['branch', '--show-current'], {
241
+ cwd: checkoutRoot,
242
+ encoding: 'utf8',
243
+ });
244
+ const branch = branchResult.status === 0 ? branchResult.stdout.trim() : '';
245
+ if (branch && branch !== 'main') {
246
+ return 'next';
247
+ }
248
+
249
+ return undefined;
195
250
  }
196
251
 
197
252
  function installPackages(pm, packages, options = {}) {
198
- const { dev = false, dryRun = false, label = 'packages', tag } = options;
253
+ const { dev = false, dryRun = false, label = 'packages', tag, cwd } = options;
199
254
  const resolvedPackages = tag ? applyTagToPackages(packages, tag) : packages;
200
255
  const argsByPm = {
201
256
  pnpm: dev
@@ -220,24 +275,74 @@ function installPackages(pm, packages, options = {}) {
220
275
 
221
276
  const tagInfo = tag ? ` (tag: ${tag})` : '';
222
277
  logInfo(`Installing ${label} with ${pm}${tagInfo}...`);
223
- run(pm, installArgs, { dryRun });
278
+ run(pm, installArgs, { dryRun, cwd });
224
279
  logOk(`Installed ${label}.`);
225
280
  }
226
281
 
227
- function installRuntimePackages(pm, dryRun, tag) {
228
- installPackages(pm, RUNTIME_PACKAGES, {
282
+ function resolveLocalRsxSpecs(projectRoot, flags, options = {}) {
283
+ const tarballsDir =
284
+ typeof flags?.['tarballs-dir'] === 'string'
285
+ ? path.resolve(projectRoot, flags['tarballs-dir'])
286
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
287
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
288
+ ? path.resolve(projectRoot, process.env.RSX_TARBALLS_DIR)
289
+ : null;
290
+ const workspaceRoot = findRepoRoot(projectRoot);
291
+ return resolveProjectRsxSpecs(
292
+ projectRoot,
293
+ workspaceRoot,
294
+ tarballsDir,
295
+ options,
296
+ );
297
+ }
298
+
299
+ function installResolvedPackages(pm, packageNames, options = {}) {
300
+ const {
301
+ dryRun = false,
302
+ label = 'packages',
303
+ tag,
304
+ cwd,
305
+ specs,
306
+ dev = false,
307
+ } = options;
308
+ const resolvedPackages = packageNames.map((packageName) => {
309
+ const spec = specs?.[packageName];
310
+ return spec ? `${packageName}@${spec}` : packageName;
311
+ });
312
+
313
+ installPackages(pm, resolvedPackages, {
314
+ dev,
315
+ dryRun,
316
+ label,
317
+ tag: specs ? undefined : tag,
318
+ cwd,
319
+ });
320
+ }
321
+
322
+ function installRuntimePackages(pm, dryRun, tag, projectRoot, flags) {
323
+ const specs = resolveLocalRsxSpecs(projectRoot ?? process.cwd(), flags, {
324
+ tag,
325
+ });
326
+ installResolvedPackages(pm, RUNTIME_PACKAGES, {
229
327
  dev: false,
230
328
  dryRun,
231
329
  tag,
330
+ specs,
331
+ cwd: projectRoot,
232
332
  label: 'runtime RS-X packages',
233
333
  });
234
334
  }
235
335
 
236
- function installCompilerPackages(pm, dryRun, tag) {
237
- installPackages(pm, COMPILER_PACKAGES, {
336
+ function installCompilerPackages(pm, dryRun, tag, projectRoot, flags) {
337
+ const specs = resolveLocalRsxSpecs(projectRoot ?? process.cwd(), flags, {
338
+ tag,
339
+ });
340
+ installResolvedPackages(pm, COMPILER_PACKAGES, {
238
341
  dev: true,
239
342
  dryRun,
240
343
  tag,
344
+ specs,
345
+ cwd: projectRoot,
241
346
  label: 'compiler tooling',
242
347
  });
243
348
  }
@@ -262,14 +367,50 @@ function installVsCodeExtension(flags) {
262
367
  return;
263
368
  }
264
369
 
265
- const args = ['--install-extension', VS_CODE_EXTENSION_ID];
370
+ installBundledVsix(dryRun, force);
371
+ }
372
+
373
+ function resolveBundledVsix() {
374
+ const packageRoot = path.resolve(__dirname, '..');
375
+ const candidates = fs
376
+ .readdirSync(packageRoot)
377
+ .filter((name) => /^rs-x-vscode-extension-.*\.vsix$/u.test(name))
378
+ .map((name) => path.join(packageRoot, name));
379
+
380
+ if (candidates.length === 0) {
381
+ return null;
382
+ }
383
+
384
+ const latest = candidates
385
+ .map((fullPath) => ({
386
+ fullPath,
387
+ mtimeMs: fs.statSync(fullPath).mtimeMs,
388
+ }))
389
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
390
+
391
+ return latest?.fullPath ?? null;
392
+ }
393
+
394
+ function installBundledVsix(dryRun, force) {
395
+ const bundledVsix = resolveBundledVsix();
396
+ if (!bundledVsix) {
397
+ logWarn(
398
+ 'No bundled VSIX found in @rs-x/cli. Skipping VS Code extension install.',
399
+ );
400
+ logInfo(
401
+ 'If you are developing in the rs-x repo, use `rsx install vscode --local` instead.',
402
+ );
403
+ return;
404
+ }
405
+
406
+ const args = ['--install-extension', bundledVsix];
266
407
  if (force) {
267
408
  args.push('--force');
268
409
  }
269
410
 
270
- logInfo(`Installing ${VS_CODE_EXTENSION_ID} from VS Code marketplace...`);
411
+ logInfo(`Installing bundled VSIX from ${bundledVsix}...`);
271
412
  run('code', args, { dryRun });
272
- logOk('VS Code extension installed.');
413
+ logOk('VS Code extension installed from bundled VSIX.');
273
414
  }
274
415
 
275
416
  function installLocalVsix(dryRun, force) {
@@ -585,6 +726,210 @@ function writeFileWithDryRun(filePath, content, dryRun) {
585
726
  fs.writeFileSync(filePath, content, 'utf8');
586
727
  }
587
728
 
729
+ function stripJsonComments(content) {
730
+ let result = '';
731
+ let inString = false;
732
+ let stringDelimiter = '"';
733
+ let inLineComment = false;
734
+ let inBlockComment = false;
735
+
736
+ for (let index = 0; index < content.length; index += 1) {
737
+ const current = content[index];
738
+ const next = content[index + 1];
739
+
740
+ if (inLineComment) {
741
+ if (current === '\n') {
742
+ inLineComment = false;
743
+ result += current;
744
+ }
745
+ continue;
746
+ }
747
+
748
+ if (inBlockComment) {
749
+ if (current === '*' && next === '/') {
750
+ inBlockComment = false;
751
+ index += 1;
752
+ }
753
+ continue;
754
+ }
755
+
756
+ if (inString) {
757
+ result += current;
758
+ if (current === '\\') {
759
+ index += 1;
760
+ if (index < content.length) {
761
+ result += content[index];
762
+ }
763
+ continue;
764
+ }
765
+ if (current === stringDelimiter) {
766
+ inString = false;
767
+ }
768
+ continue;
769
+ }
770
+
771
+ if (current === '"' || current === "'") {
772
+ inString = true;
773
+ stringDelimiter = current;
774
+ result += current;
775
+ continue;
776
+ }
777
+
778
+ if (current === '/' && next === '/') {
779
+ inLineComment = true;
780
+ index += 1;
781
+ continue;
782
+ }
783
+
784
+ if (current === '/' && next === '*') {
785
+ inBlockComment = true;
786
+ index += 1;
787
+ continue;
788
+ }
789
+
790
+ result += current;
791
+ }
792
+
793
+ return result;
794
+ }
795
+
796
+ function parseJsonc(content) {
797
+ return JSON.parse(stripJsonComments(content.replace(/^\uFEFF/u, '')));
798
+ }
799
+
800
+ function copyPathWithDryRun(sourcePath, targetPath, dryRun) {
801
+ if (dryRun) {
802
+ logInfo(`[dry-run] copy ${sourcePath} -> ${targetPath}`);
803
+ return;
804
+ }
805
+
806
+ const stat = fs.statSync(sourcePath);
807
+ if (stat.isDirectory()) {
808
+ fs.mkdirSync(targetPath, { recursive: true });
809
+ for (const entry of fs.readdirSync(sourcePath, { withFileTypes: true })) {
810
+ copyPathWithDryRun(
811
+ path.join(sourcePath, entry.name),
812
+ path.join(targetPath, entry.name),
813
+ false,
814
+ );
815
+ }
816
+ return;
817
+ }
818
+
819
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
820
+ fs.copyFileSync(sourcePath, targetPath);
821
+ }
822
+
823
+ function removeFileOrDirectoryWithDryRun(targetPath, dryRun) {
824
+ if (!fs.existsSync(targetPath)) {
825
+ return;
826
+ }
827
+
828
+ if (dryRun) {
829
+ logInfo(`[dry-run] remove ${targetPath}`);
830
+ return;
831
+ }
832
+
833
+ fs.rmSync(targetPath, { recursive: true, force: true });
834
+ }
835
+
836
+ function resolveAngularProjectTsConfig(projectRoot) {
837
+ const appTsConfigPath = path.join(projectRoot, 'tsconfig.app.json');
838
+ if (fs.existsSync(appTsConfigPath)) {
839
+ return appTsConfigPath;
840
+ }
841
+
842
+ return path.join(projectRoot, 'tsconfig.json');
843
+ }
844
+
845
+ function upsertTypescriptPluginInTsConfig(configPath, dryRun) {
846
+ if (!fs.existsSync(configPath)) {
847
+ logWarn(`TypeScript config not found: ${configPath}`);
848
+ return;
849
+ }
850
+
851
+ const tsConfig = parseJsonc(fs.readFileSync(configPath, 'utf8'));
852
+ const compilerOptions = tsConfig.compilerOptions ?? {};
853
+ const plugins = Array.isArray(compilerOptions.plugins)
854
+ ? compilerOptions.plugins
855
+ : [];
856
+
857
+ if (
858
+ !plugins.some(
859
+ (plugin) =>
860
+ plugin &&
861
+ typeof plugin === 'object' &&
862
+ plugin.name === '@rs-x/typescript-plugin',
863
+ )
864
+ ) {
865
+ plugins.push({ name: '@rs-x/typescript-plugin' });
866
+ }
867
+
868
+ compilerOptions.plugins = plugins;
869
+ tsConfig.compilerOptions = compilerOptions;
870
+
871
+ if (dryRun) {
872
+ logInfo(`[dry-run] patch ${configPath}`);
873
+ return;
874
+ }
875
+
876
+ fs.writeFileSync(
877
+ configPath,
878
+ `${JSON.stringify(tsConfig, null, 2)}\n`,
879
+ 'utf8',
880
+ );
881
+ }
882
+
883
+ function ensureTsConfigIncludePattern(configPath, pattern, dryRun) {
884
+ if (!fs.existsSync(configPath)) {
885
+ return;
886
+ }
887
+
888
+ const tsConfig = parseJsonc(fs.readFileSync(configPath, 'utf8'));
889
+ const include = Array.isArray(tsConfig.include) ? tsConfig.include : [];
890
+ if (!include.includes(pattern)) {
891
+ include.push(pattern);
892
+ }
893
+ tsConfig.include = include;
894
+
895
+ if (dryRun) {
896
+ logInfo(`[dry-run] patch ${configPath}`);
897
+ return;
898
+ }
899
+
900
+ fs.writeFileSync(
901
+ configPath,
902
+ `${JSON.stringify(tsConfig, null, 2)}\n`,
903
+ 'utf8',
904
+ );
905
+ }
906
+
907
+ function ensureVueEnvTypes(projectRoot, dryRun) {
908
+ const envTypesPath = path.join(projectRoot, 'src', 'vite-env.d.ts');
909
+ const envTypesSource = `/// <reference types="vite/client" />
910
+
911
+ declare module '*.vue' {
912
+ import type { DefineComponent } from 'vue';
913
+
914
+ const component: DefineComponent<{}, {}, any>;
915
+ export default component;
916
+ }
917
+ `;
918
+
919
+ if (fs.existsSync(envTypesPath)) {
920
+ return;
921
+ }
922
+
923
+ if (dryRun) {
924
+ logInfo(`[dry-run] create ${envTypesPath}`);
925
+ return;
926
+ }
927
+
928
+ fs.mkdirSync(path.dirname(envTypesPath), { recursive: true });
929
+ fs.writeFileSync(envTypesPath, envTypesSource, 'utf8');
930
+ logOk(`Created ${envTypesPath}`);
931
+ }
932
+
588
933
  function toFileDependencySpec(fromDir, targetPath) {
589
934
  const relative = path.relative(fromDir, targetPath).replace(/\\/gu, '/');
590
935
  const normalized = relative.startsWith('.') ? relative : `./${relative}`;
@@ -634,6 +979,8 @@ function resolveProjectRsxSpecs(
634
979
  options = {},
635
980
  ) {
636
981
  const includeAngularPackage = Boolean(options.includeAngularPackage);
982
+ const includeReactPackage = Boolean(options.includeReactPackage);
983
+ const includeVuePackage = Boolean(options.includeVuePackage);
637
984
  const versionSpec = options.tag ? options.tag : RSX_PACKAGE_VERSION;
638
985
  const defaults = {
639
986
  '@rs-x/core': versionSpec,
@@ -642,7 +989,9 @@ function resolveProjectRsxSpecs(
642
989
  '@rs-x/compiler': versionSpec,
643
990
  '@rs-x/typescript-plugin': versionSpec,
644
991
  ...(includeAngularPackage ? { '@rs-x/angular': versionSpec } : {}),
645
- '@rs-x/cli': null,
992
+ ...(includeReactPackage ? { '@rs-x/react': versionSpec } : {}),
993
+ ...(includeVuePackage ? { '@rs-x/vue': versionSpec } : {}),
994
+ '@rs-x/cli': versionSpec,
646
995
  };
647
996
 
648
997
  const tarballSlugs = {
@@ -652,6 +1001,8 @@ function resolveProjectRsxSpecs(
652
1001
  '@rs-x/compiler': 'rs-x-compiler',
653
1002
  '@rs-x/typescript-plugin': 'rs-x-typescript-plugin',
654
1003
  ...(includeAngularPackage ? { '@rs-x/angular': 'rs-x-angular' } : {}),
1004
+ ...(includeReactPackage ? { '@rs-x/react': 'rs-x-react' } : {}),
1005
+ ...(includeVuePackage ? { '@rs-x/vue': 'rs-x-vue' } : {}),
655
1006
  '@rs-x/cli': 'rs-x-cli',
656
1007
  };
657
1008
 
@@ -674,6 +1025,16 @@ function resolveProjectRsxSpecs(
674
1025
  'rs-x-angular': path.join(tarballsDir, 'rs-x-angular'),
675
1026
  }
676
1027
  : {}),
1028
+ ...(includeReactPackage
1029
+ ? {
1030
+ 'rs-x-react': path.join(tarballsDir, 'rs-x-react'),
1031
+ }
1032
+ : {}),
1033
+ ...(includeVuePackage
1034
+ ? {
1035
+ 'rs-x-vue': path.join(tarballsDir, 'rs-x-vue'),
1036
+ }
1037
+ : {}),
677
1038
  'rs-x-cli': path.join(tarballsDir, 'rs-x-cli'),
678
1039
  };
679
1040
 
@@ -711,10 +1072,21 @@ function resolveProjectRsxSpecs(
711
1072
  ),
712
1073
  ...(includeAngularPackage
713
1074
  ? {
714
- '@rs-x/angular': path.join(
715
- workspaceRoot,
716
- 'rs-x-angular/projects/rsx',
717
- ),
1075
+ '@rs-x/angular': fs.existsSync(
1076
+ path.join(workspaceRoot, 'rs-x-angular/dist/rsx'),
1077
+ )
1078
+ ? path.join(workspaceRoot, 'rs-x-angular/dist/rsx')
1079
+ : path.join(workspaceRoot, 'rs-x-angular/projects/rsx'),
1080
+ }
1081
+ : {}),
1082
+ ...(includeReactPackage
1083
+ ? {
1084
+ '@rs-x/react': path.join(workspaceRoot, 'rs-x-react'),
1085
+ }
1086
+ : {}),
1087
+ ...(includeVuePackage
1088
+ ? {
1089
+ '@rs-x/vue': path.join(workspaceRoot, 'rs-x-vue'),
718
1090
  }
719
1091
  : {}),
720
1092
  '@rs-x/cli': path.join(workspaceRoot, 'rs-x-cli'),
@@ -777,6 +1149,22 @@ function createProjectPackageJson(projectName, rsxSpecs) {
777
1149
  );
778
1150
  }
779
1151
 
1152
+ function resolveProjectRoot(projectName, flags) {
1153
+ const parentDir =
1154
+ typeof flags?.['project-parent-dir'] === 'string'
1155
+ ? flags['project-parent-dir']
1156
+ : typeof process.env.RSX_PROJECT_PARENT_DIR === 'string' &&
1157
+ process.env.RSX_PROJECT_PARENT_DIR.trim().length > 0
1158
+ ? process.env.RSX_PROJECT_PARENT_DIR
1159
+ : null;
1160
+
1161
+ if (parentDir) {
1162
+ return path.resolve(parentDir, projectName);
1163
+ }
1164
+
1165
+ return path.resolve(process.cwd(), projectName);
1166
+ }
1167
+
780
1168
  function createProjectTsConfig() {
781
1169
  return (
782
1170
  JSON.stringify(
@@ -961,7 +1349,7 @@ async function runProject(flags) {
961
1349
  }
962
1350
  }
963
1351
 
964
- const projectRoot = path.resolve(process.cwd(), projectName);
1352
+ const projectRoot = resolveProjectRoot(projectName, flags);
965
1353
  const tarballsDir =
966
1354
  typeof flags['tarballs-dir'] === 'string'
967
1355
  ? path.resolve(process.cwd(), flags['tarballs-dir'])
@@ -1115,9 +1503,24 @@ async function resolveProjectName(nameFromFlags, fallbackName) {
1115
1503
  }
1116
1504
  }
1117
1505
 
1118
- function scaffoldProjectTemplate(template, projectName, pm, flags) {
1506
+ function scaffoldProjectTemplate(
1507
+ template,
1508
+ projectName,
1509
+ projectRoot,
1510
+ pm,
1511
+ flags,
1512
+ ) {
1119
1513
  const dryRun = Boolean(flags['dry-run']);
1120
1514
  const skipInstall = Boolean(flags['skip-install']);
1515
+ const scaffoldCwd = path.dirname(projectRoot);
1516
+ const scaffoldProjectArg = `./${projectName}`;
1517
+ const scaffoldEnv = {
1518
+ INIT_CWD: scaffoldCwd,
1519
+ npm_config_local_prefix: scaffoldCwd,
1520
+ npm_prefix: scaffoldCwd,
1521
+ PWD: scaffoldCwd,
1522
+ CI: 'true',
1523
+ };
1121
1524
 
1122
1525
  if (template === 'angular') {
1123
1526
  const args = [
@@ -1125,6 +1528,8 @@ function scaffoldProjectTemplate(template, projectName, pm, flags) {
1125
1528
  '@angular/cli@latest',
1126
1529
  'new',
1127
1530
  projectName,
1531
+ '--directory',
1532
+ scaffoldProjectArg,
1128
1533
  '--defaults',
1129
1534
  '--standalone',
1130
1535
  '--routing',
@@ -1135,21 +1540,45 @@ function scaffoldProjectTemplate(template, projectName, pm, flags) {
1135
1540
  if (skipInstall) {
1136
1541
  args.push('--skip-install');
1137
1542
  }
1138
- run('npx', args, { dryRun });
1543
+ run('npx', args, { dryRun, cwd: scaffoldCwd, env: scaffoldEnv });
1139
1544
  return;
1140
1545
  }
1141
1546
 
1142
1547
  if (template === 'react') {
1143
- run('npx', ['create-vite@latest', projectName, '--template', 'react-ts'], {
1144
- dryRun,
1145
- });
1548
+ run(
1549
+ 'npx',
1550
+ [
1551
+ 'create-vite@latest',
1552
+ scaffoldProjectArg,
1553
+ '--no-interactive',
1554
+ '--template',
1555
+ 'react-ts',
1556
+ ],
1557
+ {
1558
+ dryRun,
1559
+ cwd: scaffoldCwd,
1560
+ env: scaffoldEnv,
1561
+ },
1562
+ );
1146
1563
  return;
1147
1564
  }
1148
1565
 
1149
1566
  if (template === 'vuejs') {
1150
- run('npx', ['create-vite@latest', projectName, '--template', 'vue-ts'], {
1151
- dryRun,
1152
- });
1567
+ run(
1568
+ 'npx',
1569
+ [
1570
+ 'create-vite@latest',
1571
+ scaffoldProjectArg,
1572
+ '--no-interactive',
1573
+ '--template',
1574
+ 'vue-ts',
1575
+ ],
1576
+ {
1577
+ dryRun,
1578
+ cwd: scaffoldCwd,
1579
+ env: scaffoldEnv,
1580
+ },
1581
+ );
1153
1582
  return;
1154
1583
  }
1155
1584
 
@@ -1162,7 +1591,7 @@ function scaffoldProjectTemplate(template, projectName, pm, flags) {
1162
1591
  };
1163
1592
  const args = [
1164
1593
  'create-next-app@latest',
1165
- projectName,
1594
+ scaffoldProjectArg,
1166
1595
  '--yes',
1167
1596
  '--ts',
1168
1597
  '--app',
@@ -1174,7 +1603,7 @@ function scaffoldProjectTemplate(template, projectName, pm, flags) {
1174
1603
  if (skipInstall) {
1175
1604
  args.push('--skip-install');
1176
1605
  }
1177
- run('npx', args, { dryRun });
1606
+ run('npx', args, { dryRun, cwd: scaffoldCwd, env: scaffoldEnv });
1178
1607
  return;
1179
1608
  }
1180
1609
 
@@ -1182,57 +1611,589 @@ function scaffoldProjectTemplate(template, projectName, pm, flags) {
1182
1611
  process.exit(1);
1183
1612
  }
1184
1613
 
1185
- async function runProjectWithTemplate(template, flags) {
1186
- const normalizedTemplate = normalizeProjectTemplate(template);
1187
- if (!normalizedTemplate) {
1188
- logError(
1189
- `Unsupported template '${template}'. Choose one of: ${PROJECT_TEMPLATES.join(', ')}`,
1614
+ function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
1615
+ const dryRun = Boolean(flags['dry-run']);
1616
+ const tag = resolveInstallTag(flags);
1617
+ const tarballsDir =
1618
+ typeof flags['tarballs-dir'] === 'string'
1619
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
1620
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
1621
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
1622
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
1623
+ : null;
1624
+ const workspaceRoot = findRepoRoot(projectRoot);
1625
+ const rsxSpecs = resolveProjectRsxSpecs(
1626
+ projectRoot,
1627
+ workspaceRoot,
1628
+ tarballsDir,
1629
+ { tag, includeAngularPackage: true },
1630
+ );
1631
+
1632
+ const templateFiles = ['README.md', 'src'];
1633
+ for (const entry of templateFiles) {
1634
+ copyPathWithDryRun(
1635
+ path.join(ANGULAR_DEMO_TEMPLATE_DIR, entry),
1636
+ path.join(projectRoot, entry),
1637
+ dryRun,
1190
1638
  );
1191
- process.exit(1);
1192
1639
  }
1193
1640
 
1194
- if (normalizedTemplate === 'nodejs') {
1195
- await runProject(flags);
1196
- return;
1641
+ const staleAngularFiles = [
1642
+ path.join(projectRoot, 'src/app/app.ts'),
1643
+ path.join(projectRoot, 'src/app/app.spec.ts'),
1644
+ path.join(projectRoot, 'src/app/app.html'),
1645
+ path.join(projectRoot, 'src/app/app.css'),
1646
+ path.join(projectRoot, 'src/app/app.routes.ts'),
1647
+ path.join(projectRoot, 'src/app/app.config.ts'),
1648
+ ];
1649
+ for (const stalePath of staleAngularFiles) {
1650
+ removeFileOrDirectoryWithDryRun(stalePath, dryRun);
1197
1651
  }
1198
1652
 
1199
- const pm = detectPackageManager(flags.pm);
1200
- const projectName = await resolveProjectName(flags.name, flags._nameHint);
1201
- const projectRoot = path.resolve(process.cwd(), projectName);
1202
- if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
1203
- logError(`Target directory is not empty: ${projectRoot}`);
1653
+ const readmePath = path.join(projectRoot, 'README.md');
1654
+ if (fs.existsSync(readmePath)) {
1655
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
1656
+ const nextReadme = readmeSource.replace(
1657
+ /^#\s+rsx-angular-example/mu,
1658
+ `# ${projectName}`,
1659
+ );
1660
+ if (dryRun) {
1661
+ logInfo(`[dry-run] patch ${readmePath}`);
1662
+ } else {
1663
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
1664
+ }
1665
+ }
1666
+
1667
+ const packageJsonPath = path.join(projectRoot, 'package.json');
1668
+ if (!fs.existsSync(packageJsonPath)) {
1669
+ logError(
1670
+ `package.json not found in generated Angular app: ${packageJsonPath}`,
1671
+ );
1204
1672
  process.exit(1);
1205
1673
  }
1206
1674
 
1207
- scaffoldProjectTemplate(normalizedTemplate, projectName, pm, flags);
1208
- const dryRun = Boolean(flags['dry-run']);
1675
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1676
+ const angularTsConfigPath = resolveAngularProjectTsConfig(projectRoot);
1677
+ const angularTsConfigRelative = path
1678
+ .relative(projectRoot, angularTsConfigPath)
1679
+ .replace(/\\/gu, '/');
1680
+ packageJson.name = projectName;
1681
+ packageJson.private = true;
1682
+ packageJson.version = '0.1.0';
1683
+ packageJson.scripts = {
1684
+ 'build:rsx': `rsx build --project ${angularTsConfigRelative} --no-emit --prod`,
1685
+ 'typecheck:rsx': `rsx typecheck --project ${angularTsConfigRelative}`,
1686
+ prebuild: 'npm run build:rsx',
1687
+ start: 'npm run build:rsx && ng serve',
1688
+ build: 'ng build',
1689
+ };
1690
+ packageJson.rsx = {
1691
+ build: {
1692
+ preparse: true,
1693
+ preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
1694
+ compiled: true,
1695
+ compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
1696
+ registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
1697
+ compiledResolvedEvaluator: false,
1698
+ },
1699
+ };
1700
+ packageJson.dependencies = {
1701
+ ...(packageJson.dependencies ?? {}),
1702
+ '@rs-x/angular': rsxSpecs['@rs-x/angular'],
1703
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
1704
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
1705
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
1706
+ };
1707
+ packageJson.devDependencies = {
1708
+ ...(packageJson.devDependencies ?? {}),
1709
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
1710
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
1711
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
1712
+ };
1713
+
1209
1714
  if (dryRun) {
1210
- logInfo(`[dry-run] setup RS-X in ${projectRoot}`);
1211
- return;
1715
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
1716
+ } else {
1717
+ fs.writeFileSync(
1718
+ packageJsonPath,
1719
+ `${JSON.stringify(packageJson, null, 2)}\n`,
1720
+ 'utf8',
1721
+ );
1212
1722
  }
1213
1723
 
1214
- withWorkingDirectory(projectRoot, () => {
1724
+ const angularJsonPath = path.join(projectRoot, 'angular.json');
1725
+ if (!fs.existsSync(angularJsonPath)) {
1726
+ logError(
1727
+ `angular.json not found in generated Angular app: ${angularJsonPath}`,
1728
+ );
1729
+ process.exit(1);
1730
+ }
1731
+
1732
+ const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
1733
+ const projects = angularJson.projects ?? {};
1734
+ const [angularProjectName] = Object.keys(projects);
1735
+ if (!angularProjectName) {
1736
+ logError('Generated angular.json does not define any projects.');
1737
+ process.exit(1);
1738
+ }
1739
+
1740
+ const angularProject = projects[angularProjectName];
1741
+ const architect = angularProject.architect ?? angularProject.targets;
1742
+ const build = architect?.build;
1743
+ if (!build) {
1744
+ logError('Generated Angular project is missing a build target.');
1745
+ process.exit(1);
1746
+ }
1747
+
1748
+ const buildOptions = build.options ?? {};
1749
+ const styles = Array.isArray(buildOptions.styles) ? buildOptions.styles : [];
1750
+ if (!styles.includes('src/styles.css')) {
1751
+ styles.push('src/styles.css');
1752
+ }
1753
+ buildOptions.styles = styles;
1754
+ buildOptions.preserveSymlinks = true;
1755
+
1756
+ const registrationFile =
1757
+ 'src/rsx-generated/rsx-aot-registration.generated.ts';
1758
+ let polyfills = buildOptions.polyfills;
1759
+ if (typeof polyfills === 'string') {
1760
+ polyfills = [polyfills];
1761
+ } else if (!Array.isArray(polyfills)) {
1762
+ polyfills = [];
1763
+ }
1764
+ if (!polyfills.includes(registrationFile)) {
1765
+ polyfills.push(registrationFile);
1766
+ }
1767
+ buildOptions.polyfills = polyfills;
1768
+ build.options = buildOptions;
1769
+
1770
+ if (build.configurations?.production?.budgets) {
1771
+ delete build.configurations.production.budgets;
1772
+ }
1773
+
1774
+ if (dryRun) {
1775
+ logInfo(`[dry-run] patch ${angularJsonPath}`);
1776
+ } else {
1777
+ fs.writeFileSync(
1778
+ angularJsonPath,
1779
+ `${JSON.stringify(angularJson, null, 2)}\n`,
1780
+ 'utf8',
1781
+ );
1782
+ }
1783
+
1784
+ if (!Boolean(flags['skip-install'])) {
1785
+ logInfo(`Refreshing ${pm} dependencies for the RS-X Angular starter...`);
1786
+ run(pm, ['install'], { dryRun });
1787
+ logOk('Angular starter dependencies are up to date.');
1788
+ }
1789
+ }
1790
+
1791
+ function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
1792
+ const dryRun = Boolean(flags['dry-run']);
1793
+ const tag = resolveInstallTag(flags);
1794
+ const tarballsDir =
1795
+ typeof flags['tarballs-dir'] === 'string'
1796
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
1797
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
1798
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
1799
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
1800
+ : null;
1801
+ const workspaceRoot = findRepoRoot(projectRoot);
1802
+ const rsxSpecs = resolveProjectRsxSpecs(
1803
+ projectRoot,
1804
+ workspaceRoot,
1805
+ tarballsDir,
1806
+ { tag, includeReactPackage: true },
1807
+ );
1808
+
1809
+ const templateFiles = [
1810
+ 'README.md',
1811
+ 'index.html',
1812
+ 'src',
1813
+ 'tsconfig.json',
1814
+ 'vite.config.ts',
1815
+ ];
1816
+ for (const entry of templateFiles) {
1817
+ copyPathWithDryRun(
1818
+ path.join(REACT_DEMO_TEMPLATE_DIR, entry),
1819
+ path.join(projectRoot, entry),
1820
+ dryRun,
1821
+ );
1822
+ }
1823
+
1824
+ const staleReactFiles = [
1825
+ path.join(projectRoot, 'src/App.tsx'),
1826
+ path.join(projectRoot, 'src/App.css'),
1827
+ path.join(projectRoot, 'src/index.css'),
1828
+ path.join(projectRoot, 'src/vite-env.d.ts'),
1829
+ path.join(projectRoot, 'src/assets'),
1830
+ path.join(projectRoot, 'public'),
1831
+ path.join(projectRoot, 'eslint.config.js'),
1832
+ path.join(projectRoot, 'eslint.config.ts'),
1833
+ path.join(projectRoot, 'tsconfig.app.json'),
1834
+ path.join(projectRoot, 'tsconfig.node.json'),
1835
+ ];
1836
+ for (const stalePath of staleReactFiles) {
1837
+ removeFileOrDirectoryWithDryRun(stalePath, dryRun);
1838
+ }
1839
+
1840
+ const readmePath = path.join(projectRoot, 'README.md');
1841
+ if (fs.existsSync(readmePath)) {
1842
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
1843
+ const nextReadme = readmeSource.replace(
1844
+ /^#\s+rsx-react-example/mu,
1845
+ `# ${projectName}`,
1846
+ );
1847
+ if (dryRun) {
1848
+ logInfo(`[dry-run] patch ${readmePath}`);
1849
+ } else {
1850
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
1851
+ }
1852
+ }
1853
+
1854
+ const packageJsonPath = path.join(projectRoot, 'package.json');
1855
+ if (!fs.existsSync(packageJsonPath)) {
1856
+ logError(
1857
+ `package.json not found in generated React app: ${packageJsonPath}`,
1858
+ );
1859
+ process.exit(1);
1860
+ }
1861
+
1862
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1863
+ packageJson.name = projectName;
1864
+ packageJson.private = true;
1865
+ packageJson.version = '0.1.0';
1866
+ packageJson.type = 'module';
1867
+ packageJson.scripts = {
1868
+ 'build:rsx': 'rsx build --project tsconfig.json --no-emit --prod',
1869
+ dev: 'npm run build:rsx && vite',
1870
+ build: 'npm run build:rsx && vite build',
1871
+ preview: 'vite preview',
1872
+ };
1873
+ packageJson.rsx = {
1874
+ build: {
1875
+ preparse: true,
1876
+ preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
1877
+ compiled: true,
1878
+ compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
1879
+ compiledResolvedEvaluator: false,
1880
+ },
1881
+ };
1882
+ packageJson.dependencies = {
1883
+ react: packageJson.dependencies?.react ?? '^19.2.4',
1884
+ 'react-dom': packageJson.dependencies?.['react-dom'] ?? '^19.2.4',
1885
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
1886
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
1887
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
1888
+ '@rs-x/react': rsxSpecs['@rs-x/react'],
1889
+ };
1890
+ packageJson.devDependencies = {
1891
+ typescript: packageJson.devDependencies?.typescript ?? '^5.9.3',
1892
+ vite: packageJson.devDependencies?.vite ?? '^7.3.1',
1893
+ '@vitejs/plugin-react':
1894
+ packageJson.devDependencies?.['@vitejs/plugin-react'] ?? '^5.1.4',
1895
+ '@types/react': packageJson.devDependencies?.['@types/react'] ?? '^19.2.2',
1896
+ '@types/react-dom':
1897
+ packageJson.devDependencies?.['@types/react-dom'] ?? '^19.2.2',
1898
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
1899
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
1900
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
1901
+ };
1902
+
1903
+ if (dryRun) {
1904
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
1905
+ } else {
1906
+ fs.writeFileSync(
1907
+ packageJsonPath,
1908
+ `${JSON.stringify(packageJson, null, 2)}\n`,
1909
+ 'utf8',
1910
+ );
1911
+ }
1912
+
1913
+ const tsConfigPath = path.join(projectRoot, 'tsconfig.json');
1914
+ if (fs.existsSync(tsConfigPath)) {
1915
+ upsertTypescriptPluginInTsConfig(tsConfigPath, dryRun);
1916
+ }
1917
+
1918
+ if (!Boolean(flags['skip-install'])) {
1919
+ logInfo(`Refreshing ${pm} dependencies for the RS-X React starter...`);
1920
+ run(pm, ['install'], { dryRun });
1921
+ logOk('React starter dependencies are up to date.');
1922
+ }
1923
+ }
1924
+
1925
+ function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
1926
+ const dryRun = Boolean(flags['dry-run']);
1927
+ const tag = resolveInstallTag(flags);
1928
+ const tarballsDir =
1929
+ typeof flags['tarballs-dir'] === 'string'
1930
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
1931
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
1932
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
1933
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
1934
+ : null;
1935
+ const workspaceRoot = findRepoRoot(projectRoot);
1936
+ const rsxSpecs = resolveProjectRsxSpecs(
1937
+ projectRoot,
1938
+ workspaceRoot,
1939
+ tarballsDir,
1940
+ { tag, includeVuePackage: true },
1941
+ );
1942
+
1943
+ const templateFiles = ['README.md', 'src'];
1944
+ for (const entry of templateFiles) {
1945
+ copyPathWithDryRun(
1946
+ path.join(VUE_DEMO_TEMPLATE_DIR, entry),
1947
+ path.join(projectRoot, entry),
1948
+ dryRun,
1949
+ );
1950
+ }
1951
+
1952
+ const staleVueFiles = [
1953
+ path.join(projectRoot, 'public'),
1954
+ path.join(projectRoot, 'src/components/HelloWorld.vue'),
1955
+ path.join(projectRoot, 'src/assets'),
1956
+ ];
1957
+ for (const stalePath of staleVueFiles) {
1958
+ removeFileOrDirectoryWithDryRun(stalePath, dryRun);
1959
+ }
1960
+
1961
+ const readmePath = path.join(projectRoot, 'README.md');
1962
+ if (fs.existsSync(readmePath)) {
1963
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
1964
+ const nextReadme = readmeSource.replace(
1965
+ /^#\s+rsx-vue-example/mu,
1966
+ `# ${projectName}`,
1967
+ );
1968
+ if (dryRun) {
1969
+ logInfo(`[dry-run] patch ${readmePath}`);
1970
+ } else {
1971
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
1972
+ }
1973
+ }
1974
+
1975
+ const packageJsonPath = path.join(projectRoot, 'package.json');
1976
+ if (!fs.existsSync(packageJsonPath)) {
1977
+ logError(`package.json not found in generated Vue app: ${packageJsonPath}`);
1978
+ process.exit(1);
1979
+ }
1980
+
1981
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1982
+ packageJson.name = projectName;
1983
+ packageJson.private = true;
1984
+ packageJson.version = '0.1.0';
1985
+ packageJson.type = 'module';
1986
+ packageJson.scripts = {
1987
+ 'build:rsx': 'rsx build --project tsconfig.app.json --no-emit --prod',
1988
+ 'typecheck:rsx': 'rsx typecheck --project tsconfig.app.json',
1989
+ dev: 'npm run build:rsx && vite',
1990
+ build: 'npm run build:rsx && vue-tsc -b && vite build',
1991
+ preview: 'vite preview',
1992
+ };
1993
+ packageJson.rsx = {
1994
+ build: {
1995
+ preparse: true,
1996
+ preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
1997
+ compiled: true,
1998
+ compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
1999
+ compiledResolvedEvaluator: false,
2000
+ },
2001
+ };
2002
+ packageJson.dependencies = {
2003
+ vue: packageJson.dependencies?.vue ?? '^3.5.30',
2004
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
2005
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
2006
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
2007
+ '@rs-x/vue': rsxSpecs['@rs-x/vue'],
2008
+ };
2009
+ packageJson.devDependencies = {
2010
+ ...(packageJson.devDependencies ?? {}),
2011
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
2012
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
2013
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
2014
+ };
2015
+
2016
+ if (dryRun) {
2017
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
2018
+ } else {
2019
+ fs.writeFileSync(
2020
+ packageJsonPath,
2021
+ `${JSON.stringify(packageJson, null, 2)}\n`,
2022
+ 'utf8',
2023
+ );
2024
+ }
2025
+
2026
+ const tsConfigAppPath = path.join(projectRoot, 'tsconfig.app.json');
2027
+ if (fs.existsSync(tsConfigAppPath)) {
2028
+ upsertTypescriptPluginInTsConfig(tsConfigAppPath, dryRun);
2029
+ ensureTsConfigIncludePattern(tsConfigAppPath, 'src/**/*.d.ts', dryRun);
2030
+ }
2031
+
2032
+ if (!Boolean(flags['skip-install'])) {
2033
+ logInfo(`Refreshing ${pm} dependencies for the RS-X Vue starter...`);
2034
+ run(pm, ['install'], { dryRun });
2035
+ logOk('Vue starter dependencies are up to date.');
2036
+ }
2037
+ }
2038
+
2039
+ function applyNextDemoStarter(projectRoot, projectName, pm, flags) {
2040
+ const dryRun = Boolean(flags['dry-run']);
2041
+ const tag = resolveInstallTag(flags);
2042
+ const tarballsDir =
2043
+ typeof flags['tarballs-dir'] === 'string'
2044
+ ? path.resolve(process.cwd(), flags['tarballs-dir'])
2045
+ : typeof process.env.RSX_TARBALLS_DIR === 'string' &&
2046
+ process.env.RSX_TARBALLS_DIR.trim().length > 0
2047
+ ? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
2048
+ : null;
2049
+ const workspaceRoot = findRepoRoot(projectRoot);
2050
+ const rsxSpecs = resolveProjectRsxSpecs(
2051
+ projectRoot,
2052
+ workspaceRoot,
2053
+ tarballsDir,
2054
+ { tag, includeReactPackage: true },
2055
+ );
2056
+
2057
+ const templateFiles = ['README.md', 'app', 'components', 'hooks', 'lib'];
2058
+ for (const entry of templateFiles) {
2059
+ copyPathWithDryRun(
2060
+ path.join(NEXT_DEMO_TEMPLATE_DIR, entry),
2061
+ path.join(projectRoot, entry),
2062
+ dryRun,
2063
+ );
2064
+ }
2065
+
2066
+ const readmePath = path.join(projectRoot, 'README.md');
2067
+ if (fs.existsSync(readmePath)) {
2068
+ const readmeSource = fs.readFileSync(readmePath, 'utf8');
2069
+ const nextReadme = readmeSource.replace(
2070
+ /^#\s+rsx-next-example/mu,
2071
+ `# ${projectName}`,
2072
+ );
2073
+ if (dryRun) {
2074
+ logInfo(`[dry-run] patch ${readmePath}`);
2075
+ } else {
2076
+ fs.writeFileSync(readmePath, nextReadme, 'utf8');
2077
+ }
2078
+ }
2079
+
2080
+ const publicDir = path.join(projectRoot, 'public');
2081
+ removeFileOrDirectoryWithDryRun(publicDir, dryRun);
2082
+
2083
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2084
+ if (!fs.existsSync(packageJsonPath)) {
2085
+ logError(
2086
+ `package.json not found in generated Next.js app: ${packageJsonPath}`,
2087
+ );
2088
+ process.exit(1);
2089
+ }
2090
+
2091
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2092
+ packageJson.name = projectName;
2093
+ packageJson.private = true;
2094
+ packageJson.version = '0.1.0';
2095
+ packageJson.scripts = {
2096
+ ...packageJson.scripts,
2097
+ 'build:rsx': 'rsx build --project tsconfig.json --no-emit --prod',
2098
+ dev: 'npm run build:rsx && next dev',
2099
+ build: 'npm run build:rsx && next build',
2100
+ start: 'next start',
2101
+ };
2102
+ packageJson.rsx = {
2103
+ build: {
2104
+ preparse: true,
2105
+ preparseFile: 'app/rsx-generated/rsx-aot-preparsed.generated.ts',
2106
+ compiled: true,
2107
+ compiledFile: 'app/rsx-generated/rsx-aot-compiled.generated.ts',
2108
+ compiledResolvedEvaluator: false,
2109
+ },
2110
+ };
2111
+ packageJson.dependencies = {
2112
+ ...(packageJson.dependencies ?? {}),
2113
+ '@rs-x/core': rsxSpecs['@rs-x/core'],
2114
+ '@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
2115
+ '@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
2116
+ '@rs-x/react': rsxSpecs['@rs-x/react'],
2117
+ };
2118
+ packageJson.devDependencies = {
2119
+ ...(packageJson.devDependencies ?? {}),
2120
+ '@rs-x/cli': rsxSpecs['@rs-x/cli'],
2121
+ '@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
2122
+ '@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
2123
+ };
2124
+
2125
+ if (dryRun) {
2126
+ logInfo(`[dry-run] patch ${packageJsonPath}`);
2127
+ } else {
2128
+ fs.writeFileSync(
2129
+ packageJsonPath,
2130
+ `${JSON.stringify(packageJson, null, 2)}\n`,
2131
+ 'utf8',
2132
+ );
2133
+ }
2134
+
2135
+ const tsConfigPath = path.join(projectRoot, 'tsconfig.json');
2136
+ if (fs.existsSync(tsConfigPath)) {
2137
+ upsertTypescriptPluginInTsConfig(tsConfigPath, dryRun);
2138
+ }
2139
+
2140
+ if (!Boolean(flags['skip-install'])) {
2141
+ logInfo(`Refreshing ${pm} dependencies for the RS-X Next.js starter...`);
2142
+ run(pm, ['install'], { dryRun });
2143
+ logOk('Next.js starter dependencies are up to date.');
2144
+ }
2145
+ }
2146
+
2147
+ async function runProjectWithTemplate(template, flags) {
2148
+ const normalizedTemplate = normalizeProjectTemplate(template);
2149
+ if (!normalizedTemplate) {
2150
+ logError(
2151
+ `Unsupported template '${template}'. Choose one of: ${PROJECT_TEMPLATES.join(', ')}`,
2152
+ );
2153
+ process.exit(1);
2154
+ }
2155
+
2156
+ if (normalizedTemplate === 'nodejs') {
2157
+ await runProject(flags);
2158
+ return;
2159
+ }
2160
+
2161
+ const pm = detectPackageManager(flags.pm);
2162
+ const projectName = await resolveProjectName(flags.name, flags._nameHint);
2163
+ const projectRoot = resolveProjectRoot(projectName, flags);
2164
+ if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
2165
+ logError(`Target directory is not empty: ${projectRoot}`);
2166
+ process.exit(1);
2167
+ }
2168
+
2169
+ scaffoldProjectTemplate(
2170
+ normalizedTemplate,
2171
+ projectName,
2172
+ projectRoot,
2173
+ pm,
2174
+ flags,
2175
+ );
2176
+ const dryRun = Boolean(flags['dry-run']);
2177
+ if (dryRun) {
2178
+ logInfo(`[dry-run] setup RS-X in ${projectRoot}`);
2179
+ return;
2180
+ }
2181
+
2182
+ withWorkingDirectory(projectRoot, () => {
1215
2183
  if (normalizedTemplate === 'angular') {
1216
- runSetupAngular(flags);
2184
+ applyAngularDemoStarter(projectRoot, projectName, pm, flags);
1217
2185
  return;
1218
2186
  }
1219
2187
  if (normalizedTemplate === 'react') {
1220
- runSetupReact({
1221
- ...flags,
1222
- entry: flags.entry ?? 'src/main.tsx',
1223
- });
2188
+ applyReactDemoStarter(projectRoot, projectName, pm, flags);
1224
2189
  return;
1225
2190
  }
1226
2191
  if (normalizedTemplate === 'nextjs') {
1227
- runSetupNext(flags);
2192
+ applyNextDemoStarter(projectRoot, projectName, pm, flags);
1228
2193
  return;
1229
2194
  }
1230
2195
  if (normalizedTemplate === 'vuejs') {
1231
- runSetupVue({
1232
- ...flags,
1233
- entry: flags.entry ?? 'src/main.ts',
1234
- });
1235
- applyVueRsxTemplate(projectRoot, dryRun);
2196
+ applyVueDemoStarter(projectRoot, projectName, pm, flags);
1236
2197
  }
1237
2198
  });
1238
2199
 
@@ -1507,12 +2468,12 @@ function ensureNextGateFile(gateFile, bootstrapFile, dryRun) {
1507
2468
  const content = useTypeScript
1508
2469
  ? `'use client';
1509
2470
 
1510
- import { type ReactNode, useEffect, useState } from 'react';
2471
+ import { type ReactElement, type ReactNode, useEffect, useState } from 'react';
1511
2472
 
1512
2473
  import { initRsx } from '${importPath}';
1513
2474
 
1514
2475
  // Generated by rsx init
1515
- export function RsxBootstrapGate(props: { children: ReactNode }): JSX.Element | null {
2476
+ export function RsxBootstrapGate(props: { children: ReactNode }): ReactElement | null {
1516
2477
  const [ready, setReady] = useState(false);
1517
2478
 
1518
2479
  useEffect(() => {
@@ -1733,8 +2694,8 @@ function runInit(flags) {
1733
2694
  const projectRoot = process.cwd();
1734
2695
 
1735
2696
  if (!skipInstall) {
1736
- installRuntimePackages(pm, dryRun, tag);
1737
- installCompilerPackages(pm, dryRun, tag);
2697
+ installRuntimePackages(pm, dryRun, tag, projectRoot, flags);
2698
+ installCompilerPackages(pm, dryRun, tag, projectRoot, flags);
1738
2699
  } else {
1739
2700
  logInfo('Skipping package installation (--skip-install).');
1740
2701
  }
@@ -1788,11 +2749,117 @@ function runInit(flags) {
1788
2749
  }
1789
2750
  }
1790
2751
 
1791
- if (!skipVscode) {
1792
- installVsCodeExtension(flags);
2752
+ logOk('RS-X init completed.');
2753
+ }
2754
+
2755
+ function upsertRsxBuildConfigInPackageJson(projectRoot, dryRun) {
2756
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2757
+ if (!fs.existsSync(packageJsonPath)) {
2758
+ return false;
1793
2759
  }
1794
2760
 
1795
- logOk('RS-X init completed.');
2761
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2762
+ const currentRsx = packageJson.rsx ?? {};
2763
+ const currentBuild = currentRsx.build ?? {};
2764
+ const nextBuild = {
2765
+ preparse: true,
2766
+ preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
2767
+ compiled: true,
2768
+ compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
2769
+ registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
2770
+ compiledResolvedEvaluator: false,
2771
+ ...currentBuild,
2772
+ };
2773
+
2774
+ const nextPackageJson = {
2775
+ ...packageJson,
2776
+ rsx: {
2777
+ ...currentRsx,
2778
+ build: nextBuild,
2779
+ },
2780
+ };
2781
+
2782
+ if (dryRun) {
2783
+ logInfo(`[dry-run] patch ${packageJsonPath} (rsx.build)`);
2784
+ return true;
2785
+ }
2786
+
2787
+ fs.writeFileSync(
2788
+ packageJsonPath,
2789
+ `${JSON.stringify(nextPackageJson, null, 2)}\n`,
2790
+ 'utf8',
2791
+ );
2792
+ logOk(`Patched ${packageJsonPath} (rsx.build)`);
2793
+ return true;
2794
+ }
2795
+
2796
+ function ensureAngularProvidersInEntry(entryFile, dryRun) {
2797
+ if (!fs.existsSync(entryFile)) {
2798
+ return false;
2799
+ }
2800
+
2801
+ const original = fs.readFileSync(entryFile, 'utf8');
2802
+ if (original.includes('providexRsx')) {
2803
+ logInfo(`Angular entry already includes providexRsx: ${entryFile}`);
2804
+ return true;
2805
+ }
2806
+
2807
+ if (!original.includes('bootstrapApplication(')) {
2808
+ logWarn(
2809
+ `Could not automatically patch Angular providers in ${entryFile}. Expected bootstrapApplication(...).`,
2810
+ );
2811
+ logInfo(
2812
+ "Manual setup: import { providexRsx } from '@rs-x/angular' and add providers: [...providexRsx()] to bootstrapApplication(...).",
2813
+ );
2814
+ return false;
2815
+ }
2816
+
2817
+ const sourceWithImport = injectImport(
2818
+ original,
2819
+ "import { providexRsx } from '@rs-x/angular';",
2820
+ );
2821
+
2822
+ let updated = sourceWithImport;
2823
+ if (/bootstrapApplication\([\s\S]*?,\s*appConfig\s*\)/mu.test(updated)) {
2824
+ updated = updated.replace(
2825
+ /bootstrapApplication\(([\s\S]*?),\s*appConfig\s*\)/mu,
2826
+ 'bootstrapApplication($1, {\n ...appConfig,\n providers: [...(appConfig.providers ?? []), ...providexRsx()],\n})',
2827
+ );
2828
+ } else if (
2829
+ /bootstrapApplication\([\s\S]*?,\s*\{[\s\S]*?providers\s*:/mu.test(updated)
2830
+ ) {
2831
+ updated = updated.replace(
2832
+ /providers\s*:\s*\[/mu,
2833
+ 'providers: [...providexRsx(), ',
2834
+ );
2835
+ } else if (/bootstrapApplication\([\s\S]*?,\s*\{/mu.test(updated)) {
2836
+ updated = updated.replace(
2837
+ /bootstrapApplication\(([\s\S]*?),\s*\{/mu,
2838
+ 'bootstrapApplication($1, {\n providers: [...providexRsx()],',
2839
+ );
2840
+ } else {
2841
+ updated = updated.replace(
2842
+ /bootstrapApplication\(([\s\S]*?)\)\s*(?:\.catch\([\s\S]*?\))?\s*;/mu,
2843
+ 'bootstrapApplication($1, {\n providers: [...providexRsx()],\n}).catch((error) => {\n console.error(error);\n});',
2844
+ );
2845
+ }
2846
+
2847
+ if (updated === sourceWithImport) {
2848
+ logWarn(`Could not automatically inject providexRsx into ${entryFile}.`);
2849
+ logInfo(
2850
+ "Manual setup: import { providexRsx } from '@rs-x/angular' and add providers: [...providexRsx()] to bootstrapApplication(...).",
2851
+ );
2852
+ return false;
2853
+ }
2854
+
2855
+ if (dryRun) {
2856
+ logInfo(`[dry-run] patch ${entryFile} (providexRsx)`);
2857
+ return true;
2858
+ }
2859
+
2860
+ fs.writeFileSync(entryFile, updated, 'utf8');
2861
+ logOk(`Patched ${entryFile} to include providexRsx.`);
2862
+ return true;
1796
2863
  }
1797
2864
 
1798
2865
  function upsertScriptInPackageJson(
@@ -1906,109 +2973,6 @@ module.exports = function rsxWebpackLoader(source) {
1906
2973
  }
1907
2974
 
1908
2975
  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
2976
  const viteConfigCandidates = [
2013
2977
  'vite.config.ts',
2014
2978
  'vite.config.mts',
@@ -2018,61 +2982,47 @@ export function rsxVitePlugin(tsconfigPath = 'tsconfig.json') {
2018
2982
  const viteConfigPath = viteConfigCandidates.find((candidate) =>
2019
2983
  fs.existsSync(candidate),
2020
2984
  );
2985
+ const stalePluginFiles = [
2986
+ path.join(projectRoot, 'rsx-vite-plugin.ts'),
2987
+ path.join(projectRoot, 'rsx-vite-plugin.mjs'),
2988
+ path.join(projectRoot, 'rsx-vite-plugin.d.ts'),
2989
+ ];
2990
+
2021
2991
  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
- );
2992
+ for (const staleFile of stalePluginFiles) {
2993
+ removeFileOrDirectoryWithDryRun(staleFile, dryRun);
2994
+ }
2028
2995
  return;
2029
2996
  }
2030
2997
 
2031
2998
  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;
2999
+ const updated = original
3000
+ .replace(
3001
+ /import\s+\{\s*rsxVitePlugin\s*\}\s+from\s+['"]\.\/rsx-vite-plugin(?:\.mjs)?['"];\n?/gu,
3002
+ '',
3003
+ )
3004
+ .replace(/rsxVitePlugin\(\)\s*,\s*/gu, '')
3005
+ .replace(/,\s*rsxVitePlugin\(\)/gu, '')
3006
+ .replace(/\[\s*rsxVitePlugin\(\)\s*\]/gu, '[]');
3007
+
3008
+ if (updated !== original) {
3009
+ if (dryRun) {
3010
+ logInfo(
3011
+ `[dry-run] patch ${viteConfigPath} (remove legacy RS-X Vite plugin)`,
3012
+ );
3013
+ } else {
3014
+ fs.writeFileSync(viteConfigPath, updated, 'utf8');
3015
+ logOk(`Patched ${viteConfigPath} (removed legacy RS-X Vite plugin).`);
2048
3016
  }
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
3017
  } else {
2064
- logWarn(`Could not patch Vite config automatically: ${viteConfigPath}`);
2065
- logInfo('Add `rsxVitePlugin()` to your Vite plugins manually.');
2066
- return;
3018
+ logInfo(
3019
+ `Vite config already uses the default plugin list: ${viteConfigPath}`,
3020
+ );
2067
3021
  }
2068
3022
 
2069
- if (dryRun) {
2070
- logInfo(`[dry-run] patch ${viteConfigPath}`);
2071
- return;
3023
+ for (const staleFile of stalePluginFiles) {
3024
+ removeFileOrDirectoryWithDryRun(staleFile, dryRun);
2072
3025
  }
2073
-
2074
- fs.writeFileSync(viteConfigPath, updated, 'utf8');
2075
- logOk(`Patched ${viteConfigPath} with RS-X Vite plugin.`);
2076
3026
  }
2077
3027
 
2078
3028
  function wireRsxNextWebpack(projectRoot, dryRun) {
@@ -2154,91 +3104,6 @@ ${patchBlock}
2154
3104
  logOk(`Patched ${nextConfigJs} with RS-X webpack loader.`);
2155
3105
  }
2156
3106
 
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
3107
  function runSetupReact(flags) {
2243
3108
  const dryRun = Boolean(flags['dry-run']);
2244
3109
  const pm = detectPackageManager(flags.pm);
@@ -2266,19 +3131,48 @@ function runSetupReact(flags) {
2266
3131
  'skip-vscode': true,
2267
3132
  });
2268
3133
  if (!Boolean(flags['skip-install'])) {
2269
- installPackages(pm, ['@rs-x/react'], {
3134
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
3135
+ tag,
3136
+ includeReactPackage: true,
3137
+ });
3138
+ installResolvedPackages(pm, ['@rs-x/react'], {
2270
3139
  dev: false,
2271
3140
  dryRun,
2272
3141
  tag,
3142
+ specs,
3143
+ cwd: projectRoot,
2273
3144
  label: 'RS-X React bindings',
2274
3145
  });
3146
+ installResolvedPackages(pm, ['@rs-x/cli'], {
3147
+ dev: true,
3148
+ dryRun,
3149
+ tag,
3150
+ specs,
3151
+ cwd: projectRoot,
3152
+ label: 'RS-X CLI',
3153
+ });
2275
3154
  } else {
2276
3155
  logInfo('Skipping RS-X React bindings install (--skip-install).');
2277
3156
  }
3157
+ upsertScriptInPackageJson(
3158
+ projectRoot,
3159
+ 'build:rsx',
3160
+ 'rsx build --project tsconfig.json --no-emit --prod',
3161
+ dryRun,
3162
+ );
3163
+ upsertScriptInPackageJson(
3164
+ projectRoot,
3165
+ 'dev',
3166
+ 'npm run build:rsx && vite',
3167
+ dryRun,
3168
+ );
3169
+ upsertScriptInPackageJson(
3170
+ projectRoot,
3171
+ 'build',
3172
+ 'npm run build:rsx && vite build',
3173
+ dryRun,
3174
+ );
2278
3175
  wireRsxVitePlugin(projectRoot, dryRun);
2279
- if (!Boolean(flags['skip-vscode'])) {
2280
- installVsCodeExtension(flags);
2281
- }
2282
3176
  logOk('RS-X React setup completed.');
2283
3177
  }
2284
3178
 
@@ -2286,24 +3180,28 @@ function runSetupNext(flags) {
2286
3180
  const dryRun = Boolean(flags['dry-run']);
2287
3181
  const pm = detectPackageManager(flags.pm);
2288
3182
  const tag = resolveInstallTag(flags);
3183
+ const projectRoot = process.cwd();
2289
3184
  runInit({
2290
3185
  ...flags,
2291
3186
  'skip-vscode': true,
2292
3187
  });
2293
3188
  if (!Boolean(flags['skip-install'])) {
2294
- installPackages(pm, ['@rs-x/react'], {
3189
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
3190
+ tag,
3191
+ includeReactPackage: true,
3192
+ });
3193
+ installResolvedPackages(pm, ['@rs-x/react'], {
2295
3194
  dev: false,
2296
3195
  dryRun,
2297
3196
  tag,
3197
+ specs,
3198
+ cwd: projectRoot,
2298
3199
  label: 'RS-X React bindings',
2299
3200
  });
2300
3201
  } else {
2301
3202
  logInfo('Skipping RS-X React bindings install (--skip-install).');
2302
3203
  }
2303
- wireRsxNextWebpack(process.cwd(), dryRun);
2304
- if (!Boolean(flags['skip-vscode'])) {
2305
- installVsCodeExtension(flags);
2306
- }
3204
+ wireRsxNextWebpack(projectRoot, dryRun);
2307
3205
  logOk('RS-X Next.js setup completed.');
2308
3206
  }
2309
3207
 
@@ -2311,24 +3209,64 @@ function runSetupVue(flags) {
2311
3209
  const dryRun = Boolean(flags['dry-run']);
2312
3210
  const pm = detectPackageManager(flags.pm);
2313
3211
  const tag = resolveInstallTag(flags);
3212
+ const projectRoot = process.cwd();
2314
3213
  runInit({
2315
3214
  ...flags,
2316
3215
  'skip-vscode': true,
2317
3216
  });
2318
3217
  if (!Boolean(flags['skip-install'])) {
2319
- installPackages(pm, ['@rs-x/vue'], {
3218
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
3219
+ tag,
3220
+ includeVuePackage: true,
3221
+ });
3222
+ installResolvedPackages(pm, ['@rs-x/vue'], {
2320
3223
  dev: false,
2321
3224
  dryRun,
2322
3225
  tag,
3226
+ specs,
3227
+ cwd: projectRoot,
2323
3228
  label: 'RS-X Vue bindings',
2324
3229
  });
3230
+ installResolvedPackages(pm, ['@rs-x/cli'], {
3231
+ dev: true,
3232
+ dryRun,
3233
+ tag,
3234
+ specs,
3235
+ cwd: projectRoot,
3236
+ label: 'RS-X CLI',
3237
+ });
2325
3238
  } else {
2326
3239
  logInfo('Skipping RS-X Vue bindings install (--skip-install).');
2327
3240
  }
2328
- wireRsxVitePlugin(process.cwd(), dryRun);
2329
- if (!Boolean(flags['skip-vscode'])) {
2330
- installVsCodeExtension(flags);
2331
- }
3241
+ upsertScriptInPackageJson(
3242
+ projectRoot,
3243
+ 'build:rsx',
3244
+ 'rsx build --project tsconfig.app.json --no-emit --prod',
3245
+ dryRun,
3246
+ );
3247
+ upsertScriptInPackageJson(
3248
+ projectRoot,
3249
+ 'typecheck:rsx',
3250
+ 'rsx typecheck --project tsconfig.app.json',
3251
+ dryRun,
3252
+ );
3253
+ upsertScriptInPackageJson(
3254
+ projectRoot,
3255
+ 'dev',
3256
+ 'npm run build:rsx && vite',
3257
+ dryRun,
3258
+ );
3259
+ upsertScriptInPackageJson(
3260
+ projectRoot,
3261
+ 'build',
3262
+ 'npm run build:rsx && vue-tsc -b && vite build',
3263
+ dryRun,
3264
+ );
3265
+ const vueTsConfigPath = path.join(projectRoot, 'tsconfig.app.json');
3266
+ upsertTypescriptPluginInTsConfig(vueTsConfigPath, dryRun);
3267
+ ensureTsConfigIncludePattern(vueTsConfigPath, 'src/**/*.d.ts', dryRun);
3268
+ ensureVueEnvTypes(projectRoot, dryRun);
3269
+ wireRsxVitePlugin(projectRoot, dryRun);
2332
3270
  logOk('RS-X Vue setup completed.');
2333
3271
  }
2334
3272
 
@@ -2336,47 +3274,118 @@ function runSetupAngular(flags) {
2336
3274
  const dryRun = Boolean(flags['dry-run']);
2337
3275
  const pm = detectPackageManager(flags.pm);
2338
3276
  const tag = resolveInstallTag(flags);
2339
-
2340
- runInit({
2341
- ...flags,
2342
- 'skip-vscode': true,
2343
- });
3277
+ const projectRoot = process.cwd();
3278
+ const angularTsConfigPath = resolveAngularProjectTsConfig(projectRoot);
3279
+ const angularTsConfigRelative = path
3280
+ .relative(projectRoot, angularTsConfigPath)
3281
+ .replace(/\\/gu, '/');
2344
3282
 
2345
3283
  if (!Boolean(flags['skip-install'])) {
2346
- installPackages(pm, ['@rs-x/angular'], {
3284
+ installRuntimePackages(pm, dryRun, tag, projectRoot, flags);
3285
+ installCompilerPackages(pm, dryRun, tag, projectRoot, flags);
3286
+ const specs = resolveLocalRsxSpecs(projectRoot, flags, {
3287
+ tag,
3288
+ includeAngularPackage: true,
3289
+ });
3290
+ installResolvedPackages(pm, ['@rs-x/angular'], {
2347
3291
  dev: false,
2348
3292
  dryRun,
2349
3293
  tag,
3294
+ specs,
3295
+ cwd: projectRoot,
2350
3296
  label: 'RS-X Angular bindings',
2351
3297
  });
2352
- installPackages(pm, ['@angular-builders/custom-webpack'], {
3298
+ installResolvedPackages(pm, ['@rs-x/cli'], {
2353
3299
  dev: true,
2354
3300
  dryRun,
2355
- label: 'Angular custom webpack builder',
3301
+ tag,
3302
+ specs,
3303
+ cwd: projectRoot,
3304
+ label: 'RS-X CLI',
2356
3305
  });
2357
3306
  } else {
3307
+ logInfo('Skipping package installation (--skip-install).');
3308
+ }
3309
+
3310
+ const entryFile = resolveEntryFile(projectRoot, 'angular', flags.entry);
3311
+ if (entryFile) {
3312
+ logInfo(`Using Angular entry file: ${entryFile}`);
3313
+ ensureAngularProvidersInEntry(entryFile, dryRun);
3314
+ } else {
3315
+ logWarn('Could not detect an Angular entry file automatically.');
2358
3316
  logInfo(
2359
- 'Skipping Angular custom webpack builder install (--skip-install).',
3317
+ 'Manual setup: add providexRsx() to bootstrapApplication(...) in your main entry file.',
2360
3318
  );
2361
3319
  }
2362
3320
 
2363
- wireRsxAngularWebpack(process.cwd(), dryRun);
3321
+ upsertRsxBuildConfigInPackageJson(projectRoot, dryRun);
3322
+
2364
3323
  upsertScriptInPackageJson(
2365
- process.cwd(),
3324
+ projectRoot,
2366
3325
  'build:rsx',
2367
- 'rsx build --project tsconfig.json',
3326
+ `rsx build --project ${angularTsConfigRelative} --no-emit --prod`,
2368
3327
  dryRun,
2369
3328
  );
2370
3329
  upsertScriptInPackageJson(
2371
- process.cwd(),
3330
+ projectRoot,
2372
3331
  'typecheck:rsx',
2373
- 'rsx typecheck --project tsconfig.json',
3332
+ `rsx typecheck --project ${angularTsConfigRelative}`,
3333
+ dryRun,
3334
+ );
3335
+ upsertScriptInPackageJson(
3336
+ projectRoot,
3337
+ 'prebuild',
3338
+ 'npm run build:rsx',
3339
+ dryRun,
3340
+ );
3341
+ upsertScriptInPackageJson(
3342
+ projectRoot,
3343
+ 'start',
3344
+ 'npm run build:rsx && ng serve',
2374
3345
  dryRun,
2375
3346
  );
2376
3347
 
2377
- if (!Boolean(flags['skip-vscode'])) {
2378
- installVsCodeExtension(flags);
3348
+ const rsxRegistrationFile = path.join(
3349
+ projectRoot,
3350
+ 'src/rsx-generated/rsx-aot-registration.generated.ts',
3351
+ );
3352
+ const angularJsonPath = path.join(projectRoot, 'angular.json');
3353
+ if (fs.existsSync(angularJsonPath)) {
3354
+ const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
3355
+ const projects = angularJson.projects ?? {};
3356
+ for (const projectConfig of Object.values(projects)) {
3357
+ const buildOptions = projectConfig?.architect?.build?.options;
3358
+ if (buildOptions && typeof buildOptions === 'object') {
3359
+ buildOptions.preserveSymlinks = true;
3360
+ }
3361
+ if (
3362
+ projectConfig?.architect?.build?.configurations?.production?.budgets
3363
+ ) {
3364
+ delete projectConfig.architect.build.configurations.production.budgets;
3365
+ }
3366
+ }
3367
+ if (dryRun) {
3368
+ logInfo(
3369
+ `[dry-run] patch ${angularJsonPath} (preserveSymlinks, production budgets)`,
3370
+ );
3371
+ } else {
3372
+ fs.writeFileSync(
3373
+ angularJsonPath,
3374
+ `${JSON.stringify(angularJson, null, 2)}\n`,
3375
+ 'utf8',
3376
+ );
3377
+ logOk(
3378
+ `Patched ${angularJsonPath} (preserveSymlinks, production budgets).`,
3379
+ );
3380
+ }
2379
3381
  }
3382
+ ensureAngularPolyfillsContainsFile({
3383
+ projectRoot,
3384
+ configPath: angularTsConfigPath,
3385
+ filePath: rsxRegistrationFile,
3386
+ dryRun,
3387
+ });
3388
+
2380
3389
  logOk('RS-X Angular setup completed.');
2381
3390
  }
2382
3391
 
@@ -2411,9 +3420,20 @@ function runSetupAuto(flags) {
2411
3420
 
2412
3421
  logInfo('No framework-specific setup detected; running generic setup.');
2413
3422
  const pm = detectPackageManager(flags.pm);
2414
- installRuntimePackages(pm, Boolean(flags['dry-run']), tag);
2415
- installCompilerPackages(pm, Boolean(flags['dry-run']), tag);
2416
- installVsCodeExtension(flags);
3423
+ installRuntimePackages(
3424
+ pm,
3425
+ Boolean(flags['dry-run']),
3426
+ tag,
3427
+ projectRoot,
3428
+ flags,
3429
+ );
3430
+ installCompilerPackages(
3431
+ pm,
3432
+ Boolean(flags['dry-run']),
3433
+ tag,
3434
+ projectRoot,
3435
+ flags,
3436
+ );
2417
3437
  }
2418
3438
 
2419
3439
  function resolveProjectModule(projectRoot, moduleName) {
@@ -2978,9 +3998,14 @@ function ensureAngularPolyfillsContainsFile({
2978
3998
  });
2979
3999
 
2980
4000
  const selectedEntries = targetEntries.length > 0 ? targetEntries : entries;
2981
- const polyfillsPath = path
4001
+ const polyfillsRelativePath = path
2982
4002
  .relative(projectRoot, filePath)
2983
4003
  .replace(/\\/g, '/');
4004
+ const polyfillsPath =
4005
+ polyfillsRelativePath.startsWith('./') ||
4006
+ polyfillsRelativePath.startsWith('../')
4007
+ ? polyfillsRelativePath
4008
+ : `./${polyfillsRelativePath}`;
2984
4009
 
2985
4010
  let changed = false;
2986
4011
  const isRsxAotRegistrationEntry = (entry) =>
@@ -3256,12 +4281,17 @@ function printProjectHelp() {
3256
4281
  console.log('What it does:');
3257
4282
  console.log(' - Creates a new project folder');
3258
4283
  console.log(' - Supports templates: angular, vuejs, react, nextjs, nodejs');
4284
+ console.log(
4285
+ ' - Angular generates the RS-X virtual-table demo starter on top of the latest Angular scaffold',
4286
+ );
3259
4287
  console.log(' - Scaffolds framework app and wires RS-X bootstrap/setup');
3260
4288
  console.log(' - Writes package.json with RS-X dependencies');
3261
4289
  console.log(
3262
4290
  ' - Adds tsconfig + TypeScript plugin config for editor support',
3263
4291
  );
3264
- console.log(' - For Angular template: also installs @rs-x/angular');
4292
+ console.log(
4293
+ ' - For Angular template: uses the latest Angular CLI scaffold, then applies the RS-X demo starter',
4294
+ );
3265
4295
  console.log(' - For React/Next templates: also installs @rs-x/react');
3266
4296
  console.log(' - For Vue template: also installs @rs-x/vue');
3267
4297
  console.log(' - Installs dependencies (unless --skip-install)');
@@ -3338,6 +4368,7 @@ function printTypecheckHelp() {
3338
4368
  function printVersionHelp() {
3339
4369
  console.log('Usage:');
3340
4370
  console.log(' rsx version');
4371
+ console.log(' rsx v');
3341
4372
  console.log(' rsx -v');
3342
4373
  console.log(' rsx -version');
3343
4374
  console.log(' rsx --version');
@@ -3349,6 +4380,7 @@ function isHelpToken(value) {
3349
4380
 
3350
4381
  function isVersionToken(value) {
3351
4382
  return (
4383
+ value === 'v' ||
3352
4384
  value === '-v' ||
3353
4385
  value === '--version' ||
3354
4386
  value === '-version' ||
@@ -3409,6 +4441,7 @@ function printHelpFor(command, target) {
3409
4441
  }
3410
4442
 
3411
4443
  if (
4444
+ command === 'v' ||
3412
4445
  command === 'version' ||
3413
4446
  command === '-v' ||
3414
4447
  command === '--version' ||
@@ -3495,7 +4528,13 @@ function main() {
3495
4528
  if (command === 'install' && target === 'compiler') {
3496
4529
  const pm = detectPackageManager(flags.pm);
3497
4530
  const tag = resolveInstallTag(flags);
3498
- installCompilerPackages(pm, Boolean(flags['dry-run']), tag);
4531
+ installCompilerPackages(
4532
+ pm,
4533
+ Boolean(flags['dry-run']),
4534
+ tag,
4535
+ process.cwd(),
4536
+ flags,
4537
+ );
3499
4538
  return;
3500
4539
  }
3501
4540