From 0fa844e6a6eb5d74a9e36fcb8d39114a82537d56 Mon Sep 17 00:00:00 2001 From: Martijn van Dijk Date: Fri, 28 Aug 2026 13:06:51 +0200 Subject: [PATCH 1/2] fix: read hydrated permissions on first Solid, Svelte, and Vue render Subscribe to setup/ready during render instead of after an effect so dehydrated booleans and already-initialized rules are visible on the first paint. Co-authored-by: Cursor --- permix/src/solid/components.test.tsx | 37 ++++++++++++++++ permix/src/solid/components.tsx | 9 +--- permix/src/solid/hooks.test.tsx | 29 +++++++++++++ .../__fixtures__/HydrateConsumer.svelte | 1 + permix/src/svelte/components.test.ts | 25 +++++++++++ permix/src/svelte/context.svelte.ts | 22 +++++----- permix/src/svelte/hooks.test.ts | 18 ++++++++ permix/src/vue/components.test.ts | 42 +++++++++++++++++++ permix/src/vue/composables.test.ts | 24 +++++++++++ 9 files changed, 188 insertions(+), 19 deletions(-) diff --git a/permix/src/solid/components.test.tsx b/permix/src/solid/components.test.tsx index bbe8f56a..484a98e7 100644 --- a/permix/src/solid/components.test.tsx +++ b/permix/src/solid/components.test.tsx @@ -41,6 +41,43 @@ describe('components', () => { expect(container.firstChild).toHaveTextContent('true') }) + it('uses dehydrated rules on the first render', () => { + const permixServer = createPermix<{ + post: ['create', 'read'] + }>() + + permixServer.setup({ + post: { + create: true, + read: false, + }, + }) + + const dehydrated = permixServer.dehydrate() + const permixClient = createPermix<{ + post: ['create', 'read'] + }>() + + const TestComponent = () => { + const { check, isReady } = usePermix(permixClient) + return ( +
+ {check('post.create').toString()}:{isReady().toString()} +
+ ) + } + + const { container } = render(() => ( + + + + + + )) + + expect(container.firstChild).toHaveTextContent('true:false') + }) + it('should work with Check component', () => { const permix = createPermix<{ post: ['create'] diff --git a/permix/src/solid/components.tsx b/permix/src/solid/components.tsx index cf12e87a..1d5b2cd5 100644 --- a/permix/src/solid/components.tsx +++ b/permix/src/solid/components.tsx @@ -1,10 +1,5 @@ import type { JSX } from 'solid-js' -import { - createEffect, - createMemo, - createRenderEffect, - onCleanup, -} from 'solid-js' +import { createMemo, createRenderEffect, onCleanup } from 'solid-js' import { createStore } from 'solid-js/store' import type { @@ -33,7 +28,7 @@ export function PermixProvider(props: { rules: props.permix.getRules(), }) - createEffect(() => { + createRenderEffect(() => { const setup = props.permix.hook('setup', () => { setContext('rules', props.permix.getRules()) }) diff --git a/permix/src/solid/hooks.test.tsx b/permix/src/solid/hooks.test.tsx index fb84133b..ab1b63f6 100644 --- a/permix/src/solid/hooks.test.tsx +++ b/permix/src/solid/hooks.test.tsx @@ -30,6 +30,35 @@ describe('permix solid', () => { expect(result.check('post.read')).toBe(false) }) + it('reads ready state on the first render when setup ran before subscribe', () => { + const permix = createPermix<{ + post: ['create'] + }>() + + permix.setup({ + post: { + create: true, + }, + }) + + const TestComponent = () => { + const { isReady, check } = usePermix(permix) + return ( +
+ {isReady().toString()}:{check('post.create').toString()} +
+ ) + } + + const { container } = render(() => , { + wrapper: (props) => ( + {props.children} + ), + }) + + expect(container.firstChild).toHaveTextContent('true:true') + }) + it('should work with DOM rerender', async () => { const permix = createPermix<{ post: [{ name: 'create'; type: { id: string } }, 'read'] diff --git a/permix/src/svelte/__fixtures__/HydrateConsumer.svelte b/permix/src/svelte/__fixtures__/HydrateConsumer.svelte index d00a05bb..90e92694 100644 --- a/permix/src/svelte/__fixtures__/HydrateConsumer.svelte +++ b/permix/src/svelte/__fixtures__/HydrateConsumer.svelte @@ -9,3 +9,4 @@ const permissions = usePermix(permix)
{permissions.check('post.create').toString()}
+
{permissions.isReady.toString()}
diff --git a/permix/src/svelte/components.test.ts b/permix/src/svelte/components.test.ts index a999f398..366b15a1 100644 --- a/permix/src/svelte/components.test.ts +++ b/permix/src/svelte/components.test.ts @@ -35,6 +35,31 @@ describe('components', () => { expect(getByTestId('create')).toHaveTextContent('true') }) + it('uses dehydrated rules on the first render', () => { + const permixServer = createPermix<{ + post: ['create', 'read'] + }>() + + permixServer.setup({ + post: { + create: true, + read: false, + }, + }) + + const dehydrated = permixServer.dehydrate() + const permixClient = createPermix<{ + post: ['create', 'read'] + }>() + + const { getByTestId } = render(HydrateApp, { + props: { permix: permixClient, state: dehydrated }, + }) + + expect(getByTestId('create')).toHaveTextContent('true') + expect(getByTestId('ready')).toHaveTextContent('false') + }) + it('should work with Check component', () => { const permix = createPermix<{ post: ['create'] diff --git a/permix/src/svelte/context.svelte.ts b/permix/src/svelte/context.svelte.ts index 14ab311f..bf98e819 100644 --- a/permix/src/svelte/context.svelte.ts +++ b/permix/src/svelte/context.svelte.ts @@ -1,4 +1,4 @@ -import { getContext, setContext } from 'svelte' +import { getContext, onDestroy, setContext } from 'svelte' import type { Definition, Permix, Rules } from '../core' import { createCheck } from '../core' @@ -27,18 +27,16 @@ export function providePermix(permix: Permix): void { setContext(PERMIX_CONTEXT_KEY, context) - $effect(() => { - const setup = permix.hook('setup', () => { - context.rules = permix.getRules() - }) - const ready = permix.hook('ready', () => { - context.isReady = permix.isReady() - }) + const setup = permix.hook('setup', () => { + context.rules = permix.getRules() + }) + const ready = permix.hook('ready', () => { + context.isReady = permix.isReady() + }) - return () => { - setup() - ready() - } + onDestroy(() => { + setup() + ready() }) } diff --git a/permix/src/svelte/hooks.test.ts b/permix/src/svelte/hooks.test.ts index 8e37ca67..88f53366 100644 --- a/permix/src/svelte/hooks.test.ts +++ b/permix/src/svelte/hooks.test.ts @@ -25,6 +25,24 @@ describe('permix svelte', () => { expect(getByTestId('read')).toHaveTextContent('false') }) + it('reads ready state on the first render when setup ran before subscribe', () => { + const permix = createPermix<{ + post: [{ name: 'create'; type: { id: string } }, 'read'] + }>() + + permix.setup({ + post: { + create: () => true, + read: true, + }, + }) + + const { getByTestId } = render(HookApp, { props: { permix } }) + + expect(getByTestId('ready')).toHaveTextContent('true') + expect(getByTestId('read')).toHaveTextContent('true') + }) + it('should work with DOM rerender', async () => { const permix = createPermix<{ post: [{ name: 'create'; type: { id: string } }, 'read'] diff --git a/permix/src/vue/components.test.ts b/permix/src/vue/components.test.ts index 83508d29..769e61e6 100644 --- a/permix/src/vue/components.test.ts +++ b/permix/src/vue/components.test.ts @@ -50,6 +50,48 @@ describe('components', () => { expect(wrapper.text()).toBe('true') }) + it('uses dehydrated rules on the first render', () => { + const permixServer = createPermix<{ + post: ['create', 'read'] + }>() + + permixServer.setup({ + post: { + create: true, + read: false, + }, + }) + + const dehydrated = permixServer.dehydrate() + const permixClient = createPermix<{ + post: ['create', 'read'] + }>() + + const TestComponent = { + template: "
{{ check('post.create') }}:{{ isReady }}
", + setup() { + const { check, isReady } = usePermix(permixClient) + return { check, isReady } + }, + } + + const wrapper = mount({ + template: ` + + + + + + `, + components: { PermixProvider, PermixHydrate, TestComponent }, + setup() { + return { permix: permixClient, dehydrated } + }, + }) + + expect(wrapper.text()).toBe('true:false') + }) + it('should work with Check component', () => { const permix = createPermix<{ post: ['create'] diff --git a/permix/src/vue/composables.test.ts b/permix/src/vue/composables.test.ts index e9807aaf..341fb560 100644 --- a/permix/src/vue/composables.test.ts +++ b/permix/src/vue/composables.test.ts @@ -56,6 +56,30 @@ describe('composables', () => { expect(wrapper.get('[data-testid="read"]').text()).toBe('false') }) + it('reads ready state on the first render when setup ran before subscribe', () => { + const permix = createPermix<{ + post: ['create'] + }>() + + permix.setup({ + post: { + create: true, + }, + }) + + const TestWrapper = defineComponent({ + template: '
{{ isReady }}:{{ check("post.create") }}
', + setup() { + const { check, isReady } = usePermix(permix) + return { check, isReady } + }, + }) + + const wrapper = mountWithPermix(TestWrapper, permix) + + expect(wrapper.get('div').text()).toBe('true:true') + }) + it('should work with DOM rerender', async () => { const permix = createPermix<{ post: [{ name: 'create'; type: { id: string } }, 'read'] From 3fddf3c398aa969d05c8ab7edd3a8a7bb15d2184 Mon Sep 17 00:00:00 2001 From: Martijn van Dijk Date: Fri, 28 Aug 2026 13:30:34 +0200 Subject: [PATCH 2/2] fix: skip vendored agent skills in Oxfmt and Oxlint Toolchain skills from skills.sh fail format check and are not project source. Co-authored-by: Cursor --- ignores.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ignores.ts b/ignores.ts index 9e77439e..8edf9972 100644 --- a/ignores.ts +++ b/ignores.ts @@ -9,6 +9,7 @@ export const ignorePatterns = [ '**/out/**', '**/node_modules/**', '**/_artifacts/**', + '**/.agents/**', '**/next-env.d.ts', '**/*.gen.ts', '**/*.generated.ts',