Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1b700b2
feat:added v0.1 scoped styles
Austin1serb Jun 24, 2025
9cce88e
feat: added scoped UI state logic
Austin1serb Jun 28, 2025
bf2efe3
Merge branch 'main' into feat-scoped-styles
Austin1serb Jun 28, 2025
eb9746a
fix: tests to use new @custom-variant selector
Austin1serb Jun 28, 2025
75bcde3
feat:scoped styles, FAQ tests written
Austin1serb Jun 29, 2025
cc83fb4
fix:starting the backup, before updating AST parsing
Austin1serb Jun 29, 2025
290314a
refactor: update bodyAttributes and UI state handling; remove unused …
Austin1serb Jun 29, 2025
909b697
migrate:start micro-migration to .ts
Austin1serb Jun 29, 2025
322d7d2
scoped styles work, with better ast, and partial typescript move
Austin1serb Jun 29, 2025
6ab7d45
feat: scoped styles v1 working, now writing tests
Austin1serb Jun 29, 2025
59e07bc
Merge branch 'main' into feat-scoped-styles
Austin1serb Jun 30, 2025
5d946bc
feat:start writing vite tests for scoped styles
Austin1serb Jun 30, 2025
e2812ab
test: added vite tests
Austin1serb Jun 30, 2025
693213f
feat:start moving more files to typescript
Austin1serb Jun 30, 2025
acd021c
merge: typescript v1 merge complete
Austin1serb Jun 30, 2025
f0cf83d
fix: format
Austin1serb Jun 30, 2025
3814ffb
fix: add build command
Austin1serb Jun 30, 2025
e4c6a15
fix: add release please.
Austin1serb Jun 30, 2025
cc78546
feat: finish parsing logic for member expressions
Austin1serb Jul 1, 2025
147cc84
Added new babel resolve logic
Austin1serb Jul 1, 2025
381a60b
support all the way up to option chaining
Austin1serb Jul 2, 2025
ec9bdbc
Adding quick-lru
Austin1serb Jul 2, 2025
30e0951
feat:added advanced caching
Austin1serb Jul 2, 2025
8a8ba5e
collectUseUIsetters function complete
Austin1serb Jul 2, 2025
5785009
finished parser
Austin1serb Jul 2, 2025
ab76ea3
Test Suite Pass
Austin1serb Jul 2, 2025
b175602
chore: format files
Austin1serb Jul 6, 2025
cfc3783
feat:write internal doc on babel compiler
Austin1serb Jul 6, 2025
1c05190
added babel doc
Austin1serb Jul 7, 2025
8ac85e1
unit tests written
Austin1serb Jul 8, 2025
6cd9815
updated tests
Austin1serb Jul 8, 2025
33d8cbb
Feat: Scoped UI State
Austin1serb Jul 8, 2025
0a1c79f
chore: sync lockfile (next 15.3.5)
Austin1serb Jul 8, 2025
91d0c2a
Merge branch 'main' into feat-scoped-styles
Austin1serb Jul 8, 2025
f9c924c
chore:update CI to individual tests vs test:all
Austin1serb Jul 8, 2025
35ba9aa
chore:update CI workflow
Austin1serb Jul 8, 2025
150225a
fix: CI
Austin1serb Jul 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for git-diff later

# 2 ▸ Set up pnpm
- uses: pnpm/action-setup@v3
with:
Expand All @@ -49,34 +48,34 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
key: ${{ runner.os }}-playwright-${{ hashFiles('packages/core/package.json') }}
restore-keys: |
${{ runner.os }}-playwright-

- name: Install Playwright browsers (if cache miss)
if: steps.pw-cache.outputs.cache-hit != 'true'
run: pnpm --filter @react-zero-ui/core exec playwright install --with-deps

- name: Save Playwright cache
if: steps.pw-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/.cache/ms-playwright
key: ${{ steps.pw-cache.outputs.cache-primary-key }}

# 6 ▸ Lint fast, fail fast
- name: Lint
run: pnpm lint

# 7 ▸ Pack → inject tar-ball → run whole test suite
- name: Pack core tarball
run: pnpm run prepack:core
# 7 ▸ Run Build
- name: Run Build
run: pnpm build

# 8 ▸ Run Prepack
- name: Run Prepack
run: pnpm prepack:core

- name: Inject tarball into fixtures
run: node scripts/install-local-tarball.js
# 9 ▸ Run Install Tarball
- name: Run Install Tarball
run: pnpm i-tarball

# 10 ▸ Run all tests
- name: Run Vite tests
run: pnpm test:vite

- name: Run Next.js tests
run: pnpm test:next

Expand All @@ -85,3 +84,6 @@ jobs:

- name: Run CLI tests
run: pnpm test:cli

- name: Run integration tests
run: pnpm test:integration
57 changes: 52 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ name: Release

on:
workflow_dispatch:
workflow_run:
workflows: ['CI']
types: [completed]
branches: [main]

permissions:
contents: write
Expand All @@ -14,13 +10,64 @@ permissions:

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

- name: Check CI Status
run: |
# Get the latest CI workflow run for this branch
CI_STATUS=$(gh run list --workflow=ci.yml --branch=${{ github.ref_name }} --limit=1 --json conclusion --jq '.[0].conclusion')

