@rs-x/cli 2.0.0-next.5 → 2.0.0-next.7
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/README.md +5 -0
- package/bin/rsx.cjs +354 -18
- package/package.json +2 -1
- package/{rs-x-vscode-extension-2.0.0-next.5.vsix → rs-x-vscode-extension-2.0.0-next.7.vsix} +0 -0
- package/templates/angular-demo/README.md +115 -0
- package/templates/angular-demo/src/app/app.component.css +97 -0
- package/templates/angular-demo/src/app/app.component.html +58 -0
- package/templates/angular-demo/src/app/app.component.ts +52 -0
- package/templates/angular-demo/src/app/virtual-table/row-data.ts +35 -0
- package/templates/angular-demo/src/app/virtual-table/row-model.ts +45 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table-data.service.ts +136 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table-model.ts +224 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table.component.css +174 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table.component.html +50 -0
- package/templates/angular-demo/src/app/virtual-table/virtual-table.component.ts +83 -0
- package/templates/angular-demo/src/index.html +11 -0
- package/templates/angular-demo/src/main.ts +16 -0
- package/templates/angular-demo/src/styles.css +261 -0
package/README.md
CHANGED
|
@@ -18,6 +18,11 @@ RSX_SKIP_VSCODE_EXTENSION_INSTALL=true
|
|
|
18
18
|
## What gets installed
|
|
19
19
|
|
|
20
20
|
Installing `@rs-x/cli` gives you the `rsx` command.
|
|
21
|
+
For prerelease builds, install globally to make the `rsx` binary available:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @rs-x/cli@next
|
|
25
|
+
```
|
|
21
26
|
|
|
22
27
|
Running `rsx init` installs:
|
|
23
28
|
|
package/bin/rsx.cjs
CHANGED
|
@@ -7,6 +7,12 @@ const { spawnSync } = require('node:child_process');
|
|
|
7
7
|
|
|
8
8
|
const CLI_VERSION = '0.2.0';
|
|
9
9
|
const VS_CODE_EXTENSION_ID = 'rs-x.rs-x-vscode-extension';
|
|
10
|
+
const ANGULAR_DEMO_TEMPLATE_DIR = path.join(
|
|
11
|
+
__dirname,
|
|
12
|
+
'..',
|
|
13
|
+
'templates',
|
|
14
|
+
'angular-demo',
|
|
15
|
+
);
|
|
10
16
|
const RUNTIME_PACKAGES = [
|
|
11
17
|
'@rs-x/core',
|
|
12
18
|
'@rs-x/state-manager',
|
|
@@ -585,6 +591,42 @@ function writeFileWithDryRun(filePath, content, dryRun) {
|
|
|
585
591
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
586
592
|
}
|
|
587
593
|
|
|
594
|
+
function copyPathWithDryRun(sourcePath, targetPath, dryRun) {
|
|
595
|
+
if (dryRun) {
|
|
596
|
+
logInfo(`[dry-run] copy ${sourcePath} -> ${targetPath}`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const stat = fs.statSync(sourcePath);
|
|
601
|
+
if (stat.isDirectory()) {
|
|
602
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
603
|
+
for (const entry of fs.readdirSync(sourcePath, { withFileTypes: true })) {
|
|
604
|
+
copyPathWithDryRun(
|
|
605
|
+
path.join(sourcePath, entry.name),
|
|
606
|
+
path.join(targetPath, entry.name),
|
|
607
|
+
false,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
614
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function removeFileOrDirectoryWithDryRun(targetPath, dryRun) {
|
|
618
|
+
if (!fs.existsSync(targetPath)) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (dryRun) {
|
|
623
|
+
logInfo(`[dry-run] remove ${targetPath}`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
628
|
+
}
|
|
629
|
+
|
|
588
630
|
function toFileDependencySpec(fromDir, targetPath) {
|
|
589
631
|
const relative = path.relative(fromDir, targetPath).replace(/\\/gu, '/');
|
|
590
632
|
const normalized = relative.startsWith('.') ? relative : `./${relative}`;
|
|
@@ -642,7 +684,7 @@ function resolveProjectRsxSpecs(
|
|
|
642
684
|
'@rs-x/compiler': versionSpec,
|
|
643
685
|
'@rs-x/typescript-plugin': versionSpec,
|
|
644
686
|
...(includeAngularPackage ? { '@rs-x/angular': versionSpec } : {}),
|
|
645
|
-
'@rs-x/cli':
|
|
687
|
+
'@rs-x/cli': versionSpec,
|
|
646
688
|
};
|
|
647
689
|
|
|
648
690
|
const tarballSlugs = {
|
|
@@ -1182,6 +1224,173 @@ function scaffoldProjectTemplate(template, projectName, pm, flags) {
|
|
|
1182
1224
|
process.exit(1);
|
|
1183
1225
|
}
|
|
1184
1226
|
|
|
1227
|
+
function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
|
|
1228
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
1229
|
+
const tag = resolveInstallTag(flags);
|
|
1230
|
+
const tarballsDir =
|
|
1231
|
+
typeof flags['tarballs-dir'] === 'string'
|
|
1232
|
+
? path.resolve(process.cwd(), flags['tarballs-dir'])
|
|
1233
|
+
: typeof process.env.RSX_TARBALLS_DIR === 'string' &&
|
|
1234
|
+
process.env.RSX_TARBALLS_DIR.trim().length > 0
|
|
1235
|
+
? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
|
|
1236
|
+
: null;
|
|
1237
|
+
const workspaceRoot = findRepoRoot(projectRoot);
|
|
1238
|
+
const rsxSpecs = resolveProjectRsxSpecs(
|
|
1239
|
+
projectRoot,
|
|
1240
|
+
workspaceRoot,
|
|
1241
|
+
tarballsDir,
|
|
1242
|
+
{ tag, includeAngularPackage: true },
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
const templateFiles = ['README.md', 'src'];
|
|
1246
|
+
for (const entry of templateFiles) {
|
|
1247
|
+
copyPathWithDryRun(
|
|
1248
|
+
path.join(ANGULAR_DEMO_TEMPLATE_DIR, entry),
|
|
1249
|
+
path.join(projectRoot, entry),
|
|
1250
|
+
dryRun,
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const staleAngularFiles = [
|
|
1255
|
+
path.join(projectRoot, 'src/app/app.ts'),
|
|
1256
|
+
path.join(projectRoot, 'src/app/app.spec.ts'),
|
|
1257
|
+
path.join(projectRoot, 'src/app/app.html'),
|
|
1258
|
+
path.join(projectRoot, 'src/app/app.css'),
|
|
1259
|
+
path.join(projectRoot, 'src/app/app.routes.ts'),
|
|
1260
|
+
path.join(projectRoot, 'src/app/app.config.ts'),
|
|
1261
|
+
];
|
|
1262
|
+
for (const stalePath of staleAngularFiles) {
|
|
1263
|
+
removeFileOrDirectoryWithDryRun(stalePath, dryRun);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const readmePath = path.join(projectRoot, 'README.md');
|
|
1267
|
+
if (fs.existsSync(readmePath)) {
|
|
1268
|
+
const readmeSource = fs.readFileSync(readmePath, 'utf8');
|
|
1269
|
+
const nextReadme = readmeSource.replace(
|
|
1270
|
+
/^#\s+rsx-angular-example/mu,
|
|
1271
|
+
`# ${projectName}`,
|
|
1272
|
+
);
|
|
1273
|
+
if (dryRun) {
|
|
1274
|
+
logInfo(`[dry-run] patch ${readmePath}`);
|
|
1275
|
+
} else {
|
|
1276
|
+
fs.writeFileSync(readmePath, nextReadme, 'utf8');
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
1281
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1282
|
+
logError(
|
|
1283
|
+
`package.json not found in generated Angular app: ${packageJsonPath}`,
|
|
1284
|
+
);
|
|
1285
|
+
process.exit(1);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
1289
|
+
packageJson.name = projectName;
|
|
1290
|
+
packageJson.private = true;
|
|
1291
|
+
packageJson.version = '0.1.0';
|
|
1292
|
+
packageJson.scripts = {
|
|
1293
|
+
prebuild: 'rsx build --project tsconfig.json --no-emit --prod',
|
|
1294
|
+
start: 'npm run build && ng serve',
|
|
1295
|
+
build: 'ng build',
|
|
1296
|
+
};
|
|
1297
|
+
packageJson.rsx = {
|
|
1298
|
+
build: {
|
|
1299
|
+
preparse: true,
|
|
1300
|
+
preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
|
|
1301
|
+
compiled: true,
|
|
1302
|
+
compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
|
|
1303
|
+
registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
|
|
1304
|
+
compiledResolvedEvaluator: false,
|
|
1305
|
+
},
|
|
1306
|
+
};
|
|
1307
|
+
packageJson.dependencies = {
|
|
1308
|
+
...(packageJson.dependencies ?? {}),
|
|
1309
|
+
'@rs-x/angular': rsxSpecs['@rs-x/angular'],
|
|
1310
|
+
'@rs-x/core': rsxSpecs['@rs-x/core'],
|
|
1311
|
+
'@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
|
|
1312
|
+
'@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
|
|
1313
|
+
};
|
|
1314
|
+
packageJson.devDependencies = {
|
|
1315
|
+
...(packageJson.devDependencies ?? {}),
|
|
1316
|
+
'@rs-x/cli': rsxSpecs['@rs-x/cli'],
|
|
1317
|
+
'@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
|
|
1318
|
+
'@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
if (dryRun) {
|
|
1322
|
+
logInfo(`[dry-run] patch ${packageJsonPath}`);
|
|
1323
|
+
} else {
|
|
1324
|
+
fs.writeFileSync(
|
|
1325
|
+
packageJsonPath,
|
|
1326
|
+
`${JSON.stringify(packageJson, null, 2)}\n`,
|
|
1327
|
+
'utf8',
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const angularJsonPath = path.join(projectRoot, 'angular.json');
|
|
1332
|
+
if (!fs.existsSync(angularJsonPath)) {
|
|
1333
|
+
logError(
|
|
1334
|
+
`angular.json not found in generated Angular app: ${angularJsonPath}`,
|
|
1335
|
+
);
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
|
|
1340
|
+
const projects = angularJson.projects ?? {};
|
|
1341
|
+
const [angularProjectName] = Object.keys(projects);
|
|
1342
|
+
if (!angularProjectName) {
|
|
1343
|
+
logError('Generated angular.json does not define any projects.');
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const angularProject = projects[angularProjectName];
|
|
1348
|
+
const architect = angularProject.architect ?? angularProject.targets;
|
|
1349
|
+
const build = architect?.build;
|
|
1350
|
+
if (!build) {
|
|
1351
|
+
logError('Generated Angular project is missing a build target.');
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const buildOptions = build.options ?? {};
|
|
1356
|
+
const styles = Array.isArray(buildOptions.styles) ? buildOptions.styles : [];
|
|
1357
|
+
if (!styles.includes('src/styles.css')) {
|
|
1358
|
+
styles.push('src/styles.css');
|
|
1359
|
+
}
|
|
1360
|
+
buildOptions.styles = styles;
|
|
1361
|
+
buildOptions.preserveSymlinks = true;
|
|
1362
|
+
|
|
1363
|
+
const registrationFile =
|
|
1364
|
+
'src/rsx-generated/rsx-aot-registration.generated.ts';
|
|
1365
|
+
let polyfills = buildOptions.polyfills;
|
|
1366
|
+
if (typeof polyfills === 'string') {
|
|
1367
|
+
polyfills = [polyfills];
|
|
1368
|
+
} else if (!Array.isArray(polyfills)) {
|
|
1369
|
+
polyfills = [];
|
|
1370
|
+
}
|
|
1371
|
+
if (!polyfills.includes(registrationFile)) {
|
|
1372
|
+
polyfills.push(registrationFile);
|
|
1373
|
+
}
|
|
1374
|
+
buildOptions.polyfills = polyfills;
|
|
1375
|
+
build.options = buildOptions;
|
|
1376
|
+
|
|
1377
|
+
if (dryRun) {
|
|
1378
|
+
logInfo(`[dry-run] patch ${angularJsonPath}`);
|
|
1379
|
+
} else {
|
|
1380
|
+
fs.writeFileSync(
|
|
1381
|
+
angularJsonPath,
|
|
1382
|
+
`${JSON.stringify(angularJson, null, 2)}\n`,
|
|
1383
|
+
'utf8',
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (!Boolean(flags['skip-install'])) {
|
|
1388
|
+
logInfo(`Refreshing ${pm} dependencies for the RS-X Angular starter...`);
|
|
1389
|
+
run(pm, ['install'], { dryRun });
|
|
1390
|
+
logOk('Angular starter dependencies are up to date.');
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1185
1394
|
async function runProjectWithTemplate(template, flags) {
|
|
1186
1395
|
const normalizedTemplate = normalizeProjectTemplate(template);
|
|
1187
1396
|
if (!normalizedTemplate) {
|
|
@@ -1213,7 +1422,10 @@ async function runProjectWithTemplate(template, flags) {
|
|
|
1213
1422
|
|
|
1214
1423
|
withWorkingDirectory(projectRoot, () => {
|
|
1215
1424
|
if (normalizedTemplate === 'angular') {
|
|
1216
|
-
|
|
1425
|
+
applyAngularDemoStarter(projectRoot, projectName, pm, flags);
|
|
1426
|
+
if (!Boolean(flags['skip-vscode'])) {
|
|
1427
|
+
installVsCodeExtension(flags);
|
|
1428
|
+
}
|
|
1217
1429
|
return;
|
|
1218
1430
|
}
|
|
1219
1431
|
if (normalizedTemplate === 'react') {
|
|
@@ -1795,6 +2007,111 @@ function runInit(flags) {
|
|
|
1795
2007
|
logOk('RS-X init completed.');
|
|
1796
2008
|
}
|
|
1797
2009
|
|
|
2010
|
+
function upsertRsxBuildConfigInPackageJson(projectRoot, dryRun) {
|
|
2011
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
2012
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
2017
|
+
const currentRsx = packageJson.rsx ?? {};
|
|
2018
|
+
const currentBuild = currentRsx.build ?? {};
|
|
2019
|
+
const nextBuild = {
|
|
2020
|
+
preparse: true,
|
|
2021
|
+
preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
|
|
2022
|
+
compiled: true,
|
|
2023
|
+
compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
|
|
2024
|
+
registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
|
|
2025
|
+
compiledResolvedEvaluator: false,
|
|
2026
|
+
...currentBuild,
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
const nextPackageJson = {
|
|
2030
|
+
...packageJson,
|
|
2031
|
+
rsx: {
|
|
2032
|
+
...currentRsx,
|
|
2033
|
+
build: nextBuild,
|
|
2034
|
+
},
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
if (dryRun) {
|
|
2038
|
+
logInfo(`[dry-run] patch ${packageJsonPath} (rsx.build)`);
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
fs.writeFileSync(
|
|
2043
|
+
packageJsonPath,
|
|
2044
|
+
`${JSON.stringify(nextPackageJson, null, 2)}\n`,
|
|
2045
|
+
'utf8',
|
|
2046
|
+
);
|
|
2047
|
+
logOk(`Patched ${packageJsonPath} (rsx.build)`);
|
|
2048
|
+
return true;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function ensureAngularProvidersInEntry(entryFile, dryRun) {
|
|
2052
|
+
if (!fs.existsSync(entryFile)) {
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
const original = fs.readFileSync(entryFile, 'utf8');
|
|
2057
|
+
if (original.includes('providexRsx')) {
|
|
2058
|
+
logInfo(`Angular entry already includes providexRsx: ${entryFile}`);
|
|
2059
|
+
return true;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
if (!original.includes('bootstrapApplication(')) {
|
|
2063
|
+
logWarn(
|
|
2064
|
+
`Could not automatically patch Angular providers in ${entryFile}. Expected bootstrapApplication(...).`,
|
|
2065
|
+
);
|
|
2066
|
+
logInfo(
|
|
2067
|
+
"Manual setup: import { providexRsx } from '@rs-x/angular' and add providers: [...providexRsx()] to bootstrapApplication(...).",
|
|
2068
|
+
);
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const sourceWithImport = injectImport(
|
|
2073
|
+
original,
|
|
2074
|
+
"import { providexRsx } from '@rs-x/angular';",
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
let updated = sourceWithImport;
|
|
2078
|
+
if (
|
|
2079
|
+
/bootstrapApplication\([\s\S]*?,\s*\{[\s\S]*?providers\s*:/mu.test(updated)
|
|
2080
|
+
) {
|
|
2081
|
+
updated = updated.replace(
|
|
2082
|
+
/providers\s*:\s*\[/mu,
|
|
2083
|
+
'providers: [...providexRsx(), ',
|
|
2084
|
+
);
|
|
2085
|
+
} else if (/bootstrapApplication\([\s\S]*?,\s*\{/mu.test(updated)) {
|
|
2086
|
+
updated = updated.replace(
|
|
2087
|
+
/bootstrapApplication\(([\s\S]*?),\s*\{/mu,
|
|
2088
|
+
'bootstrapApplication($1, {\n providers: [...providexRsx()],',
|
|
2089
|
+
);
|
|
2090
|
+
} else {
|
|
2091
|
+
updated = updated.replace(
|
|
2092
|
+
/bootstrapApplication\(([\s\S]*?)\)\s*(?:\.catch\([\s\S]*?\))?\s*;/mu,
|
|
2093
|
+
'bootstrapApplication($1, {\n providers: [...providexRsx()],\n}).catch((error) => {\n console.error(error);\n});',
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (updated === sourceWithImport) {
|
|
2098
|
+
logWarn(`Could not automatically inject providexRsx into ${entryFile}.`);
|
|
2099
|
+
logInfo(
|
|
2100
|
+
"Manual setup: import { providexRsx } from '@rs-x/angular' and add providers: [...providexRsx()] to bootstrapApplication(...).",
|
|
2101
|
+
);
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (dryRun) {
|
|
2106
|
+
logInfo(`[dry-run] patch ${entryFile} (providexRsx)`);
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
fs.writeFileSync(entryFile, updated, 'utf8');
|
|
2111
|
+
logOk(`Patched ${entryFile} to include providexRsx.`);
|
|
2112
|
+
return true;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1798
2115
|
function upsertScriptInPackageJson(
|
|
1799
2116
|
projectRoot,
|
|
1800
2117
|
scriptName,
|
|
@@ -2336,44 +2653,58 @@ function runSetupAngular(flags) {
|
|
|
2336
2653
|
const dryRun = Boolean(flags['dry-run']);
|
|
2337
2654
|
const pm = detectPackageManager(flags.pm);
|
|
2338
2655
|
const tag = resolveInstallTag(flags);
|
|
2339
|
-
|
|
2340
|
-
runInit({
|
|
2341
|
-
...flags,
|
|
2342
|
-
'skip-vscode': true,
|
|
2343
|
-
});
|
|
2656
|
+
const projectRoot = process.cwd();
|
|
2344
2657
|
|
|
2345
2658
|
if (!Boolean(flags['skip-install'])) {
|
|
2659
|
+
installRuntimePackages(pm, dryRun, tag);
|
|
2660
|
+
installCompilerPackages(pm, dryRun, tag);
|
|
2346
2661
|
installPackages(pm, ['@rs-x/angular'], {
|
|
2347
2662
|
dev: false,
|
|
2348
2663
|
dryRun,
|
|
2349
2664
|
tag,
|
|
2350
2665
|
label: 'RS-X Angular bindings',
|
|
2351
2666
|
});
|
|
2352
|
-
installPackages(pm, ['@angular-builders/custom-webpack'], {
|
|
2353
|
-
dev: true,
|
|
2354
|
-
dryRun,
|
|
2355
|
-
label: 'Angular custom webpack builder',
|
|
2356
|
-
});
|
|
2357
2667
|
} else {
|
|
2668
|
+
logInfo('Skipping package installation (--skip-install).');
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
const entryFile = resolveEntryFile(projectRoot, 'angular', flags.entry);
|
|
2672
|
+
if (entryFile) {
|
|
2673
|
+
logInfo(`Using Angular entry file: ${entryFile}`);
|
|
2674
|
+
ensureAngularProvidersInEntry(entryFile, dryRun);
|
|
2675
|
+
} else {
|
|
2676
|
+
logWarn('Could not detect an Angular entry file automatically.');
|
|
2358
2677
|
logInfo(
|
|
2359
|
-
'
|
|
2678
|
+
'Manual setup: add providexRsx() to bootstrapApplication(...) in your main entry file.',
|
|
2360
2679
|
);
|
|
2361
2680
|
}
|
|
2362
2681
|
|
|
2363
|
-
|
|
2682
|
+
upsertRsxBuildConfigInPackageJson(projectRoot, dryRun);
|
|
2683
|
+
|
|
2364
2684
|
upsertScriptInPackageJson(
|
|
2365
|
-
|
|
2685
|
+
projectRoot,
|
|
2366
2686
|
'build:rsx',
|
|
2367
|
-
'rsx build --project tsconfig.json',
|
|
2687
|
+
'rsx build --project tsconfig.json --no-emit --prod',
|
|
2368
2688
|
dryRun,
|
|
2369
2689
|
);
|
|
2370
2690
|
upsertScriptInPackageJson(
|
|
2371
|
-
|
|
2691
|
+
projectRoot,
|
|
2372
2692
|
'typecheck:rsx',
|
|
2373
2693
|
'rsx typecheck --project tsconfig.json',
|
|
2374
2694
|
dryRun,
|
|
2375
2695
|
);
|
|
2376
2696
|
|
|
2697
|
+
const rsxRegistrationFile = path.join(
|
|
2698
|
+
projectRoot,
|
|
2699
|
+
'src/rsx-generated/rsx-aot-registration.generated.ts',
|
|
2700
|
+
);
|
|
2701
|
+
ensureAngularPolyfillsContainsFile({
|
|
2702
|
+
projectRoot,
|
|
2703
|
+
configPath: path.join(projectRoot, 'tsconfig.json'),
|
|
2704
|
+
filePath: rsxRegistrationFile,
|
|
2705
|
+
dryRun,
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2377
2708
|
if (!Boolean(flags['skip-vscode'])) {
|
|
2378
2709
|
installVsCodeExtension(flags);
|
|
2379
2710
|
}
|
|
@@ -3256,12 +3587,17 @@ function printProjectHelp() {
|
|
|
3256
3587
|
console.log('What it does:');
|
|
3257
3588
|
console.log(' - Creates a new project folder');
|
|
3258
3589
|
console.log(' - Supports templates: angular, vuejs, react, nextjs, nodejs');
|
|
3590
|
+
console.log(
|
|
3591
|
+
' - Angular generates the RS-X virtual-table demo starter on top of the latest Angular scaffold',
|
|
3592
|
+
);
|
|
3259
3593
|
console.log(' - Scaffolds framework app and wires RS-X bootstrap/setup');
|
|
3260
3594
|
console.log(' - Writes package.json with RS-X dependencies');
|
|
3261
3595
|
console.log(
|
|
3262
3596
|
' - Adds tsconfig + TypeScript plugin config for editor support',
|
|
3263
3597
|
);
|
|
3264
|
-
console.log(
|
|
3598
|
+
console.log(
|
|
3599
|
+
' - For Angular template: uses the latest Angular CLI scaffold, then applies the RS-X demo starter',
|
|
3600
|
+
);
|
|
3265
3601
|
console.log(' - For React/Next templates: also installs @rs-x/react');
|
|
3266
3602
|
console.log(' - For Vue template: also installs @rs-x/vue');
|
|
3267
3603
|
console.log(' - Installs dependencies (unless --skip-install)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rs-x/cli",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.7",
|
|
4
4
|
"description": "CLI for installing RS-X compiler tooling and VS Code integration",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rsx": "./bin/rsx.cjs"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"bin",
|
|
10
10
|
"scripts",
|
|
11
|
+
"templates",
|
|
11
12
|
"*.vsix",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
Binary file
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# rsx-angular-example
|
|
2
|
+
|
|
3
|
+
Angular demo app for RS-X.
|
|
4
|
+
|
|
5
|
+
**Website & docs:** [rsxjs.com](https://www.rsxjs.com/)
|
|
6
|
+
|
|
7
|
+
This example shows a million-row virtual table that:
|
|
8
|
+
|
|
9
|
+
- uses the `rsx` pipe from `@rs-x/angular`
|
|
10
|
+
- creates row expressions with `rsx(...)`
|
|
11
|
+
- keeps a fixed pool of row models and expressions
|
|
12
|
+
- loads pages on demand while scrolling
|
|
13
|
+
- keeps memory bounded by reusing the row pool and pruning old page data
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd rsx-angular-example
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm start
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`npm start` first runs the RS-X build step, then starts Angular.
|
|
29
|
+
|
|
30
|
+
## Build
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This runs:
|
|
37
|
+
|
|
38
|
+
1. `rsx build --project tsconfig.json --no-emit --prod`
|
|
39
|
+
2. `ng build`
|
|
40
|
+
|
|
41
|
+
So the example gets:
|
|
42
|
+
|
|
43
|
+
- RS-X semantic checks
|
|
44
|
+
- generated AOT RS-X caches
|
|
45
|
+
- Angular production build output
|
|
46
|
+
|
|
47
|
+
## Basic RS-X Angular setup
|
|
48
|
+
|
|
49
|
+
The example uses the normal Angular RS-X setup:
|
|
50
|
+
|
|
51
|
+
### 1. Register RS-X providers at bootstrap
|
|
52
|
+
|
|
53
|
+
In `src/main.ts`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
57
|
+
import { providexRsx } from '@rs-x/angular';
|
|
58
|
+
|
|
59
|
+
bootstrapApplication(AppComponent, {
|
|
60
|
+
providers: [...providexRsx()],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Create expressions with `rsx(...)`
|
|
65
|
+
|
|
66
|
+
In `src/app/virtual-table/row-model.ts`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
idExpr: rsx<number>('id')(model),
|
|
70
|
+
nameExpr: rsx<string>('name')(model),
|
|
71
|
+
totalExpr: rsx<number>('price * quantity')(model),
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Bind them with `RsxPipe`
|
|
75
|
+
|
|
76
|
+
In `src/app/virtual-table/virtual-table.component.html`:
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<div *ngFor="let item of state.rowsExpression | rsx; trackBy: trackByIndex">
|
|
80
|
+
<span>{{ item.row.nameExpr | rsx }}</span>
|
|
81
|
+
<span>{{ item.row.totalExpr | rsx }}</span>
|
|
82
|
+
</div>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Why this example is useful
|
|
86
|
+
|
|
87
|
+
The point of the demo is not just rendering a table. It shows how RS-X behaves in a realistic Angular scenario:
|
|
88
|
+
|
|
89
|
+
- large logical dataset: `1,000,000` rows
|
|
90
|
+
- small live expression pool: only the pooled row models stay active
|
|
91
|
+
- page loading is async to simulate real server requests
|
|
92
|
+
- old loaded pages are pruned so scrolling does not grow memory forever
|
|
93
|
+
|
|
94
|
+
## About the `rsx` pipe in this demo
|
|
95
|
+
|
|
96
|
+
This example uses the `rsx` pipe directly in the template so the RS-X behavior is easy to see.
|
|
97
|
+
|
|
98
|
+
That is a demo choice, not a restriction.
|
|
99
|
+
|
|
100
|
+
In a real Angular app, you can also adapt RS-X values into standard Angular constructs such as signals if that fits your component architecture better.
|
|
101
|
+
|
|
102
|
+
## Key files
|
|
103
|
+
|
|
104
|
+
- `src/main.ts`
|
|
105
|
+
- `src/app/app.component.ts`
|
|
106
|
+
- `src/app/app.component.html`
|
|
107
|
+
- `src/app/virtual-table/virtual-table.component.ts`
|
|
108
|
+
- `src/app/virtual-table/virtual-table.component.html`
|
|
109
|
+
- `src/app/virtual-table/virtual-table-model.ts`
|
|
110
|
+
- `src/app/virtual-table/virtual-table-data.service.ts`
|
|
111
|
+
- `src/app/virtual-table/row-model.ts`
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
- The virtual table uses a bounded pool and bounded page retention on purpose, so performance characteristics stay visible while memory stays under control.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
min-height: 100vh;
|
|
4
|
+
color: var(--text);
|
|
5
|
+
background: transparent;
|
|
6
|
+
transition: color 180ms ease;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.app-shell {
|
|
10
|
+
max-width: 1120px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 32px 24px 72px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.app-header {
|
|
16
|
+
margin-bottom: 24px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.app-header-top {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: flex-start;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
gap: 20px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.app-eyebrow {
|
|
27
|
+
letter-spacing: 0.2em;
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
font-size: 12px;
|
|
30
|
+
color: var(--brand);
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
margin-bottom: 8px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.app-header h1 {
|
|
36
|
+
margin: 0 0 12px;
|
|
37
|
+
font-size: 34px;
|
|
38
|
+
line-height: 1.08;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.app-subtitle {
|
|
42
|
+
margin: 0;
|
|
43
|
+
color: var(--muted);
|
|
44
|
+
max-width: 640px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.app-panel {
|
|
48
|
+
background: var(--surface);
|
|
49
|
+
border: 1px solid var(--border-soft);
|
|
50
|
+
border-radius: 24px;
|
|
51
|
+
padding: 24px;
|
|
52
|
+
box-shadow: var(--shadow-2);
|
|
53
|
+
backdrop-filter: blur(12px);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.theme-toggle {
|
|
57
|
+
border: 1px solid var(--border);
|
|
58
|
+
background: linear-gradient(
|
|
59
|
+
135deg,
|
|
60
|
+
color-mix(in srgb, var(--surface-solid) 92%, var(--brand) 8%),
|
|
61
|
+
color-mix(in srgb, var(--surface-solid) 92%, var(--brand-2) 8%)
|
|
62
|
+
);
|
|
63
|
+
color: var(--text);
|
|
64
|
+
border-radius: 999px;
|
|
65
|
+
padding: 10px 14px;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
box-shadow: var(--shadow-1);
|
|
68
|
+
transition:
|
|
69
|
+
transform 160ms ease,
|
|
70
|
+
border-color 160ms ease,
|
|
71
|
+
background 160ms ease;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.theme-toggle:hover {
|
|
75
|
+
transform: translateY(-1px);
|
|
76
|
+
border-color: var(--focus);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.theme-toggle:focus-visible {
|
|
80
|
+
outline: 2px solid var(--focus);
|
|
81
|
+
outline-offset: 2px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (max-width: 760px) {
|
|
85
|
+
.app-shell {
|
|
86
|
+
padding: 24px 16px 48px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.app-header-top {
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: stretch;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.theme-toggle {
|
|
95
|
+
align-self: flex-start;
|
|
96
|
+
}
|
|
97
|
+
}
|