@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.
- package/README.md +60 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +221 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +57 -0
- package/dist/utils.js.map +1 -0
- package/package.json +48 -0
- package/src/__tests__/extraction.test.ts +415 -0
- package/src/__tests__/plugin.test.ts +346 -0
- package/src/index.ts +276 -0
- package/src/types.ts +44 -0
- package/src/utils.ts +40 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import compiledVitePlugin from '../index';
|
|
2
|
+
|
|
3
|
+
describe('compiledVitePlugin', () => {
|
|
4
|
+
it('should create a plugin with the correct name', () => {
|
|
5
|
+
const plugin = compiledVitePlugin();
|
|
6
|
+
|
|
7
|
+
expect(plugin.name).toBe('@compiled/vite-plugin');
|
|
8
|
+
expect(plugin.enforce).toBe('pre');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should transform code with Compiled imports', async () => {
|
|
12
|
+
const plugin = compiledVitePlugin();
|
|
13
|
+
const code = `
|
|
14
|
+
import { css } from '@compiled/react';
|
|
15
|
+
|
|
16
|
+
export const Component = () => (
|
|
17
|
+
<div css={css({ color: 'red', fontSize: '12px' })}>
|
|
18
|
+
Hello
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
24
|
+
|
|
25
|
+
expect(result).toBeTruthy();
|
|
26
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
27
|
+
// Check for the atomic class structure
|
|
28
|
+
expect(result.code).toContain('_syaz5scu');
|
|
29
|
+
expect(result.code).toContain('color:red');
|
|
30
|
+
expect(result.code).toContain('font-size:9pt'); // 12px gets normalized to 9pt
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should skip files without Compiled imports', async () => {
|
|
35
|
+
const plugin = compiledVitePlugin();
|
|
36
|
+
const code = `
|
|
37
|
+
import React from 'react';
|
|
38
|
+
|
|
39
|
+
export const Component = () => <div>Hello</div>;
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
43
|
+
|
|
44
|
+
expect(result).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should skip non-JS/TS files', async () => {
|
|
48
|
+
const plugin = compiledVitePlugin();
|
|
49
|
+
const code = '.some-class { color: red; }';
|
|
50
|
+
|
|
51
|
+
const result = await plugin.transform!(code, 'test.css');
|
|
52
|
+
|
|
53
|
+
expect(result).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should skip node_modules/@compiled/react', async () => {
|
|
57
|
+
const plugin = compiledVitePlugin();
|
|
58
|
+
const code = `
|
|
59
|
+
import { css } from '@compiled/react';
|
|
60
|
+
export const styled = {};
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const result = await plugin.transform!(code, '/node_modules/@compiled/react/dist/index.js');
|
|
64
|
+
|
|
65
|
+
expect(result).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle styled components', async () => {
|
|
69
|
+
const plugin = compiledVitePlugin();
|
|
70
|
+
const code = `
|
|
71
|
+
import { styled } from '@compiled/react';
|
|
72
|
+
|
|
73
|
+
export const StyledDiv = styled.div({
|
|
74
|
+
color: 'blue',
|
|
75
|
+
padding: '8px',
|
|
76
|
+
});
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
80
|
+
|
|
81
|
+
expect(result).toBeTruthy();
|
|
82
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
83
|
+
expect(result.code).toContain('color:blue');
|
|
84
|
+
// Padding gets split into longhand properties
|
|
85
|
+
expect(result.code).toContain('padding-top:8px');
|
|
86
|
+
expect(result.code).toContain('forwardRef'); // Styled components use forwardRef
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should respect custom importSources', async () => {
|
|
91
|
+
const plugin = compiledVitePlugin({
|
|
92
|
+
importSources: ['@custom/styled'],
|
|
93
|
+
});
|
|
94
|
+
const code = `
|
|
95
|
+
import { css } from '@custom/styled';
|
|
96
|
+
|
|
97
|
+
export const Component = () => (
|
|
98
|
+
<div css={css({ margin: '16px' })}>
|
|
99
|
+
Hello
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
105
|
+
|
|
106
|
+
expect(result).toBeTruthy();
|
|
107
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
108
|
+
// Margin gets split into longhand properties, and 16px may be normalized to 1pc
|
|
109
|
+
expect(result.code).toContain('margin-top:');
|
|
110
|
+
expect(result.code).toContain('CC'); // Check for Compiled runtime components
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle errors gracefully', async () => {
|
|
115
|
+
const plugin = compiledVitePlugin();
|
|
116
|
+
const mockError = jest.fn();
|
|
117
|
+
|
|
118
|
+
// Create a mock context with an error method
|
|
119
|
+
const context = {
|
|
120
|
+
error: mockError,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const invalidCode = `
|
|
124
|
+
import { css } from '@compiled/react';
|
|
125
|
+
|
|
126
|
+
// Invalid syntax
|
|
127
|
+
const broken = css({
|
|
128
|
+
color
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
await plugin.transform!.call(context, invalidCode, 'test.tsx');
|
|
132
|
+
|
|
133
|
+
expect(mockError).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should apply default options', () => {
|
|
137
|
+
const plugin = compiledVitePlugin();
|
|
138
|
+
|
|
139
|
+
expect(plugin.name).toBe('@compiled/vite-plugin');
|
|
140
|
+
expect(plugin.enforce).toBe('pre');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should accept custom options', async () => {
|
|
144
|
+
const plugin = compiledVitePlugin({
|
|
145
|
+
bake: true,
|
|
146
|
+
extract: false,
|
|
147
|
+
ssr: false,
|
|
148
|
+
addComponentName: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const code = `
|
|
152
|
+
import { styled } from '@compiled/react';
|
|
153
|
+
|
|
154
|
+
export const Button = styled.button({ color: 'green' });
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
158
|
+
|
|
159
|
+
expect(result).toBeTruthy();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle keyframes', async () => {
|
|
163
|
+
const plugin = compiledVitePlugin();
|
|
164
|
+
const code = `
|
|
165
|
+
import { keyframes, css } from '@compiled/react';
|
|
166
|
+
|
|
167
|
+
const fadeIn = keyframes({
|
|
168
|
+
from: { opacity: 0 },
|
|
169
|
+
to: { opacity: 1 },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const Component = () => (
|
|
173
|
+
<div css={css({ animation: \`\${fadeIn} 0.3s ease-out\` })}>
|
|
174
|
+
Animated
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
180
|
+
|
|
181
|
+
expect(result).toBeTruthy();
|
|
182
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
183
|
+
// Should contain keyframes reference
|
|
184
|
+
expect(result.code).toContain('keyframes');
|
|
185
|
+
expect(result.code).toContain('opacity');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle cssMap', async () => {
|
|
190
|
+
const plugin = compiledVitePlugin();
|
|
191
|
+
const code = `
|
|
192
|
+
import { cssMap } from '@compiled/react';
|
|
193
|
+
|
|
194
|
+
const styles = cssMap({
|
|
195
|
+
primary: { backgroundColor: '#0052CC', color: 'white' },
|
|
196
|
+
secondary: { backgroundColor: '#E0E0E0', color: 'black' },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export const Component = ({ variant }) => (
|
|
200
|
+
<div css={styles[variant]}>
|
|
201
|
+
Button
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
207
|
+
|
|
208
|
+
expect(result).toBeTruthy();
|
|
209
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
210
|
+
// Should contain the color values (normalized to lowercase)
|
|
211
|
+
expect(result.code).toContain('#0052cc');
|
|
212
|
+
expect(result.code).toContain('#fff');
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should handle ClassNames component', async () => {
|
|
217
|
+
const plugin = compiledVitePlugin();
|
|
218
|
+
const code = `
|
|
219
|
+
import { ClassNames } from '@compiled/react';
|
|
220
|
+
|
|
221
|
+
export const Component = () => (
|
|
222
|
+
<ClassNames>
|
|
223
|
+
{({ css, style }) => (
|
|
224
|
+
<div
|
|
225
|
+
style={style}
|
|
226
|
+
className={css({ fontSize: '20px', fontWeight: 'bold' })}
|
|
227
|
+
>
|
|
228
|
+
Dynamic
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</ClassNames>
|
|
232
|
+
);
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
236
|
+
|
|
237
|
+
expect(result).toBeTruthy();
|
|
238
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
239
|
+
expect(result.code).toContain('font-size');
|
|
240
|
+
expect(result.code).toContain('font-weight');
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should handle nested pseudo-selectors', async () => {
|
|
245
|
+
const plugin = compiledVitePlugin();
|
|
246
|
+
const code = `
|
|
247
|
+
import { css } from '@compiled/react';
|
|
248
|
+
|
|
249
|
+
export const Component = () => (
|
|
250
|
+
<div css={css({
|
|
251
|
+
color: 'blue',
|
|
252
|
+
':hover': { color: 'red' },
|
|
253
|
+
':focus': { outline: '2px solid blue' },
|
|
254
|
+
})}>
|
|
255
|
+
Interactive
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
261
|
+
|
|
262
|
+
expect(result).toBeTruthy();
|
|
263
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
264
|
+
expect(result.code).toContain('color:blue');
|
|
265
|
+
expect(result.code).toContain(':hover');
|
|
266
|
+
expect(result.code).toContain('color:red');
|
|
267
|
+
expect(result.code).toContain(':focus');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle media queries', async () => {
|
|
272
|
+
const plugin = compiledVitePlugin();
|
|
273
|
+
const code = `
|
|
274
|
+
import { css } from '@compiled/react';
|
|
275
|
+
|
|
276
|
+
export const Component = () => (
|
|
277
|
+
<div css={css({
|
|
278
|
+
padding: '16px',
|
|
279
|
+
'@media (min-width: 768px)': {
|
|
280
|
+
padding: '32px',
|
|
281
|
+
},
|
|
282
|
+
})}>
|
|
283
|
+
Responsive
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
289
|
+
|
|
290
|
+
expect(result).toBeTruthy();
|
|
291
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
292
|
+
expect(result.code).toContain('padding');
|
|
293
|
+
expect(result.code).toContain('@media');
|
|
294
|
+
expect(result.code).toContain('min-width');
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should handle template literal styles', async () => {
|
|
299
|
+
const plugin = compiledVitePlugin();
|
|
300
|
+
const code = `
|
|
301
|
+
import { css } from '@compiled/react';
|
|
302
|
+
|
|
303
|
+
export const Component = () => (
|
|
304
|
+
<div css={css\`
|
|
305
|
+
color: purple;
|
|
306
|
+
font-size: 18px;
|
|
307
|
+
&:hover {
|
|
308
|
+
color: darkpurple;
|
|
309
|
+
}
|
|
310
|
+
\`}>
|
|
311
|
+
Styled
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
`;
|
|
315
|
+
|
|
316
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
317
|
+
|
|
318
|
+
expect(result).toBeTruthy();
|
|
319
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
320
|
+
expect(result.code).toContain('color:purple');
|
|
321
|
+
expect(result.code).toContain('font-size');
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should handle styled component with template literal', async () => {
|
|
326
|
+
const plugin = compiledVitePlugin();
|
|
327
|
+
const code = `
|
|
328
|
+
import { styled } from '@compiled/react';
|
|
329
|
+
|
|
330
|
+
export const Card = styled.div\`
|
|
331
|
+
background: white;
|
|
332
|
+
border-radius: 8px;
|
|
333
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
334
|
+
\`;
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
const result = await plugin.transform!(code, 'test.tsx');
|
|
338
|
+
|
|
339
|
+
expect(result).toBeTruthy();
|
|
340
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
341
|
+
expect(result.code).toContain('background');
|
|
342
|
+
expect(result.code).toContain('border-radius');
|
|
343
|
+
expect(result.code).toContain('box-shadow');
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { parseAsync, transformFromAstAsync } from '@babel/core';
|
|
2
|
+
import type { PluginOptions as BabelPluginOptions } from '@compiled/babel-plugin';
|
|
3
|
+
import type {
|
|
4
|
+
PluginOptions as BabelStripRuntimePluginOptions,
|
|
5
|
+
BabelFileMetadata,
|
|
6
|
+
} from '@compiled/babel-plugin-strip-runtime';
|
|
7
|
+
import { sort } from '@compiled/css';
|
|
8
|
+
import { DEFAULT_IMPORT_SOURCES, DEFAULT_PARSER_BABEL_PLUGINS, toBoolean } from '@compiled/utils';
|
|
9
|
+
import type { OutputAsset, OutputBundle } from 'rollup';
|
|
10
|
+
|
|
11
|
+
import type { PluginOptions } from './types';
|
|
12
|
+
import { createDefaultResolver } from './utils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compiled Vite plugin.
|
|
16
|
+
*
|
|
17
|
+
* Transforms CSS-in-JS to atomic CSS at build time using Babel.
|
|
18
|
+
*
|
|
19
|
+
* @param userOptions - Plugin configuration options
|
|
20
|
+
* @returns Vite plugin object
|
|
21
|
+
*/
|
|
22
|
+
export default function compiledVitePlugin(userOptions: PluginOptions = {}): any {
|
|
23
|
+
const options: PluginOptions = {
|
|
24
|
+
// Vite-specific
|
|
25
|
+
bake: true,
|
|
26
|
+
extract: false,
|
|
27
|
+
transformerBabelPlugins: undefined,
|
|
28
|
+
ssr: false,
|
|
29
|
+
extractStylesToDirectory: undefined,
|
|
30
|
+
sortShorthand: true,
|
|
31
|
+
|
|
32
|
+
// Babel-inherited
|
|
33
|
+
importReact: true,
|
|
34
|
+
nonce: undefined,
|
|
35
|
+
importSources: undefined,
|
|
36
|
+
optimizeCss: true,
|
|
37
|
+
resolver: undefined,
|
|
38
|
+
extensions: undefined,
|
|
39
|
+
parserBabelPlugins: undefined,
|
|
40
|
+
addComponentName: false,
|
|
41
|
+
classNameCompressionMap: undefined,
|
|
42
|
+
processXcss: undefined,
|
|
43
|
+
increaseSpecificity: undefined,
|
|
44
|
+
sortAtRules: true,
|
|
45
|
+
classHashPrefix: undefined,
|
|
46
|
+
flattenMultipleSelectors: undefined,
|
|
47
|
+
|
|
48
|
+
...userOptions,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Storage for collected style rules during transformation
|
|
52
|
+
const collectedStyleRules = new Set<string>();
|
|
53
|
+
|
|
54
|
+
// Store the emitted CSS filename for HTML injection
|
|
55
|
+
// This gets set in generateBundle after the file is emitted
|
|
56
|
+
let extractedCssFileName: string | undefined;
|
|
57
|
+
|
|
58
|
+
// Name used for the extracted CSS asset
|
|
59
|
+
const EXTRACTED_CSS_NAME = 'compiled-extracted.css';
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
name: '@compiled/vite-plugin',
|
|
63
|
+
enforce: 'pre', // Run before other plugins
|
|
64
|
+
|
|
65
|
+
async transform(code: string, id: string): Promise<any> {
|
|
66
|
+
// Filter out node_modules (except for specific includes if needed)
|
|
67
|
+
if (id.includes('/node_modules/@compiled/react')) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Only process JS/TS/JSX/TSX files
|
|
72
|
+
if (!/\.[jt]sx?$/.test(id)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const importSources = [...DEFAULT_IMPORT_SOURCES, ...(options.importSources || [])];
|
|
77
|
+
|
|
78
|
+
// Bail early if Compiled (via an importSource) isn't in the module
|
|
79
|
+
if (!importSources.some((name) => code.includes(name))) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const includedFiles: string[] = [];
|
|
85
|
+
|
|
86
|
+
// Parse to AST using Babel
|
|
87
|
+
const ast = await parseAsync(code, {
|
|
88
|
+
filename: id,
|
|
89
|
+
babelrc: false,
|
|
90
|
+
configFile: false,
|
|
91
|
+
caller: { name: 'compiled' },
|
|
92
|
+
rootMode: 'upward-optional',
|
|
93
|
+
parserOpts: {
|
|
94
|
+
plugins: options.parserBabelPlugins ?? DEFAULT_PARSER_BABEL_PLUGINS,
|
|
95
|
+
},
|
|
96
|
+
plugins: options.transformerBabelPlugins ?? undefined,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!ast) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Disable stylesheet extraction in development mode
|
|
104
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
105
|
+
const extract = options.extract && !isDevelopment;
|
|
106
|
+
|
|
107
|
+
// Transform using the Compiled Babel Plugin
|
|
108
|
+
const result = await transformFromAstAsync(ast, code, {
|
|
109
|
+
babelrc: false,
|
|
110
|
+
configFile: false,
|
|
111
|
+
sourceMaps: true,
|
|
112
|
+
filename: id,
|
|
113
|
+
parserOpts: {
|
|
114
|
+
plugins: options.parserBabelPlugins ?? DEFAULT_PARSER_BABEL_PLUGINS,
|
|
115
|
+
},
|
|
116
|
+
plugins: [
|
|
117
|
+
...(options.transformerBabelPlugins ?? []),
|
|
118
|
+
options.bake && [
|
|
119
|
+
'@compiled/babel-plugin',
|
|
120
|
+
{
|
|
121
|
+
...options,
|
|
122
|
+
// Turn off compressing class names if stylesheet extraction is off
|
|
123
|
+
classNameCompressionMap: extract && options.classNameCompressionMap,
|
|
124
|
+
onIncludedFiles: (files: string[]) => includedFiles.push(...files),
|
|
125
|
+
resolver: options.resolver ? options.resolver : createDefaultResolver(options),
|
|
126
|
+
cache: false,
|
|
127
|
+
} as BabelPluginOptions,
|
|
128
|
+
],
|
|
129
|
+
extract && [
|
|
130
|
+
'@compiled/babel-plugin-strip-runtime',
|
|
131
|
+
{
|
|
132
|
+
compiledRequireExclude: options.ssr || extract,
|
|
133
|
+
extractStylesToDirectory: options.extractStylesToDirectory,
|
|
134
|
+
} as BabelStripRuntimePluginOptions,
|
|
135
|
+
],
|
|
136
|
+
].filter(toBoolean),
|
|
137
|
+
caller: {
|
|
138
|
+
name: 'compiled',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Store metadata for CSS extraction if enabled
|
|
143
|
+
if (extract && result?.metadata) {
|
|
144
|
+
const metadata = result.metadata as BabelFileMetadata;
|
|
145
|
+
// Collect style rules from this file
|
|
146
|
+
if (metadata.styleRules && metadata.styleRules.length > 0) {
|
|
147
|
+
metadata.styleRules.forEach((rule: string) => collectedStyleRules.add(rule));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Return transformed code and source map
|
|
152
|
+
if (result?.code) {
|
|
153
|
+
return {
|
|
154
|
+
code: result.code,
|
|
155
|
+
map: result.map ?? null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
} catch (e: unknown) {
|
|
161
|
+
// Throw error to be displayed by Vite
|
|
162
|
+
const error = e as Error;
|
|
163
|
+
this.error({
|
|
164
|
+
message: `[@compiled/vite-plugin] Failed to transform: ${error.message}`,
|
|
165
|
+
stack: error.stack,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
generateBundle(_outputOptions: any, bundle: OutputBundle) {
|
|
171
|
+
// Post-process CSS assets to apply Compiled's sorting and deduplication
|
|
172
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
173
|
+
const extract = options.extract && !isDevelopment;
|
|
174
|
+
|
|
175
|
+
// Process each CSS asset in the bundle
|
|
176
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
177
|
+
// Only process CSS assets
|
|
178
|
+
if (!fileName.endsWith('.css') || output.type !== 'asset') {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const asset = output as OutputAsset;
|
|
183
|
+
const cssContent = asset.source as string;
|
|
184
|
+
|
|
185
|
+
// Check if this CSS contains Compiled atomic classes (starts with underscore)
|
|
186
|
+
// This is a heuristic to identify CSS that came from .compiled.css files
|
|
187
|
+
if (cssContent.includes('._')) {
|
|
188
|
+
try {
|
|
189
|
+
// Apply Compiled's CSS sorting and deduplication
|
|
190
|
+
const sortConfig = {
|
|
191
|
+
sortAtRulesEnabled: options.sortAtRules,
|
|
192
|
+
sortShorthandEnabled: options.sortShorthand,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const sortedCss = sort(cssContent, sortConfig);
|
|
196
|
+
|
|
197
|
+
// Update the asset with sorted CSS
|
|
198
|
+
asset.source = sortedCss;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const err = error as Error;
|
|
201
|
+
this.warn({
|
|
202
|
+
message: `[@compiled/vite-plugin] Failed to sort CSS in ${fileName}: ${err.message}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Also emit extracted styles if we collected any from local code
|
|
209
|
+
if (extract && collectedStyleRules.size > 0) {
|
|
210
|
+
try {
|
|
211
|
+
// Convert Set to array and sort for determinism
|
|
212
|
+
const allRules = Array.from(collectedStyleRules).sort();
|
|
213
|
+
|
|
214
|
+
// Join all rules and apply CSS sorting
|
|
215
|
+
const combinedCss = allRules.join('\n');
|
|
216
|
+
const sortConfig = {
|
|
217
|
+
sortAtRulesEnabled: options.sortAtRules,
|
|
218
|
+
sortShorthandEnabled: options.sortShorthand,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const sortedCss = sort(combinedCss, sortConfig);
|
|
222
|
+
|
|
223
|
+
// Emit the CSS file with content-based naming
|
|
224
|
+
// Vite will add a content hash to the filename automatically
|
|
225
|
+
this.emitFile({
|
|
226
|
+
type: 'asset',
|
|
227
|
+
name: EXTRACTED_CSS_NAME,
|
|
228
|
+
source: sortedCss,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Mark that we've emitted the file so transformIndexHtml can inject it
|
|
232
|
+
// The actual filename will be determined in transformIndexHtml from the bundle
|
|
233
|
+
extractedCssFileName = EXTRACTED_CSS_NAME;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const err = error as Error;
|
|
236
|
+
this.warn({
|
|
237
|
+
message: `[@compiled/vite-plugin] Failed to generate CSS bundle: ${err.message}`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
transformIndexHtml(
|
|
244
|
+
_html: string,
|
|
245
|
+
ctx: { bundle?: OutputBundle; [key: string]: any }
|
|
246
|
+
): { tag: string; attrs: Record<string, string>; injectTo: string }[] {
|
|
247
|
+
// Inject the extracted CSS file into HTML if it was emitted
|
|
248
|
+
if (!extractedCssFileName || !ctx.bundle) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Find the emitted CSS asset in the bundle by its name
|
|
253
|
+
// The actual fileName will have a content hash added by Vite
|
|
254
|
+
const cssAsset = Object.values(ctx.bundle).find(
|
|
255
|
+
(asset): asset is OutputAsset => asset.type === 'asset' && asset.name === EXTRACTED_CSS_NAME
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (!cssAsset) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
tag: 'link',
|
|
265
|
+
attrs: {
|
|
266
|
+
rel: 'stylesheet',
|
|
267
|
+
href: `/${cssAsset.fileName}`,
|
|
268
|
+
},
|
|
269
|
+
injectTo: 'head',
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export type { PluginOptions as VitePluginOptions };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PluginItem } from '@babel/core';
|
|
2
|
+
import type { PluginOptions as BabelPluginOptions } from '@compiled/babel-plugin';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vite plugin options extending the babel-plugin options with Vite-specific configuration.
|
|
6
|
+
*/
|
|
7
|
+
export interface PluginOptions extends BabelPluginOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Converts your source code into a Compiled component.
|
|
10
|
+
* Defaults to `true`.
|
|
11
|
+
*/
|
|
12
|
+
bake?: boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extracts to CSS when `true`.
|
|
16
|
+
* Defaults to `false`.
|
|
17
|
+
*/
|
|
18
|
+
extract?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* List of transformer babel plugins to be applied to evaluated files
|
|
22
|
+
*
|
|
23
|
+
* See the [babel docs](https://babeljs.io/docs/en/plugins/#transform-plugins)
|
|
24
|
+
*/
|
|
25
|
+
transformerBabelPlugins?: PluginItem[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build in a node environment.
|
|
29
|
+
* Defaults to `false`.
|
|
30
|
+
*/
|
|
31
|
+
ssr?: boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* When set, extract styles to an external CSS file.
|
|
35
|
+
*/
|
|
36
|
+
extractStylesToDirectory?: { source: string; dest: string };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether to sort shorthand and longhand properties,
|
|
40
|
+
* eg. `margin` before `margin-top` for enforced determinism.
|
|
41
|
+
* Defaults to `true`.
|
|
42
|
+
*/
|
|
43
|
+
sortShorthand?: boolean;
|
|
44
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
import type { Resolver } from '@compiled/babel-plugin';
|
|
5
|
+
|
|
6
|
+
import type { PluginOptions } from './types';
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
9
|
+
const enhancedResolve = require('enhanced-resolve');
|
|
10
|
+
|
|
11
|
+
// Handle both ESM and CJS imports
|
|
12
|
+
const { CachedInputFileSystem, ResolverFactory } = enhancedResolve.CachedInputFileSystem
|
|
13
|
+
? enhancedResolve
|
|
14
|
+
: enhancedResolve.default || enhancedResolve;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a default resolver using enhanced-resolve.
|
|
18
|
+
* This is the same resolver used by webpack and other bundlers,
|
|
19
|
+
* providing robust module resolution with caching.
|
|
20
|
+
*
|
|
21
|
+
* @param config - Vite plugin configuration
|
|
22
|
+
* @returns Resolver compatible with @compiled/babel-plugin
|
|
23
|
+
*/
|
|
24
|
+
export function createDefaultResolver(config: PluginOptions): Resolver {
|
|
25
|
+
const resolver = ResolverFactory.createResolver({
|
|
26
|
+
fileSystem: new CachedInputFileSystem(fs, 4000),
|
|
27
|
+
...(config.extensions && {
|
|
28
|
+
extensions: config.extensions,
|
|
29
|
+
}),
|
|
30
|
+
// This makes the resolver invoke the callback synchronously
|
|
31
|
+
useSyncFileSystemCalls: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
// The resolver needs to be synchronous, as babel plugins must be synchronous
|
|
36
|
+
resolveSync(context: string, request: string) {
|
|
37
|
+
return resolver.resolveSync({}, dirname(context), request) as string;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|