@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.
Files changed (2) hide show
  1. package/index.js +268 -0
  2. 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
+ }