@rs-x/cli 2.0.0-next.0 → 2.0.0-next.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +10 -4
  2. package/bin/rsx.cjs +679 -153
  3. package/package.json +14 -1
  4. package/{rs-x-vscode-extension-2.0.0-next.0.vsix → rs-x-vscode-extension-2.0.0-next.11.vsix} +0 -0
  5. package/templates/angular-demo/README.md +115 -0
  6. package/templates/angular-demo/src/app/app.component.css +97 -0
  7. package/templates/angular-demo/src/app/app.component.html +58 -0
  8. package/templates/angular-demo/src/app/app.component.ts +52 -0
  9. package/templates/angular-demo/src/app/virtual-table/row-data.ts +35 -0
  10. package/templates/angular-demo/src/app/virtual-table/row-model.ts +45 -0
  11. package/templates/angular-demo/src/app/virtual-table/virtual-table-data.service.ts +136 -0
  12. package/templates/angular-demo/src/app/virtual-table/virtual-table-model.ts +224 -0
  13. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.css +174 -0
  14. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.html +50 -0
  15. package/templates/angular-demo/src/app/virtual-table/virtual-table.component.ts +83 -0
  16. package/templates/angular-demo/src/index.html +11 -0
  17. package/templates/angular-demo/src/main.ts +16 -0
  18. package/templates/angular-demo/src/styles.css +261 -0
  19. package/templates/react-demo/README.md +113 -0
  20. package/templates/react-demo/index.html +12 -0
  21. package/templates/react-demo/src/app/app.tsx +87 -0
  22. package/templates/react-demo/src/app/hooks/use-virtual-table-controller.ts +24 -0
  23. package/templates/react-demo/src/app/hooks/use-virtual-table-viewport.ts +39 -0
  24. package/templates/react-demo/src/app/virtual-table/row-data.ts +35 -0
  25. package/templates/react-demo/src/app/virtual-table/row-model.ts +45 -0
  26. package/templates/react-demo/src/app/virtual-table/virtual-table-controller.ts +247 -0
  27. package/templates/react-demo/src/app/virtual-table/virtual-table-data.service.ts +126 -0
  28. package/templates/react-demo/src/app/virtual-table/virtual-table-row.tsx +38 -0
  29. package/templates/react-demo/src/app/virtual-table/virtual-table-shell.tsx +83 -0
  30. package/templates/react-demo/src/main.tsx +23 -0
  31. package/templates/react-demo/src/rsx-bootstrap.ts +18 -0
  32. package/templates/react-demo/src/styles.css +422 -0
  33. package/templates/react-demo/tsconfig.json +17 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rs-x/cli",
3
- "version": "2.0.0-next.0",
3
+ "version": "2.0.0-next.11",
4
4
  "description": "CLI for installing RS-X compiler tooling and VS Code integration",
