@rolldown/plugin-emotion 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present, rolldown/plugins repository contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @rolldown/plugin-emotion [![npm](https://img.shields.io/npm/v/@rolldown/plugin-emotion.svg)](https://npmx.dev/package/@rolldown/plugin-emotion)
2
+
3
+ Rolldown plugin for minification and optimization of [Emotion](https://emotion.sh/) styles.
4
+
5
+ This plugin utilizes Rolldown's [native magic string API](https://rolldown.rs/in-depth/native-magic-string) instead of Babel and is more performant than using [`@emotion/babel-plugin`](https://emotion.sh/docs/@emotion/babel-plugin) with [`@rolldown/plugin-babel`](https://npmx.dev/package/@rolldown/plugin-babel).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add -D @rolldown/plugin-emotion
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ import emotion from '@rolldown/plugin-emotion'
17
+
18
+ export default {
19
+ plugins: [
20
+ emotion({
21
+ // options
22
+ }),
23
+ ],
24
+ }
25
+ ```
26
+
27
+ ### Supported Libraries
28
+
29
+ The plugin handles imports from these Emotion packages out of the box:
30
+
31
+ - `@emotion/css`
32
+ - `@emotion/styled`
33
+ - `@emotion/react`
34
+ - `@emotion/primitives`
35
+ - `@emotion/native`
36
+
37
+ ## Options
38
+
39
+ ### `sourceMap`
40
+
41
+ - **Type:** `boolean`
42
+ - **Default:** `true` in development, `false` otherwise
43
+
44
+ Generate source maps for Emotion CSS. Source maps help trace styles back to their original source in browser DevTools.
45
+
46
+ ### `autoLabel`
47
+
48
+ - **Type:** `'never' | 'dev-only' | 'always'`
49
+ - **Default:** `'dev-only'`
50
+
51
+ Controls when debug labels are added to styled components and `css` calls.
52
+
53
+ - `'never'` — Never add labels
54
+ - `'dev-only'` — Only add labels in development mode
55
+ - `'always'` — Always add labels
56
+
57
+ ### `labelFormat`
58
+
59
+ - **Type:** `string`
60
+ - **Default:** `"[local]"`
61
+
62
+ Defines the format of generated debug labels. Only relevant when `autoLabel` is not `'never'`.
63
+
64
+ Supports placeholders:
65
+
66
+ - `[local]` — The variable name that the result of `css` or `styled` call is assigned to
67
+ - `[filename]` — The file name (without extension)
68
+ - `[dirname]` — The directory name of the file
69
+
70
+ ```js
71
+ emotion({
72
+ autoLabel: 'always',
73
+ labelFormat: '[dirname]--[filename]--[local]',
74
+ })
75
+ ```
76
+
77
+ ### `importMap`
78
+
79
+ - **Type:** `Record<string, ImportMapConfig>`
80
+
81
+ Custom import mappings for non-standard Emotion packages. Maps package names to their export configurations, allowing the plugin to transform custom libraries that re-export Emotion utilities.
82
+
83
+ ```js
84
+ emotion({
85
+ importMap: {
86
+ 'my-emotion-lib': {
87
+ myStyled: {
88
+ canonicalImport: ['@emotion/styled', 'default'],
89
+ },
90
+ myCss: {
91
+ canonicalImport: ['@emotion/react', 'css'],
92
+ },
93
+ },
94
+ },
95
+ })
96
+ ```
97
+
98
+ Each entry maps an export name to its canonical Emotion equivalent via `canonicalImport: [packageName, exportName]`.
99
+
100
+ ## Benchmark
101
+
102
+ Results of the benchmark that can be run by `pnpm bench` in `./benchmark` directory:
103
+
104
+ ```
105
+ name hz min max mean p75 p99 p995 p999 rme samples
106
+ · @rolldown/plugin-emotion 9.7954 98.4954 108.83 102.09 103.34 108.83 108.83 108.83 ±2.23% 10
107
+ · @rolldown/plugin-babel 3.7139 254.48 295.01 269.26 277.63 295.01 295.01 295.01 ±3.49% 10
108
+ · @rollup/plugin-swc 7.5542 128.56 139.14 132.38 134.82 139.14 139.14 139.14 ±1.78% 10
109
+
110
+ @rolldown/plugin-emotion - bench/emotion.bench.ts > Emotion Benchmark
111
+ 1.30x faster than @rollup/plugin-swc
112
+ 2.64x faster than @rolldown/plugin-babel
113
+ ```
114
+
115
+ The benchmark was ran on the following environment:
116
+
117
+ ```
118
+ OS: macOS Tahoe 26.3
119
+ CPU: Apple M4
120
+ Memory: LPDDR5X-7500 32GB
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT
126
+
127
+ ## Credits
128
+
129
+ The implementation is based on [swc-project/plugins/packages/emotion](https://github.com/swc-project/plugins/tree/main/packages/emotion) ([Apache License 2.0](https://github.com/swc-project/plugins/blob/main/LICENSE)). Test cases are also adapted from it.
@@ -0,0 +1,78 @@
1
+ import { Plugin } from "rolldown";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Configuration for custom emotion-like packages
6
+ * Maps export names to their canonical emotion equivalents
7
+ */
8
+ interface ImportMapEntry {
9
+ /**
10
+ * The canonical emotion import this maps to
11
+ * @example ["@emotion/styled", "default"]
12
+ */
13
+ canonicalImport: [packageName: string, exportName: string];
14
+ /**
15
+ * The styled base import for this package
16
+ * @example ["package/base", "something"]
17
+ */
18
+ styledBaseImport?: [packageName: string, exportName: string];
19
+ }
20
+ type ImportMapConfig = Record<string, ImportMapEntry>;
21
+ interface EmotionPluginOptions {
22
+ /**
23
+ * Generate source maps for emotion CSS.
24
+ * @default true for development, otherwise false
25
+ */
26
+ sourceMap?: boolean;
27
+ /**
28
+ * When to add debug labels to styled components.
29
+ * - 'never': Never add labels
30
+ * - 'dev-only': Only add labels in development mode (default)
31
+ * - 'always': Always add labels
32
+ * @default 'dev-only'
33
+ */
34
+ autoLabel?: 'never' | 'dev-only' | 'always';
35
+ /**
36
+ * Label format template.
37
+ *
38
+ * Defines the format of the generated debug labels.
39
+ * This option is only relevant if `autoLabel` is not set to 'never'.
40
+ *
41
+ * Supports placeholders:
42
+ * - [local]: The variable name that the result of `css` or `styled` call is assigned to
43
+ * - [filename]: The file name (without extension) that the `css` or `styled` call is in
44
+ * - [dirname]: The directory name of the file that the `css` or `styled` call is in
45
+ *
46
+ * @default "[local]"
47
+ * @example "[dirname]--[filename]--[local]"
48
+ */
49
+ labelFormat?: string;
50
+ /**
51
+ * Custom import mappings for non-standard emotion packages.
52
+ * Maps package names to their export configurations.
53
+ *
54
+ * @example
55
+ * If you have a custom library "my-emotion-lib" that re-exports
56
+ * the default export of `@emotion/styled` as `myStyled` and
57
+ * the `css` export of `@emotion/react` as `myCss`,
58
+ * then you can configure it like this:
59
+ * ```
60
+ * {
61
+ * "my-emotion-lib": {
62
+ * "myStyled": {
63
+ * canonicalImport: ["@emotion/styled", "default"]
64
+ * },
65
+ * "myCss": {
66
+ * canonicalImport: ["@emotion/react", "css"]
67
+ * }
68
+ * }
69
+ * }
70
+ * ```
71
+ */
72
+ importMap?: Record<string, ImportMapConfig>;
73
+ }
74
+ //#endregion
75
+ //#region src/index.d.ts
76
+ declare function emotionPlugin(options?: EmotionPluginOptions): Plugin;
77
+ //#endregion
78
+ export { type EmotionPluginOptions, emotionPlugin as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,853 @@
1
+ import { withMagicString } from "rolldown-string";
2
+ import { Visitor } from "rolldown/utils";
3
+ import { GenMapping, addSegment, setSourceContent, toEncodedMap } from "@jridgewell/gen-mapping";
4
+ import path from "node:path";
5
+ import hashString from "@emotion/hash";
6
+ //#region ../../internal-packages/oxc-unshadowed-visitor/src/scope-tracker.ts
7
+ var ScopeTracker = class {
8
+ /** per tracked name, 0 = not shadowed */
9
+ shadowDepth;
10
+ nameCount;
11
+ scopeStack;
12
+ constructor(nameCount) {
13
+ this.nameCount = nameCount;
14
+ this.shadowDepth = Array.from({ length: nameCount }).fill(0);
15
+ this.scopeStack = [];
16
+ }
17
+ pushScope(kind, recordsLength) {
18
+ this.scopeStack.push({
19
+ kind,
20
+ shadows: Array.from({ length: this.nameCount }).fill(false),
21
+ recordsStartIdx: recordsLength
22
+ });
23
+ }
24
+ popScope() {
25
+ const frame = this.scopeStack.pop();
26
+ if (!frame) return;
27
+ for (let i = 0; i < this.nameCount; i++) if (frame.shadows[i]) this.shadowDepth[i]--;
28
+ }
29
+ /**
30
+ * Declare a block-scoped binding (let, const, class, catch param).
31
+ * Declares at the top of the scope stack.
32
+ * If the stack is empty (module level), returns without shadowing.
33
+ */
34
+ declareBlock(nameIdx, records) {
35
+ if (this.scopeStack.length === 0) return;
36
+ const frame = this.scopeStack[this.scopeStack.length - 1];
37
+ this._declare(frame, nameIdx, records);
38
+ }
39
+ /**
40
+ * Declare a var-scoped binding.
41
+ * Walks up the scope stack to find the nearest 'function' scope.
42
+ * If none found (module level), returns without shadowing.
43
+ */
44
+ declareVar(nameIdx, records) {
45
+ for (let i = this.scopeStack.length - 1; i >= 0; i--) if (this.scopeStack[i].kind === "function") {
46
+ this._declare(this.scopeStack[i], nameIdx, records);
47
+ return;
48
+ }
49
+ }
50
+ isShadowed(nameIdx) {
51
+ return this.shadowDepth[nameIdx] > 0;
52
+ }
53
+ _declare(frame, nameIdx, records) {
54
+ if (frame.shadows[nameIdx]) return;
55
+ frame.shadows[nameIdx] = true;
56
+ this.shadowDepth[nameIdx]++;
57
+ this._retroactiveInvalidate(frame.recordsStartIdx, nameIdx, records);
58
+ }
59
+ _retroactiveInvalidate(fromIdx, nameIdx, records) {
60
+ for (let i = fromIdx; i < records.length; i++) if (records[i].nameIdx === nameIdx) records[i].invalidated = true;
61
+ }
62
+ };
63
+ //#endregion
64
+ //#region ../../internal-packages/oxc-unshadowed-visitor/src/binding-names.ts
65
+ /**
66
+ * Recursively extracts binding names from a pattern node.
67
+ */
68
+ function extractBindingNames(pattern, names) {
69
+ switch (pattern.type) {
70
+ case "Identifier":
71
+ names.push(pattern.name);
72
+ break;
73
+ case "ArrayPattern":
74
+ for (const element of pattern.elements) if (element != null) extractBindingNames(element, names);
75
+ break;
76
+ case "ObjectPattern":
77
+ for (const prop of pattern.properties) if (prop.type === "RestElement") extractBindingNames(prop, names);
78
+ else extractBindingNames(prop.value, names);
79
+ break;
80
+ case "AssignmentPattern":
81
+ extractBindingNames(pattern.left, names);
82
+ break;
83
+ case "RestElement":
84
+ extractBindingNames(pattern.argument, names);
85
+ break;
86
+ }
87
+ }
88
+ //#endregion
89
+ //#region ../../internal-packages/oxc-unshadowed-visitor/src/merge-visitors.ts
90
+ /**
91
+ * Merge user visitors with internal scope-tracking visitors.
92
+ * Enter: internal runs FIRST, then user.
93
+ * Exit: user runs FIRST, then internal.
94
+ */
95
+ function mergeVisitors(userVisitor, ctx, internalEnter, internalExit) {
96
+ const merged = {};
97
+ for (const key of Object.keys(userVisitor)) {
98
+ const userFn = userVisitor[key];
99
+ const isExit = key.endsWith(":exit");
100
+ const baseKey = isExit ? key.slice(0, -5) : key;
101
+ if (isExit) {
102
+ const internalExitFn = internalExit[key];
103
+ if (internalExitFn) merged[key] = (node) => {
104
+ userFn?.(node, ctx);
105
+ internalExitFn(node);
106
+ };
107
+ else merged[key] = (node) => {
108
+ userFn?.(node, ctx);
109
+ };
110
+ } else {
111
+ const internalEnterFn = internalEnter[baseKey];
112
+ if (internalEnterFn) merged[key] = (node) => {
113
+ internalEnterFn(node);
114
+ userFn(node, ctx);
115
+ };
116
+ else merged[key] = (node) => {
117
+ userFn(node, ctx);
118
+ };
119
+ }
120
+ }
121
+ for (const [key, fn] of Object.entries(internalEnter)) if (!(key in merged)) merged[key] = fn;
122
+ for (const [key, fn] of Object.entries(internalExit)) if (!(key in merged)) merged[key] = fn;
123
+ return merged;
124
+ }
125
+ //#endregion
126
+ //#region ../../internal-packages/oxc-unshadowed-visitor/src/scoped-visitor.ts
127
+ var ScopedVisitor = class {
128
+ trackedNames;
129
+ userVisitor;
130
+ constructor(options) {
131
+ this.trackedNames = options.trackedNames;
132
+ this.userVisitor = options.visitor;
133
+ }
134
+ walk(program) {
135
+ const records = [];
136
+ const trackedNames = this.trackedNames;
137
+ const tracker = new ScopeTracker(trackedNames.length);
138
+ const ctx = { record(opts) {
139
+ const nameIdx = trackedNames.indexOf(opts.name);
140
+ if (nameIdx === -1) return;
141
+ records.push({
142
+ name: opts.name,
143
+ node: opts.node,
144
+ data: opts.data,
145
+ nameIdx,
146
+ invalidated: tracker.isShadowed(nameIdx)
147
+ });
148
+ } };
149
+ const tempNames = [];
150
+ /**
151
+ * Declare all binding names from a pattern for a given declaration style.
152
+ */
153
+ const declarePattern = (pattern, mode) => {
154
+ tempNames.length = 0;
155
+ extractBindingNames(pattern, tempNames);
156
+ for (const name of tempNames) {
157
+ const idx = trackedNames.indexOf(name);
158
+ if (idx === -1) continue;
159
+ if (mode === "block") tracker.declareBlock(idx, records);
160
+ else tracker.declareVar(idx, records);
161
+ }
162
+ };
163
+ /**
164
+ * Declare all function params as block-scoped bindings.
165
+ */
166
+ const declareParams = (params) => {
167
+ for (const param of params) declarePattern(param, "block");
168
+ };
169
+ const scopeEnter = {
170
+ FunctionDeclaration(node) {
171
+ if (node.id) declarePattern(node.id, "block");
172
+ tracker.pushScope("function", records.length);
173
+ if (node.params) declareParams(node.params);
174
+ },
175
+ FunctionExpression(node) {
176
+ tracker.pushScope("function", records.length);
177
+ if (node.id) declarePattern(node.id, "block");
178
+ if (node.params) declareParams(node.params);
179
+ },
180
+ ArrowFunctionExpression(node) {
181
+ tracker.pushScope("function", records.length);
182
+ if (node.params) declareParams(node.params);
183
+ },
184
+ BlockStatement(_node) {
185
+ tracker.pushScope("block", records.length);
186
+ },
187
+ ForStatement(_node) {
188
+ tracker.pushScope("block", records.length);
189
+ },
190
+ ForInStatement(_node) {
191
+ tracker.pushScope("block", records.length);
192
+ },
193
+ ForOfStatement(_node) {
194
+ tracker.pushScope("block", records.length);
195
+ },
196
+ SwitchStatement(_node) {
197
+ tracker.pushScope("block", records.length);
198
+ },
199
+ StaticBlock(_node) {
200
+ tracker.pushScope("block", records.length);
201
+ },
202
+ CatchClause(node) {
203
+ tracker.pushScope("block", records.length);
204
+ if (node.param) declarePattern(node.param, "block");
205
+ }
206
+ };
207
+ const scopeExit = {};
208
+ for (const key of Object.keys(scopeEnter)) scopeExit[`${key}:exit`] = () => tracker.popScope();
209
+ const declarationOnlyEnter = {
210
+ VariableDeclaration(node) {
211
+ const mode = node.kind === "var" ? "var" : "block";
212
+ for (const declarator of node.declarations) declarePattern(declarator.id, mode);
213
+ },
214
+ ClassDeclaration(node) {
215
+ if (node.id) declarePattern(node.id, "block");
216
+ }
217
+ };
218
+ new Visitor(mergeVisitors(this.userVisitor, ctx, {
219
+ ...scopeEnter,
220
+ ...declarationOnlyEnter
221
+ }, scopeExit)).visit(program);
222
+ return records.filter((r) => !r.invalidated).map(({ name, node, data }) => ({
223
+ name,
224
+ node,
225
+ data
226
+ }));
227
+ }
228
+ };
229
+ //#endregion
230
+ //#region src/css-minify.ts
231
+ const MULTI_LINE_COMMENT = /\/\*[\s\S]*?\*\//g;
232
+ const SINGLE_LINE_COMMENT = /(^|[^:'^"]|\s)\/\/.*$/gm;
233
+ const SPACE_AROUND_COLON = /\s*([:;,{}])\s*/g;
234
+ function minifyCSSString(input, isFirst, isLast) {
235
+ let result = input.replace(MULTI_LINE_COMMENT, "");
236
+ result = result.replace(SINGLE_LINE_COMMENT, "$1");
237
+ if (isFirst) result = result.replace(/^[\s]+/, "");
238
+ else result = result.replace(/^\n+/, "");
239
+ if (isLast) result = result.replace(/[\s]+$/, "");
240
+ else result = result.replace(/\n+$/, "");
241
+ result = result.replace(SPACE_AROUND_COLON, "$1");
242
+ return result;
243
+ }
244
+ //#endregion
245
+ //#region src/source-map.ts
246
+ /**
247
+ * Create an inline source map comment string.
248
+ *
249
+ * @param sourceContent - The full original source code
250
+ * @param filename - The source filename (with extension stripped)
251
+ * @param pos - The 0-indexed line and column number of the expression in the original source
252
+ */
253
+ function createSourceMap(sourceContent, filename, pos) {
254
+ const map = new GenMapping({ file: filename });
255
+ setSourceContent(map, filename, sourceContent);
256
+ addSegment(map, 0, 0, filename, pos.line, pos.column);
257
+ return `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${btoa(JSON.stringify(toEncodedMap(map)))} */`;
258
+ }
259
+ const LF = "\n".charCodeAt(0);
260
+ /**
261
+ * Get the 0-indexed line number and column for a character offset in the source.
262
+ */
263
+ function getPos(source, offset) {
264
+ let line = 0;
265
+ let column = 0;
266
+ for (let i = 0; i < offset && i < source.length; i++) if (source.charCodeAt(i) === LF) {
267
+ line++;
268
+ column = 0;
269
+ } else column++;
270
+ return {
271
+ line,
272
+ column
273
+ };
274
+ }
275
+ //#endregion
276
+ //#region src/common.ts
277
+ const ExprKind = {
278
+ Css: 0,
279
+ Styled: 1,
280
+ GlobalJSX: 2
281
+ };
282
+ function regexEscape(str) {
283
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
284
+ }
285
+ /**
286
+ * Unescape template literal raw text to get the actual string value.
287
+ * Template literal raw text preserves escape sequences as written in source.
288
+ * We need to convert them to their actual characters.
289
+ */
290
+ function unescapeTemplateRaw(raw) {
291
+ return raw.replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\b/g, "\b").replace(/\\f/g, "\f").replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\v/g, "\v").replace(/\\\\/g, "\\");
292
+ }
293
+ /**
294
+ * Escape a string for use inside a JS double-quoted string literal.
295
+ */
296
+ function escapeJSString(str) {
297
+ return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\f/g, "\\f").replace(/\u000b/g, "\\v").replace(/[\b]/g, "\\b");
298
+ }
299
+ const SPACE_REGEX = /\s/;
300
+ function checkTrailingCommaExistence(str, endIndex) {
301
+ for (let i = endIndex - 1; i >= 0; i--) {
302
+ const char = str[i];
303
+ if (char === ",") return true;
304
+ if (!SPACE_REGEX.test(char)) break;
305
+ }
306
+ return false;
307
+ }
308
+ function maybeComma(needed) {
309
+ return needed ? ", " : "";
310
+ }
311
+ //#endregion
312
+ //#region src/import-map.ts
313
+ const EMOTION_OFFICIAL_LIBRARIES = {
314
+ "@emotion/css": {
315
+ css: ExprKind.Css,
316
+ default: ExprKind.Css
317
+ },
318
+ "@emotion/styled": { default: ExprKind.Styled },
319
+ "@emotion/react": {
320
+ css: ExprKind.Css,
321
+ keyframes: ExprKind.Css,
322
+ Global: ExprKind.GlobalJSX
323
+ },
324
+ "@emotion/primitives": {
325
+ css: ExprKind.Css,
326
+ default: ExprKind.Styled
327
+ },
328
+ "@emotion/native": {
329
+ css: ExprKind.Css,
330
+ default: ExprKind.Styled
331
+ }
332
+ };
333
+ function expandImportMap(importMap) {
334
+ const configs = JSON.parse(JSON.stringify(EMOTION_OFFICIAL_LIBRARIES));
335
+ if (!importMap) return configs;
336
+ for (const [importSource, exports] of Object.entries(importMap)) for (const [localExportName, entry] of Object.entries(exports)) {
337
+ const [packageName, exportName] = entry.canonicalImport;
338
+ if (packageName === "@emotion/react" && exportName === "jsx") continue;
339
+ const canonicalConfig = EMOTION_OFFICIAL_LIBRARIES[packageName];
340
+ if (canonicalConfig === void 0) throw new Error(`Import map entry for "${importSource}" references unknown package "${packageName}". Must be one of: ${Object.keys(EMOTION_OFFICIAL_LIBRARIES).join(", ")}`);
341
+ const kind = canonicalConfig[exportName];
342
+ if (kind === void 0) throw new Error(`Import map entry for "${importSource}" references unknown export "${exportName}" in package "${packageName}". Must be one of: ${Object.keys(canonicalConfig).join(", ")}`);
343
+ configs[importSource] ??= {};
344
+ configs[importSource][localExportName] = kind;
345
+ }
346
+ return configs;
347
+ }
348
+ function createImportMap(registeredImports) {
349
+ const importPackages = /* @__PURE__ */ new Map();
350
+ return {
351
+ addFromImportDecl(importDecl) {
352
+ if (importDecl.importKind === "type") return;
353
+ const config = registeredImports[importDecl.source.value];
354
+ if (!config) return;
355
+ for (const spec of importDecl.specifiers) if (spec.type === "ImportSpecifier") {
356
+ const kind = config[spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value];
357
+ if (kind !== void 0) importPackages.set(spec.local.name, {
358
+ type: "named",
359
+ kind
360
+ });
361
+ } else if (spec.type === "ImportDefaultSpecifier") {
362
+ const kind = config.default;
363
+ if (kind !== void 0) importPackages.set(spec.local.name, {
364
+ type: "named",
365
+ kind
366
+ });
367
+ } else if (spec.type === "ImportNamespaceSpecifier") importPackages.set(spec.local.name, {
368
+ type: "namespace",
369
+ config
370
+ });
371
+ },
372
+ get(importedName) {
373
+ return importPackages.get(importedName);
374
+ },
375
+ getTrackedNames() {
376
+ return [...importPackages.keys()];
377
+ },
378
+ isEmpty() {
379
+ return importPackages.size === 0;
380
+ }
381
+ };
382
+ }
383
+ //#endregion
384
+ //#region src/label.ts
385
+ const INVALID_LABEL_SPACES = /\s+/g;
386
+ const INVALID_CSS_CLASS_NAME_CHARS = /[!"#$%&'()*+,./:;<=>?@[\\\]^`|}~{]/g;
387
+ function sanitizeLabelPart(part) {
388
+ return part.replace(INVALID_LABEL_SPACES, "-").replace(INVALID_CSS_CLASS_NAME_CHARS, "-");
389
+ }
390
+ function createLabelWithInfo(labelFormat, context, fileStem, dirName, withPrefix) {
391
+ if (context == null) return "";
392
+ let label = `${withPrefix ? "label:" : ""}${labelFormat}`;
393
+ label = label.replace("[local]", sanitizeLabelPart(context));
394
+ if (fileStem) label = label.replace("[filename]", sanitizeLabelPart(fileStem));
395
+ if (dirName) label = label.replace("[dirname]", sanitizeLabelPart(dirName));
396
+ return label;
397
+ }
398
+ //#endregion
399
+ //#region src/index.ts
400
+ function emotionPlugin(options = {}) {
401
+ const sourceMapEnabled = options.sourceMap;
402
+ const autoLabel = options.autoLabel ?? "dev-only";
403
+ const labelFormat = options.labelFormat ?? "[local]";
404
+ const registeredImports = expandImportMap(options.importMap);
405
+ let isDev = false;
406
+ return {
407
+ name: "rolldown-plugin-emotion",
408
+ enforce: "pre",
409
+ configResolved(config) {
410
+ isDev = !config.isProduction;
411
+ },
412
+ outputOptions() {
413
+ if ("viteVersion" in this.meta) return;
414
+ isDev = process.env.NODE_ENV === "development";
415
+ },
416
+ transform: {
417
+ filter: {
418
+ id: /\.[jt]sx?$/,
419
+ code: new RegExp(Object.keys(registeredImports).map(regexEscape).join("|"))
420
+ },
421
+ handler: withMagicString(function(s, id, meta) {
422
+ const lang = id.endsWith(".tsx") ? "tsx" : id.endsWith(".ts") ? "ts" : id.endsWith(".jsx") ? "jsx" : "js";
423
+ const program = meta?.ast ?? this.parse(s.original, { lang });
424
+ const sourceContent = s.original;
425
+ const srcFileHash = hashString(sourceContent);
426
+ const fileStem = path.basename(id, path.extname(id));
427
+ const dirName = path.basename(path.dirname(id));
428
+ let targetCount = 0;
429
+ const importMap = createImportMap(registeredImports);
430
+ function shouldAddLabel() {
431
+ switch (autoLabel) {
432
+ case "always": return true;
433
+ case "never": return false;
434
+ case "dev-only": return isDev;
435
+ default: return false;
436
+ }
437
+ }
438
+ function createLabel(context, withPrefix) {
439
+ return createLabelWithInfo(labelFormat, context, fileStem, dirName ?? "", withPrefix);
440
+ }
441
+ function makeSourceMap(offset) {
442
+ if (!(sourceMapEnabled ?? isDev)) return null;
443
+ return createSourceMap(sourceContent, id, getPos(sourceContent, offset));
444
+ }
445
+ function buildTaggedTemplateArgs(quasi, inJsx, labelContext, sourceMapOffset, withLabelPrefix, includeLabel = true) {
446
+ const quasis = quasi.quasis;
447
+ const expressions = quasi.expressions;
448
+ const argsLen = quasis.length + expressions.length;
449
+ const parts = [];
450
+ for (let index = 0; index < argsLen; index++) {
451
+ const i = Math.floor(index / 2);
452
+ if (index % 2 === 0) {
453
+ const raw = quasis[i].value.raw;
454
+ const minified = minifyCSSString(unescapeTemplateRaw(raw), index === 0, index === argsLen - 1);
455
+ if (minified.replaceAll(" ", "") === "") {
456
+ if (index !== 0 && index !== argsLen - 1) parts.push("\" \"");
457
+ } else parts.push(`"${escapeJSString(minified)}"`);
458
+ } else {
459
+ const expr = expressions[i];
460
+ parts.push(s.slice(expr.start, expr.end));
461
+ }
462
+ }
463
+ if (!inJsx) {
464
+ if (includeLabel && shouldAddLabel()) {
465
+ const label = createLabel(labelContext, withLabelPrefix);
466
+ parts.push(`"${escapeJSString(label)}"`);
467
+ }
468
+ const sm = makeSourceMap(sourceMapOffset);
469
+ if (sm) parts.push(`"${escapeJSString(sm)}"`);
470
+ }
471
+ return parts.join(", ");
472
+ }
473
+ for (const node of program.body) if (node.type === "ImportDeclaration") importMap.addFromImportDecl(node);
474
+ const trackedNames = importMap.getTrackedNames();
475
+ if (trackedNames.length === 0) return;
476
+ const labelContextStack = [null];
477
+ let inJsx = false;
478
+ const records = new ScopedVisitor({
479
+ trackedNames,
480
+ visitor: {
481
+ VariableDeclarator(node) {
482
+ let ctx = null;
483
+ if (node.id.type === "Identifier") ctx = node.id.name;
484
+ if (node.init?.type === "FunctionExpression" && node.init.id) ctx = node.init.id.name;
485
+ labelContextStack.push(ctx);
486
+ },
487
+ "VariableDeclarator:exit"() {
488
+ labelContextStack.pop();
489
+ },
490
+ FunctionDeclaration(node) {
491
+ labelContextStack.push(node.id.name);
492
+ },
493
+ "FunctionDeclaration:exit"() {
494
+ labelContextStack.pop();
495
+ },
496
+ Property(node) {
497
+ let ctx = null;
498
+ if (!node.computed) {
499
+ if (node.key.type === "Identifier") ctx = node.key.name;
500
+ else if (node.key.type === "Literal" && typeof node.key.value === "string") ctx = node.key.value;
501
+ }
502
+ labelContextStack.push(ctx);
503
+ },
504
+ "Property:exit"() {
505
+ labelContextStack.pop();
506
+ },
507
+ ClassDeclaration(node) {
508
+ const name = node.id?.name ?? labelContextStack[labelContextStack.length - 1];
509
+ labelContextStack.push(name);
510
+ },
511
+ "ClassDeclaration:exit"() {
512
+ labelContextStack.pop();
513
+ },
514
+ PropertyDefinition(node) {
515
+ let ctx = labelContextStack[labelContextStack.length - 1];
516
+ if (node.key.type === "Identifier" && !node.computed) ctx = node.key.name;
517
+ labelContextStack.push(ctx);
518
+ },
519
+ "PropertyDefinition:exit"() {
520
+ labelContextStack.pop();
521
+ },
522
+ JSXElement(node, ctx) {
523
+ const opening = node.openingElement;
524
+ let isGlobal = false;
525
+ let smOffset = node.start;
526
+ let recordName = null;
527
+ if (opening.name.type === "JSXIdentifier") {
528
+ const meta = importMap.get(opening.name.name);
529
+ if (meta?.type === "named" && meta.kind === ExprKind.GlobalJSX) {
530
+ isGlobal = true;
531
+ smOffset = opening.name.start;
532
+ recordName = opening.name.name;
533
+ }
534
+ }
535
+ if (!isGlobal && opening.name.type === "JSXMemberExpression") {
536
+ const obj = opening.name.object;
537
+ const prop = opening.name.property;
538
+ if (obj.type === "JSXIdentifier" && prop.type === "JSXIdentifier") {
539
+ const meta = importMap.get(obj.name);
540
+ if (meta?.type === "namespace" && meta.config[prop.name] === ExprKind.GlobalJSX) {
541
+ isGlobal = true;
542
+ smOffset = obj.start;
543
+ recordName = obj.name;
544
+ }
545
+ }
546
+ }
547
+ if (isGlobal && recordName) {
548
+ const stylesAttr = opening.attributes.find((a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === "styles");
549
+ if (stylesAttr?.value) {
550
+ inJsx = true;
551
+ const attrValue = stylesAttr.value;
552
+ let exprStart;
553
+ let exprEnd;
554
+ if (attrValue.type === "JSXExpressionContainer") {
555
+ exprStart = attrValue.expression.start;
556
+ exprEnd = attrValue.expression.end;
557
+ } else {
558
+ exprStart = attrValue.start;
559
+ exprEnd = attrValue.end;
560
+ }
561
+ const capturedSmOffset = smOffset;
562
+ ctx.record({
563
+ name: recordName,
564
+ node,
565
+ data: {
566
+ nodeStart: node.start,
567
+ nodeEnd: node.end,
568
+ isFullReplace: false,
569
+ apply: () => {
570
+ const sm = makeSourceMap(capturedSmOffset);
571
+ if (sm) {
572
+ s.appendLeft(exprStart, "[");
573
+ s.appendRight(exprEnd, `, "${escapeJSString(sm)}"]`);
574
+ }
575
+ }
576
+ }
577
+ });
578
+ }
579
+ }
580
+ },
581
+ "JSXElement:exit"() {
582
+ inJsx = false;
583
+ },
584
+ TaggedTemplateExpression(node, ctx) {
585
+ const tag = node.tag;
586
+ const quasi = node.quasi;
587
+ const labelContext = labelContextStack[labelContextStack.length - 1];
588
+ if (tag.type === "Identifier") {
589
+ const meta = importMap.get(tag.name);
590
+ if (meta?.type === "named" && meta.kind === ExprKind.Css) {
591
+ let wasInJsx = inJsx;
592
+ ctx.record({
593
+ name: tag.name,
594
+ node,
595
+ data: {
596
+ nodeStart: node.start,
597
+ nodeEnd: node.end,
598
+ isFullReplace: true,
599
+ apply: () => {
600
+ const args = buildTaggedTemplateArgs(quasi, wasInJsx, labelContext, node.start, false);
601
+ const tagText = s.slice(tag.start, tag.end);
602
+ s.update(node.start, node.end, `${tagText}(${args})`);
603
+ if (!wasInJsx) s.appendLeft(node.start, "/* @__PURE__ */ ");
604
+ }
605
+ }
606
+ });
607
+ return;
608
+ }
609
+ }
610
+ if (tag.type === "MemberExpression" && !tag.computed && tag.object.type === "Identifier") {
611
+ const meta = importMap.get(tag.object.name);
612
+ if (meta?.type === "named" && meta.kind === ExprKind.Styled) {
613
+ ctx.record({
614
+ name: tag.object.name,
615
+ node,
616
+ data: {
617
+ nodeStart: node.start,
618
+ nodeEnd: node.end,
619
+ isFullReplace: true,
620
+ apply: (getTarget) => {
621
+ let labelObj = `target: "${getTarget()}"`;
622
+ if (shouldAddLabel()) {
623
+ const label = createLabel(labelContext, false);
624
+ labelObj += `, label: "${escapeJSString(label)}"`;
625
+ }
626
+ const styledArgs = buildTaggedTemplateArgs(quasi, false, labelContext, node.start, false, false);
627
+ const styledName = s.slice(tag.object.start, tag.object.end);
628
+ const propName = tag.property.name;
629
+ s.update(node.start, node.end, `${styledName}("${escapeJSString(propName)}", {\n${labelObj}\n})(${styledArgs})`);
630
+ s.appendLeft(node.start, "/* @__PURE__ */ ");
631
+ }
632
+ }
633
+ });
634
+ return;
635
+ }
636
+ if (meta?.type === "namespace") {
637
+ const propName = tag.property.type === "Identifier" ? tag.property.name : null;
638
+ if (!propName || meta.config[propName] !== ExprKind.Css) return;
639
+ let wasInJsx = inJsx;
640
+ ctx.record({
641
+ name: tag.object.name,
642
+ node,
643
+ data: {
644
+ nodeStart: node.start,
645
+ nodeEnd: node.end,
646
+ isFullReplace: true,
647
+ apply: () => {
648
+ const tagText = s.slice(tag.start, tag.end);
649
+ const args = buildTaggedTemplateArgs(quasi, wasInJsx, labelContext, node.start, true);
650
+ s.update(node.start, node.end, `${tagText}(${args})`);
651
+ s.appendLeft(node.start, "/* @__PURE__ */ ");
652
+ }
653
+ }
654
+ });
655
+ return;
656
+ }
657
+ }
658
+ if (tag.type === "CallExpression" && tag.callee.type === "Identifier") {
659
+ const meta = importMap.get(tag.callee.name);
660
+ if (meta?.type === "named" && meta.kind === ExprKind.Styled) {
661
+ ctx.record({
662
+ name: tag.callee.name,
663
+ node,
664
+ data: {
665
+ nodeStart: node.start,
666
+ nodeEnd: node.end,
667
+ isFullReplace: true,
668
+ apply: (getTarget) => {
669
+ const styledName = s.slice(tag.callee.start, tag.callee.end);
670
+ let labelObj = `target: "${getTarget()}"`;
671
+ if (shouldAddLabel()) {
672
+ const label = createLabel(labelContext, false);
673
+ labelObj += `, label: "${escapeJSString(label)}"`;
674
+ }
675
+ const existingArgs = tag.arguments;
676
+ const firstArgText = existingArgs.length > 0 ? s.slice(existingArgs[0].start, existingArgs[0].end) : "";
677
+ let innerCallText;
678
+ if (existingArgs.length <= 1) innerCallText = `${styledName}(${firstArgText}, {\n${labelObj}\n})`;
679
+ else {
680
+ const secondArg = existingArgs[1];
681
+ if (secondArg.type === "ObjectExpression") {
682
+ const objText = s.slice(secondArg.start + 1, secondArg.end - 1);
683
+ const isEmpty = objText.trim() === "";
684
+ const hasTrailingComma = !isEmpty && objText.trimEnd().endsWith(",");
685
+ innerCallText = `${styledName}(${firstArgText}, { ${isEmpty ? "" : `${objText}${maybeComma(!hasTrailingComma)} `}${labelObj} })`;
686
+ } else {
687
+ const secondArgText = s.slice(secondArg.start, secondArg.end);
688
+ innerCallText = `${styledName}(${firstArgText}, {\n${labelObj},\n\t...${secondArgText}\n})`;
689
+ }
690
+ }
691
+ const styledArgs = buildTaggedTemplateArgs(quasi, false, labelContext, node.start, false, false);
692
+ s.update(node.start, node.end, `${innerCallText}(${styledArgs})`);
693
+ s.appendLeft(node.start, "/* @__PURE__ */ ");
694
+ }
695
+ }
696
+ });
697
+ return;
698
+ }
699
+ }
700
+ },
701
+ CallExpression(node, ctx) {
702
+ const callee = node.callee;
703
+ const args = node.arguments;
704
+ const labelContext = labelContextStack[labelContextStack.length - 1];
705
+ if (callee.type === "Identifier") {
706
+ const meta = importMap.get(callee.name);
707
+ if (meta?.type === "named" && meta.kind === ExprKind.Css && args.length > 0 && !inJsx) {
708
+ ctx.record({
709
+ name: callee.name,
710
+ node,
711
+ data: {
712
+ nodeStart: node.start,
713
+ nodeEnd: node.end,
714
+ isFullReplace: false,
715
+ apply: () => {
716
+ s.appendLeft(node.start, "/* @__PURE__ */ ");
717
+ let hasTrailingComma = checkTrailingCommaExistence(s.original, node.end - 1);
718
+ if (shouldAddLabel()) {
719
+ const label = createLabel(labelContext, true);
720
+ s.appendRight(node.end - 1, `${maybeComma(!hasTrailingComma)}"${escapeJSString(label)}"`);
721
+ hasTrailingComma = false;
722
+ }
723
+ const sm = makeSourceMap(node.start);
724
+ if (sm) s.appendRight(node.end - 1, `${maybeComma(!hasTrailingComma)}"${escapeJSString(sm)}"`);
725
+ }
726
+ }
727
+ });
728
+ return;
729
+ }
730
+ }
731
+ if (callee.type === "CallExpression") {
732
+ const innerCallee = callee.callee;
733
+ if (innerCallee.type === "Identifier") {
734
+ const meta = importMap.get(innerCallee.name);
735
+ if (meta?.type === "named" && meta.kind === ExprKind.Styled && callee.arguments.length > 0) {
736
+ ctx.record({
737
+ name: innerCallee.name,
738
+ node,
739
+ data: {
740
+ nodeStart: node.start,
741
+ nodeEnd: node.end,
742
+ isFullReplace: false,
743
+ apply: (getTarget) => {
744
+ s.appendLeft(node.start, "/* @__PURE__ */ ");
745
+ let labelObj = `target: "${getTarget()}"`;
746
+ if (shouldAddLabel()) {
747
+ const label = createLabel(labelContext, false);
748
+ labelObj += `, label: "${escapeJSString(label)}"`;
749
+ }
750
+ if (callee.arguments.length === 1) s.appendLeft(callee.end - 1, `, {\n\t${labelObj}\n}`);
751
+ else if (callee.arguments.length >= 2) {
752
+ const secondArg = callee.arguments[1];
753
+ if (secondArg.type === "ObjectExpression") if (secondArg.properties.length === 0) s.appendLeft(secondArg.end - 1, ` ${labelObj} `);
754
+ else {
755
+ const hasTrailingComma = checkTrailingCommaExistence(s.original, secondArg.end - 1);
756
+ s.appendLeft(secondArg.end - 1, `${maybeComma(!hasTrailingComma)} ${labelObj}`);
757
+ }
758
+ else {
759
+ const secondArgText = s.slice(secondArg.start, secondArg.end);
760
+ s.update(secondArg.start, secondArg.end, `{ ${labelObj}, ...${secondArgText} }`);
761
+ }
762
+ }
763
+ const sm = makeSourceMap(node.start);
764
+ if (sm) {
765
+ const hasTrailingComma = checkTrailingCommaExistence(s.original, node.end - 1);
766
+ s.appendLeft(node.end - 1, `${maybeComma(!hasTrailingComma)}"${escapeJSString(sm)}"`);
767
+ }
768
+ }
769
+ }
770
+ });
771
+ return;
772
+ }
773
+ }
774
+ }
775
+ if (callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier") {
776
+ const meta = importMap.get(callee.object.name);
777
+ if (meta?.type === "named" && meta.kind === ExprKind.Styled) {
778
+ let wasInJsx = inJsx;
779
+ ctx.record({
780
+ name: callee.object.name,
781
+ node,
782
+ data: {
783
+ nodeStart: node.start,
784
+ nodeEnd: node.end,
785
+ isFullReplace: false,
786
+ apply: (getTarget) => {
787
+ let labelObj = "";
788
+ if (!wasInJsx) {
789
+ labelObj += `target: "${getTarget()}"`;
790
+ s.appendLeft(node.start, "/* @__PURE__ */ ");
791
+ if (shouldAddLabel()) {
792
+ const label = createLabel(labelContext, false);
793
+ labelObj += `, label: "${escapeJSString(label)}"`;
794
+ }
795
+ }
796
+ const styledName = s.slice(callee.object.start, callee.object.end);
797
+ const propName = callee.property.name;
798
+ s.update(callee.start, callee.end, `${styledName}("${escapeJSString(propName)}"${labelObj ? `, { ${labelObj} }` : ""})`);
799
+ if (!wasInJsx) {
800
+ const sm = makeSourceMap(node.start);
801
+ if (sm) {
802
+ const hasTrailingComma = checkTrailingCommaExistence(s.original, node.end - 1);
803
+ s.appendLeft(node.end - 1, `${maybeComma(!hasTrailingComma)}"${escapeJSString(sm)}"`);
804
+ }
805
+ }
806
+ }
807
+ }
808
+ });
809
+ return;
810
+ }
811
+ if (meta?.type === "namespace") {
812
+ const propName = callee.property.type === "Identifier" ? callee.property.name : null;
813
+ if (!propName || meta.config[propName] !== ExprKind.Css) return;
814
+ ctx.record({
815
+ name: callee.object.name,
816
+ node,
817
+ data: {
818
+ nodeStart: node.start,
819
+ nodeEnd: node.end,
820
+ isFullReplace: false,
821
+ apply: () => {
822
+ s.appendLeft(node.start, "/* @__PURE__ */ ");
823
+ let hasTrailingComma = checkTrailingCommaExistence(s.original, node.end - 1);
824
+ if (shouldAddLabel()) {
825
+ const label = createLabel(labelContext, true);
826
+ s.appendRight(node.end - 1, `${maybeComma(!hasTrailingComma)}"${escapeJSString(label)}"`);
827
+ hasTrailingComma = false;
828
+ }
829
+ const sm = makeSourceMap(node.start);
830
+ if (sm) s.appendRight(node.end - 1, `${maybeComma(!hasTrailingComma)}"${escapeJSString(sm)}"`);
831
+ }
832
+ }
833
+ });
834
+ return;
835
+ }
836
+ }
837
+ }
838
+ }
839
+ }).walk(program);
840
+ if (records.length === 0) return;
841
+ const consumedRanges = [];
842
+ for (const record of records) {
843
+ const { nodeStart, nodeEnd, isFullReplace, apply } = record.data;
844
+ if (consumedRanges.some(([cs, ce]) => nodeStart >= cs && nodeEnd <= ce)) continue;
845
+ apply(() => `e${srcFileHash}${targetCount++}`);
846
+ if (isFullReplace) consumedRanges.push([nodeStart, nodeEnd]);
847
+ }
848
+ })
849
+ }
850
+ };
851
+ }
852
+ //#endregion
853
+ export { emotionPlugin as default };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@rolldown/plugin-emotion",
3
+ "version": "0.1.0",
4
+ "description": "Rolldown plugin for Emotion CSS-in-JS",
5
+ "keywords": [
6
+ "css-in-js",
7
+ "emotion",
8
+ "plugin",
9
+ "rolldown",
10
+ "rolldown-plugin"
11
+ ],
12
+ "homepage": "https://github.com/rolldown/plugins/tree/main/packages/emotion#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/rolldown/plugins/issues"
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/rolldown/plugins.git",
20
+ "directory": "packages/emotion"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "exports": "./dist/index.mjs",
27
+ "dependencies": {
28
+ "@emotion/hash": "^0.9.2",
29
+ "@jridgewell/gen-mapping": "^0.3.13",
30
+ "rolldown-string": "^0.3.0"
31
+ },
32
+ "devDependencies": {
33
+ "rolldown": "^1.0.0-rc.9",
34
+ "tinyglobby": "^0.2.15",
35
+ "vite": "^8.0.0",
36
+ "@rolldown/oxc-unshadowed-visitor": "0.0.0"
37
+ },
38
+ "peerDependencies": {
39
+ "rolldown": "^1.0.0-rc.9",
40
+ "vite": "^8.0.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "vite": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "engines": {
48
+ "node": ">=22.12.0 || ^24.0.0"
49
+ },
50
+ "compatiblePackages": {
51
+ "schemaVersion": 1,
52
+ "rollup": {
53
+ "type": "incompatible",
54
+ "reason": "Uses Rolldown-specific APIs"
55
+ }
56
+ },
57
+ "scripts": {
58
+ "dev": "tsdown --watch",
59
+ "build": "tsdown",
60
+ "test": "vitest --project emotion"
61
+ }
62
+ }