diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 07c7ba32..51fd2305 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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 @@ -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 }} diff --git a/CONTEXT.md b/CONTEXT.md index 2f72a3aa..394aa141 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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. @@ -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. diff --git a/packages/uniwind/src/core/web/cssListener.ts b/packages/uniwind/src/core/web/cssListener.ts index 6e961c13..245a6ebc 100644 --- a/packages/uniwind/src/core/web/cssListener.ts +++ b/packages/uniwind/src/core/web/cssListener.ts @@ -123,6 +123,11 @@ class CSSListenerBuilder { private initialize() { this.pendingInitialization = undefined + + if (typeof document === 'undefined') { + return + } + this.pruneStaleRules() let added = false diff --git a/packages/uniwind/tests/web/core/css-listener.test.ts b/packages/uniwind/tests/web/core/css-listener.test.ts index 8c07e8c7..9e09f86f 100644 --- a/packages/uniwind/tests/web/core/css-listener.test.ts +++ b/packages/uniwind/tests/web/core/css-listener.test.ts @@ -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)