5
5
  "bin": {
6
6
  "rsx": "./bin/rsx.cjs"
@@ -8,9 +8,22 @@
8
8
  "files": [
9
9
  "bin",
10
10
  "scripts",
11
+ "templates",
11
12
  "*.vsix",
12
13
  "README.md"
13
14
  ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/robert-sanders-software-ontwikkeling/rs-x"
18
+ },
19
+ "homepage": "https://github.com/robert-sanders-software-ontwikkeling/rs-x",
20
+ "bugs": {
21
+ "url": "https://github.com/robert-sanders-software-ontwikkeling/rs-x/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "provenance": true
26
+ },
14
27
  "engines": {
15
28
  "node": ">=20"
16
29
  },
@@ -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
+ }
@@ -0,0 +1,58 @@
1
+ <main class="app-shell">
2
+ <section class="hero">
3
+ <div class="container">
4
+ <div class="heroGrid">
5
+ <div class="heroLeft">
6
+ <p class="app-eyebrow">RS-X Angular Demo</p>
7
+ <h1 class="hTitle">Virtual Table</h1>
8
+ <p class="hSubhead">
9
+ Million-row scrolling with a fixed RS-X expression pool.
10
+ </p>
11
+ <p class="hSub">
12
+ This demo keeps rendering bounded while streaming pages on demand,
13
+ so scrolling stays smooth without growing expression memory with the
14
+ dataset.
15
+ </p>
16
+
17
+ <div class="heroActions">
18
+ <a
19
+ class="btn btnGhost"
20
+ href="https://www.rsxjs.com/"
21
+ target="_blank"
22
+ rel="noreferrer"
23
+ >
24
+ rs-x
25
+ </a>
26
+ <button
27
+ type="button"
28
+ class="btn btnGhost theme-toggle"
29
+ (click)="toggleTheme()"
30
+ [attr.aria-label]="
31
+ 'Switch to ' + (theme === 'dark' ? 'light' : 'dark') + ' mode'
32
+ "
33
+ >
34
+ {{ theme === 'dark' ? 'Light mode' : 'Dark mode' }}
35
+ </button>
36
+ </div>
37
+ </div>
38
+
39
+ <aside class="card heroNote">
40
+ <h2 class="cardTitle">What This Shows</h2>
41
+ <p class="cardText">
42
+ Only a small row-model pool stays alive while pages stream in around
43
+ the viewport. That means one million logical rows without one
44
+ million live bindings.
45
+ </p>
46
+ </aside>
47
+ </div>
48
+ </div>
49
+ </section>
50
+
51
+ <section class="section">
52
+ <div class="container">
53
+ <section class="app-panel card">
54
+ <rsx-virtual-table></rsx-virtual-table>
55
+ </section>
56
+ </div>
57
+ </section>
58
+ </main>
@@ -0,0 +1,52 @@
1
+ import { DOCUMENT } from '@angular/common';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ HostBinding,
6
+ inject,
7
+ OnInit,
8
+ } from '@angular/core';
9
+
10
+ import { VirtualTableComponent } from './virtual-table/virtual-table.component';
11
+
12
+ type ThemeMode = 'light' | 'dark';
13
+
14
+ @Component({
15
+ selector: 'app-root',
16
+ standalone: true,
17
+ imports: [VirtualTableComponent],
18
+ templateUrl: './app.component.html',
19
+ styleUrls: ['./app.component.css'],
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ })
22
+ export class AppComponent implements OnInit {
23
+ private readonly _document = inject(DOCUMENT);
24
+ @HostBinding('class.theme-dark') public isDarkTheme = false;
25
+ public theme: ThemeMode = 'dark';
26
+
27
+ public ngOnInit(): void {
28
+ this.theme = this.getInitialTheme();
29
+ this.applyTheme(this.theme);
30
+ }
31
+
32
+ public toggleTheme(): void {
33
+ this.theme = this.theme === 'dark' ? 'light' : 'dark';
34
+ this.applyTheme(this.theme);
35
+ window.localStorage.setItem('rsx-theme', this.theme);
36
+ }
37
+
38
+ private getInitialTheme(): ThemeMode {
39
+ const storedTheme = window.localStorage.getItem('rsx-theme');
40
+ if (storedTheme === 'light' || storedTheme === 'dark') {
41
+ return storedTheme;
42
+ }
43
+
44
+ return 'dark';
45
+ }
46
+
47
+ private applyTheme(theme: ThemeMode): void {
48
+ this.isDarkTheme = theme === 'dark';
49
+ this._document.documentElement.setAttribute('data-theme', theme);
50
+ this._document.body.setAttribute('data-theme', theme);
51
+ }
52
+ }
@@ -0,0 +1,35 @@
1
+ export type SortKey = 'id' | 'name' | 'price' | 'quantity' | 'category';
2
+ export type SortDirection = 'asc' | 'desc';
3
+
4
+ export type RowData = {
5
+ id: number;
6
+ name: string;
7
+ price: number;
8
+ quantity: number;
9
+ category: string;
10
+ updatedAt: string;
11
+ };
12
+
13
+ const categories = ['Hardware', 'Software', 'Design', 'Ops'];
14
+
15
+ function pad(value: number): string {
16
+ return value.toString().padStart(2, '0');
17
+ }
18
+
19
+ export function createRowData(id: number): RowData {
20
+ const zeroBasedId = id - 1;
21
+ const price = 25 + (zeroBasedId % 1000) / 10;
22
+ const quantity = 1 + (Math.floor(zeroBasedId / 1000) % 100);
23
+ const category = categories[zeroBasedId % categories.length] ?? 'General';
24
+ const month = pad(((zeroBasedId * 7) % 12) + 1);
25
+ const day = pad(((zeroBasedId * 11) % 28) + 1);
26
+
27
+ return {
28
+ id,
29
+ name: `Product ${id.toString().padStart(7, '0')}`,
30
+ price,
31
+ quantity,
32
+ category,
33
+ updatedAt: `2026-${month}-${day}`,
34
+ };
35
+ }
@@ -0,0 +1,45 @@
1
+ import { type IExpression, rsx } from '@rs-x/expression-parser';
2
+
3
+ import type { RowData } from './row-data';
4
+
5
+ export type RowModel = {
6
+ model: RowData;
7
+ idExpr: IExpression<number>;
8
+ nameExpr: IExpression<string>;
9
+ categoryExpr: IExpression<string>;
10
+ priceExpr: IExpression<number>;
11
+ quantityExpr: IExpression<number>;
12
+ updatedAtExpr: IExpression<string>;
13
+ totalExpr: IExpression<number>;
14
+ };
15
+
16
+ export function createRowModel(): RowModel {
17
+ const model: RowData = {
18
+ id: 0,
19
+ name: '',
20
+ price: 0,
21
+ quantity: 0,
22
+ category: 'General',
23
+ updatedAt: '2026-01-01',
24
+ };
25
+
26
+ return {
27
+ model,
28
+ idExpr: rsx<number>('id')(model),
29
+ nameExpr: rsx<string>('name')(model),
30
+ categoryExpr: rsx<string>('category')(model),
31
+ priceExpr: rsx<number>('price')(model),
32
+ quantityExpr: rsx<number>('quantity')(model),
33
+ updatedAtExpr: rsx<string>('updatedAt')(model),
34
+ totalExpr: rsx<number>('price * quantity')(model),
35
+ };
36
+ }
37
+
38
+ export function updateRowModel(target: RowModel, data: RowData): void {
39
+ target.model.id = data.id;
40
+ target.model.name = data.name;
41
+ target.model.price = data.price;
42
+ target.model.quantity = data.quantity;
43
+ target.model.category = data.category;
44
+ target.model.updatedAt = data.updatedAt;
45
+ }
@@ -0,0 +1,136 @@
1
+ import {
2
+ createRowData,
3
+ type RowData,
4
+ type SortDirection,
5
+ type SortKey,
6
+ } from './row-data';
7
+
8
+ export type VirtualTablePage = {
9
+ total: number;
10
+ items: RowData[];
11
+ };
12
+
13
+ const TOTAL_ROWS = 1_000_000;
14
+ const REQUEST_DELAY_MS = 120;
15
+ const CATEGORY_COUNT = 4;
16
+ const PRICE_BUCKET_COUNT = 1_000;
17
+ const QUANTITY_BUCKET_COUNT = 100;
18
+ const MAX_CACHED_PAGES = 24;
19
+
20
+ export class VirtualTableDataService {
21
+ private readonly _pageCache = new Map<string, VirtualTablePage>();
22
+
23
+ public get totalRows(): number {
24
+ return TOTAL_ROWS;
25
+ }
26
+
27
+ public async fetchPage(
28
+ pageIndex: number,
29
+ pageSize: number,
30
+ sortKey: SortKey,
31
+ sortDirection: SortDirection,
32
+ ): Promise<VirtualTablePage> {
33
+ const cacheKey = `${sortKey}:${sortDirection}:${pageIndex}:${pageSize}`;
34
+ const cached = this._pageCache.get(cacheKey);
35
+ if (cached) {
36
+ return cached;
37
+ }
38
+
39
+ await this.delay(REQUEST_DELAY_MS + (pageIndex % 5) * 35);
40
+
41
+ const startIndex = pageIndex * pageSize;
42
+ const items: RowData[] = [];
43
+ const endIndex = Math.min(startIndex + pageSize, TOTAL_ROWS);
44
+
45
+ for (
46
+ let visualIndex = startIndex;
47
+ visualIndex < endIndex;
48
+ visualIndex += 1
49
+ ) {
50
+ const id = this.getIdAtVisualIndex(visualIndex, sortKey, sortDirection);
51
+ items.push(createRowData(id));
52
+ }
53
+
54
+ const page = {
55
+ total: TOTAL_ROWS,
56
+ items,
57
+ };
58
+ this._pageCache.set(cacheKey, page);
59
+ this.trimCache();
60
+ return page;
61
+ }
62
+
63
+ private getIdAtVisualIndex(
64
+ visualIndex: number,
65
+ sortKey: SortKey,
66
+ sortDirection: SortDirection,
67
+ ): number {
68
+ const normalizedIndex =
69
+ sortDirection === 'asc' ? visualIndex : TOTAL_ROWS - 1 - visualIndex;
70
+
71
+ if (sortKey === 'price') {
72
+ return this.getPriceSortedId(normalizedIndex);
73
+ }
74
+
75
+ if (sortKey === 'quantity') {
76
+ return this.getQuantitySortedId(normalizedIndex);
77
+ }
78
+
79
+ if (sortKey === 'category') {
80
+ return this.getCategorySortedId(normalizedIndex);
81
+ }
82
+
83
+ // Name follows id order because names are deterministic from the row id.
84
+ return normalizedIndex + 1;
85
+ }
86
+
87
+ private getPriceSortedId(visualIndex: number): number {
88
+ const groupSize = TOTAL_ROWS / PRICE_BUCKET_COUNT;
89
+ const priceBucket = Math.floor(visualIndex / groupSize);
90
+ const offsetInBucket = visualIndex % groupSize;
91
+
92
+ return priceBucket + offsetInBucket * PRICE_BUCKET_COUNT + 1;
93
+ }
94
+
95
+ private getQuantitySortedId(visualIndex: number): number {
96
+ const groupSize = TOTAL_ROWS / QUANTITY_BUCKET_COUNT;
97
+ const quantityBucket = Math.floor(visualIndex / groupSize);
98
+ const offsetInBucket = visualIndex % groupSize;
99
+ const quantityStride = PRICE_BUCKET_COUNT * QUANTITY_BUCKET_COUNT;
100
+ const quantityBlock = Math.floor(offsetInBucket / PRICE_BUCKET_COUNT);
101
+ const priceBucket = offsetInBucket % PRICE_BUCKET_COUNT;
102
+
103
+ return (
104
+ priceBucket +
105
+ quantityBucket * PRICE_BUCKET_COUNT +
106
+ quantityBlock * quantityStride +
107
+ 1
108
+ );
109
+ }
110
+
111
+ private getCategorySortedId(visualIndex: number): number {
112
+ const groupSize = TOTAL_ROWS / CATEGORY_COUNT;
113
+ const categoryBucket = Math.floor(visualIndex / groupSize);
114
+ const offsetInBucket = visualIndex % groupSize;
115
+
116
+ return categoryBucket + offsetInBucket * CATEGORY_COUNT + 1;
117
+ }
118
+
119
+ private delay(durationMs: number): Promise<void> {
120
+ return new Promise((resolve) => {
121
+ window.setTimeout(resolve, durationMs);
122
+ });
123
+ }
124
+
125
+ private trimCache(): void {
126
+ while (this._pageCache.size > MAX_CACHED_PAGES) {
127
+ const oldestKey = this._pageCache.keys().next().value as
128
+ | string
129
+ | undefined;
130
+ if (!oldestKey) {
131
+ return;
132
+ }
133
+ this._pageCache.delete(oldestKey);
134
+ }
135
+ }
136
+ }