@rs-x/cli 2.0.0-next.6 → 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 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': null,
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
- runSetupAngular(flags);
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
- 'Skipping Angular custom webpack builder install (--skip-install).',
2678
+ 'Manual setup: add providexRsx() to bootstrapApplication(...) in your main entry file.',
2360
2679
  );
2361
2680
  }
2362
2681
 
2363
- wireRsxAngularWebpack(process.cwd(), dryRun);
2682
+ upsertRsxBuildConfigInPackageJson(projectRoot, dryRun);
2683
+
2364
2684
  upsertScriptInPackageJson(
2365
- process.cwd(),
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
- process.cwd(),
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(' - For Angular template: also installs @rs-x/angular');
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.6",
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
  ],
@@ -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
+ }