Skip to content

Commit 33d8cbb

Browse files
committed
Feat: Scoped UI State
1 parent 6cd9815 commit 33d8cbb

File tree

13 files changed

+298
-312
lines changed

13 files changed

+298
-312
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,8 @@ jobs:
7070

7171
# 7 ▸ Run Build
7272
- name: Run Build
73-
run: pnpm build
74-
75-
# 8 ▸ Pack → inject tar-ball → run whole test suite
76-
- name: Pack core tarball
77-
run: pnpm run prepack:core
78-
79-
- name: Inject tarball into fixtures
80-
run: node scripts/install-local-tarball.js
73+
run: pnpm bootstrap
8174

75+
# 8 ▸ Run all tests
8276
- name: Run Vite tests
83-
run: pnpm test:vite
84-
- name: Run Next.js tests
85-
run: pnpm test:next
86-
87-
- name: Run unit tests
88-
run: pnpm test:unit
89-
90-
- name: Run CLI tests
91-
run: pnpm test:cli
77+
run: pnpm test

docs/assets/internal.md

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
1-
Below is a **“mental model”** of the Zero-UI variant extractor. distilled so that *another* human (or LLM) can reason about, extend, or safely refactor the code-base.
1+
Below is a **“mental model”** of the Zero-UI variant extractor. distilled so that _another_ human (or LLM) can reason about, extend, or safely refactor the code-base.
22

33
---
44

55
## 1. Top-level goal
66

7-
* **Locate every call** to a user-supplied React hook
7+
- **Locate every call** to a user-supplied React hook
88

99
```js
10-
const [value, setterFn] = useUI('stateKey', 'initialValue')
10+
const [value, setterFn] = useUI('stateKey', 'initialValue');
1111
```
12-
* Statically discover **all possible string values** that flow into
13-
`stateKey`, `initialValue`, and `setterFn()` arguments.
14-
* `stateKey` can resolve to a local static string.
15-
* `initialValue` is the same rule as above.
16-
* `setterFn()` argument is many forms allowed (see table 3.1) but **must be resolvable**; otherwise the value is ignored (silent) *unless* it looked resolvable but failed inside the helpers, in which case a targeted error is thrown.
17-
* Imported bindings are never allowed - the dev must re-cast them through a local `const`.
1812

13+
- Statically discover **all possible string values** that flow into
14+
`stateKey`, `initialValue`, and `setterFn()` arguments.
15+
- `stateKey` can resolve to a local static string.
16+
- `initialValue` is the same rule as above.
17+
- `setterFn()` argument is many forms allowed (see table 3.1) but **must be resolvable**; otherwise the value is ignored (silent) _unless_ it looked resolvable but failed inside the helpers, in which case a targeted error is thrown.
18+
- Imported bindings are never allowed - the dev must re-cast them through a local `const`.
1919

20-
* Report the result as a list of `VariantData` objects.
20+
- Report the result as a list of `VariantData` objects.
2121

2222
```ts
2323
type VariantData = {
24-
key: string; // 'stateKey'
25-
values: string[]; // ['light','dark',…] (unique, sorted)
26-
initialValue: string; // from 2nd arg of useUI()
27-
}
24+
key: string; // 'stateKey'
25+
values: string[]; // ['light','dark',…] (unique, sorted)
26+
initialValue: string; // from 2nd arg of useUI()
27+
};
2828
```
2929

3030
---
3131

3232
## 2. Two-pass pipeline
3333

34-
| Pass | File-scope work | Output |
35-
| --- | --- | --- |
36-
| **Pass 1 - `collectUseUISetters`** | 1. Traverse the AST once.<br>2. For each `useUI()` destructuring:<br>• validate shapes & count.<br>• resolve the **state key** and **initial value** with **`literalFromNode`** (see rules below).<br>• grab the **binding** of the setter variable. | `SetterMeta[]` = `{ binding, setterName, stateKey, initialValue }[]` |
37-
| **Pass 2 - `harvestSetterValues`** | 1. For every `binding.referencePaths` (i.e. every place the setter is used)<br>2. Only keep `CallExpression`s: `setX(…)`<br>3. Examine the first argument:<br>• direct literal / identifier / template → resolve via `literalFromNode`.<br>• conditional `cond ? a : b` → resolve both arms.<br>• logical fallback `a \|\| b`, `a ?? b` → resolve each side.<br>&nbsp;&nbsp; arrow / function bodies → collect every returned expression and resolve.<br>4. Add every successfully-resolved string to a `Set` bucket **per stateKey**. | `Map< stateKey, Set<string> >` |
34+
| Pass | File-scope work | Output |
35+
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
36+
| **Pass 1 - `collectUseUISetters`** | 1. Traverse the AST once.<br>2. For each `useUI()` destructuring:<br>• validate shapes & count.<br>• resolve the **state key** and **initial value** with **`literalFromNode`** (see rules below).<br>• grab the **binding** of the setter variable. | `SetterMeta[]` = `{ binding, setterName, stateKey, initialValue }[]` |
37+
| **Pass 2 - `harvestSetterValues`** | 1. For every `binding.referencePaths` (i.e. every place the setter is used)<br>2. Only keep `CallExpression`s: `setX(…)`<br>3. Examine the first argument:<br>• direct literal / identifier / template → resolve via `literalFromNode`.<br>• conditional `cond ? a : b` → resolve both arms.<br>• logical fallback `a \|\| b`, `a ?? b` → resolve each side.<br>&nbsp;&nbsp; arrow / function bodies → collect every returned expression and resolve.<br>4. Add every successfully-resolved string to a `Set` bucket **per stateKey**. | `Map< stateKey, Set<string> >` |
3838

