@csszyx/dynamic 0.7.0 → 0.9.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/dist/index.d.mts +92 -0
- package/dist/index.mjs +2 -0
- package/dist/react.d.mts +86 -0
- package/dist/react.mjs +37 -0
- package/dist/shared/dynamic.DJAOP9e8.mjs +1149 -0
- package/package.json +22 -8
- package/src/css-generator.ts +0 -1003
- package/src/index.ts +0 -122
- package/src/injector.ts +0 -226
- package/src/manifest.ts +0 -119
- package/src/react.ts +0 -166
- package/src/ssr.ts +0 -5
- package/tests/css-generator.test.ts +0 -339
- package/tests/injector.test.ts +0 -222
- package/tests/manifest.test.ts +0 -177
- package/tests/react.test.ts +0 -221
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -8
package/tests/manifest.test.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for manifest module.
|
|
3
|
-
* Verifies manifest loading, caching, and delta check logic.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
type CSSManifest,
|
|
10
|
-
ensureManifest,
|
|
11
|
-
isManifestLoaded,
|
|
12
|
-
lookupManifest,
|
|
13
|
-
preloadManifest,
|
|
14
|
-
resetManifest,
|
|
15
|
-
} from '../src/manifest.js';
|
|
16
|
-
|
|
17
|
-
// Mock fetch globally
|
|
18
|
-
const mockFetch = vi.fn();
|
|
19
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Creates a complete CSSManifest fixture with sensible defaults for testing.
|
|
23
|
-
*
|
|
24
|
-
* @param partial - optional overrides to merge into the default manifest
|
|
25
|
-
* @returns a fully-formed CSSManifest object suitable for use in tests
|
|
26
|
-
*/
|
|
27
|
-
function makeMockManifest(partial: Partial<CSSManifest> = {}): CSSManifest {
|
|
28
|
-
return {
|
|
29
|
-
version: '0.4.0',
|
|
30
|
-
buildId: 'test-build-id',
|
|
31
|
-
classes: ['p-4', 'bg-blue-500', 'flex', 'text-white'],
|
|
32
|
-
...partial,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Configures the global fetch mock to resolve successfully with the given manifest.
|
|
38
|
-
*
|
|
39
|
-
* @param manifest - the CSSManifest object that the mocked fetch should return
|
|
40
|
-
* @returns void
|
|
41
|
-
*/
|
|
42
|
-
function setupFetchSuccess(manifest: CSSManifest): void {
|
|
43
|
-
mockFetch.mockResolvedValueOnce({
|
|
44
|
-
ok: true,
|
|
45
|
-
json: () => Promise.resolve(manifest),
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe('manifest loading', () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
resetManifest();
|
|
52
|
-
mockFetch.mockReset();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
resetManifest();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('isManifestLoaded returns false before loading', () => {
|
|
60
|
-
expect(isManifestLoaded()).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('lookupManifest returns null before manifest loads', () => {
|
|
64
|
-
expect(lookupManifest('p-4')).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('preloadManifest fetches and caches manifest', async () => {
|
|
68
|
-
setupFetchSuccess(makeMockManifest());
|
|
69
|
-
await preloadManifest('/csszyx-manifest.json');
|
|
70
|
-
|
|
71
|
-
expect(isManifestLoaded()).toBe(true);
|
|
72
|
-
expect(mockFetch).toHaveBeenCalledWith('/csszyx-manifest.json');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('uses custom URL passed to preloadManifest', async () => {
|
|
76
|
-
setupFetchSuccess(makeMockManifest());
|
|
77
|
-
await preloadManifest('/custom/path/manifest.json');
|
|
78
|
-
|
|
79
|
-
expect(mockFetch).toHaveBeenCalledWith('/custom/path/manifest.json');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('coalesces concurrent ensureManifest calls', async () => {
|
|
83
|
-
setupFetchSuccess(makeMockManifest());
|
|
84
|
-
|
|
85
|
-
// Trigger two concurrent loads
|
|
86
|
-
const p1 = ensureManifest();
|
|
87
|
-
const p2 = ensureManifest();
|
|
88
|
-
await Promise.all([p1, p2]);
|
|
89
|
-
|
|
90
|
-
// Only one fetch should have been made
|
|
91
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('gracefully handles fetch failure — treats all classes as new', async () => {
|
|
95
|
-
mockFetch.mockRejectedValueOnce(new Error('network error'));
|
|
96
|
-
await ensureManifest();
|
|
97
|
-
|
|
98
|
-
expect(isManifestLoaded()).toBe(true); // loaded (empty fallback)
|
|
99
|
-
expect(lookupManifest('p-4')).toBeNull(); // not in empty set
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('gracefully handles non-ok response', async () => {
|
|
103
|
-
mockFetch.mockResolvedValueOnce({ ok: false, status: 404 });
|
|
104
|
-
await ensureManifest();
|
|
105
|
-
|
|
106
|
-
expect(isManifestLoaded()).toBe(true);
|
|
107
|
-
expect(lookupManifest('p-4')).toBeNull();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('manifest delta check', () => {
|
|
112
|
-
beforeEach(async () => {
|
|
113
|
-
resetManifest();
|
|
114
|
-
mockFetch.mockReset();
|
|
115
|
-
setupFetchSuccess(makeMockManifest());
|
|
116
|
-
await preloadManifest();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
afterEach(() => {
|
|
120
|
-
resetManifest();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('returns original class name for class in manifest (no mangle map)', () => {
|
|
124
|
-
expect(lookupManifest('p-4')).toBe('p-4');
|
|
125
|
-
expect(lookupManifest('bg-blue-500')).toBe('bg-blue-500');
|
|
126
|
-
expect(lookupManifest('flex')).toBe('flex');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('returns null for class not in manifest', () => {
|
|
130
|
-
expect(lookupManifest('p-7')).toBeNull(); // not in test manifest
|
|
131
|
-
expect(lookupManifest('bg-red-300')).toBeNull();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('returns mangled name when mangle map is present', async () => {
|
|
135
|
-
resetManifest();
|
|
136
|
-
mockFetch.mockReset();
|
|
137
|
-
setupFetchSuccess(makeMockManifest({
|
|
138
|
-
classes: ['p-4', 'bg-blue-500'],
|
|
139
|
-
mangleMap: { 'p-4': 'z', 'bg-blue-500': 'y' },
|
|
140
|
-
}));
|
|
141
|
-
await preloadManifest();
|
|
142
|
-
|
|
143
|
-
expect(lookupManifest('p-4')).toBe('z');
|
|
144
|
-
expect(lookupManifest('bg-blue-500')).toBe('y');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('returns original name for class in manifest but not in mangle map', async () => {
|
|
148
|
-
resetManifest();
|
|
149
|
-
mockFetch.mockReset();
|
|
150
|
-
setupFetchSuccess(makeMockManifest({
|
|
151
|
-
classes: ['p-4', 'flex'],
|
|
152
|
-
mangleMap: { 'p-4': 'z' }, // 'flex' not in mangle map
|
|
153
|
-
}));
|
|
154
|
-
await preloadManifest();
|
|
155
|
-
|
|
156
|
-
expect(lookupManifest('p-4')).toBe('z');
|
|
157
|
-
expect(lookupManifest('flex')).toBe('flex'); // original name
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe('resetManifest', () => {
|
|
162
|
-
it('clears loaded state and allows re-fetch', async () => {
|
|
163
|
-
setupFetchSuccess(makeMockManifest());
|
|
164
|
-
await preloadManifest();
|
|
165
|
-
expect(isManifestLoaded()).toBe(true);
|
|
166
|
-
|
|
167
|
-
resetManifest();
|
|
168
|
-
expect(isManifestLoaded()).toBe(false);
|
|
169
|
-
expect(lookupManifest('p-4')).toBeNull();
|
|
170
|
-
|
|
171
|
-
// Can reload after reset
|
|
172
|
-
setupFetchSuccess(makeMockManifest({ classes: ['new-class'] }));
|
|
173
|
-
await preloadManifest();
|
|
174
|
-
expect(lookupManifest('new-class')).toBe('new-class');
|
|
175
|
-
expect(lookupManifest('p-4')).toBeNull(); // old class gone
|
|
176
|
-
});
|
|
177
|
-
});
|
package/tests/react.test.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
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
|
-
});
|
package/tsconfig.json
DELETED