@rs-x/cli 2.0.0-next.20 → 2.0.0-next.22
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.
package/bin/rsx.cjs
CHANGED
|
@@ -211,6 +211,22 @@ function detectPackageManager(explicitPm) {
|
|
|
211
211
|
return 'npm';
|
|
212
212
|
}
|
|
213
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
|
+
|
|
214
230
|
function applyTagToPackages(packages, tag) {
|
|
215
231
|
return packages.map((pkg) => {
|
|
216
232
|
const lastAt = pkg.lastIndexOf('@');
|
|
@@ -249,6 +265,41 @@ function resolveInstallTag(flags) {
|
|
|
249
265
|
return undefined;
|
|
250
266
|
}
|
|
251
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;
|
|
301
|
+
}
|
|
302
|
+
|
|
252
303
|
function installPackages(pm, packages, options = {}) {
|
|
253
304
|
const { dev = false, dryRun = false, label = 'packages', tag, cwd } = options;
|
|
254
305
|
const resolvedPackages = tag ? applyTagToPackages(packages, tag) : packages;
|
|
@@ -484,6 +535,13 @@ function findRepoRoot(startDir) {
|
|
|
484
535
|
function runDoctor() {
|
|
485
536
|
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
486
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
|
+
: [];
|
|
487
545
|
const checks = [
|
|
488
546
|
{
|
|
489
547
|
name: 'Node.js >= 20',
|
|
@@ -497,12 +555,25 @@ function runDoctor() {
|
|
|
497
555
|
},
|
|
498
556
|
{
|
|
499
557
|
name: 'Package manager (pnpm/npm/yarn/bun)',
|
|
500
|
-
ok:
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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',
|
|
506
577
|
},
|
|
507
578
|
];
|
|
508
579
|
|
|
@@ -510,6 +581,149 @@ function runDoctor() {
|
|
|
510
581
|
const tag = check.ok ? '[OK]' : '[WARN]';
|
|
511
582
|
console.log(`${tag} ${check.name} - ${check.details}`);
|
|
512
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 }));
|
|
513
727
|
}
|
|
514
728
|
|
|
515
729
|
function isValidTsIdentifier(input) {
|
|
@@ -587,25 +801,311 @@ function stripTsLikeExtension(fileName) {
|
|
|
587
801
|
return fileName.replace(/\.[cm]?[jt]sx?$/u, '');
|
|
588
802
|
}
|
|
589
803
|
|
|
590
|
-
function
|
|
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
|
+
|
|
591
848
|
return `export const model = {
|
|
592
|
-
|
|
849
|
+
${modelBody}
|
|
593
850
|
};
|
|
594
851
|
`;
|
|
595
852
|
}
|
|
596
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
|
+
|
|
597
886
|
function createExpressionTemplate(
|
|
598
887
|
expressionName,
|
|
888
|
+
expressionSource,
|
|
599
889
|
modelImportPath,
|
|
600
890
|
modelExportName,
|
|
601
891
|
) {
|
|
602
892
|
return `import { rsx } from '@rs-x/expression-parser';
|
|
603
893
|
import { ${modelExportName} } from '${modelImportPath}';
|
|
604
894
|
|
|
605
|
-
export const ${expressionName} = rsx(
|
|
895
|
+
export const ${expressionName} = rsx(${JSON.stringify(expressionSource)})(${modelExportName});
|
|
606
896
|
`;
|
|
607
897
|
}
|
|
608
898
|
|
|
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;
|
|
907
|
+
}
|
|
908
|
+
|
|
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;
|
|
929
|
+
}
|
|
930
|
+
|
|
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
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
visit(searchRoot);
|
|
945
|
+
return results.sort((left, right) => left.localeCompare(right));
|
|
946
|
+
}
|
|
947
|
+
|
|
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
|
+
);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
return preferredCandidates.length > 0 ? preferredCandidates : candidates;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function rankExpressionFiles(candidates, resolvedDirectory, projectRoot) {
|
|
987
|
+
const sourceRootPatterns = ['/src/', '/app/', '/expressions/'];
|
|
988
|
+
|
|
989
|
+
function score(filePath) {
|
|
990
|
+
const normalized = filePath.replace(/\\/gu, '/');
|
|
991
|
+
let value = 0;
|
|
992
|
+
|
|
993
|
+
if (normalized.startsWith(resolvedDirectory.replace(/\\/gu, '/'))) {
|
|
994
|
+
value += 1000;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (sourceRootPatterns.some((pattern) => normalized.includes(pattern))) {
|
|
998
|
+
value += 100;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const relativeSegments = path
|
|
1002
|
+
.relative(projectRoot, filePath)
|
|
1003
|
+
.replace(/\\/gu, '/')
|
|
1004
|
+
.split('/').length;
|
|
1005
|
+
value -= relativeSegments;
|
|
1006
|
+
return value;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
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
|
+
}
|
|
1017
|
+
|
|
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
|
+
|
|
609
1109
|
async function askForIdentifierWithDefault(rl, prompt, defaultValue) {
|
|
610
1110
|
while (true) {
|
|
611
1111
|
const answer = (await rl.question(prompt)).trim();
|
|
@@ -621,6 +1121,64 @@ async function askForIdentifierWithDefault(rl, prompt, defaultValue) {
|
|
|
621
1121
|
}
|
|
622
1122
|
}
|
|
623
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
|
+
|
|
624
1182
|
async function runAdd() {
|
|
625
1183
|
const rl = readline.createInterface({
|
|
626
1184
|
input: process.stdin,
|
|
@@ -628,18 +1186,21 @@ async function runAdd() {
|
|
|
628
1186
|
});
|
|
629
1187
|
|
|
630
1188
|
try {
|
|
1189
|
+
const projectRoot = process.cwd();
|
|
1190
|
+
const addConfig = resolveRsxCliAddConfig(projectRoot);
|
|
631
1191
|
const expressionName = await askUntilValidIdentifier(rl);
|
|
1192
|
+
const expressionSource = await askForExpressionSource(rl);
|
|
632
1193
|
|
|
633
1194
|
const kebabAnswer = await rl.question('Use kebab-case file name? [Y/n]: ');
|
|
634
1195
|
const useKebabCase = normalizeYesNo(kebabAnswer, true);
|
|
635
1196
|
|
|
636
|
-
const directoryInput = await
|
|
1197
|
+
const directoryInput = await askForDirectoryPath(
|
|
637
1198
|
rl,
|
|
638
|
-
|
|
1199
|
+
addConfig.defaultDirectory,
|
|
639
1200
|
);
|
|
640
1201
|
const resolvedDirectory = path.isAbsolute(directoryInput)
|
|
641
1202
|
? directoryInput
|
|
642
|
-
: path.resolve(
|
|
1203
|
+
: path.resolve(projectRoot, directoryInput);
|
|
643
1204
|
|
|
644
1205
|
const baseFileName = useKebabCase
|
|
645
1206
|
? toKebabCase(expressionName)
|
|
@@ -649,14 +1210,93 @@ async function runAdd() {
|
|
|
649
1210
|
const modelFileName = `${expressionFileBase}.model.ts`;
|
|
650
1211
|
const expressionPath = path.join(resolvedDirectory, expressionFileName);
|
|
651
1212
|
const modelPath = path.join(resolvedDirectory, modelFileName);
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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;
|
|
656
1296
|
|
|
657
1297
|
if (
|
|
658
1298
|
fs.existsSync(expressionPath) ||
|
|
659
|
-
(!useExistingModel && fs.existsSync(modelPath))
|
|
1299
|
+
(!keepModelInSameFile && !useExistingModel && fs.existsSync(modelPath))
|
|
660
1300
|
) {
|
|
661
1301
|
const overwriteAnswer = await rl.question(
|
|
662
1302
|
`One or more target files already exist. Overwrite? [y/N]: `,
|
|
@@ -672,6 +1312,16 @@ async function runAdd() {
|
|
|
672
1312
|
let modelImportPath = `./${expressionFileBase}.model`;
|
|
673
1313
|
let modelExportName = 'model';
|
|
674
1314
|
|
|
1315
|
+
if (keepModelInSameFile) {
|
|
1316
|
+
fs.writeFileSync(
|
|
1317
|
+
expressionPath,
|
|
1318
|
+
createInlineExpressionTemplate(expressionName, expressionSource),
|
|
1319
|
+
'utf8',
|
|
1320
|
+
);
|
|
1321
|
+
logOk(`Created ${expressionPath}`);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
675
1325
|
if (useExistingModel) {
|
|
676
1326
|
const existingModelPathInput = await askUntilNonEmpty(
|
|
677
1327
|
rl,
|
|
@@ -696,7 +1346,11 @@ async function runAdd() {
|
|
|
696
1346
|
'model',
|
|
697
1347
|
);
|
|
698
1348
|
} else {
|
|
699
|
-
fs.writeFileSync(
|
|
1349
|
+
fs.writeFileSync(
|
|
1350
|
+
modelPath,
|
|
1351
|
+
createModelTemplate(expressionSource),
|
|
1352
|
+
'utf8',
|
|
1353
|
+
);
|
|
700
1354
|
logOk(`Created ${modelPath}`);
|
|
701
1355
|
}
|
|
702
1356
|
|
|
@@ -704,6 +1358,7 @@ async function runAdd() {
|
|
|
704
1358
|
expressionPath,
|
|
705
1359
|
createExpressionTemplate(
|
|
706
1360
|
expressionName,
|
|
1361
|
+
expressionSource,
|
|
707
1362
|
modelImportPath,
|
|
708
1363
|
modelExportName,
|
|
709
1364
|
),
|
|
@@ -833,7 +1488,7 @@ function removeFileOrDirectoryWithDryRun(targetPath, dryRun) {
|
|
|
833
1488
|
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
834
1489
|
}
|
|
835
1490
|
|
|
836
|
-
function
|
|
1491
|
+
function resolveProjectTsConfig(projectRoot) {
|
|
837
1492
|
const appTsConfigPath = path.join(projectRoot, 'tsconfig.app.json');
|
|
838
1493
|
if (fs.existsSync(appTsConfigPath)) {
|
|
839
1494
|
return appTsConfigPath;
|
|
@@ -842,6 +1497,10 @@ function resolveAngularProjectTsConfig(projectRoot) {
|
|
|
842
1497
|
return path.join(projectRoot, 'tsconfig.json');
|
|
843
1498
|
}
|
|
844
1499
|
|
|
1500
|
+
function resolveAngularProjectTsConfig(projectRoot) {
|
|
1501
|
+
return resolveProjectTsConfig(projectRoot);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
845
1504
|
function upsertTypescriptPluginInTsConfig(configPath, dryRun) {
|
|
846
1505
|
if (!fs.existsSync(configPath)) {
|
|
847
1506
|
logWarn(`TypeScript config not found: ${configPath}`);
|
|
@@ -1333,8 +1992,9 @@ function applyVueRsxTemplate(projectRoot, dryRun) {
|
|
|
1333
1992
|
async function runProject(flags) {
|
|
1334
1993
|
const dryRun = Boolean(flags['dry-run']);
|
|
1335
1994
|
const skipInstall = Boolean(flags['skip-install']);
|
|
1336
|
-
const
|
|
1337
|
-
const
|
|
1995
|
+
const invocationRoot = process.cwd();
|
|
1996
|
+
const pm = resolveCliPackageManager(invocationRoot, flags.pm);
|
|
1997
|
+
const tag = resolveConfiguredInstallTag(invocationRoot, flags);
|
|
1338
1998
|
let projectName = typeof flags.name === 'string' ? flags.name.trim() : '';
|
|
1339
1999
|
|
|
1340
2000
|
if (!projectName) {
|
|
@@ -1613,6 +2273,7 @@ function scaffoldProjectTemplate(
|
|
|
1613
2273
|
|
|
1614
2274
|
function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
|
|
1615
2275
|
const dryRun = Boolean(flags['dry-run']);
|
|
2276
|
+
ensureRsxConfigFile(projectRoot, 'angular', dryRun);
|
|
1616
2277
|
const tag = resolveInstallTag(flags);
|
|
1617
2278
|
const tarballsDir =
|
|
1618
2279
|
typeof flags['tarballs-dir'] === 'string'
|
|
@@ -1687,16 +2348,6 @@ function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
|
|
|
1687
2348
|
start: 'npm run build:rsx && ng serve',
|
|
1688
2349
|
build: 'ng build',
|
|
1689
2350
|
};
|
|
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
2351
|
packageJson.dependencies = {
|
|
1701
2352
|
...(packageJson.dependencies ?? {}),
|
|
1702
2353
|
'@rs-x/angular': rsxSpecs['@rs-x/angular'],
|
|
@@ -1790,6 +2441,7 @@ function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
|
|
|
1790
2441
|
|
|
1791
2442
|
function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
|
|
1792
2443
|
const dryRun = Boolean(flags['dry-run']);
|
|
2444
|
+
ensureRsxConfigFile(projectRoot, 'react', dryRun);
|
|
1793
2445
|
const tag = resolveInstallTag(flags);
|
|
1794
2446
|
const tarballsDir =
|
|
1795
2447
|
typeof flags['tarballs-dir'] === 'string'
|
|
@@ -1870,15 +2522,6 @@ function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
|
|
|
1870
2522
|
build: 'npm run build:rsx && vite build',
|
|
1871
2523
|
preview: 'vite preview',
|
|
1872
2524
|
};
|
|
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
2525
|
packageJson.dependencies = {
|
|
1883
2526
|
react: packageJson.dependencies?.react ?? '^19.2.4',
|
|
1884
2527
|
'react-dom': packageJson.dependencies?.['react-dom'] ?? '^19.2.4',
|
|
@@ -1924,6 +2567,7 @@ function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
|
|
|
1924
2567
|
|
|
1925
2568
|
function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
|
|
1926
2569
|
const dryRun = Boolean(flags['dry-run']);
|
|
2570
|
+
ensureRsxConfigFile(projectRoot, 'vuejs', dryRun);
|
|
1927
2571
|
const tag = resolveInstallTag(flags);
|
|
1928
2572
|
const tarballsDir =
|
|
1929
2573
|
typeof flags['tarballs-dir'] === 'string'
|
|
@@ -1990,15 +2634,6 @@ function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
|
|
|
1990
2634
|
build: 'npm run build:rsx && vue-tsc -b && vite build',
|
|
1991
2635
|
preview: 'vite preview',
|
|
1992
2636
|
};
|
|
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
2637
|
packageJson.dependencies = {
|
|
2003
2638
|
vue: packageJson.dependencies?.vue ?? '^3.5.30',
|
|
2004
2639
|
'@rs-x/core': rsxSpecs['@rs-x/core'],
|
|
@@ -2038,6 +2673,7 @@ function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
|
|
|
2038
2673
|
|
|
2039
2674
|
function applyNextDemoStarter(projectRoot, projectName, pm, flags) {
|
|
2040
2675
|
const dryRun = Boolean(flags['dry-run']);
|
|
2676
|
+
ensureRsxConfigFile(projectRoot, 'nextjs', dryRun);
|
|
2041
2677
|
const tag = resolveInstallTag(flags);
|
|
2042
2678
|
const tarballsDir =
|
|
2043
2679
|
typeof flags['tarballs-dir'] === 'string'
|
|
@@ -2099,15 +2735,6 @@ function applyNextDemoStarter(projectRoot, projectName, pm, flags) {
|
|
|
2099
2735
|
build: 'npm run build:rsx && next build',
|
|
2100
2736
|
start: 'next start',
|
|
2101
2737
|
};
|
|
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
2738
|
packageJson.dependencies = {
|
|
2112
2739
|
...(packageJson.dependencies ?? {}),
|
|
2113
2740
|
'@rs-x/core': rsxSpecs['@rs-x/core'],
|
|
@@ -2158,7 +2785,8 @@ async function runProjectWithTemplate(template, flags) {
|
|
|
2158
2785
|
return;
|
|
2159
2786
|
}
|
|
2160
2787
|
|
|
2161
|
-
const
|
|
2788
|
+
const invocationRoot = process.cwd();
|
|
2789
|
+
const pm = resolveCliPackageManager(invocationRoot, flags.pm);
|
|
2162
2790
|
const projectName = await resolveProjectName(flags.name, flags._nameHint);
|
|
2163
2791
|
const projectRoot = resolveProjectRoot(projectName, flags);
|
|
2164
2792
|
if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
|
|
@@ -2197,6 +2825,12 @@ async function runProjectWithTemplate(template, flags) {
|
|
|
2197
2825
|
}
|
|
2198
2826
|
});
|
|
2199
2827
|
|
|
2828
|
+
verifyGeneratedProject(projectRoot, normalizedTemplate);
|
|
2829
|
+
if (resolveCliVerifyFlag(invocationRoot, flags, 'project')) {
|
|
2830
|
+
logInfo('Re-running starter verification (--verify)...');
|
|
2831
|
+
verifyGeneratedProject(projectRoot, normalizedTemplate);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2200
2834
|
logOk(`Created RS-X ${normalizedTemplate} project: ${projectRoot}`);
|
|
2201
2835
|
logInfo('Next steps:');
|
|
2202
2836
|
console.log(` cd ${projectName}`);
|
|
@@ -2210,6 +2844,188 @@ async function runProjectWithTemplate(template, flags) {
|
|
|
2210
2844
|
}
|
|
2211
2845
|
}
|
|
2212
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);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
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
|
+
}
|
|
3018
|
+
}
|
|
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);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
logOk(`Verified ${template} setup output.`);
|
|
3027
|
+
}
|
|
3028
|
+
|
|
2213
3029
|
function detectProjectContext(projectRoot) {
|
|
2214
3030
|
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
2215
3031
|
let dependencies = {};
|
|
@@ -2326,11 +3142,14 @@ function rsxBootstrapFilePath(entryFile) {
|
|
|
2326
3142
|
}
|
|
2327
3143
|
|
|
2328
3144
|
function ensureRsxBootstrapFile(bootstrapFile, dryRun) {
|
|
2329
|
-
const content = `import { InjectionContainer } from '@rs-x/core';\nimport { RsXExpressionParserModule } from '@rs-x/expression-parser';\n\n// Generated by rsx init
|
|
3145
|
+
const content = `import { InjectionContainer } from '@rs-x/core';\nimport { RsXExpressionParserModule } from '@rs-x/expression-parser';\n\n// Generated by rsx init — top-level await ensures DI is ready before any expression module evaluates\nawait InjectionContainer.load(RsXExpressionParserModule);\n`;
|
|
2330
3146
|
|
|
2331
3147
|
if (fs.existsSync(bootstrapFile)) {
|
|
2332
3148
|
const existing = fs.readFileSync(bootstrapFile, 'utf8');
|
|
2333
|
-
if (
|
|
3149
|
+
if (
|
|
3150
|
+
existing.includes('await InjectionContainer.load') ||
|
|
3151
|
+
existing.includes('export async function initRsx')
|
|
3152
|
+
) {
|
|
2334
3153
|
logInfo(`Bootstrap module already exists: ${bootstrapFile}`);
|
|
2335
3154
|
return;
|
|
2336
3155
|
}
|
|
@@ -2393,16 +3212,14 @@ function indentBlock(text, spaces) {
|
|
|
2393
3212
|
}
|
|
2394
3213
|
|
|
2395
3214
|
function wrapReactEntry(source) {
|
|
3215
|
+
// top-level await in rsx-bootstrap.ts ensures DI is loaded before any
|
|
3216
|
+
// expression module evaluates — no async wrapper needed here
|
|
2396
3217
|
const reactStartPattern =
|
|
2397
3218
|
/(ReactDOM\s*\.\s*)?createRoot\([\s\S]*?\)\s*\.\s*render\([\s\S]*?\);/mu;
|
|
2398
|
-
|
|
2399
|
-
if (!match) {
|
|
3219
|
+
if (!source.match(reactStartPattern)) {
|
|
2400
3220
|
return null;
|
|
2401
3221
|
}
|
|
2402
|
-
|
|
2403
|
-
const renderCall = match[0].trim();
|
|
2404
|
-
const replacement = `const __rsxStart = async () => {\n await initRsx();\n${indentBlock(renderCall, 2)}\n};\n\nvoid __rsxStart();`;
|
|
2405
|
-
return source.replace(reactStartPattern, replacement);
|
|
3222
|
+
return source;
|
|
2406
3223
|
}
|
|
2407
3224
|
|
|
2408
3225
|
function wrapAngularEntry(source) {
|
|
@@ -2427,16 +3244,14 @@ function wrapAngularEntry(source) {
|
|
|
2427
3244
|
}
|
|
2428
3245
|
|
|
2429
3246
|
function wrapVueEntry(source) {
|
|
3247
|
+
// top-level await in rsx-bootstrap.ts ensures DI is loaded before any
|
|
3248
|
+
// expression module evaluates — no async wrapper needed here
|
|
2430
3249
|
const vueStartPattern =
|
|
2431
3250
|
/createApp\([\s\S]*?\)\s*\.\s*mount\([\s\S]*?\)\s*;/mu;
|
|
2432
|
-
|
|
2433
|
-
if (!match) {
|
|
3251
|
+
if (!source.match(vueStartPattern)) {
|
|
2434
3252
|
return null;
|
|
2435
3253
|
}
|
|
2436
|
-
|
|
2437
|
-
const mountCall = match[0].trim();
|
|
2438
|
-
const replacement = `const __rsxBootstrap = async () => {\n await initRsx();\n${indentBlock(mountCall, 2)}\n};\n\nvoid __rsxBootstrap();`;
|
|
2439
|
-
return source.replace(vueStartPattern, replacement);
|
|
3254
|
+
return source;
|
|
2440
3255
|
}
|
|
2441
3256
|
|
|
2442
3257
|
function wrapGenericEntry(source) {
|
|
@@ -2632,13 +3447,19 @@ function patchNextEntryFile(entryFile, gateFile, dryRun) {
|
|
|
2632
3447
|
|
|
2633
3448
|
function patchEntryFileForRsx(entryFile, bootstrapFile, context, dryRun) {
|
|
2634
3449
|
const original = fs.readFileSync(entryFile, 'utf8');
|
|
2635
|
-
if (original.includes('
|
|
3450
|
+
if (original.includes('rsx-bootstrap')) {
|
|
2636
3451
|
logInfo(`Entry already wired for RS-X bootstrap: ${entryFile}`);
|
|
2637
3452
|
return true;
|
|
2638
3453
|
}
|
|
2639
3454
|
|
|
2640
3455
|
const importPath = toModuleImportPath(entryFile, bootstrapFile);
|
|
2641
|
-
|
|
3456
|
+
// For React/Vue: side-effect import only — top-level await in bootstrap
|
|
3457
|
+
// ensures DI is ready before any expression module evaluates.
|
|
3458
|
+
// For Angular/generic: named import used by the async wrapper.
|
|
3459
|
+
const importStatement =
|
|
3460
|
+
context === 'react' || context === 'vuejs'
|
|
3461
|
+
? `import '${importPath}';`
|
|
3462
|
+
: `import { initRsx } from '${importPath}';`;
|
|
2642
3463
|
|
|
2643
3464
|
let updated = injectImport(original, importStatement);
|
|
2644
3465
|
|
|
@@ -2687,11 +3508,10 @@ function patchEntryFileForRsx(entryFile, bootstrapFile, context, dryRun) {
|
|
|
2687
3508
|
|
|
2688
3509
|
function runInit(flags) {
|
|
2689
3510
|
const dryRun = Boolean(flags['dry-run']);
|
|
2690
|
-
const skipVscode = Boolean(flags['skip-vscode']);
|
|
2691
3511
|
const skipInstall = Boolean(flags['skip-install']);
|
|
2692
|
-
const pm = detectPackageManager(flags.pm);
|
|
2693
|
-
const tag = resolveInstallTag(flags);
|
|
2694
3512
|
const projectRoot = process.cwd();
|
|
3513
|
+
const pm = resolveCliPackageManager(projectRoot, flags.pm);
|
|
3514
|
+
const tag = resolveConfiguredInstallTag(projectRoot, flags);
|
|
2695
3515
|
|
|
2696
3516
|
if (!skipInstall) {
|
|
2697
3517
|
installRuntimePackages(pm, dryRun, tag, projectRoot, flags);
|
|
@@ -2744,53 +3564,13 @@ function runInit(flags) {
|
|
|
2744
3564
|
|
|
2745
3565
|
if (!patched) {
|
|
2746
3566
|
logInfo('Manual fallback snippet:');
|
|
2747
|
-
console.log(" import
|
|
2748
|
-
console.log(' await initRsx(); // before first rsx(...)');
|
|
3567
|
+
console.log(" import './rsx-bootstrap'; // must be the first import in your entry file");
|
|
2749
3568
|
}
|
|
2750
3569
|
}
|
|
2751
3570
|
|
|
2752
|
-
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
|
-
function upsertRsxBuildConfigInPackageJson(projectRoot, dryRun) {
|
|
2756
|
-
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
2757
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
2758
|
-
return false;
|
|
2759
|
-
}
|
|
2760
|
-
|
|
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
|
-
}
|
|
3571
|
+
ensureRsxConfigFile(projectRoot, effectiveContext, dryRun);
|
|
2786
3572
|
|
|
2787
|
-
|
|
2788
|
-
packageJsonPath,
|
|
2789
|
-
`${JSON.stringify(nextPackageJson, null, 2)}\n`,
|
|
2790
|
-
'utf8',
|
|
2791
|
-
);
|
|
2792
|
-
logOk(`Patched ${packageJsonPath} (rsx.build)`);
|
|
2793
|
-
return true;
|
|
3573
|
+
logOk('RS-X init completed.');
|
|
2794
3574
|
}
|
|
2795
3575
|
|
|
2796
3576
|
function ensureAngularProvidersInEntry(entryFile, dryRun) {
|
|
@@ -2799,10 +3579,6 @@ function ensureAngularProvidersInEntry(entryFile, dryRun) {
|
|
|
2799
3579
|
}
|
|
2800
3580
|
|
|
2801
3581
|
const original = fs.readFileSync(entryFile, 'utf8');
|
|
2802
|
-
if (original.includes('providexRsx')) {
|
|
2803
|
-
logInfo(`Angular entry already includes providexRsx: ${entryFile}`);
|
|
2804
|
-
return true;
|
|
2805
|
-
}
|
|
2806
3582
|
|
|
2807
3583
|
if (!original.includes('bootstrapApplication(')) {
|
|
2808
3584
|
logWarn(
|
|
@@ -2814,6 +3590,109 @@ function ensureAngularProvidersInEntry(entryFile, dryRun) {
|
|
|
2814
3590
|
return false;
|
|
2815
3591
|
}
|
|
2816
3592
|
|
|
3593
|
+
const bootstrapCallMatch = original.match(
|
|
3594
|
+
/bootstrapApplication\(\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*|\{[\s\S]*?\}))?\s*\)/mu,
|
|
3595
|
+
);
|
|
3596
|
+
const componentIdentifier = bootstrapCallMatch?.[1] ?? null;
|
|
3597
|
+
const configArgument = bootstrapCallMatch?.[2] ?? null;
|
|
3598
|
+
const configImportIdentifier =
|
|
3599
|
+
configArgument && /^[A-Za-z_$][\w$]*$/u.test(configArgument)
|
|
3600
|
+
? configArgument
|
|
3601
|
+
: original.includes('appConfig')
|
|
3602
|
+
? 'appConfig'
|
|
3603
|
+
: null;
|
|
3604
|
+
|
|
3605
|
+
const staticImportPattern = new RegExp(
|
|
3606
|
+
String.raw`^\s*import\s+\{\s*([^}]*)\b${componentIdentifier ?? ''}\b([^}]*)\}\s+from\s+['"]([^'"]+)['"];\s*$`,
|
|
3607
|
+
'mu',
|
|
3608
|
+
);
|
|
3609
|
+
const componentImportMatch = componentIdentifier
|
|
3610
|
+
? original.match(staticImportPattern)
|
|
3611
|
+
: null;
|
|
3612
|
+
const dynamicComponentImportMatch = componentIdentifier
|
|
3613
|
+
? original.match(
|
|
3614
|
+
new RegExp(
|
|
3615
|
+
String.raw`const\s+\{\s*${componentIdentifier}\s*\}\s*=\s*await\s+import\('([^']+)'\);`,
|
|
3616
|
+
'mu',
|
|
3617
|
+
),
|
|
3618
|
+
)
|
|
3619
|
+
: null;
|
|
3620
|
+
const componentImportPath =
|
|
3621
|
+
componentImportMatch?.[3] ?? dynamicComponentImportMatch?.[1] ?? null;
|
|
3622
|
+
|
|
3623
|
+
const configImportMatch = configImportIdentifier
|
|
3624
|
+
? original.match(
|
|
3625
|
+
new RegExp(
|
|
3626
|
+
String.raw`^\s*import\s+\{\s*([^}]*)\b${configImportIdentifier}\b([^}]*)\}\s+from\s+['"]([^'"]+)['"];\s*$`,
|
|
3627
|
+
'mu',
|
|
3628
|
+
),
|
|
3629
|
+
)
|
|
3630
|
+
: null;
|
|
3631
|
+
const configImportPath = configImportMatch?.[3] ?? null;
|
|
3632
|
+
|
|
3633
|
+
const canPreloadBeforeComponentImport =
|
|
3634
|
+
componentIdentifier !== null && componentImportPath !== null;
|
|
3635
|
+
|
|
3636
|
+
if (canPreloadBeforeComponentImport) {
|
|
3637
|
+
const importLines = original
|
|
3638
|
+
.split('\n')
|
|
3639
|
+
.filter((line) => /^\s*import\s+/u.test(line))
|
|
3640
|
+
.filter((line) => !line.match(staticImportPattern))
|
|
3641
|
+
.filter((line) =>
|
|
3642
|
+
configImportMatch
|
|
3643
|
+
? !line.match(
|
|
3644
|
+
new RegExp(
|
|
3645
|
+
String.raw`^\s*import\s+\{\s*([^}]*)\b${configImportIdentifier}\b([^}]*)\}\s+from\s+['"][^'"]+['"];\s*$`,
|
|
3646
|
+
'u',
|
|
3647
|
+
),
|
|
3648
|
+
)
|
|
3649
|
+
: true,
|
|
3650
|
+
)
|
|
3651
|
+
.filter(
|
|
3652
|
+
(line) =>
|
|
3653
|
+
!line.includes("import { providexRsx } from '@rs-x/angular';"),
|
|
3654
|
+
)
|
|
3655
|
+
.filter(
|
|
3656
|
+
(line) =>
|
|
3657
|
+
!line.includes("import { InjectionContainer } from '@rs-x/core';"),
|
|
3658
|
+
)
|
|
3659
|
+
.filter(
|
|
3660
|
+
(line) =>
|
|
3661
|
+
!line.includes(
|
|
3662
|
+
"import { RsXExpressionParserModule } from '@rs-x/expression-parser';",
|
|
3663
|
+
),
|
|
3664
|
+
);
|
|
3665
|
+
|
|
3666
|
+
const bootstrapConfigExpression =
|
|
3667
|
+
configImportPath && configImportIdentifier
|
|
3668
|
+
? `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 });`
|
|
3669
|
+
: `const { ${componentIdentifier} } = await import('${componentImportPath}');\n\n await bootstrapApplication(${componentIdentifier}, {\n providers: [...providexRsx()],\n });`;
|
|
3670
|
+
|
|
3671
|
+
const rewritten = `${importLines.join('\n')}
|
|
3672
|
+
import { providexRsx } from '@rs-x/angular';
|
|
3673
|
+
import { InjectionContainer } from '@rs-x/core';
|
|
3674
|
+
import { RsXExpressionParserModule } from '@rs-x/expression-parser';
|
|
3675
|
+
|
|
3676
|
+
const bootstrap = async (): Promise<void> => {
|
|
3677
|
+
await InjectionContainer.load(RsXExpressionParserModule);
|
|
3678
|
+
${bootstrapConfigExpression}
|
|
3679
|
+
};
|
|
3680
|
+
|
|
3681
|
+
void bootstrap().catch((error) => {
|
|
3682
|
+
console.error(error);
|
|
3683
|
+
});
|
|
3684
|
+
`;
|
|
3685
|
+
|
|
3686
|
+
if (dryRun) {
|
|
3687
|
+
logInfo(`[dry-run] patch ${entryFile} (providexRsx + preload)`);
|
|
3688
|
+
return true;
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
fs.writeFileSync(entryFile, rewritten, 'utf8');
|
|
3692
|
+
logOk(`Patched ${entryFile} to preload RS-X and include providexRsx.`);
|
|
3693
|
+
return true;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
2817
3696
|
const sourceWithImport = injectImport(
|
|
2818
3697
|
original,
|
|
2819
3698
|
"import { providexRsx } from '@rs-x/angular';",
|
|
@@ -3106,9 +3985,13 @@ ${patchBlock}
|
|
|
3106
3985
|
|
|
3107
3986
|
function runSetupReact(flags) {
|
|
3108
3987
|
const dryRun = Boolean(flags['dry-run']);
|
|
3109
|
-
const pm = detectPackageManager(flags.pm);
|
|
3110
|
-
const tag = resolveInstallTag(flags);
|
|
3111
3988
|
const projectRoot = process.cwd();
|
|
3989
|
+
const pm = resolveCliPackageManager(projectRoot, flags.pm);
|
|
3990
|
+
const tag = resolveConfiguredInstallTag(projectRoot, flags);
|
|
3991
|
+
const reactTsConfigPath = resolveProjectTsConfig(projectRoot);
|
|
3992
|
+
const reactTsConfigRelative = path
|
|
3993
|
+
.relative(projectRoot, reactTsConfigPath)
|
|
3994
|
+
.replace(/\\/gu, '/');
|
|
3112
3995
|
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
3113
3996
|
if (!fs.existsSync(packageJsonPath)) {
|
|
3114
3997
|
logError(`package.json not found in ${projectRoot}`);
|
|
@@ -3154,10 +4037,17 @@ function runSetupReact(flags) {
|
|
|
3154
4037
|
} else {
|
|
3155
4038
|
logInfo('Skipping RS-X React bindings install (--skip-install).');
|
|
3156
4039
|
}
|
|
4040
|
+
ensureRsxConfigFile(projectRoot, 'react', dryRun);
|
|
3157
4041
|
upsertScriptInPackageJson(
|
|
3158
4042
|
projectRoot,
|
|
3159
4043
|
'build:rsx',
|
|
3160
|
-
|
|
4044
|
+
`rsx build --project ${reactTsConfigRelative} --no-emit --prod`,
|
|
4045
|
+
dryRun,
|
|
4046
|
+
);
|
|
4047
|
+
upsertScriptInPackageJson(
|
|
4048
|
+
projectRoot,
|
|
4049
|
+
'typecheck:rsx',
|
|
4050
|
+
`rsx typecheck --project ${reactTsConfigRelative}`,
|
|
3161
4051
|
dryRun,
|
|
3162
4052
|
);
|
|
3163
4053
|
upsertScriptInPackageJson(
|
|
@@ -3173,14 +4063,21 @@ function runSetupReact(flags) {
|
|
|
3173
4063
|
dryRun,
|
|
3174
4064
|
);
|
|
3175
4065
|
wireRsxVitePlugin(projectRoot, dryRun);
|
|
4066
|
+
if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
|
|
4067
|
+
verifySetupOutput(projectRoot, 'react');
|
|
4068
|
+
}
|
|
3176
4069
|
logOk('RS-X React setup completed.');
|
|
3177
4070
|
}
|
|
3178
4071
|
|
|
3179
4072
|
function runSetupNext(flags) {
|
|
3180
4073
|
const dryRun = Boolean(flags['dry-run']);
|
|
3181
|
-
const pm = detectPackageManager(flags.pm);
|
|
3182
|
-
const tag = resolveInstallTag(flags);
|
|
3183
4074
|
const projectRoot = process.cwd();
|
|
4075
|
+
const pm = resolveCliPackageManager(projectRoot, flags.pm);
|
|
4076
|
+
const tag = resolveConfiguredInstallTag(projectRoot, flags);
|
|
4077
|
+
const nextTsConfigPath = resolveProjectTsConfig(projectRoot);
|
|
4078
|
+
const nextTsConfigRelative = path
|
|
4079
|
+
.relative(projectRoot, nextTsConfigPath)
|
|
4080
|
+
.replace(/\\/gu, '/');
|
|
3184
4081
|
runInit({
|
|
3185
4082
|
...flags,
|
|
3186
4083
|
'skip-vscode': true,
|
|
@@ -3201,15 +4098,43 @@ function runSetupNext(flags) {
|
|
|
3201
4098
|
} else {
|
|
3202
4099
|
logInfo('Skipping RS-X React bindings install (--skip-install).');
|
|
3203
4100
|
}
|
|
4101
|
+
ensureRsxConfigFile(projectRoot, 'next', dryRun);
|
|
4102
|
+
upsertScriptInPackageJson(
|
|
4103
|
+
projectRoot,
|
|
4104
|
+
'build:rsx',
|
|
4105
|
+
`rsx build --project ${nextTsConfigRelative} --no-emit --prod`,
|
|
4106
|
+
dryRun,
|
|
4107
|
+
);
|
|
4108
|
+
upsertScriptInPackageJson(
|
|
4109
|
+
projectRoot,
|
|
4110
|
+
'typecheck:rsx',
|
|
4111
|
+
`rsx typecheck --project ${nextTsConfigRelative}`,
|
|
4112
|
+
dryRun,
|
|
4113
|
+
);
|
|
4114
|
+
upsertScriptInPackageJson(
|
|
4115
|
+
projectRoot,
|
|
4116
|
+
'dev',
|
|
4117
|
+
'npm run build:rsx && next dev',
|
|
4118
|
+
dryRun,
|
|
4119
|
+
);
|
|
4120
|
+
upsertScriptInPackageJson(
|
|
4121
|
+
projectRoot,
|
|
4122
|
+
'build',
|
|
4123
|
+
'npm run build:rsx && next build',
|
|
4124
|
+
dryRun,
|
|
4125
|
+
);
|
|
3204
4126
|
wireRsxNextWebpack(projectRoot, dryRun);
|
|
4127
|
+
if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
|
|
4128
|
+
verifySetupOutput(projectRoot, 'next');
|
|
4129
|
+
}
|
|
3205
4130
|
logOk('RS-X Next.js setup completed.');
|
|
3206
4131
|
}
|
|
3207
4132
|
|
|
3208
4133
|
function runSetupVue(flags) {
|
|
3209
4134
|
const dryRun = Boolean(flags['dry-run']);
|
|
3210
|
-
const pm = detectPackageManager(flags.pm);
|
|
3211
|
-
const tag = resolveInstallTag(flags);
|
|
3212
4135
|
const projectRoot = process.cwd();
|
|
4136
|
+
const pm = resolveCliPackageManager(projectRoot, flags.pm);
|
|
4137
|
+
const tag = resolveConfiguredInstallTag(projectRoot, flags);
|
|
3213
4138
|
runInit({
|
|
3214
4139
|
...flags,
|
|
3215
4140
|
'skip-vscode': true,
|
|
@@ -3238,6 +4163,7 @@ function runSetupVue(flags) {
|
|
|
3238
4163
|
} else {
|
|
3239
4164
|
logInfo('Skipping RS-X Vue bindings install (--skip-install).');
|
|
3240
4165
|
}
|
|
4166
|
+
ensureRsxConfigFile(projectRoot, 'vuejs', dryRun);
|
|
3241
4167
|
upsertScriptInPackageJson(
|
|
3242
4168
|
projectRoot,
|
|
3243
4169
|
'build:rsx',
|
|
@@ -3267,14 +4193,17 @@ function runSetupVue(flags) {
|
|
|
3267
4193
|
ensureTsConfigIncludePattern(vueTsConfigPath, 'src/**/*.d.ts', dryRun);
|
|
3268
4194
|
ensureVueEnvTypes(projectRoot, dryRun);
|
|
3269
4195
|
wireRsxVitePlugin(projectRoot, dryRun);
|
|
4196
|
+
if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
|
|
4197
|
+
verifySetupOutput(projectRoot, 'vuejs');
|
|
4198
|
+
}
|
|
3270
4199
|
logOk('RS-X Vue setup completed.');
|
|
3271
4200
|
}
|
|
3272
4201
|
|
|
3273
4202
|
function runSetupAngular(flags) {
|
|
3274
4203
|
const dryRun = Boolean(flags['dry-run']);
|
|
3275
|
-
const pm = detectPackageManager(flags.pm);
|
|
3276
|
-
const tag = resolveInstallTag(flags);
|
|
3277
4204
|
const projectRoot = process.cwd();
|
|
4205
|
+
const pm = resolveCliPackageManager(projectRoot, flags.pm);
|
|
4206
|
+
const tag = resolveConfiguredInstallTag(projectRoot, flags);
|
|
3278
4207
|
const angularTsConfigPath = resolveAngularProjectTsConfig(projectRoot);
|
|
3279
4208
|
const angularTsConfigRelative = path
|
|
3280
4209
|
.relative(projectRoot, angularTsConfigPath)
|
|
@@ -3318,7 +4247,7 @@ function runSetupAngular(flags) {
|
|
|
3318
4247
|
);
|
|
3319
4248
|
}
|
|
3320
4249
|
|
|
3321
|
-
|
|
4250
|
+
ensureRsxConfigFile(projectRoot, 'angular', dryRun);
|
|
3322
4251
|
|
|
3323
4252
|
upsertScriptInPackageJson(
|
|
3324
4253
|
projectRoot,
|
|
@@ -3386,13 +4315,17 @@ function runSetupAngular(flags) {
|
|
|
3386
4315
|
dryRun,
|
|
3387
4316
|
});
|
|
3388
4317
|
|
|
4318
|
+
if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
|
|
4319
|
+
verifySetupOutput(projectRoot, 'angular');
|
|
4320
|
+
}
|
|
4321
|
+
|
|
3389
4322
|
logOk('RS-X Angular setup completed.');
|
|
3390
4323
|
}
|
|
3391
4324
|
|
|
3392
4325
|
function runSetupAuto(flags) {
|
|
3393
4326
|
const projectRoot = process.cwd();
|
|
3394
4327
|
const context = detectProjectContext(projectRoot);
|
|
3395
|
-
const tag =
|
|
4328
|
+
const tag = resolveConfiguredInstallTag(projectRoot, flags);
|
|
3396
4329
|
|
|
3397
4330
|
if (context === 'react') {
|
|
3398
4331
|
logInfo('Auto-detected framework: react');
|
|
@@ -3419,7 +4352,7 @@ function runSetupAuto(flags) {
|
|
|
3419
4352
|
}
|
|
3420
4353
|
|
|
3421
4354
|
logInfo('No framework-specific setup detected; running generic setup.');
|
|
3422
|
-
const pm =
|
|
4355
|
+
const pm = resolveCliPackageManager(projectRoot, flags.pm);
|
|
3423
4356
|
installRuntimePackages(
|
|
3424
4357
|
pm,
|
|
3425
4358
|
Boolean(flags['dry-run']),
|
|
@@ -3450,8 +4383,13 @@ function runBuild(flags) {
|
|
|
3450
4383
|
const dryRun = Boolean(flags['dry-run']);
|
|
3451
4384
|
const noEmit = Boolean(flags['no-emit']);
|
|
3452
4385
|
const prodMode = parseBooleanFlag(flags.prod, false);
|
|
4386
|
+
const invocationConfig = resolveRsxBuildConfig(invocationRoot);
|
|
3453
4387
|
const projectArg =
|
|
3454
|
-
typeof flags.project === 'string'
|
|
4388
|
+
typeof flags.project === 'string'
|
|
4389
|
+
? flags.project
|
|
4390
|
+
: typeof invocationConfig.tsconfig === 'string'
|
|
4391
|
+
? invocationConfig.tsconfig
|
|
4392
|
+
: 'tsconfig.json';
|
|
3455
4393
|
const configPath = path.resolve(invocationRoot, projectArg);
|
|
3456
4394
|
const projectRoot = path.dirname(configPath);
|
|
3457
4395
|
const context = detectProjectContext(projectRoot);
|
|
@@ -3550,7 +4488,9 @@ function runBuild(flags) {
|
|
|
3550
4488
|
const outDirOverride =
|
|
3551
4489
|
typeof flags['out-dir'] === 'string'
|
|
3552
4490
|
? path.resolve(projectRoot, flags['out-dir'])
|
|
3553
|
-
:
|
|
4491
|
+
: typeof rsxBuildConfig.outDir === 'string'
|
|
4492
|
+
? path.resolve(projectRoot, rsxBuildConfig.outDir)
|
|
4493
|
+
: null;
|
|
3554
4494
|
const outDir =
|
|
3555
4495
|
outDirOverride ??
|
|
3556
4496
|
parsedConfig.options.outDir ??
|
|
@@ -4072,20 +5012,286 @@ function ensureAngularPolyfillsContainsFile({
|
|
|
4072
5012
|
logOk(`Updated angular.json to inject RS-X AOT runtime registration.`);
|
|
4073
5013
|
}
|
|
4074
5014
|
|
|
4075
|
-
function
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
return {};
|
|
5015
|
+
function readJsonFileIfPresent(filePath) {
|
|
5016
|
+
if (!fs.existsSync(filePath)) {
|
|
5017
|
+
return null;
|
|
4079
5018
|
}
|
|
4080
5019
|
|
|
4081
5020
|
try {
|
|
4082
|
-
|
|
4083
|
-
const rsxConfig = packageJson.rsx ?? {};
|
|
4084
|
-
const buildConfig = rsxConfig.build ?? {};
|
|
4085
|
-
return typeof buildConfig === 'object' && buildConfig ? buildConfig : {};
|
|
5021
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
4086
5022
|
} catch {
|
|
4087
|
-
return
|
|
5023
|
+
return null;
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
function validateRsxConfigShape(config, filePath) {
|
|
5028
|
+
if (typeof config !== 'object' || !config || Array.isArray(config)) {
|
|
5029
|
+
logError(
|
|
5030
|
+
`Invalid RS-X config in ${filePath}: expected a JSON object at the top level.`,
|
|
5031
|
+
);
|
|
5032
|
+
process.exit(1);
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
const build = config.build;
|
|
5036
|
+
if (build !== undefined) {
|
|
5037
|
+
if (typeof build !== 'object' || !build || Array.isArray(build)) {
|
|
5038
|
+
logError(
|
|
5039
|
+
`Invalid RS-X config in ${filePath}: "build" must be an object.`,
|
|
5040
|
+
);
|
|
5041
|
+
process.exit(1);
|
|
5042
|
+
}
|
|
5043
|
+
|
|
5044
|
+
const stringKeys = ['preparseFile', 'compiledFile', 'registrationFile'];
|
|
5045
|
+
const extraStringKeys = ['tsconfig', 'outDir'];
|
|
5046
|
+
for (const key of extraStringKeys) {
|
|
5047
|
+
if (build[key] !== undefined && typeof build[key] !== 'string') {
|
|
5048
|
+
logError(
|
|
5049
|
+
`Invalid RS-X config in ${filePath}: "build.${key}" must be a string.`,
|
|
5050
|
+
);
|
|
5051
|
+
process.exit(1);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
for (const key of stringKeys) {
|
|
5055
|
+
if (build[key] !== undefined && typeof build[key] !== 'string') {
|
|
5056
|
+
logError(
|
|
5057
|
+
`Invalid RS-X config in ${filePath}: "build.${key}" must be a string.`,
|
|
5058
|
+
);
|
|
5059
|
+
process.exit(1);
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
const booleanKeys = ['preparse', 'compiled', 'compiledResolvedEvaluator'];
|
|
5064
|
+
for (const key of booleanKeys) {
|
|
5065
|
+
if (build[key] !== undefined && typeof build[key] !== 'boolean') {
|
|
5066
|
+
logError(
|
|
5067
|
+
`Invalid RS-X config in ${filePath}: "build.${key}" must be a boolean.`,
|
|
5068
|
+
);
|
|
5069
|
+
process.exit(1);
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
const cli = config.cli;
|
|
5075
|
+
if (cli !== undefined) {
|
|
5076
|
+
if (typeof cli !== 'object' || !cli || Array.isArray(cli)) {
|
|
5077
|
+
logError(`Invalid RS-X config in ${filePath}: "cli" must be an object.`);
|
|
5078
|
+
process.exit(1);
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
const cliStringKeys = ['packageManager', 'installTag'];
|
|
5082
|
+
for (const key of cliStringKeys) {
|
|
5083
|
+
if (cli[key] !== undefined && typeof cli[key] !== 'string') {
|
|
5084
|
+
logError(
|
|
5085
|
+
`Invalid RS-X config in ${filePath}: "cli.${key}" must be a string.`,
|
|
5086
|
+
);
|
|
5087
|
+
process.exit(1);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
if (
|
|
5092
|
+
cli.packageManager !== undefined &&
|
|
5093
|
+
!['pnpm', 'npm', 'yarn', 'bun'].includes(cli.packageManager)
|
|
5094
|
+
) {
|
|
5095
|
+
logError(
|
|
5096
|
+
`Invalid RS-X config in ${filePath}: "cli.packageManager" must be one of pnpm, npm, yarn, or bun.`,
|
|
5097
|
+
);
|
|
5098
|
+
process.exit(1);
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
if (
|
|
5102
|
+
cli.installTag !== undefined &&
|
|
5103
|
+
!['latest', 'next'].includes(cli.installTag)
|
|
5104
|
+
) {
|
|
5105
|
+
logError(
|
|
5106
|
+
`Invalid RS-X config in ${filePath}: "cli.installTag" must be "latest" or "next".`,
|
|
5107
|
+
);
|
|
5108
|
+
process.exit(1);
|
|
5109
|
+
}
|
|
5110
|
+
|
|
5111
|
+
const cliBooleanSections = ['setup', 'project'];
|
|
5112
|
+
for (const key of cliBooleanSections) {
|
|
5113
|
+
if (cli[key] !== undefined) {
|
|
5114
|
+
if (
|
|
5115
|
+
typeof cli[key] !== 'object' ||
|
|
5116
|
+
!cli[key] ||
|
|
5117
|
+
Array.isArray(cli[key])
|
|
5118
|
+
) {
|
|
5119
|
+
logError(
|
|
5120
|
+
`Invalid RS-X config in ${filePath}: "cli.${key}" must be an object.`,
|
|
5121
|
+
);
|
|
5122
|
+
process.exit(1);
|
|
5123
|
+
}
|
|
5124
|
+
if (
|
|
5125
|
+
cli[key].verify !== undefined &&
|
|
5126
|
+
typeof cli[key].verify !== 'boolean'
|
|
5127
|
+
) {
|
|
5128
|
+
logError(
|
|
5129
|
+
`Invalid RS-X config in ${filePath}: "cli.${key}.verify" must be a boolean.`,
|
|
5130
|
+
);
|
|
5131
|
+
process.exit(1);
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
const add = cli.add;
|
|
5137
|
+
if (add !== undefined) {
|
|
5138
|
+
if (typeof add !== 'object' || !add || Array.isArray(add)) {
|
|
5139
|
+
logError(
|
|
5140
|
+
`Invalid RS-X config in ${filePath}: "cli.add" must be an object.`,
|
|
5141
|
+
);
|
|
5142
|
+
process.exit(1);
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
if (
|
|
5146
|
+
add.defaultDirectory !== undefined &&
|
|
5147
|
+
typeof add.defaultDirectory !== 'string'
|
|
5148
|
+
) {
|
|
5149
|
+
logError(
|
|
5150
|
+
`Invalid RS-X config in ${filePath}: "cli.add.defaultDirectory" must be a string.`,
|
|
5151
|
+
);
|
|
5152
|
+
process.exit(1);
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
if (add.searchRoots !== undefined) {
|
|
5156
|
+
if (
|
|
5157
|
+
!Array.isArray(add.searchRoots) ||
|
|
5158
|
+
add.searchRoots.some((entry) => typeof entry !== 'string')
|
|
5159
|
+
) {
|
|
5160
|
+
logError(
|
|
5161
|
+
`Invalid RS-X config in ${filePath}: "cli.add.searchRoots" must be an array of strings.`,
|
|
5162
|
+
);
|
|
5163
|
+
process.exit(1);
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
|
|
5170
|
+
function mergeRsxConfig(baseConfig, overrideConfig) {
|
|
5171
|
+
const base = typeof baseConfig === 'object' && baseConfig ? baseConfig : {};
|
|
5172
|
+
const override =
|
|
5173
|
+
typeof overrideConfig === 'object' && overrideConfig ? overrideConfig : {};
|
|
5174
|
+
|
|
5175
|
+
return {
|
|
5176
|
+
...base,
|
|
5177
|
+
...override,
|
|
5178
|
+
build: {
|
|
5179
|
+
...(typeof base.build === 'object' && base.build ? base.build : {}),
|
|
5180
|
+
...(typeof override.build === 'object' && override.build
|
|
5181
|
+
? override.build
|
|
5182
|
+
: {}),
|
|
5183
|
+
},
|
|
5184
|
+
cli: {
|
|
5185
|
+
...(typeof base.cli === 'object' && base.cli ? base.cli : {}),
|
|
5186
|
+
...(typeof override.cli === 'object' && override.cli ? override.cli : {}),
|
|
5187
|
+
add: {
|
|
5188
|
+
...(typeof base.cli?.add === 'object' && base.cli?.add
|
|
5189
|
+
? base.cli.add
|
|
5190
|
+
: {}),
|
|
5191
|
+
...(typeof override.cli?.add === 'object' && override.cli?.add
|
|
5192
|
+
? override.cli.add
|
|
5193
|
+
: {}),
|
|
5194
|
+
},
|
|
5195
|
+
},
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
function resolveRsxProjectConfig(projectRoot) {
|
|
5200
|
+
const fileConfigPath = path.join(projectRoot, 'rsx.config.json');
|
|
5201
|
+
const fileRsxConfig = readJsonFileIfPresent(fileConfigPath) ?? {};
|
|
5202
|
+
validateRsxConfigShape(fileRsxConfig, fileConfigPath);
|
|
5203
|
+
return mergeRsxConfig({}, fileRsxConfig);
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
function defaultCliAddConfigForTemplate(template) {
|
|
5207
|
+
if (template === 'next' || template === 'nextjs') {
|
|
5208
|
+
return {
|
|
5209
|
+
defaultDirectory: 'app/expressions',
|
|
5210
|
+
searchRoots: ['app', 'src', 'expressions'],
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
|
|
5214
|
+
return {
|
|
5215
|
+
defaultDirectory: 'src/expressions',
|
|
5216
|
+
searchRoots: ['src', 'app', 'expressions'],
|
|
5217
|
+
};
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
function defaultCliConfigForTemplate(template) {
|
|
5221
|
+
return {
|
|
5222
|
+
packageManager: 'npm',
|
|
5223
|
+
installTag: 'next',
|
|
5224
|
+
setup: {
|
|
5225
|
+
verify: false,
|
|
5226
|
+
},
|
|
5227
|
+
project: {
|
|
5228
|
+
verify: false,
|
|
5229
|
+
},
|
|
5230
|
+
add: defaultCliAddConfigForTemplate(template),
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
function defaultRsxBuildConfigForTemplate(template) {
|
|
5235
|
+
if (template === 'next' || template === 'nextjs') {
|
|
5236
|
+
return {
|
|
5237
|
+
preparse: true,
|
|
5238
|
+
preparseFile: 'app/rsx-generated/rsx-aot-preparsed.generated.ts',
|
|
5239
|
+
compiled: true,
|
|
5240
|
+
compiledFile: 'app/rsx-generated/rsx-aot-compiled.generated.ts',
|
|
5241
|
+
registrationFile: 'app/rsx-generated/rsx-aot-registration.generated.ts',
|
|
5242
|
+
compiledResolvedEvaluator: false,
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
return {
|
|
5247
|
+
preparse: true,
|
|
5248
|
+
preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
|
|
5249
|
+
compiled: true,
|
|
5250
|
+
compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
|
|
5251
|
+
registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
|
|
5252
|
+
compiledResolvedEvaluator: false,
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
function ensureRsxConfigFile(projectRoot, template, dryRun) {
|
|
5257
|
+
const configPath = path.join(projectRoot, 'rsx.config.json');
|
|
5258
|
+
const defaultConfig = {
|
|
5259
|
+
build: defaultRsxBuildConfigForTemplate(template),
|
|
5260
|
+
cli: defaultCliConfigForTemplate(template),
|
|
5261
|
+
};
|
|
5262
|
+
|
|
5263
|
+
const existingConfig = readJsonFileIfPresent(configPath);
|
|
5264
|
+
const nextConfig = mergeRsxConfig(defaultConfig, existingConfig ?? {});
|
|
5265
|
+
|
|
5266
|
+
if (
|
|
5267
|
+
existingConfig &&
|
|
5268
|
+
JSON.stringify(existingConfig) === JSON.stringify(nextConfig)
|
|
5269
|
+
) {
|
|
5270
|
+
return false;
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
if (dryRun) {
|
|
5274
|
+
logInfo(`[dry-run] ${existingConfig ? 'patch' : 'create'} ${configPath}`);
|
|
5275
|
+
return true;
|
|
4088
5276
|
}
|
|
5277
|
+
|
|
5278
|
+
fs.writeFileSync(
|
|
5279
|
+
configPath,
|
|
5280
|
+
`${JSON.stringify(nextConfig, null, 2)}\n`,
|
|
5281
|
+
'utf8',
|
|
5282
|
+
);
|
|
5283
|
+
logOk(`${existingConfig ? 'Patched' : 'Created'} ${configPath}`);
|
|
5284
|
+
return true;
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
function resolveRsxBuildConfig(projectRoot) {
|
|
5288
|
+
const buildConfig = resolveRsxProjectConfig(projectRoot).build ?? {};
|
|
5289
|
+
return typeof buildConfig === 'object' && buildConfig ? buildConfig : {};
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
function resolveRsxCliConfig(projectRoot) {
|
|
5293
|
+
const cliConfig = resolveRsxProjectConfig(projectRoot).cli ?? {};
|
|
5294
|
+
return typeof cliConfig === 'object' && cliConfig ? cliConfig : {};
|
|
4089
5295
|
}
|
|
4090
5296
|
|
|
4091
5297
|
function parseBooleanFlag(value, defaultValue) {
|
|
@@ -4146,7 +5352,7 @@ function printGeneralHelp() {
|
|
|
4146
5352
|
console.log(
|
|
4147
5353
|
' typecheck Type-check project + RS-X semantic checks',
|
|
4148
5354
|
);
|
|
4149
|
-
console.log(' version | -v
|
|
5355
|
+
console.log(' version | v | -v Print CLI version');
|
|
4150
5356
|
console.log('');
|
|
4151
5357
|
console.log('Help Aliases:');
|
|
4152
5358
|
console.log(' rsx -h');
|
|
@@ -4180,14 +5386,28 @@ function printAddHelp() {
|
|
|
4180
5386
|
console.log(
|
|
4181
5387
|
' - Prompts for expression export name (must be valid TS identifier)',
|
|
4182
5388
|
);
|
|
5389
|
+
console.log(" - Prompts for the initial expression string (default: 'a')");
|
|
5390
|
+
console.log(
|
|
5391
|
+
' - Seeds the generated model with top-level identifiers found in that expression when possible',
|
|
5392
|
+
);
|
|
4183
5393
|
console.log(
|
|
4184
5394
|
' - Prompts whether file name should be kebab-case (default: yes)',
|
|
4185
5395
|
);
|
|
4186
5396
|
console.log(' - Prompts for output directory (relative or absolute)');
|
|
4187
|
-
console.log(' - Prompts whether to reuse an existing model file');
|
|
4188
|
-
console.log(' - Creates <name>.ts and optionally creates <name>.model.ts');
|
|
4189
5397
|
console.log(
|
|
4190
|
-
' -
|
|
5398
|
+
' - Prompts for add mode: new one-file, new separate-model, or update existing',
|
|
5399
|
+
);
|
|
5400
|
+
console.log(
|
|
5401
|
+
' - Defaults to keeping the model in the same file as the expression',
|
|
5402
|
+
);
|
|
5403
|
+
console.log(
|
|
5404
|
+
' - If you choose an existing file, shows a list of files that already contain RS-X expressions',
|
|
5405
|
+
);
|
|
5406
|
+
console.log(
|
|
5407
|
+
' - Can still create or reuse a separate model file when you opt out of same-file mode',
|
|
5408
|
+
);
|
|
5409
|
+
console.log(
|
|
5410
|
+
' - Respects rsx.config.json (`cli.add`) for add defaults and file discovery',
|
|
4191
5411
|
);
|
|
4192
5412
|
}
|
|
4193
5413
|
|
|
@@ -4226,7 +5446,7 @@ function printInstallHelp(target) {
|
|
|
4226
5446
|
function printSetupHelp() {
|
|
4227
5447
|
console.log('Usage:');
|
|
4228
5448
|
console.log(
|
|
4229
|
-
' rsx setup [--pm <pnpm|npm|yarn|bun>] [--next] [--force] [--local] [--dry-run]',
|
|
5449
|
+
' rsx setup [--pm <pnpm|npm|yarn|bun>] [--next] [--verify] [--force] [--local] [--dry-run]',
|
|
4230
5450
|
);
|
|
4231
5451
|
console.log('');
|
|
4232
5452
|
console.log('What it does:');
|
|
@@ -4235,12 +5455,17 @@ function printSetupHelp() {
|
|
|
4235
5455
|
);
|
|
4236
5456
|
console.log(' - Installs runtime packages');
|
|
4237
5457
|
console.log(' - Installs compiler tooling packages');
|
|
4238
|
-
console.log(' -
|
|
5458
|
+
console.log(' - Writes rsx.build config plus build/typecheck scripts');
|
|
5459
|
+
console.log(' - Creates rsx.config.json with CLI defaults you can override');
|
|
4239
5460
|
console.log(' - Applies framework-specific transform/build integration');
|
|
5461
|
+
console.log(' - Does not install the VS Code extension automatically');
|
|
4240
5462
|
console.log('');
|
|
4241
5463
|
console.log('Options:');
|
|
4242
5464
|
console.log(' --pm Explicit package manager');
|
|
4243
5465
|
console.log(' --next Install prerelease versions (dist-tag next)');
|
|
5466
|
+
console.log(
|
|
5467
|
+
' --verify Validate the resulting setup output before returning',
|
|
5468
|
+
);
|
|
4244
5469
|
console.log(' --force Reinstall extension if already installed');
|
|
4245
5470
|
console.log(' --local Build/install local VSIX from repo workspace');
|
|
4246
5471
|
console.log(' --dry-run Print commands without executing them');
|
|
@@ -4259,14 +5484,17 @@ function printInitHelp() {
|
|
|
4259
5484
|
console.log(
|
|
4260
5485
|
' - Detects project context and wires RS-X bootstrap in entry file',
|
|
4261
5486
|
);
|
|
4262
|
-
console.log(' -
|
|
5487
|
+
console.log(' - Creates rsx.config.json with CLI defaults you can override');
|
|
5488
|
+
console.log(' - Does not install the VS Code extension automatically');
|
|
4263
5489
|
console.log('');
|
|
4264
5490
|
console.log('Options:');
|
|
4265
5491
|
console.log(' --pm Explicit package manager');
|
|
4266
5492
|
console.log(' --entry Explicit application entry file');
|
|
4267
5493
|
console.log(' --next Install prerelease versions (dist-tag next)');
|
|
4268
5494
|
console.log(' --skip-install Skip npm/pnpm/yarn/bun package installation');
|
|
4269
|
-
console.log(
|
|
5495
|
+
console.log(
|
|
5496
|
+
' --skip-vscode Accepted for compatibility; VS Code is not auto-installed',
|
|
5497
|
+
);
|
|
4270
5498
|
console.log(' --force Reinstall extension if already installed');
|
|
4271
5499
|
console.log(' --local Build/install local VSIX from repo workspace');
|
|
4272
5500
|
console.log(' --dry-run Print commands without executing them');
|
|
@@ -4275,7 +5503,7 @@ function printInitHelp() {
|
|
|
4275
5503
|
function printProjectHelp() {
|
|
4276
5504
|
console.log('Usage:');
|
|
4277
5505
|
console.log(
|
|
4278
|
-
' 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]',
|
|
5506
|
+
' 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]',
|
|
4279
5507
|
);
|
|
4280
5508
|
console.log('');
|
|
4281
5509
|
console.log('What it does:');
|
|
@@ -4286,6 +5514,9 @@ function printProjectHelp() {
|
|
|
4286
5514
|
);
|
|
4287
5515
|
console.log(' - Scaffolds framework app and wires RS-X bootstrap/setup');
|
|
4288
5516
|
console.log(' - Writes package.json with RS-X dependencies');
|
|
5517
|
+
console.log(
|
|
5518
|
+
' - Creates rsx.config.json with starter CLI defaults you can override',
|
|
5519
|
+
);
|
|
4289
5520
|
console.log(
|
|
4290
5521
|
' - Adds tsconfig + TypeScript plugin config for editor support',
|
|
4291
5522
|
);
|
|
@@ -4295,6 +5526,7 @@ function printProjectHelp() {
|
|
|
4295
5526
|
console.log(' - For React/Next templates: also installs @rs-x/react');
|
|
4296
5527
|
console.log(' - For Vue template: also installs @rs-x/vue');
|
|
4297
5528
|
console.log(' - Installs dependencies (unless --skip-install)');
|
|
5529
|
+
console.log(' - Verifies the generated starter before reporting success');
|
|
4298
5530
|
console.log('');
|
|
4299
5531
|
console.log('Options:');
|
|
4300
5532
|
console.log(' --name Project folder/package name');
|
|
@@ -4308,7 +5540,12 @@ function printProjectHelp() {
|
|
|
4308
5540
|
);
|
|
4309
5541
|
console.log(' (or set RSX_TARBALLS_DIR env var)');
|
|
4310
5542
|
console.log(' --skip-install Skip dependency installation');
|
|
4311
|
-
console.log(
|
|
5543
|
+
console.log(
|
|
5544
|
+
' --skip-vscode Accepted for compatibility; VS Code is not auto-installed',
|
|
5545
|
+
);
|
|
5546
|
+
console.log(
|
|
5547
|
+
' --verify Re-run starter structure checks explicitly after generation',
|
|
5548
|
+
);
|
|
4312
5549
|
console.log(' --dry-run Print actions without writing files');
|
|
4313
5550
|
}
|
|
4314
5551
|
|
|
@@ -4526,13 +5763,14 @@ function main() {
|
|
|
4526
5763
|
}
|
|
4527
5764
|
|
|
4528
5765
|
if (command === 'install' && target === 'compiler') {
|
|
4529
|
-
const
|
|
4530
|
-
const
|
|
5766
|
+
const projectRoot = process.cwd();
|
|
5767
|
+
const pm = resolveCliPackageManager(projectRoot, flags.pm);
|
|
5768
|
+
const tag = resolveConfiguredInstallTag(projectRoot, flags);
|
|
4531
5769
|
installCompilerPackages(
|
|
4532
5770
|
pm,
|
|
4533
5771
|
Boolean(flags['dry-run']),
|
|
4534
5772
|
tag,
|
|
4535
|
-
|
|
5773
|
+
projectRoot,
|
|
4536
5774
|
flags,
|
|
4537
5775
|
);
|
|
4538
5776
|
return;
|