Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ jobs:
run: bun run check:typescript:test
working-directory: packages/uniwind

- name: Check linting and formatting
- name: Check linting
run: bun run lint

- name: Check formatting
run: bun run check:format

- name: Check circular dependencies
run: bun run circular:check

Expand Down Expand Up @@ -101,9 +104,10 @@ jobs:
ARGS="$ARGS --dry-run"
fi

npx release-it $ARGS
bun run release $ARGS
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HUSKY: '0'

- name: Close pending release issues
if: ${{ inputs.dry_run == false }}
Expand Down
4 changes: 3 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Web runtime:

- Web keeps styles in CSS and passes `{ $$css: true, tailwind: className }` through RNW style arrays.
- `getWebStyles` uses a hidden DOM element to compute style values when a JS value is needed, such as color extraction or `useResolveClassNames`.
- `CSSListener` tracks active CSS rules and media queries, then notifies subscribers when class-dependent media rules change. After scanning newly discovered stylesheets, it emits a variables notification so JS-resolved styles refresh when CSS arrives after module initialization, including Metro web development startup.
- `CSSListener` tracks active CSS rules and media queries, then notifies subscribers when class-dependent media rules change. After scanning newly discovered stylesheets, it emits a variables notification so JS-resolved styles refresh when CSS arrives after module initialization, including Metro web development startup. Deferred scans safely return if `document` has been removed before they run, such as during test environment teardown.
- `ScopedTheme` renders a `div` with the theme class and `display: contents` on web.
- `LayoutDirection` renders a contents-style wrapper with `direction`/`dir` semantics so RTL/LTR variants can be scoped to a subtree.
- `ScopedVariables` renders a `display: contents` wrapper and sets its variables as inline custom properties on that wrapper, so the real DOM cascade resolves `var(--name)` to the scoped value for every descendant (numbers become px). During JS reads (`getWebVariable` / `useResolveClassNames`) it also applies the variables to the hidden `dummyParent`, then clears them.
Expand Down Expand Up @@ -203,6 +203,8 @@ Package scripts:

Root scripts use Turbo for monorepo-wide build, typecheck, lint, test, format, and circular checks.

The release workflow runs the build, type checks, lint, formatting, circular dependency checks, and all test suites before releasing. It uses release-it to bump the version and generate the changelog, then follows release-it's default order: publish to npm, push the release commit/tag, and create the GitHub release. Husky is disabled for the release commit because the workflow has already run the checks. The package's release-it configuration controls npm provenance, public access, and prerelease tags. Dry runs use release-it's `--dry-run`, and pending release issues are closed only after the full release succeeds.

Testing layout:

- `tests/native`: component behavior and native style parsing.
Expand Down
5 changes: 5 additions & 0 deletions packages/uniwind/src/core/web/cssListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ class CSSListenerBuilder {

private initialize() {
this.pendingInitialization = undefined

if (typeof document === 'undefined') {
return
}

this.pruneStaleRules()
let added = false

Expand Down
35 changes: 35 additions & 0 deletions packages/uniwind/tests/web/core/css-listener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ afterEach(() => {
vi.unstubAllGlobals()
})

test.each(['timeout', 'idle callback'])('ignores a pending %s scan after document teardown', async scheduler => {
vi.useFakeTimers()
vi.stubGlobal(
'requestIdleCallback',
scheduler === 'idle callback'
? (callback: VoidFunction) => setTimeout(callback, 50)
: undefined,
)
vi.stubGlobal('cancelIdleCallback', (handle: number) => clearTimeout(handle))

const style = document.createElement('style')
style.textContent = '.accent-teardown { accent-color: rgb(255, 0, 0); }'

const listener = vi.fn()
const dispose = CSSListener.subscribeToClassName('accent-teardown', listener)

try {
document.head.appendChild(style)
await Promise.resolve()
expect(vi.getTimerCount()).toBeGreaterThan(0)

vi.stubGlobal('document', undefined)

await vi.advanceTimersByTimeAsync(50)
expect(vi.getTimerCount()).toBe(0)
expect(listener).not.toHaveBeenCalled()
} finally {
vi.unstubAllGlobals()
dispose()
style.remove()
await Promise.resolve()
await vi.advanceTimersByTimeAsync(50)
}
})

test('notifies class subscribers after discovering a stylesheet injected later', async () => {
vi.useFakeTimers()
vi.stubGlobal('requestIdleCallback', undefined)
Expand Down
Loading