3939
`normalizeVariants` just converts that map back into the
4040
`VariantData[]` shape (keeping initial values, sorting, etc.).
@@ -44,10 +44,10 @@ type VariantData = {
4444
## 3. The **literal-resolution micro-framework**
4545

4646
Everything funnels through **`literalFromNode`**.
47-
Think of it as a deterministic *static evaluator* restricted to a
48-
*very* small grammar.
47+
Think of it as a deterministic _static evaluator_ restricted to a
48+
_very_ small grammar.
4949

50-
### 3.1 Supported input forms (setterFn())
50+
### 3.1 Supported input forms (setterFn())
5151

5252
```
5353
┌────────────────────────────────┬─────────────────────────┐
@@ -67,28 +67,28 @@ Think of it as a deterministic *static evaluator* restricted to a
6767
└────────────────────────────────┴─────────────────────────┘
6868
```
6969

70-
### 3.2 Helpers
70+
### 3.2 Helpers
7171

72-
| Helper | Job |
73-
| --- | --- |
74-
| **`resolveTemplateLiteral`** | Ensures every `${expr}` itself resolves to a string via `literalFromNode`.|
75-
| **`resolveLocalConstIdentifier`** | Maps an `Identifier` → its `const` initializer *if* that initializer is an accepted string/ template. Rejects imported bindings with a *single* descriptive error.|
76-
| **`resolveMemberExpression`** | Static walk of `obj.prop`, `obj['prop']`, `obj?.prop`, etc. Works through `as const`, optional-chaining, arrays, numbers, nested chains. Throws if any hop can't be resolved. |
77-
| **`literalFromNode`** | Router that calls the above; memoised (`WeakMap`) so each AST node is evaluated once.|
72+
| Helper | Job |
73+
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74+
| **`resolveTemplateLiteral`** | Ensures every `${expr}` itself resolves to a string via `literalFromNode`. |
75+
| **`resolveLocalConstIdentifier`** | Maps an `Identifier` → its `const` initializer _if_ that initializer is an accepted string/ template. Rejects imported bindings with a _single_ descriptive error. |
76+
| **`resolveMemberExpression`** | Static walk of `obj.prop`, `obj['prop']`, `obj?.prop`, etc. Works through `as const`, optional-chaining, arrays, numbers, nested chains. Throws if any hop can't be resolved. |
77+
| **`literalFromNode`** | Router that calls the above; memoised (`WeakMap`) so each AST node is evaluated once. |
7878

79-
All helpers accept `opts:{ throwOnFail, source, hook }` so *contextual*
79+
All helpers accept `opts:{ throwOnFail, source, hook }` so _contextual_
8080
error messages can be emitted with **`throwCodeFrame`**
8181
(using `@babel/code-frame` to show a coloured snippet).
8282

8383
---
8484

8585
## 4. Validation rules (why errors occur)
8686

87-
| Position in `useUI` | Allowed value | Example error |
88-
| --- | --- | --- |
89-
| **stateKey (arg 0)** | *Local* static string | `State key cannot be resolved at build-time.` |
90-
| **initialValue (arg 1)** | Same rule as above. | `Initial value cannot be resolved` |
91-
| **setter argument** | Many forms allowed (see table 3.1) but **must be resolvable**; otherwise the value is ignored (silent) *unless* it looked resolvable but failed inside the helpers, in which case a targeted error is thrown. | |
87+
| Position in `useUI` | Allowed value | Example error |
88+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
89+
| **stateKey (arg 0)** | _Local_ static string | `State key cannot be resolved at build-time.` |
90+
| **initialValue (arg 1)** | Same rule as above. | `Initial value cannot be resolved …` |
91+
| **setter argument** | Many forms allowed (see table 3.1) but **must be resolvable**; otherwise the value is ignored (silent) _unless_ it looked resolvable but failed inside the helpers, in which case a targeted error is thrown. | |
9292

9393
Imported bindings are never allowed - the dev must re-cast them
9494
through a local `const`.
@@ -97,50 +97,50 @@ through a local `const`.
9797

9898
## 5. Optional-chain & optional-member details
9999

100-
* The updated `resolveMemberExpression` loop iterates while
100+
- The updated `resolveMemberExpression` loop iterates while
101101
`isMemberExpression || isOptionalMemberExpression`.
102-
* Inside array/obj traversal it throws a clear error if a link is
102+
- Inside array/obj traversal it throws a clear error if a link is
103103
missing instead of silently returning `null`.
104-
* `props` collects mixed `string | number` keys in **reverse** (deep → shallow) order, so they can be replayed from the root identifier outward.
104+
- `props` collects mixed `string | number` keys in **reverse** (deep → shallow) order, so they can be replayed from the root identifier outward.
105105

106106
---
107107

108108
## 6. Performance enhancements
109109

110-
* **Memoisation** (`WeakMap<node,string\|null>`) across *both* passes.
111-
* **Quick literals** - string & number keys handled without extra calls.
112-
* `throwCodeFrame` (and thus `generate(node).code`) runs **only** on
110+
- **Memoisation** (`WeakMap<node,string\|null>`) across _both_ passes.
111+
- **Quick literals** - string & number keys handled without extra calls.
112+
- `throwCodeFrame` (and thus `generate(node).code`) runs **only** on
113113
failing branches.
114-
* A small **LRU file-cache** (<5k entries) avoids re-parsing unchanged
114+
- A small **LRU file-cache** (<5k entries) avoids re-parsing unchanged
115115
files (mtime + size signature, with hash fallback).
116116

117117
---
118118

119119
## 7. What is **not** supported
120120

121-
* Runtime-only constructs (`import.meta`, env checks, dynamic imports …).
122-
* Cross-file constant propagation - the extractor is intentionally
121+
- Runtime-only constructs (`import.meta`, env checks, dynamic imports …).
122+
- Cross-file constant propagation - the extractor is intentionally
123123
single-file to keep the build independent of user bundler config.
124-
* Non-string variants (numbers, booleans) - strings only.
125-
* Private class fields in member chains.
126-
* Setter arguments that are **imported functions**.
124+
- Non-string variants (numbers, booleans) - strings only.
125+
- Private class fields in member chains.
126+
- Setter arguments that are **imported functions**.
127127

128128
---
129129

130130
## 8. How to extend
131131

132-
* **Add more expression kinds**: extend `literalFromNode` with new
133-
cases *and* unit-test them.
134-
* **Cross-file constants**: in `resolveLocalConstIdentifier`, detect
132+
- **Add more expression kinds**: extend `literalFromNode` with new
133+
cases _and_ unit-test them.
134+
- **Cross-file constants**: in `resolveLocalConstIdentifier`, detect
135135
`ImportSpecifier`, read & parse the target file, then recurse - but
136136
beware performance.
137-
* **Boolean / number variants**: relax `literalToString` and adjust
137+
- **Boolean / number variants**: relax `literalToString` and adjust
138138
variant schema.
139139

140140
---
141141

142142
> **In one sentence**:
143-
> The extractor turns *purely static, in-file JavaScript* around `useUI`
143+
> The extractor turns _purely static, in-file JavaScript_ around `useUI`
144144
> into a deterministic list of variant strings, throwing early and with
145145
> helpful frames whenever something would otherwise need runtime
146146
> evaluation.

packages/cli/bin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ exec(pm === 'yarn' ? 'add' : 'install', ['@react-zero-ui/core']);
2222
exec(pm === 'yarn' ? 'add' : 'install', ['postcss', 'tailwindcss', '@tailwindcss/postcss', '--save-dev']);
2323

2424
/* 4️⃣ handoff */
25+
// eslint-disable-next-line import/no-unresolved
2526
const { default: zeroUiCli } = await import('@react-zero-ui/core/cli');
2627
if (typeof zeroUiCli === 'function') {
2728
zeroUiCli(process.argv.slice(3));

packages/core/__tests__/config/playwright.next.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
// eslint-disable-next-line import/named
12
import { defineConfig } from '@playwright/test';
3+
24
import path from 'node:path';
35
import { fileURLToPath } from 'node:url';
46

packages/core/__tests__/config/playwright.vite.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
// eslint-disable-next-line import/named
12
import { defineConfig } from '@playwright/test';
3+
24
import path from 'node:path';
35
import { fileURLToPath } from 'node:url';
46

packages/core/__tests__/e2e/cli-next.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
// eslint-disable-next-line import/named
12
import { test, expect } from '@playwright/test';
3+
24
import path from 'node:path';
35
import { existsSync, readFileSync } from 'node:fs';
46
import { fileURLToPath } from 'node:url';

packages/core/__tests__/e2e/cli-vite.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// eslint-disable-next-line import/named
12
import { test, expect } from '@playwright/test';
23
import path from 'node:path';
34
import { existsSync, readFileSync } from 'node:fs';

packages/core/__tests__/e2e/next-scoped.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// eslint-disable-next-line import/named
12
import { test, expect } from '@playwright/test';
23

34
test.describe.configure({ mode: 'serial' });

packages/core/__tests__/e2e/vite-scopes.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// eslint-disable-next-line import/named
12
import { test, expect } from '@playwright/test';
23

34
test.describe.configure({ mode: 'serial' });

0 commit comments

Comments
 (0)