@calmdown/rolldown-plugin-lightningcss 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/index.js +268 -0
- package/package.json +20 -0
package/index.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { transform } from "lightningcss";
|
|
4
|
+
|
|
5
|
+
const PLUGIN_NAME = "LightningCss";
|
|
6
|
+
|
|
7
|
+
const RE_MODULE = /\.module\.css$/i;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} LightningCssPluginOptions
|
|
11
|
+
* @property {string|string[]} [include] glob pattern(s) of files to include, defaults to `**/*.css`
|
|
12
|
+
* @property {string|string[]} [exclude] glob pattern(s) to exclude (optional)
|
|
13
|
+
* @property {Omit<import("lightningcss").TransformOptions, "code" | "filename" | "sourceMap" | "minify">} [lightningcss] custom inline LightningCSS options
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {LightningCssPluginOptions} [pluginOptions]
|
|
18
|
+
*/
|
|
19
|
+
export default function LightningCssPlugin(pluginOptions) {
|
|
20
|
+
const lightningCssConfig = {
|
|
21
|
+
...pluginOptions?.lightningcss,
|
|
22
|
+
cssModules: pluginOptions?.lightningcss.cssModules ?? true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const chunkMap = new Map();
|
|
26
|
+
const modulesEnabled = Boolean(lightningCssConfig.cssModules);
|
|
27
|
+
let root;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: PLUGIN_NAME,
|
|
31
|
+
buildStart(inputOptions) {
|
|
32
|
+
root = inputOptions.cwd ?? process.cwd();
|
|
33
|
+
},
|
|
34
|
+
transform: {
|
|
35
|
+
filter: {
|
|
36
|
+
id: {
|
|
37
|
+
include: pluginOptions?.include ?? "**/*.css",
|
|
38
|
+
exclude: pluginOptions?.exclude,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
handler(code, moduleId) {
|
|
42
|
+
let chunk = chunkMap.get(moduleId);
|
|
43
|
+
if (chunk) {
|
|
44
|
+
return chunk.transformResult;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// prepare a buffer with the CSS code
|
|
48
|
+
const codeBuffer = Buffer.from(code, "utf8");
|
|
49
|
+
|
|
50
|
+
// Because we don't know the output options yet (there may also be more than one
|
|
51
|
+
// output), we have to pre-transform CSS modules to know the exports ahead of time.
|
|
52
|
+
let jsCode;
|
|
53
|
+
if (modulesEnabled && RE_MODULE.test(moduleId)) {
|
|
54
|
+
const { exports } = transform({
|
|
55
|
+
...lightningCssConfig,
|
|
56
|
+
filename: moduleId,
|
|
57
|
+
code: codeBuffer,
|
|
58
|
+
projectRoot: root,
|
|
59
|
+
minify: false,
|
|
60
|
+
sourceMap: false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const classMap = Object
|
|
64
|
+
.keys(exports)
|
|
65
|
+
.reduce((map, key) => (map[key] = exports[key].name, map), {});
|
|
66
|
+
|
|
67
|
+
jsCode = `export default ${JSON.stringify(classMap)};`;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
jsCode = "export {};";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// cache the current chunk
|
|
74
|
+
chunkMap.set(moduleId, chunk = {
|
|
75
|
+
moduleId,
|
|
76
|
+
code,
|
|
77
|
+
codeBuffer,
|
|
78
|
+
transformResult: {
|
|
79
|
+
moduleType: "js",
|
|
80
|
+
code: jsCode,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return chunk.transformResult;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
generateBundle(outputOptions, bundleMap) {
|
|
88
|
+
const baseDir = path.resolve(root, outputOptions.dir);
|
|
89
|
+
const baseUrl = outputOptions.sourcemapBaseUrl ? new URL(outputOptions.sourcemapBaseUrl) : null;
|
|
90
|
+
Object
|
|
91
|
+
.values(bundleMap)
|
|
92
|
+
.filter(bundle => bundle.type === "chunk")
|
|
93
|
+
.forEach(bundle => {
|
|
94
|
+
const sourcemapEnabled = outputOptions.sourcemap ?? false;
|
|
95
|
+
const fileName = `${path.parse(bundle.fileName).name}.css`;
|
|
96
|
+
|
|
97
|
+
// generate merged CSS chunk
|
|
98
|
+
const chunks = bundle.moduleIds
|
|
99
|
+
.map(moduleId => chunkMap.get(moduleId))
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.map(chunk => {
|
|
102
|
+
const result = transform({
|
|
103
|
+
...lightningCssConfig,
|
|
104
|
+
filename: chunk.moduleId,
|
|
105
|
+
code: chunk.codeBuffer,
|
|
106
|
+
projectRoot: root,
|
|
107
|
+
minify: Boolean(outputOptions.minify),
|
|
108
|
+
sourceMap: sourcemapEnabled,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// forward warnings to Rollup
|
|
112
|
+
for (const warning of result.warnings) {
|
|
113
|
+
this.warn({
|
|
114
|
+
code: warning.type,
|
|
115
|
+
message: warning.message,
|
|
116
|
+
loc: {
|
|
117
|
+
column: warning.loc.column,
|
|
118
|
+
line: warning.loc.line,
|
|
119
|
+
file: warning.loc.filename,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// get sourcemap if enabled
|
|
125
|
+
let mappings = null;
|
|
126
|
+
if (sourcemapEnabled) {
|
|
127
|
+
try {
|
|
128
|
+
const sourcemap = JSON.parse(result.map.toString("utf8"));
|
|
129
|
+
if (sourcemap?.version !== 3) {
|
|
130
|
+
throw new Error("expected sourcemap version 3");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
mappings = sourcemap.mappings ?? "";
|
|
134
|
+
}
|
|
135
|
+
catch (ex) {
|
|
136
|
+
this.warn({
|
|
137
|
+
code: "E_SOURCEMAP",
|
|
138
|
+
message: `failed to parse sourcemap, ${ex.message}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
moduleId: chunk.moduleId,
|
|
145
|
+
originalCode: chunk.code,
|
|
146
|
+
transformedCode: result.code.toString("utf8"),
|
|
147
|
+
mappings,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// merge CSS code
|
|
152
|
+
let code = chunks.map(chunk => chunk.transformedCode).join("\n");
|
|
153
|
+
|
|
154
|
+
// merge source mappings if enabled
|
|
155
|
+
if (sourcemapEnabled) {
|
|
156
|
+
const sourcemap = {
|
|
157
|
+
version: 3,
|
|
158
|
+
sources: chunks.map(chunk => normalRelativePath(baseDir, chunk.moduleId)),
|
|
159
|
+
sourcesContent: chunks.map(chunk => chunk.originalCode),
|
|
160
|
+
names: [],
|
|
161
|
+
mappings: chunks
|
|
162
|
+
.map((chunk, sourceIndex) => replaceSourceIndex(chunk.mappings, sourceIndex))
|
|
163
|
+
.join(";"),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// emit sourcemap chunk
|
|
167
|
+
const sourcemapFileName = `${fileName}.map`;
|
|
168
|
+
this.emitFile({
|
|
169
|
+
type: "prebuilt-chunk",
|
|
170
|
+
fileName: sourcemapFileName,
|
|
171
|
+
code: JSON.stringify(sourcemap),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
code += `\n/*# sourceMappingURL=${baseUrl ? new URL(sourcemapFileName, baseUrl) : sourcemapFileName} */`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// emit CSS chunk
|
|
178
|
+
this.emitFile({
|
|
179
|
+
type: "prebuilt-chunk",
|
|
180
|
+
fileName,
|
|
181
|
+
code,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// reset cache
|
|
186
|
+
chunkMap.clear();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalRelativePath(from, to) {
|
|
192
|
+
const relative = path.relative(from, to).replace(/\\/g, "/");
|
|
193
|
+
return path.posix.normalize(relative);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const VLQ_ENCODE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
197
|
+
const VLQ_DECODE = Array.prototype.reduce.call(VLQ_ENCODE, (map, char, id) => (map[char] = id, map), {});
|
|
198
|
+
|
|
199
|
+
// source index replacer for v3 sourcemap mappings
|
|
200
|
+
// assumes a valid v3 mapping, otherwise the result will most likely get mangled
|
|
201
|
+
function replaceSourceIndex(mapping, newSourceIndex) {
|
|
202
|
+
const newSourceIndexVLQ = encodeVLQ(newSourceIndex);
|
|
203
|
+
const { length } = mapping;
|
|
204
|
+
let result = "";
|
|
205
|
+
let index = 0;
|
|
206
|
+
|
|
207
|
+
const endOfSegmentOrLine = () => {
|
|
208
|
+
const char = mapping[index];
|
|
209
|
+
if (char !== "," && char !== ";") {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
result += char;
|
|
214
|
+
index += 1;
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const nextValue = () => {
|
|
219
|
+
const anchor = index;
|
|
220
|
+
while (index < length && ((VLQ_DECODE[mapping[index++]] ?? 0) & 0b100000) > 0) ;
|
|
221
|
+
|
|
222
|
+
return mapping.slice(anchor, index);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
while (index < length) {
|
|
226
|
+
if (endOfSegmentOrLine()) {
|
|
227
|
+
continue; // empty line
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
result += nextValue(); // transpiled column
|
|
231
|
+
if (endOfSegmentOrLine()) {
|
|
232
|
+
continue; // single field segment
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
nextValue();
|
|
236
|
+
result += newSourceIndexVLQ; // replaced source index
|
|
237
|
+
result += nextValue(); // original line
|
|
238
|
+
result += nextValue(); // original column
|
|
239
|
+
|
|
240
|
+
if (endOfSegmentOrLine()) {
|
|
241
|
+
continue; // four fields segment
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
result += nextValue(); // name index
|
|
245
|
+
endOfSegmentOrLine();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function encodeVLQ(value) {
|
|
252
|
+
let remainder = value < 0 ? ((-value << 1) | 1) : (value << 1); // zig-zag
|
|
253
|
+
let digit;
|
|
254
|
+
let vlq = "";
|
|
255
|
+
|
|
256
|
+
do {
|
|
257
|
+
digit = remainder & 0b11111;
|
|
258
|
+
remainder >>>= 5;
|
|
259
|
+
if (remainder > 0) {
|
|
260
|
+
digit |= 0b100000;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
vlq += VLQ_ENCODE[digit];
|
|
264
|
+
}
|
|
265
|
+
while (remainder > 0);
|
|
266
|
+
|
|
267
|
+
return vlq;
|
|
268
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@calmdown/rolldown-plugin-lightningcss",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "ISC",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js"
|
|
13
|
+
],
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"lightningcss": ">=1.32.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"lightningcss": "1.32.0"
|
|
19
|
+
}
|
|
20
|
+
}
|