@compiled/vite-plugin 1.0.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.
@@ -0,0 +1,415 @@
1
+ import compiledVitePlugin from '../index';
2
+
3
+ describe('CSS Extraction', () => {
4
+ describe('Extract mode transformation', () => {
5
+ const originalEnv = process.env.NODE_ENV;
6
+
7
+ beforeEach(() => {
8
+ process.env.NODE_ENV = 'production';
9
+ });
10
+
11
+ afterEach(() => {
12
+ process.env.NODE_ENV = originalEnv;
13
+ });
14
+
15
+ it('should transform code with extraction enabled', async () => {
16
+ const plugin = compiledVitePlugin({ extract: true });
17
+ const code = `
18
+ import { css } from '@compiled/react';
19
+
20
+ export const Component = () => (
21
+ <div css={css({ color: 'red', fontSize: '14px' })}>
22
+ Hello
23
+ </div>
24
+ );
25
+ `;
26
+
27
+ const result = await plugin.transform!(code, 'test.tsx');
28
+
29
+ expect(result).toBeTruthy();
30
+ if (result && typeof result === 'object' && 'code' in result) {
31
+ // In extract mode, the code should not include the style rules inline
32
+ // but should still have the class names
33
+ expect(result.code).toMatch(/_syaz|_1wyb/);
34
+ }
35
+ });
36
+
37
+ it('should collect style rules during transformation', async () => {
38
+ const plugin = compiledVitePlugin({ extract: true });
39
+ const code = `
40
+ import { css } from '@compiled/react';
41
+
42
+ export const Component = () => (
43
+ <div css={css({ backgroundColor: '#00875a', padding: '20px' })}>
44
+ Content
45
+ </div>
46
+ );
47
+ `;
48
+
49
+ const result = await plugin.transform!(code, 'test.tsx');
50
+
51
+ expect(result).toBeTruthy();
52
+ // The actual CSS collection happens via metadata in the plugin
53
+ // which is tested via integration in the example builds
54
+ });
55
+ });
56
+
57
+ describe('generateBundle hook', () => {
58
+ const originalEnv = process.env.NODE_ENV;
59
+
60
+ beforeEach(() => {
61
+ process.env.NODE_ENV = 'production';
62
+ });
63
+
64
+ afterEach(() => {
65
+ process.env.NODE_ENV = originalEnv;
66
+ });
67
+
68
+ it('should have generateBundle hook', () => {
69
+ const plugin = compiledVitePlugin({ extract: true });
70
+
71
+ expect(plugin.generateBundle).toBeDefined();
72
+ expect(typeof plugin.generateBundle).toBe('function');
73
+ });
74
+
75
+ it('should emit extracted CSS file when extraction is enabled', async () => {
76
+ const plugin = compiledVitePlugin({ extract: true });
77
+ const emittedFiles: any[] = [];
78
+
79
+ // Mock context with emitFile
80
+ const context = {
81
+ emitFile: (file: any) => {
82
+ emittedFiles.push(file);
83
+ return 'mock-ref';
84
+ },
85
+ warn: jest.fn(),
86
+ };
87
+
88
+ // Transform some code first to collect styles
89
+ const code = `
90
+ import { css } from '@compiled/react';
91
+ export const Component = () => <div css={css({ margin: '10px' })}>Test</div>;
92
+ `;
93
+ await plugin.transform!(code, 'test.tsx');
94
+
95
+ // Call generateBundle with empty bundle
96
+ await plugin.generateBundle.call(context, {}, {});
97
+
98
+ // Check that extracted CSS file was emitted
99
+ expect(emittedFiles.length).toBe(1);
100
+ expect(emittedFiles[0]).toMatchObject({
101
+ type: 'asset',
102
+ name: 'compiled-extracted.css',
103
+ });
104
+ expect(typeof emittedFiles[0].source).toBe('string');
105
+ expect(emittedFiles[0].source.length).toBeGreaterThan(0);
106
+ });
107
+
108
+ it('should not emit CSS file when extraction is disabled', async () => {
109
+ const plugin = compiledVitePlugin({ extract: false });
110
+ const emittedFiles: any[] = [];
111
+
112
+ const context = {
113
+ emitFile: (file: any) => {
114
+ emittedFiles.push(file);
115
+ },
116
+ warn: jest.fn(),
117
+ };
118
+
119
+ const code = `
120
+ import { css } from '@compiled/react';
121
+ export const Component = () => <div css={css({ color: 'red' })}>Test</div>;
122
+ `;
123
+ await plugin.transform!(code, 'test.tsx');
124
+
125
+ await plugin.generateBundle.call(context, {}, {});
126
+
127
+ expect(emittedFiles.length).toBe(0);
128
+ });
129
+
130
+ it('should not emit CSS file in development mode', async () => {
131
+ process.env.NODE_ENV = 'development';
132
+ const plugin = compiledVitePlugin({ extract: true });
133
+ const emittedFiles: any[] = [];
134
+
135
+ const context = {
136
+ emitFile: (file: any) => {
137
+ emittedFiles.push(file);
138
+ },
139
+ warn: jest.fn(),
140
+ };
141
+
142
+ const code = `
143
+ import { css } from '@compiled/react';
144
+ export const Component = () => <div css={css({ color: 'red' })}>Test</div>;
145
+ `;
146
+ await plugin.transform!(code, 'test.tsx');
147
+
148
+ await plugin.generateBundle.call(context, {}, {});
149
+
150
+ expect(emittedFiles.length).toBe(0);
151
+ });
152
+
153
+ it('should apply sorting and deduplication to CSS assets containing Compiled styles', async () => {
154
+ const plugin = compiledVitePlugin({ extract: true });
155
+
156
+ // Mock bundle with a CSS asset containing Compiled styles
157
+ const bundle = {
158
+ 'index.css': {
159
+ type: 'asset',
160
+ fileName: 'index.css',
161
+ source: '._syaz13q2{color:blue}\n._syaz13q2{color:blue}\n._1wyb1fwx{font-size:12px}',
162
+ },
163
+ };
164
+
165
+ const context = {
166
+ emitFile: jest.fn(),
167
+ warn: jest.fn(),
168
+ };
169
+
170
+ // Call generateBundle
171
+ await plugin.generateBundle.call(context, {}, bundle);
172
+
173
+ // Check that the CSS was processed (duplicates removed and sorted)
174
+ const processedCss = bundle['index.css'].source as string;
175
+
176
+ // Should have removed duplicate
177
+ const blueColorMatches = (processedCss.match(/color:blue/g) || []).length;
178
+ expect(blueColorMatches).toBe(1);
179
+
180
+ // Should still contain both rules
181
+ expect(processedCss).toContain('color:blue');
182
+ expect(processedCss).toContain('font-size');
183
+ });
184
+
185
+ it('should not process CSS assets without Compiled atomic classes', async () => {
186
+ const plugin = compiledVitePlugin({ extract: true });
187
+
188
+ // Mock bundle with non-Compiled CSS
189
+ const originalCss = '.regular-class { color: red; }';
190
+ const bundle = {
191
+ 'other.css': {
192
+ type: 'asset',
193
+ fileName: 'other.css',
194
+ source: originalCss,
195
+ },
196
+ };
197
+
198
+ const context = {
199
+ emitFile: jest.fn(),
200
+ warn: jest.fn(),
201
+ };
202
+
203
+ // Call generateBundle
204
+ await plugin.generateBundle.call(context, {}, bundle);
205
+
206
+ // CSS should remain unchanged (no atomic classes to process)
207
+ expect(bundle['other.css'].source).toBe(originalCss);
208
+ });
209
+
210
+ it('should sort pseudo-selectors in the correct order', async () => {
211
+ const plugin = compiledVitePlugin({ extract: true });
212
+
213
+ // Mock bundle with pseudo-selectors in wrong order
214
+ // Correct order: :link → :visited → :focus-within → :focus → :hover → :active
215
+ const bundle = {
216
+ 'index.css': {
217
+ type: 'asset',
218
+ fileName: 'index.css',
219
+ source:
220
+ '._abc:active{color:red}._abc:hover{color:blue}._abc:focus{color:green}._abc:link{color:yellow}',
221
+ },
222
+ };
223
+
224
+ const context = {
225
+ emitFile: jest.fn(),
226
+ warn: jest.fn(),
227
+ };
228
+
229
+ // Call generateBundle
230
+ await plugin.generateBundle.call(context, {}, bundle);
231
+
232
+ const processedCss = bundle['index.css'].source as string;
233
+
234
+ // Check that pseudo-selectors are in the correct order
235
+ const linkIndex = processedCss.indexOf(':link');
236
+ const focusIndex = processedCss.indexOf(':focus');
237
+ const hoverIndex = processedCss.indexOf(':hover');
238
+ const activeIndex = processedCss.indexOf(':active');
239
+
240
+ expect(linkIndex).toBeLessThan(focusIndex);
241
+ expect(focusIndex).toBeLessThan(hoverIndex);
242
+ expect(hoverIndex).toBeLessThan(activeIndex);
243
+ });
244
+
245
+ it('should preserve duplicates in different at-rule contexts', async () => {
246
+ const plugin = compiledVitePlugin({ extract: true });
247
+
248
+ // Mock bundle with same rule in different contexts
249
+ const bundle = {
250
+ 'index.css': {
251
+ type: 'asset',
252
+ fileName: 'index.css',
253
+ source: '@media(min-width:768px){._abc{color:red}}._abc{color:red}',
254
+ },
255
+ };
256
+
257
+ const context = {
258
+ emitFile: jest.fn(),
259
+ warn: jest.fn(),
260
+ };
261
+
262
+ // Call generateBundle
263
+ await plugin.generateBundle.call(context, {}, bundle);
264
+
265
+ const processedCss = bundle['index.css'].source as string;
266
+
267
+ // Both instances should be preserved (different cascade contexts)
268
+ const matches = (processedCss.match(/color:red/g) || []).length;
269
+ expect(matches).toBe(2);
270
+ expect(processedCss).toContain('@media');
271
+ });
272
+
273
+ it('should produce deterministic output across multiple runs', async () => {
274
+ const plugin1 = compiledVitePlugin({ extract: true });
275
+ const plugin2 = compiledVitePlugin({ extract: true });
276
+
277
+ const inputCss = '._z{z-index:1}._a{color:red}._m{margin:10px}._z{z-index:1}';
278
+
279
+ const bundle1 = {
280
+ 'index.css': {
281
+ type: 'asset',
282
+ fileName: 'index.css',
283
+ source: inputCss,
284
+ },
285
+ };
286
+
287
+ const bundle2 = {
288
+ 'index.css': {
289
+ type: 'asset',
290
+ fileName: 'index.css',
291
+ source: inputCss,
292
+ },
293
+ };
294
+
295
+ const context = {
296
+ emitFile: jest.fn(),
297
+ warn: jest.fn(),
298
+ };
299
+
300
+ // Process with two different plugin instances
301
+ await plugin1.generateBundle.call(context, {}, bundle1);
302
+ await plugin2.generateBundle.call(context, {}, bundle2);
303
+
304
+ // Output should be identical
305
+ expect(bundle1['index.css'].source).toBe(bundle2['index.css'].source);
306
+ });
307
+ });
308
+
309
+ describe('HTML injection', () => {
310
+ const originalEnv = process.env.NODE_ENV;
311
+
312
+ beforeEach(() => {
313
+ process.env.NODE_ENV = 'production';
314
+ });
315
+
316
+ afterEach(() => {
317
+ process.env.NODE_ENV = originalEnv;
318
+ });
319
+
320
+ it('should inject extracted CSS into HTML', async () => {
321
+ const plugin = compiledVitePlugin({ extract: true });
322
+
323
+ // Mock context for generateBundle
324
+ let emittedFileName = '';
325
+ const context = {
326
+ emitFile: (file: any) => {
327
+ // Simulate Vite's content hashing
328
+ emittedFileName = `assets/${file.name.replace('.css', '-abc123.css')}`;
329
+ return 'mock-ref-id';
330
+ },
331
+ warn: jest.fn(),
332
+ };
333
+
334
+ // Transform code to collect styles
335
+ const code = `
336
+ import { css } from '@compiled/react';
337
+ export const Component = () => <div css={css({ color: 'blue' })}>Test</div>;
338
+ `;
339
+ await plugin.transform!(code, 'test.tsx');
340
+
341
+ // Call generateBundle to emit the CSS file
342
+ await plugin.generateBundle.call(context, {}, {});
343
+
344
+ // Mock bundle with the emitted CSS
345
+ const bundle = {
346
+ [emittedFileName]: {
347
+ type: 'asset',
348
+ name: 'compiled-extracted.css',
349
+ fileName: emittedFileName,
350
+ source: '._syaz13q2{color:blue}',
351
+ },
352
+ };
353
+
354
+ const html = '<html><head></head><body></body></html>';
355
+ const ctx = { bundle };
356
+
357
+ // Call transformIndexHtml
358
+ const result = plugin.transformIndexHtml!(html, ctx);
359
+
360
+ // Should inject a link tag
361
+ expect(Array.isArray(result)).toBe(true);
362
+ expect(result.length).toBe(1);
363
+ expect(result[0]).toMatchObject({
364
+ tag: 'link',
365
+ attrs: {
366
+ rel: 'stylesheet',
367
+ href: '/assets/compiled-extracted-abc123.css',
368
+ },
369
+ injectTo: 'head',
370
+ });
371
+ });
372
+
373
+ it('should not inject HTML if no styles were extracted', async () => {
374
+ const plugin = compiledVitePlugin({ extract: true });
375
+
376
+ // Don't transform any code (no styles collected)
377
+ const html = '<html><head></head><body></body></html>';
378
+ const ctx = { bundle: {} };
379
+
380
+ // Call transformIndexHtml
381
+ const result = plugin.transformIndexHtml!(html, ctx);
382
+
383
+ // Should return empty array
384
+ expect(result).toEqual([]);
385
+ });
386
+
387
+ it('should use content-hashed filenames for extracted CSS', async () => {
388
+ const plugin = compiledVitePlugin({ extract: true });
389
+ const emittedFiles: any[] = [];
390
+
391
+ const context = {
392
+ emitFile: (file: any) => {
393
+ emittedFiles.push(file);
394
+ return 'mock-ref-id';
395
+ },
396
+ warn: jest.fn(),
397
+ };
398
+
399
+ // Transform code to collect styles
400
+ const code = `
401
+ import { css } from '@compiled/react';
402
+ export const Component = () => <div css={css({ padding: '10px' })}>Test</div>;
403
+ `;
404
+ await plugin.transform!(code, 'test.tsx');
405
+
406
+ // Call generateBundle
407
+ await plugin.generateBundle.call(context, {}, {});
408
+
409
+ // Check that it uses 'name' (for content hashing) not 'fileName' (static name)
410
+ expect(emittedFiles.length).toBe(1);
411
+ expect(emittedFiles[0]).toHaveProperty('name', 'compiled-extracted.css');
412
+ expect(emittedFiles[0]).not.toHaveProperty('fileName');
413
+ });
414
+ });
415
+ });