@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 +21 -0
- package/README.md +129 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.mjs +853 -0
- package/package.json +62 -0
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 [](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.
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|