@csszyx/dynamic 0.4.0 β 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-test.log +24 -0
- package/.turbo/turbo-type-check.log +5 -0
- package/package.json +2 -2
- package/src/index.ts +16 -6
- package/tests/react.test.ts +221 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
> @csszyx/dynamic@0.4.0 test /Users/tiennguyen/Projects/csszyx/packages/dynamic
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[7m[1m[36m RUN [39m[22m[27m [36mv1.6.1[39m [90m/Users/tiennguyen/Projects/csszyx/packages/dynamic[39m
|
|
7
|
+
|
|
8
|
+
[?25l [90mΒ·[39m [2mtests/[22mmanifest[2m.test.ts[22m[2m (12)[22m
|
|
9
|
+
[90mΒ·[39m [2mtests/[22mreact[2m.test.ts[22m[2m (14)[22m
|
|
10
|
+
[?25l[2K[1A[2K[1A[2K[G [32mβ[39m [2mtests/[22mmanifest[2m.test.ts[22m[2m (12)[22m
|
|
11
|
+
[32mβ[39m [2mtests/[22mreact[2m.test.ts[22m[2m (14)[22m
|
|
12
|
+
[32mβ[39m [2mtests/[22minjector[2m.test.ts[22m[2m (23)[22m
|
|
13
|
+
[90mΒ·[39m [2mtests/[22mcss-generator[2m.test.ts[22m[2m (65)[22m
|
|
14
|
+
[2K[1A[2K[1A[2K[1A[2K[1A[2K[G [32mβ[39m [2mtests/[22mmanifest[2m.test.ts[22m[2m (12)[22m
|
|
15
|
+
[32mβ[39m [2mtests/[22mreact[2m.test.ts[22m[2m (14)[22m
|
|
16
|
+
[32mβ[39m [2mtests/[22minjector[2m.test.ts[22m[2m (23)[22m
|
|
17
|
+
[32mβ[39m [2mtests/[22mcss-generator[2m.test.ts[22m[2m (65)[22m
|
|
18
|
+
|
|
19
|
+
[2m Test Files [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
20
|
+
[2m Tests [22m [1m[32m114 passed[39m[22m[90m (114)[39m
|
|
21
|
+
[2m Start at [22m 22:00:06
|
|
22
|
+
[2m Duration [22m 328ms[2m (transform 130ms, setup 0ms, collect 287ms, tests 38ms, environment 0ms, prepare 323ms)[22m
|
|
23
|
+
|
|
24
|
+
[?25h[?25h
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@csszyx/dynamic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Runtime CSS injection engine for @csszyx β injects only CSS not already in the pre-built stylesheet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@csszyx/compiler": "0.
|
|
26
|
+
"@csszyx/compiler": "0.6.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"react": ">=18.0.0"
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// Import only the browser-safe (pure JS, no WASM) transform sub-path.
|
|
23
23
|
// The main '@csszyx/compiler' entry bundles @csszyx/core (WASM) which cannot
|
|
24
24
|
// run in browsers without a dedicated WASM loader.
|
|
25
|
-
import type { SzObject } from '@csszyx/compiler/browser';
|
|
25
|
+
import type { ReadonlySzObject, SzObject } from '@csszyx/compiler/browser';
|
|
26
26
|
import { transform } from '@csszyx/compiler/browser';
|
|
27
27
|
|
|
28
28
|
import { generateCSSRule } from './css-generator.js';
|
|
@@ -32,7 +32,7 @@ import { isServer } from './ssr.js';
|
|
|
32
32
|
|
|
33
33
|
export type { Tier } from './css-generator.js';
|
|
34
34
|
export type { CSSManifest } from './manifest.js';
|
|
35
|
-
export type { SzObject } from '@csszyx/compiler/browser';
|
|
35
|
+
export type { ReadonlySzObject, SzObject } from '@csszyx/compiler/browser';
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Transforms sz props at runtime and injects CSS only for classes not already
|
|
@@ -40,19 +40,29 @@ export type { SzObject } from '@csszyx/compiler/browser';
|
|
|
40
40
|
*
|
|
41
41
|
* SSR-safe: on server, returns class names without CSSOM access.
|
|
42
42
|
*
|
|
43
|
-
* @param szProps - sz object (e.g. { p: 4, bg: 'blue-500', hover: { bg: 'blue-600' } })
|
|
43
|
+
* @param szProps - sz object (e.g. { p: 4, bg: 'blue-500', hover: { bg: 'blue-600' } }). Accepts both mutable and `as const` objects.
|
|
44
44
|
* @returns space-separated class string (e.g. "p-4 bg-blue-500 hover:bg-blue-600")
|
|
45
45
|
*
|
|
46
46
|
* @example
|
|
47
47
|
* const cls = dynamic({ p: 4, bg: 'blue-500' });
|
|
48
48
|
* // β "p-4 bg-blue-500" (injects CSS for classes not in built stylesheet)
|
|
49
49
|
*/
|
|
50
|
-
export function dynamic(szProps: SzObject): string {
|
|
51
|
-
const { className } = transform(szProps);
|
|
50
|
+
export function dynamic(szProps: SzObject | ReadonlySzObject): string {
|
|
51
|
+
const { className } = transform(szProps as SzObject);
|
|
52
52
|
if (!className) {return '';}
|
|
53
53
|
|
|
54
54
|
if (isServer) {
|
|
55
|
-
// SSR:
|
|
55
|
+
// SSR: apply the mangle map if the Vite plugin exposed it via globalThis.
|
|
56
|
+
// The plugin sets __csszyx_ssr_mangle_map in buildEnd(), which runs before
|
|
57
|
+
// SSG rendering (Astro, Next.js) in the same Node.js process. Without this,
|
|
58
|
+
// dynamic() returns unmangled names (e.g. "p-4") while built CSS only has
|
|
59
|
+
// mangled selectors (e.g. ".q0"), so styles silently don't apply in SSR HTML.
|
|
60
|
+
const ssrMangleMap = (globalThis as Record<string, unknown>).__csszyx_ssr_mangle_map as Record<string, string> | undefined;
|
|
61
|
+
if (ssrMangleMap) {
|
|
62
|
+
return className.split(' ').filter(Boolean)
|
|
63
|
+
.map(c => ssrMangleMap[c] ?? c)
|
|
64
|
+
.join(' ');
|
|
65
|
+
}
|
|
56
66
|
return className;
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the React integration layer (@csszyx/dynamic/react).
|
|
3
|
+
*
|
|
4
|
+
* React hooks cannot run outside a React render tree, so we mock the `react`
|
|
5
|
+
* module to capture the effect callbacks passed to useEffect/useCallback/useContext.
|
|
6
|
+
* This lets us invoke those callbacks directly and verify side-effects without
|
|
7
|
+
* React Testing Library or a DOM environment.
|
|
8
|
+
*
|
|
9
|
+
* Coverage:
|
|
10
|
+
* - sz export is the dynamic() alias
|
|
11
|
+
* - useSz() returns { sz: fn } with a stable reference
|
|
12
|
+
* - useSz() calls preloadManifest on mount
|
|
13
|
+
* - useSz() schedules deferred cleanup (injectorCleanup + resetManifest) on unmount
|
|
14
|
+
* - useSz() cancels pending cleanup when remounted (StrictMode resilience)
|
|
15
|
+
* - CsszyxProvider calls setManifestUrl + preloadManifest on mount
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
19
|
+
|
|
20
|
+
// ββ Captured hook state βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
type EffectFn = () => (() => void) | void;
|
|
26
|
+
|
|
27
|
+
let capturedEffects: EffectFn[] = [];
|
|
28
|
+
let capturedCallback: ((...args: unknown[]) => unknown) | null = null;
|
|
29
|
+
let contextValue = { manifestUrl: '/csszyx-manifest.json' };
|
|
30
|
+
|
|
31
|
+
// ββ Mock react ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
32
|
+
|
|
33
|
+
vi.mock('react', () => ({
|
|
34
|
+
createContext: vi.fn((defaultValue: unknown) => ({ _default: defaultValue, Provider: 'CsszyxContext.Provider' })),
|
|
35
|
+
createElement: vi.fn(
|
|
36
|
+
(type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) =>
|
|
37
|
+
({ type, props, children }),
|
|
38
|
+
),
|
|
39
|
+
useCallback: vi.fn((fn: (...args: unknown[]) => unknown) => {
|
|
40
|
+
capturedCallback = fn;
|
|
41
|
+
return fn;
|
|
42
|
+
}),
|
|
43
|
+
useContext: vi.fn(() => contextValue),
|
|
44
|
+
useEffect: vi.fn((fn: EffectFn) => {
|
|
45
|
+
capturedEffects.push(fn);
|
|
46
|
+
}),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// ββ Mock internal modules βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
50
|
+
|
|
51
|
+
const mockDynamic = vi.fn((props: Record<string, unknown>) => `class-${JSON.stringify(props)}`);
|
|
52
|
+
vi.mock('../src/index.js', () => ({ dynamic: mockDynamic }));
|
|
53
|
+
|
|
54
|
+
const mockInjectorCleanup = vi.fn();
|
|
55
|
+
vi.mock('../src/injector.js', () => ({ cleanup: mockInjectorCleanup }));
|
|
56
|
+
|
|
57
|
+
const mockPreloadManifest = vi.fn();
|
|
58
|
+
const mockResetManifest = vi.fn();
|
|
59
|
+
const mockSetManifestUrl = vi.fn();
|
|
60
|
+
vi.mock('../src/manifest.js', () => ({
|
|
61
|
+
preloadManifest: mockPreloadManifest,
|
|
62
|
+
resetManifest: mockResetManifest,
|
|
63
|
+
setManifestUrl: mockSetManifestUrl,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// ββ Import subject under test (after mocks) βββββββββββββββββββββββββββββββββββ
|
|
67
|
+
|
|
68
|
+
const { sz, useSz, CsszyxProvider } = await import('../src/react.js');
|
|
69
|
+
|
|
70
|
+
// ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Runs all captured effects and returns their cleanup functions.
|
|
74
|
+
* @returns array of cleanup functions returned by each effect (undefined entries filtered)
|
|
75
|
+
*/
|
|
76
|
+
function runCapturedEffects(): Array<() => void> {
|
|
77
|
+
return capturedEffects
|
|
78
|
+
.map(fn => fn())
|
|
79
|
+
.filter((r): r is () => void => typeof r === 'function');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ββ Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
83
|
+
|
|
84
|
+
describe('sz export', () => {
|
|
85
|
+
it('is the dynamic() alias', () => {
|
|
86
|
+
expect(sz).toBe(mockDynamic);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('passes props through to dynamic()', () => {
|
|
90
|
+
sz({ p: 4, bg: 'blue-500' } as never);
|
|
91
|
+
expect(mockDynamic).toHaveBeenCalledWith({ p: 4, bg: 'blue-500' });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('useSz()', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
capturedEffects = [];
|
|
98
|
+
capturedCallback = null;
|
|
99
|
+
contextValue = { manifestUrl: '/csszyx-manifest.json' };
|
|
100
|
+
vi.useFakeTimers();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
vi.useRealTimers();
|
|
105
|
+
vi.clearAllMocks();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns an object with a sz function', () => {
|
|
109
|
+
const result = useSz();
|
|
110
|
+
expect(result).toHaveProperty('sz');
|
|
111
|
+
expect(typeof result.sz).toBe('function');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sz function delegates to dynamic()', () => {
|
|
115
|
+
const { sz: hookSz } = useSz();
|
|
116
|
+
hookSz({ m: 2 } as never);
|
|
117
|
+
expect(mockDynamic).toHaveBeenCalledWith({ m: 2 });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('sz reference is stable (useCallback wraps it)', () => {
|
|
121
|
+
const { sz: hookSz } = useSz();
|
|
122
|
+
// capturedCallback is what useCallback returned β same as hookSz
|
|
123
|
+
expect(hookSz).toBe(capturedCallback);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('reads manifestUrl from context', () => {
|
|
127
|
+
contextValue = { manifestUrl: '/custom/path.json' };
|
|
128
|
+
useSz();
|
|
129
|
+
runCapturedEffects();
|
|
130
|
+
expect(mockPreloadManifest).toHaveBeenCalledWith('/custom/path.json');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('calls preloadManifest with default URL on mount', () => {
|
|
134
|
+
useSz();
|
|
135
|
+
runCapturedEffects();
|
|
136
|
+
expect(mockPreloadManifest).toHaveBeenCalledWith('/csszyx-manifest.json');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('schedules deferred cleanup on unmount', () => {
|
|
140
|
+
useSz();
|
|
141
|
+
const cleanups = runCapturedEffects();
|
|
142
|
+
// Run all cleanup functions (simulate unmount)
|
|
143
|
+
cleanups.forEach(fn => fn());
|
|
144
|
+
// Cleanup should NOT have fired yet (timer pending)
|
|
145
|
+
expect(mockInjectorCleanup).not.toHaveBeenCalled();
|
|
146
|
+
expect(mockResetManifest).not.toHaveBeenCalled();
|
|
147
|
+
// After timer fires, cleanup runs
|
|
148
|
+
vi.runAllTimers();
|
|
149
|
+
expect(mockInjectorCleanup).toHaveBeenCalledOnce();
|
|
150
|
+
expect(mockResetManifest).toHaveBeenCalledOnce();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('cancels pending cleanup when remounted (StrictMode resilience)', () => {
|
|
154
|
+
// First mount
|
|
155
|
+
useSz();
|
|
156
|
+
const firstCleanups = runCapturedEffects();
|
|
157
|
+
|
|
158
|
+
// StrictMode unmount β schedules cleanup timer
|
|
159
|
+
firstCleanups.forEach(fn => fn());
|
|
160
|
+
expect(mockInjectorCleanup).not.toHaveBeenCalled();
|
|
161
|
+
|
|
162
|
+
// StrictMode remount β second useSz() call, effect runs again, cancels timer
|
|
163
|
+
capturedEffects = [];
|
|
164
|
+
useSz();
|
|
165
|
+
runCapturedEffects();
|
|
166
|
+
|
|
167
|
+
// Timer fires β should have been cancelled by the second mount
|
|
168
|
+
vi.runAllTimers();
|
|
169
|
+
expect(mockInjectorCleanup).not.toHaveBeenCalled();
|
|
170
|
+
expect(mockResetManifest).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('runs cleanup after true unmount (no remount)', () => {
|
|
174
|
+
useSz();
|
|
175
|
+
const cleanups = runCapturedEffects();
|
|
176
|
+
cleanups.forEach(fn => fn());
|
|
177
|
+
// No remount β timer fires
|
|
178
|
+
vi.runAllTimers();
|
|
179
|
+
expect(mockInjectorCleanup).toHaveBeenCalledOnce();
|
|
180
|
+
expect(mockResetManifest).toHaveBeenCalledOnce();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('CsszyxProvider', () => {
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
capturedEffects = [];
|
|
187
|
+
vi.clearAllMocks();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('calls setManifestUrl with the manifest prop', () => {
|
|
191
|
+
CsszyxProvider({ manifest: '/api/manifest.json', children: null });
|
|
192
|
+
runCapturedEffects();
|
|
193
|
+
expect(mockSetManifestUrl).toHaveBeenCalledWith('/api/manifest.json');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('calls preloadManifest with the manifest prop', () => {
|
|
197
|
+
CsszyxProvider({ manifest: '/api/manifest.json', children: null });
|
|
198
|
+
runCapturedEffects();
|
|
199
|
+
expect(mockPreloadManifest).toHaveBeenCalledWith('/api/manifest.json');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('re-runs effect when manifest URL changes', () => {
|
|
203
|
+
CsszyxProvider({ manifest: '/v1/manifest.json', children: null });
|
|
204
|
+
CsszyxProvider({ manifest: '/v2/manifest.json', children: null });
|
|
205
|
+
runCapturedEffects();
|
|
206
|
+
expect(mockSetManifestUrl).toHaveBeenCalledWith('/v1/manifest.json');
|
|
207
|
+
expect(mockSetManifestUrl).toHaveBeenCalledWith('/v2/manifest.json');
|
|
208
|
+
expect(mockPreloadManifest).toHaveBeenCalledWith('/v2/manifest.json');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('renders children via createElement', async () => {
|
|
212
|
+
const { createElement } = await import('react');
|
|
213
|
+
const children = 'child-content';
|
|
214
|
+
CsszyxProvider({ manifest: '/manifest.json', children });
|
|
215
|
+
expect(createElement).toHaveBeenCalledWith(
|
|
216
|
+
expect.anything(),
|
|
217
|
+
expect.objectContaining({ value: { manifestUrl: '/manifest.json' } }),
|
|
218
|
+
children,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|