Skip to content

Commit 0095b68

Browse files
authored
feat(core): scoped styles, new variant parser & string-only UI keys (BREAKING CHANGE) (#14)
* feat:added v0.1 scoped styles * feat: added scoped UI state logic * fix: tests to use new @custom-variant selector * feat:scoped styles, FAQ tests written * fix:starting the backup, before updating AST parsing * refactor: update bodyAttributes and UI state handling; remove unused test file * migrate:start micro-migration to .ts * scoped styles work, with better ast, and partial typescript move * feat: scoped styles v1 working, now writing tests * feat:start writing vite tests for scoped styles * test: added vite tests * feat:start moving more files to typescript * merge: typescript v1 merge complete * fix: format * fix: add build command * fix: add release please. * feat: finish parsing logic for member expressions * Added new babel resolve logic * support all the way up to option chaining * Adding quick-lru * feat:added advanced caching * collectUseUIsetters function complete * finished parser * Test Suite Pass * chore: format files * feat:write internal doc on babel compiler * added babel doc * unit tests written * updated tests * Feat: Scoped UI State * chore: sync lockfile (next 15.3.5) * chore:update CI to individual tests vs test:all * chore:update CI workflow * fix: CI
1 parent 41bf9e2 commit 0095b68

File tree

90 files changed

+7409
-1647
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+7409
-1647
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ jobs:
2626
- uses: actions/checkout@v4
2727
with:
2828
fetch-depth: 0 # required for git-diff later
29-
3029
# 2 ▸ Set up pnpm
3130
- uses: pnpm/action-setup@v3
3231
with:
@@ -49,34 +48,34 @@ jobs:
4948
uses: actions/cache@v4
5049
with:
5150
path: ~/.cache/ms-playwright
52-
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
51+
key: ${{ runner.os }}-playwright-${{ hashFiles('packages/core/package.json') }}
5352
restore-keys: |
5453
${{ runner.os }}-playwright-
5554
5655
- name: Install Playwright browsers (if cache miss)
5756
if: steps.pw-cache.outputs.cache-hit != 'true'
5857
run: pnpm --filter @react-zero-ui/core exec playwright install --with-deps
5958

60-
- name: Save Playwright cache
61-
if: steps.pw-cache.outputs.cache-hit != 'true'
62-
uses: actions/cache/save@v4
63-
with:
64-
path: ~/.cache/ms-playwright
65-
key: ${{ steps.pw-cache.outputs.cache-primary-key }}
66-
6759
# 6 ▸ Lint fast, fail fast
6860
- name: Lint
6961
run: pnpm lint
7062

71-
# 7 ▸ Pack → inject tar-ball → run whole test suite
72-
- name: Pack core tarball
73-
run: pnpm run prepack:core
63+
# 7 ▸ Run Build
64+
- name: Run Build
65+
run: pnpm build
66+
67+
# 8 ▸ Run Prepack
68+
- name: Run Prepack
69+
run: pnpm prepack:core
7470

75-
- name: Inject tarball into fixtures
76-
run: node scripts/install-local-tarball.js
71+
# 9 ▸ Run Install Tarball
72+
- name: Run Install Tarball
73+
run: pnpm i-tarball
7774

75+
# 10 ▸ Run all tests
7876
- name: Run Vite tests
7977
run: pnpm test:vite
78+
8079
- name: Run Next.js tests
8180
run: pnpm test:next
8281

@@ -85,3 +84,6 @@ jobs:
8584

8685
- name: Run CLI tests
8786
run: pnpm test:cli
87+
88+
- name: Run integration tests
89+
run: pnpm test:integration

.github/workflows/release.yml

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ name: Release
22

33
on:
44
workflow_dispatch:
5-
workflow_run:
6-
workflows: ['CI']
7-
types: [completed]
8-
branches: [main]
95

106
permissions:
117
contents: write
@@ -14,13 +10,64 @@ permissions:
1410

1511
jobs:
1612
release:
17-
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
1813
runs-on: ubuntu-latest
1914
steps:
2015
- uses: actions/checkout@v4
2116

17+
- name: Check CI Status
18+
run: |
19+
# Get the latest CI workflow run for this branch
20+
CI_STATUS=$(gh run list --workflow=ci.yml --branch=${{ github.ref_name }} --limit=1 --json conclusion --jq '.[0].conclusion')
21+
22+
if [[ "$CI_STATUS" != "success" ]]; then
23+
echo "❌ CI must pass before releasing. Current status: $CI_STATUS"
24+
exit 1
25+
fi
26+
27+
echo "✅ CI passed. Proceeding with release..."
28+
env:
29+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
2231
- uses: googleapis/release-please-action@v4
32+
id: release
2333
with:
2434
token: ${{ secrets.GITHUB_TOKEN }}
2535
config-file: .release-please-manifest.json
2636
manifest-file: .release-please-manifest.json
37+
38+
# Publish to npm when releases are created
39+
- uses: actions/setup-node@v4
40+
if: ${{ steps.release.outputs.releases_created }}
41+
with:
42+
node-version: '18'
43+
registry-url: 'https://registry.npmjs.org'
44+
45+
- name: Install dependencies
46+
if: ${{ steps.release.outputs.releases_created }}
47+
run: |
48+
corepack enable
49+
pnpm install --frozen-lockfile
50+
51+
- name: Publish packages
52+
if: ${{ steps.release.outputs.releases_created }}
53+
run: |
54+
# Check which packages were released and publish them
55+
echo "Releases created: ${{ steps.release.outputs.releases_created }}"
56+
57+
# Publish core package if it was released
58+
if [[ '${{ steps.release.outputs.packages_core--release_created }}' == 'true' ]]; then
59+
echo "📦 Publishing @react-zero-ui/core..."
60+
cd packages/core
61+
npm publish --access public
62+
cd ../..
63+
fi
64+
65+
# Publish CLI package if it was released
66+
if [[ '${{ steps.release.outputs.packages_cli--release_created }}' == 'true' ]]; then
67+
echo "📦 Publishing create-zero-ui..."
68+
cd packages/cli
69+
npm publish --access public
70+
cd ../..
71+
fi
72+
env:
73+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# package artifacts
22
node_modules/
33
**/.next/
4-
dist/
4+
**/dist/
5+
# **/build/
56
coverage/
67
test-results/
78
.pnpm-store/
@@ -19,11 +20,8 @@ yarn-error.log*
1920
# tarballs produced during local tests
2021
*.tgz
2122

22-
# local scratch files
23-
t.py
24-
todo.md
2523

2624
# keep these files
2725
!next-env.d.ts
26+
!packages/core/src/dist/
2827

29-

.prettierrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"useTabs": true,
77
"printWidth": 160,
88
"endOfLine": "lf",
9-
"arrowParens": "avoid",
9+
"arrowParens": "always",
1010
"bracketSpacing": true,
1111
"objectWrap": "collapse",
1212
"bracketSameLine": true,

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# React Zero‑UI (Beta)
22

3-
**Instant UI state updates. ZERO React re‑renders. <1 KB runtime.**
3+
**Instant UI state updates. ZERO React re‑renders. Near ZERO runtime. <500 bytes**
44

55
Pre‑render your UI once, flip a `data-*` attribute to update — that's it.
66

@@ -156,18 +156,18 @@ Any `data-{key}="{value}"` pair becomes a variant: `{key}-{value}:`.
156156
- **Zero React re‑renders** for UI‑only state.
157157
- **Global setters** — call from any component or util.
158158
- **Tiny**: < 391 Byte gzipped runtime.
159-
- **TypeScript‑first**.
160159
- **SSR‑friendly** (Next.js & Vite SSR).
161-
- **Framework‑agnostic CSS**generated classes work in plain HTML / Vue / Svelte as well with extra config.
160+
- **Use from anywhere**Consume with tailwind variants from anywhere.
162161

163162
---
164163

165164
## 🏗 Best Practices
166165

167-
1. **UI state only** → themes, layout toggles, feature flags.
166+
1. **Global UI state only** → themes, layout toggles, feature flags.
168167
2. **Business logic stays in React** → fetching, data mutation, etc.
169168
3. **Kebab‑case keys** → e.g. `sidebar-open`.
170169
4. **Provide defaults** to avoid Flash‑Of‑Unstyled‑Content.
170+
5. **Avoid** for per-component logic or data.
171171

172172
---
173173

docs/assets/internal.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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.
2+
3+
---
4+
5+
## 1. Top-level goal
6+
7+
- **Locate every call** to a user-supplied React hook
8+
9+
```js
10+
const [value, setterFn] = useUI('stateKey', 'initialValue');
11+
```
12+
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`.
19+
20+
- Report the result as a list of `VariantData` objects.
21+
22+
```ts
23+
type VariantData = {
24+
key: string; // 'stateKey'
25+
values: string[]; // ['light','dark',…] (unique, sorted)
26+
initialValue: string; // from 2nd arg of useUI()
27+
};
28+
```
29+
30+
---
31+
32+
## 2. Two-pass pipeline
33+
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> >` |
38+
39+
`normalizeVariants` just converts that map back into the
40+
`VariantData[]` shape (keeping initial values, sorting, etc.).
41+
42+
---
43+
44+
## 3. The **literal-resolution micro-framework**
45+
46+
Everything funnels through **`literalFromNode`**.
47+
Think of it as a deterministic _static evaluator_ restricted to a
48+
_very_ small grammar.
49+
50+
### 3.1 Supported input forms (setterFn())
51+
52+
```
53+
┌────────────────────────────────┬─────────────────────────┐
54+
│ Expression │ Accepted? → Returns │
55+
├────────────────────────────────┼─────────────────────────┤
56+
│ dark │ ✔ string literal │
57+
│ `th-${COLOR}` │ ✔ if every `${}`resolves│
58+
│ const DARK │ ✔ if IDENT → const str │
59+
│ THEMES.dark/[idx]/?. │ ✔ if the whole chain is │
60+
│ │ top-level `const` │
61+
│ "a" + "b" │ ✔ "ab" │
62+
│ a ?? b or a || b │ ✔ Attempts to resolve │
63+
│ │ both sides │
64+
│prev =>(prev=== 'a' ? 'b' : 'a')│ "a" ,"b" │
65+
│ Anything imported │ ❌ Hard error │
66+
│ Anything dynamic at runtime │ ❌ Returns null / error │
67+
└────────────────────────────────┴─────────────────────────┘
68+
```
69+
70+
### 3.2 Helpers
71+
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. |
78+
79+
All helpers accept `opts:{ throwOnFail, source, hook }` so _contextual_
80+
error messages can be emitted with **`throwCodeFrame`**
81+
(using `@babel/code-frame` to show a coloured snippet).
82+
83+
---
84+
85+
## 4. Validation rules (why errors occur)
86+
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. | |
92+
93+
Imported bindings are never allowed - the dev must re-cast them
94+
through a local `const`.
95+
96+
---
97+
98+
## 5. Optional-chain & optional-member details
99+
100+
- The updated `resolveMemberExpression` loop iterates while
101+
`isMemberExpression || isOptionalMemberExpression`.
102+
- Inside array/obj traversal it throws a clear error if a link is
103+
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.
105+
106+
---
107+
108+
## 6. Performance enhancements
109+
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
113+
failing branches.
114+
- A small **LRU file-cache** (<5k entries) avoids re-parsing unchanged
115+
files (mtime + size signature, with hash fallback).
116+
117+
---
118+
119+
## 7. What is **not** supported
120+
121+
- Runtime-only constructs (`import.meta`, env checks, dynamic imports …).
122+
- Cross-file constant propagation - the extractor is intentionally
123+
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**.
127+
128+
---
129+
130+
## 8. How to extend
131+
132+
- **Add more expression kinds**: extend `literalFromNode` with new
133+
cases _and_ unit-test them.
134+
- **Cross-file constants**: in `resolveLocalConstIdentifier`, detect
135+
`ImportSpecifier`, read & parse the target file, then recurse - but
136+
beware performance.
137+
- **Boolean / number variants**: relax `literalToString` and adjust
138+
variant schema.
139+
140+
---
141+
142+
> **In one sentence**:
143+
> The extractor turns _purely static, in-file JavaScript_ around `useUI`
144+
> into a deterministic list of variant strings, throwing early and with
145+
> helpful frames whenever something would otherwise need runtime
146+
> evaluation.

0 commit comments

Comments
 (0)