if [[ "$CI_STATUS" != "success" ]]; then
echo "❌ CI must pass before releasing. Current status: $CI_STATUS"
exit 1
fi

echo "✅ CI passed. Proceeding with release..."
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: .release-please-manifest.json
manifest-file: .release-please-manifest.json

# Publish to npm when releases are created
- uses: actions/setup-node@v4
if: ${{ steps.release.outputs.releases_created }}
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
if: ${{ steps.release.outputs.releases_created }}
run: |
corepack enable
pnpm install --frozen-lockfile

- name: Publish packages
if: ${{ steps.release.outputs.releases_created }}
run: |
# Check which packages were released and publish them
echo "Releases created: ${{ steps.release.outputs.releases_created }}"

# Publish core package if it was released
if [[ '${{ steps.release.outputs.packages_core--release_created }}' == 'true' ]]; then
echo "📦 Publishing @react-zero-ui/core..."
cd packages/core
npm publish --access public
cd ../..
fi

# Publish CLI package if it was released
if [[ '${{ steps.release.outputs.packages_cli--release_created }}' == 'true' ]]; then
echo "📦 Publishing create-zero-ui..."
cd packages/cli
npm publish --access public
cd ../..
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
8 changes: 3 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# package artifacts
node_modules/
**/.next/
dist/
**/dist/
# **/build/
coverage/
test-results/
.pnpm-store/
Expand All @@ -19,11 +20,8 @@ yarn-error.log*
# tarballs produced during local tests
*.tgz

# local scratch files
t.py
todo.md

# keep these files
!next-env.d.ts
!packages/core/src/dist/


2 changes: 1 addition & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"useTabs": true,
"printWidth": 160,
"endOfLine": "lf",
"arrowParens": "avoid",
"arrowParens": "always",
"bracketSpacing": true,
"objectWrap": "collapse",
"bracketSameLine": true,
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# React Zero‑UI (Beta)

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

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

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

---

## 🏗 Best Practices

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

---

Expand Down
146 changes: 146 additions & 0 deletions docs/assets/internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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. Top-level goal

- **Locate every call** to a user-supplied React hook

```js
const [value, setterFn] = useUI('stateKey', 'initialValue');
```

- Statically discover **all possible string values** that flow into
`stateKey`, `initialValue`, and `setterFn()` arguments.
- `stateKey` can resolve to a local static string.
- `initialValue` is the same rule as above.
- `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.
- Imported bindings are never allowed - the dev must re-cast them through a local `const`.

- Report the result as a list of `VariantData` objects.

```ts
type VariantData = {
key: string; // 'stateKey'
values: string[]; // ['light','dark',…] (unique, sorted)
initialValue: string; // from 2nd arg of useUI()
};
```

---

## 2. Two-pass pipeline

| Pass | File-scope work | Output |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| **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 }[]` |
| **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> >` |

`normalizeVariants` just converts that map back into the
`VariantData[]` shape (keeping initial values, sorting, etc.).

---

## 3. The **literal-resolution micro-framework**

Everything funnels through **`literalFromNode`**.
Think of it as a deterministic _static evaluator_ restricted to a
_very_ small grammar.

### 3.1 Supported input forms (setterFn())

```
┌────────────────────────────────┬─────────────────────────┐
│ Expression │ Accepted? → Returns │
├────────────────────────────────┼─────────────────────────┤
│ dark │ ✔ string literal │
│ `th-${COLOR}` │ ✔ if every `${}`resolves│
│ const DARK │ ✔ if IDENT → const str │
│ THEMES.dark/[idx]/?. │ ✔ if the whole chain is │
│ │ top-level `const` │
│ "a" + "b" │ ✔ "ab" │
│ a ?? b or a || b │ ✔ Attempts to resolve │
│ │ both sides │
│prev =>(prev=== 'a' ? 'b' : 'a')│ "a" ,"b" │
│ Anything imported │ ❌ Hard error │
│ Anything dynamic at runtime │ ❌ Returns null / error │
└────────────────────────────────┴─────────────────────────┘
```

### 3.2 Helpers

| Helper | Job |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`resolveTemplateLiteral`** | Ensures every `${expr}` itself resolves to a string via `literalFromNode`. |
| **`resolveLocalConstIdentifier`** | Maps an `Identifier` → its `const` initializer _if_ that initializer is an accepted string/ template. Rejects imported bindings with a _single_ descriptive error. |
| **`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. |
| **`literalFromNode`** | Router that calls the above; memoised (`WeakMap`) so each AST node is evaluated once. |

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

---

## 4. Validation rules (why errors occur)

| Position in `useUI` | Allowed value | Example error |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| **stateKey (arg 0)** | _Local_ static string | `State key cannot be resolved at build-time.` |
| **initialValue (arg 1)** | Same rule as above. | `Initial value cannot be resolved …` |
| **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. | |

Imported bindings are never allowed - the dev must re-cast them
through a local `const`.

---

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

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

---

## 6. Performance enhancements

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

---

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

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

---

## 8. How to extend

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

---

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