@mochi-css/config 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +540 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +505 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import * as SWC from "@swc/core";
|
|
6
|
+
|
|
7
|
+
//#region src/merge.ts
|
|
8
|
+
function mergeArrays(a, b) {
|
|
9
|
+
if (a !== void 0 && b !== void 0) return a.concat(b);
|
|
10
|
+
return a ?? b;
|
|
11
|
+
}
|
|
12
|
+
function mergeCallbacks(a, b) {
|
|
13
|
+
if (a !== void 0 && b !== void 0) return (...args) => {
|
|
14
|
+
a(...args);
|
|
15
|
+
b(...args);
|
|
16
|
+
};
|
|
17
|
+
return a ?? b;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/plugin/TransformationPipeline.ts
|
|
22
|
+
var TransformationPipeline = class {
|
|
23
|
+
transformations = [];
|
|
24
|
+
registerTransformation(transformation) {
|
|
25
|
+
this.transformations.push(transformation);
|
|
26
|
+
}
|
|
27
|
+
async transform(value, ...args) {
|
|
28
|
+
let ret = value;
|
|
29
|
+
for (const transformation of this.transformations) ret = await transformation(ret, ...args);
|
|
30
|
+
return ret;
|
|
31
|
+
}
|
|
32
|
+
getTransformations() {
|
|
33
|
+
return [...this.transformations];
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function makePipeline(transformations) {
|
|
37
|
+
const ret = new TransformationPipeline();
|
|
38
|
+
for (const transformation of transformations) ret.registerTransformation(transformation);
|
|
39
|
+
return ret;
|
|
40
|
+
}
|
|
41
|
+
var FilteredTransformationPipeline = class {
|
|
42
|
+
transformations = [];
|
|
43
|
+
constructor(filter) {
|
|
44
|
+
this.filter = filter;
|
|
45
|
+
}
|
|
46
|
+
registerTransformation(transformation, data) {
|
|
47
|
+
this.transformations.push({
|
|
48
|
+
fn: transformation,
|
|
49
|
+
ctx: data
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async transform(value, ...args) {
|
|
53
|
+
let ret = value;
|
|
54
|
+
for (const transformation of this.transformations) {
|
|
55
|
+
if (!this.filter(transformation.ctx, ...args)) continue;
|
|
56
|
+
ret = await transformation.fn(ret, ...args);
|
|
57
|
+
}
|
|
58
|
+
return ret;
|
|
59
|
+
}
|
|
60
|
+
getTransformations() {
|
|
61
|
+
return [...this.transformations];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/plugin/globEx.ts
|
|
67
|
+
function escapeRegexLiteral(s) {
|
|
68
|
+
return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
69
|
+
}
|
|
70
|
+
function globToRegexBody(pattern) {
|
|
71
|
+
let result = "";
|
|
72
|
+
let i = 0;
|
|
73
|
+
while (i < pattern.length) {
|
|
74
|
+
const char = pattern.charAt(i);
|
|
75
|
+
if (char === "*" && pattern.charAt(i + 1) === "*") {
|
|
76
|
+
result += ".*";
|
|
77
|
+
i += 2;
|
|
78
|
+
if (pattern.charAt(i) === "/") i++;
|
|
79
|
+
} else if (char === "*") {
|
|
80
|
+
result += "[^/]*";
|
|
81
|
+
i++;
|
|
82
|
+
} else if (char === "?") {
|
|
83
|
+
result += "[^/]";
|
|
84
|
+
i++;
|
|
85
|
+
} else if (char === "{") {
|
|
86
|
+
const end = pattern.indexOf("}", i);
|
|
87
|
+
if (end !== -1) {
|
|
88
|
+
const alts = pattern.slice(i + 1, end).split(",").map(escapeRegexLiteral);
|
|
89
|
+
result += `(${alts.join("|")})`;
|
|
90
|
+
i = end + 1;
|
|
91
|
+
} else {
|
|
92
|
+
result += "\\{";
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
result += /[.+^$|()[\]\\]/.test(char) ? `\\${char}` : char;
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Converts a Turbopack loader filter glob pattern to a RegExp.
|
|
104
|
+
* - No `/` in pattern → matches filename only (anchored to last path segment)
|
|
105
|
+
* - Contains `/` → matches against the full project-relative path
|
|
106
|
+
*/
|
|
107
|
+
function globExToRegex(pattern) {
|
|
108
|
+
const isPathPattern = pattern.includes("/");
|
|
109
|
+
const body = globToRegexBody(pattern);
|
|
110
|
+
return isPathPattern ? new RegExp(body) : /* @__PURE__ */ new RegExp(`(?:^|/)${body}$`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/plugin/Context.ts
|
|
115
|
+
const fileFilterRegexCache = /* @__PURE__ */ new Map();
|
|
116
|
+
function getRegexForFilter(filter) {
|
|
117
|
+
let regex = fileFilterRegexCache.get(filter);
|
|
118
|
+
if (regex) return regex;
|
|
119
|
+
regex = globExToRegex(filter);
|
|
120
|
+
fileFilterRegexCache.set(filter, regex);
|
|
121
|
+
return regex;
|
|
122
|
+
}
|
|
123
|
+
const fileFilter = ({ filter }, { filePath }) => {
|
|
124
|
+
return filter === void 0 || getRegexForFilter(filter).test(filePath);
|
|
125
|
+
};
|
|
126
|
+
function makeFilePipeline() {
|
|
127
|
+
return new FilteredTransformationPipeline(fileFilter);
|
|
128
|
+
}
|
|
129
|
+
var AnalysisHookCollector = class {
|
|
130
|
+
hooks = [];
|
|
131
|
+
register(hook) {
|
|
132
|
+
this.hooks.push(hook);
|
|
133
|
+
}
|
|
134
|
+
getHooks() {
|
|
135
|
+
return [...this.hooks];
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var FullContext = class {
|
|
139
|
+
sourceTransform = makeFilePipeline();
|
|
140
|
+
analysisTransform = new AnalysisHookCollector();
|
|
141
|
+
getAnalysisHooks() {
|
|
142
|
+
return this.analysisTransform.getHooks();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/config/index.ts
|
|
148
|
+
function defineConfig(config) {
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
151
|
+
async function resolveConfig(fileConfig, inlineConfig, defaults) {
|
|
152
|
+
const plugins = mergeArrays(fileConfig.plugins, inlineConfig?.plugins) ?? defaults?.plugins ?? [];
|
|
153
|
+
const resolved = {
|
|
154
|
+
roots: mergeArrays(fileConfig.roots, inlineConfig?.roots) ?? defaults?.roots ?? [],
|
|
155
|
+
extractors: mergeArrays(fileConfig.extractors, inlineConfig?.extractors) ?? defaults?.extractors ?? [],
|
|
156
|
+
splitCss: inlineConfig?.splitCss ?? fileConfig.splitCss ?? defaults?.splitCss ?? false,
|
|
157
|
+
onDiagnostic: mergeCallbacks(fileConfig.onDiagnostic, inlineConfig?.onDiagnostic),
|
|
158
|
+
plugins,
|
|
159
|
+
tmpDir: inlineConfig?.tmpDir ?? fileConfig.tmpDir ?? defaults?.tmpDir,
|
|
160
|
+
debug: inlineConfig?.debug ?? fileConfig.debug ?? defaults?.debug
|
|
161
|
+
};
|
|
162
|
+
return await makePipeline(plugins.map((plugin) => plugin.onConfigResolved).filter((c) => c !== void 0)).transform(resolved);
|
|
163
|
+
}
|
|
164
|
+
const configName = "mochi.config";
|
|
165
|
+
const CONFIG_FILE_NAMES = [
|
|
166
|
+
"mts",
|
|
167
|
+
"cts",
|
|
168
|
+
"ts",
|
|
169
|
+
"mjs",
|
|
170
|
+
"cjs",
|
|
171
|
+
"js"
|
|
172
|
+
].map((ext) => `${configName}.${ext}`);
|
|
173
|
+
async function loadConfig(cwd) {
|
|
174
|
+
const dir = cwd ?? process.cwd();
|
|
175
|
+
const configFile = CONFIG_FILE_NAMES.map((name) => path.resolve(dir, name)).find((p) => fs.existsSync(p));
|
|
176
|
+
if (!configFile) return {};
|
|
177
|
+
const mod = await createJiti(import.meta.url).import(configFile);
|
|
178
|
+
const config = mod != null && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
179
|
+
if (config == null || typeof config !== "object") return {};
|
|
180
|
+
return config;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region ../vanilla/dist/index.mjs
|
|
185
|
+
/**
|
|
186
|
+
* Hashing utilities for generating short, deterministic class names.
|
|
187
|
+
* Uses djb2 algorithm for fast string hashing.
|
|
188
|
+
* @module hash
|
|
189
|
+
*/
|
|
190
|
+
/** Characters used for base-62 encoding (css-name safe variant of base-64) */
|
|
191
|
+
const hashBase = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
|
|
192
|
+
const base = 64;
|
|
193
|
+
/**
|
|
194
|
+
* Converts a number to a base-62 string representation.
|
|
195
|
+
* @param num - The number to convert
|
|
196
|
+
* @param maxLength - Optional maximum length of the output string
|
|
197
|
+
* @returns Base-62 encoded string representation of the number
|
|
198
|
+
*/
|
|
199
|
+
function numberToBase62(num, maxLength) {
|
|
200
|
+
let out = "";
|
|
201
|
+
while (num > 0 && out.length < (maxLength ?? Infinity)) {
|
|
202
|
+
out = hashBase[num % base] + out;
|
|
203
|
+
num = Math.floor(num / base);
|
|
204
|
+
}
|
|
205
|
+
return out.length > 0 ? out : "0";
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Generates a short hash string from input using the djb2 algorithm.
|
|
209
|
+
* Used to create unique, deterministic CSS class names from style content.
|
|
210
|
+
* @param input - The string to hash
|
|
211
|
+
* @param length - Maximum length of the hash output (default: 8)
|
|
212
|
+
* @returns A short, css-safe hash string
|
|
213
|
+
* @example
|
|
214
|
+
* shortHash("color: red;") // Returns something like "A1b2C3d4"
|
|
215
|
+
*/
|
|
216
|
+
function shortHash(input, length = 8) {
|
|
217
|
+
let h = 5381;
|
|
218
|
+
for (let i = 0; i < input.length; i++) h = h * 33 ^ input.charCodeAt(i);
|
|
219
|
+
h >>>= 0;
|
|
220
|
+
return numberToBase62(h, length);
|
|
221
|
+
}
|
|
222
|
+
const MOCHI_CSS_TYPEOF = Symbol.for("mochi-css.MochiCSS");
|
|
223
|
+
/**
|
|
224
|
+
* Runtime representation of a CSS style definition with variant support.
|
|
225
|
+
* Holds generated class names and provides methods to compute the final
|
|
226
|
+
* className string based on selected variants.
|
|
227
|
+
*
|
|
228
|
+
* @template V - The variant definitions type mapping variant names to their options
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* const styles = MochiCSS.from(new CSSObject({
|
|
232
|
+
* color: 'blue',
|
|
233
|
+
* variants: { size: { small: { fontSize: 12 }, large: { fontSize: 18 } } }
|
|
234
|
+
* }))
|
|
235
|
+
* styles.variant({ size: 'large' }) // Returns combined class names
|
|
236
|
+
*/
|
|
237
|
+
var MochiCSS = class MochiCSS$1 {
|
|
238
|
+
$$typeof = MOCHI_CSS_TYPEOF;
|
|
239
|
+
/**
|
|
240
|
+
* Creates a new MochiCSS instance.
|
|
241
|
+
* @param classNames - Base class names to always include
|
|
242
|
+
* @param variantClassNames - Mapping of variant names to option class names
|
|
243
|
+
* @param defaultVariants - Default variant selections when not specified
|
|
244
|
+
*/
|
|
245
|
+
constructor(classNames, variantClassNames, defaultVariants) {
|
|
246
|
+
this.classNames = classNames;
|
|
247
|
+
this.variantClassNames = variantClassNames;
|
|
248
|
+
this.defaultVariants = defaultVariants;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Computes the final className string based on variant selections.
|
|
252
|
+
* Compound variants are handled purely via CSS combined selectors,
|
|
253
|
+
* so no runtime matching is needed here.
|
|
254
|
+
* @param props - Variant selections
|
|
255
|
+
* @returns Combined className string for use in components
|
|
256
|
+
*/
|
|
257
|
+
variant(props) {
|
|
258
|
+
const keys = new Set([...Object.keys(props), ...Object.keys(this.defaultVariants)].filter((k) => k in this.variantClassNames));
|
|
259
|
+
return clsx(this.classNames, ...keys.values().map((k) => {
|
|
260
|
+
const variantGroup = this.variantClassNames[k];
|
|
261
|
+
if (!variantGroup) return false;
|
|
262
|
+
const variantKey = ((k in props ? props[k] : void 0) ?? this.defaultVariants[k])?.toString();
|
|
263
|
+
if (variantKey == null) return false;
|
|
264
|
+
const selectedClassname = variantGroup[variantKey];
|
|
265
|
+
if (selectedClassname !== void 0) return selectedClassname;
|
|
266
|
+
const defaultKey = this.defaultVariants[k];
|
|
267
|
+
if (defaultKey == null) return false;
|
|
268
|
+
return variantGroup[defaultKey.toString()];
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Returns the CSS selector for this style (e.g. `.abc123`).
|
|
273
|
+
* Useful for targeting this component from another style.
|
|
274
|
+
*/
|
|
275
|
+
get selector() {
|
|
276
|
+
return this.classNames.map((n) => `.${n}`).join();
|
|
277
|
+
}
|
|
278
|
+
toString() {
|
|
279
|
+
return this.selector;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Creates a MochiCSS instance from a CSSObject.
|
|
283
|
+
* Extracts class names from the compiled CSS blocks.
|
|
284
|
+
* @template V - The variant definitions type
|
|
285
|
+
* @param object - The compiled CSSObject to extract from
|
|
286
|
+
* @returns A new MochiCSS instance with the extracted class names
|
|
287
|
+
*/
|
|
288
|
+
static from(object) {
|
|
289
|
+
return new MochiCSS$1([object.mainBlock.className], Object.fromEntries(Object.entries(object.variantBlocks).map(([key, variantOptions]) => {
|
|
290
|
+
return [key, Object.fromEntries(Object.entries(variantOptions).map(([optionKey, block]) => {
|
|
291
|
+
return [optionKey, block.className];
|
|
292
|
+
}))];
|
|
293
|
+
})), object.variantDefaults);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Creates a CSS style definition.
|
|
298
|
+
* The primary API for defining styles in Mochi-CSS.
|
|
299
|
+
*
|
|
300
|
+
* @template V - Tuple of variant definition types
|
|
301
|
+
* @param props - One or more style objects or existing MochiCSS instances to merge
|
|
302
|
+
* @returns A MochiCSS instance with all styles and variants combined
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* // Simple usage
|
|
306
|
+
* const button = css({ padding: 8, borderRadius: 4 })
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* // With variants
|
|
310
|
+
* const button = css({
|
|
311
|
+
* padding: 8,
|
|
312
|
+
* variants: {
|
|
313
|
+
* size: {
|
|
314
|
+
* small: { padding: 4 },
|
|
315
|
+
* large: { padding: 16 }
|
|
316
|
+
* }
|
|
317
|
+
* },
|
|
318
|
+
* defaultVariants: { size: 'small' }
|
|
319
|
+
* })
|
|
320
|
+
* button.variant({ size: 'large' }) // Get class names for large size
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* // Merging multiple styles
|
|
324
|
+
* const combined = css(baseStyles, additionalStyles)
|
|
325
|
+
*/
|
|
326
|
+
const emptyMochiCSS = new MochiCSS([], {}, {});
|
|
327
|
+
/**
|
|
328
|
+
* Wraps a condition in parentheses if not already wrapped.
|
|
329
|
+
*/
|
|
330
|
+
function wrapParens(condition) {
|
|
331
|
+
const trimmed = condition.trim();
|
|
332
|
+
if (trimmed.startsWith("(") && trimmed.endsWith(")")) return trimmed;
|
|
333
|
+
return `(${trimmed})`;
|
|
334
|
+
}
|
|
335
|
+
function mediaFn(condition) {
|
|
336
|
+
return `@media ${wrapParens(condition)}`;
|
|
337
|
+
}
|
|
338
|
+
mediaFn.and = function(...conditions) {
|
|
339
|
+
return `@media ${conditions.map(wrapParens).join(" and ")}`;
|
|
340
|
+
};
|
|
341
|
+
mediaFn.or = function(...conditions) {
|
|
342
|
+
return `@media ${conditions.map(wrapParens).join(", ")}`;
|
|
343
|
+
};
|
|
344
|
+
Object.defineProperties(mediaFn, {
|
|
345
|
+
dark: {
|
|
346
|
+
get: () => "@media (prefers-color-scheme: dark)",
|
|
347
|
+
enumerable: true
|
|
348
|
+
},
|
|
349
|
+
light: {
|
|
350
|
+
get: () => "@media (prefers-color-scheme: light)",
|
|
351
|
+
enumerable: true
|
|
352
|
+
},
|
|
353
|
+
motion: {
|
|
354
|
+
get: () => "@media (prefers-reduced-motion: no-preference)",
|
|
355
|
+
enumerable: true
|
|
356
|
+
},
|
|
357
|
+
print: {
|
|
358
|
+
get: () => "@media print",
|
|
359
|
+
enumerable: true
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
function containerFn(condition) {
|
|
363
|
+
return `@container ${wrapParens(condition)}`;
|
|
364
|
+
}
|
|
365
|
+
containerFn.named = function(name, condition) {
|
|
366
|
+
return `@container ${name} ${wrapParens(condition)}`;
|
|
367
|
+
};
|
|
368
|
+
function supportsFn(condition) {
|
|
369
|
+
return `@supports ${wrapParens(condition)}`;
|
|
370
|
+
}
|
|
371
|
+
supportsFn.not = function(condition) {
|
|
372
|
+
return `@supports not ${wrapParens(condition)}`;
|
|
373
|
+
};
|
|
374
|
+
supportsFn.and = function(...conditions) {
|
|
375
|
+
return `@supports ${conditions.map(wrapParens).join(" and ")}`;
|
|
376
|
+
};
|
|
377
|
+
supportsFn.or = function(...conditions) {
|
|
378
|
+
return `@supports ${conditions.map(wrapParens).join(" or ")}`;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/styledIdTransform.ts
|
|
383
|
+
const STABLE_ID_RE = /^s-[0-9A-Za-z_-]+$/;
|
|
384
|
+
function collectStyledCalls(ast) {
|
|
385
|
+
const results = [];
|
|
386
|
+
function visitExpr(expr, varName) {
|
|
387
|
+
if (expr.type === "CallExpression") {
|
|
388
|
+
const callee = expr.callee;
|
|
389
|
+
if (callee.type === "Identifier" && callee.value === "styled" || callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.value === "styled") results.push({
|
|
390
|
+
call: expr,
|
|
391
|
+
varName
|
|
392
|
+
});
|
|
393
|
+
for (const arg of expr.arguments) visitExpr(arg.expression, null);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const item of ast.body) if (item.type === "VariableDeclaration") for (const d of item.declarations) {
|
|
397
|
+
const name = d.id.type === "Identifier" ? d.id.value : null;
|
|
398
|
+
if (d.init) visitExpr(d.init, name);
|
|
399
|
+
}
|
|
400
|
+
else if (item.type === "ExportDeclaration" && item.declaration.type === "VariableDeclaration") for (const d of item.declaration.declarations) {
|
|
401
|
+
const name = d.id.type === "Identifier" ? d.id.value : null;
|
|
402
|
+
if (d.init) visitExpr(d.init, name);
|
|
403
|
+
}
|
|
404
|
+
else if (item.type === "ExpressionStatement") visitExpr(item.expression, null);
|
|
405
|
+
else if (item.type === "ExportDefaultExpression") visitExpr(item.expression, "default");
|
|
406
|
+
return results;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Transforms source code to inject stable `s-` class IDs into every `styled()` call.
|
|
410
|
+
* Idempotent: skips calls that already have an `s-` string as the last argument.
|
|
411
|
+
*/
|
|
412
|
+
function transformStyledIds(source, filePath) {
|
|
413
|
+
if (!source.includes("styled")) return source;
|
|
414
|
+
let ast;
|
|
415
|
+
try {
|
|
416
|
+
ast = SWC.parseSync(source, {
|
|
417
|
+
syntax: "typescript",
|
|
418
|
+
tsx: filePath.endsWith(".tsx"),
|
|
419
|
+
target: "es2022"
|
|
420
|
+
});
|
|
421
|
+
} catch {
|
|
422
|
+
return source;
|
|
423
|
+
}
|
|
424
|
+
const calls = collectStyledCalls(ast);
|
|
425
|
+
if (calls.length === 0) return source;
|
|
426
|
+
const toInject = calls.filter((entry) => {
|
|
427
|
+
const args = entry.call.arguments;
|
|
428
|
+
if (args.length === 0) return true;
|
|
429
|
+
const last = args[args.length - 1];
|
|
430
|
+
return !(last?.expression.type === "StringLiteral" && STABLE_ID_RE.test(last.expression.value));
|
|
431
|
+
});
|
|
432
|
+
if (toInject.length === 0) return source;
|
|
433
|
+
const sortedAsc = [...toInject].sort((a, b) => a.call.span.start - b.call.span.start);
|
|
434
|
+
let sourceGlobalBase = 0;
|
|
435
|
+
let searchFrom = 0;
|
|
436
|
+
const callsWithOffset = [];
|
|
437
|
+
for (const entry of sortedAsc) {
|
|
438
|
+
const idx = source.indexOf("styled(", searchFrom);
|
|
439
|
+
if (idx < 0) continue;
|
|
440
|
+
if (callsWithOffset.length === 0) {
|
|
441
|
+
const callee = entry.call.callee;
|
|
442
|
+
sourceGlobalBase = (callee.type === "Identifier" ? callee.span.start : callee.property.span.start) - idx;
|
|
443
|
+
}
|
|
444
|
+
callsWithOffset.push({
|
|
445
|
+
entry,
|
|
446
|
+
offset: entry.call.span.end - sourceGlobalBase - 1
|
|
447
|
+
});
|
|
448
|
+
searchFrom = idx + 1;
|
|
449
|
+
}
|
|
450
|
+
callsWithOffset.sort((a, b) => b.offset - a.offset);
|
|
451
|
+
let result = source;
|
|
452
|
+
callsWithOffset.forEach(({ entry, offset }, i) => {
|
|
453
|
+
const sortIdx = callsWithOffset.length - 1 - i;
|
|
454
|
+
const varName = entry.varName ?? String(sortIdx);
|
|
455
|
+
const id = "s-" + shortHash(filePath + ":" + varName);
|
|
456
|
+
if (offset < 0 || offset >= result.length || result[offset] !== ")") return;
|
|
457
|
+
result = result.slice(0, offset) + `, '${id}'` + result.slice(offset);
|
|
458
|
+
});
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/styledIdPlugin.ts
|
|
464
|
+
const DUMMY_SPAN = {
|
|
465
|
+
start: 0,
|
|
466
|
+
end: 0,
|
|
467
|
+
ctxt: 0
|
|
468
|
+
};
|
|
469
|
+
function hasStableId(call) {
|
|
470
|
+
const args = call.arguments;
|
|
471
|
+
if (args.length === 0) return false;
|
|
472
|
+
const last = args[args.length - 1];
|
|
473
|
+
return last?.expression.type === "StringLiteral" && STABLE_ID_RE.test(last.expression.value);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Returns a MochiPlugin that injects stable `s-` class IDs into every `styled()` call.
|
|
477
|
+
* - Registers a `sourceTransform` for runtime source injection (Vite/Next `transform` hook).
|
|
478
|
+
* - Registers an `analysisTransform` for CSS extraction via direct AST mutation.
|
|
479
|
+
*/
|
|
480
|
+
function styledIdPlugin() {
|
|
481
|
+
return {
|
|
482
|
+
name: "mochi-styled-ids",
|
|
483
|
+
onLoad(context) {
|
|
484
|
+
context.sourceTransform.registerTransformation((source, { filePath }) => transformStyledIds(source, filePath), { filter: "*.{ts,tsx,js,jsx}" });
|
|
485
|
+
context.analysisTransform.register((index) => {
|
|
486
|
+
for (const [filePath, fileInfo] of index.files) collectStyledCalls(fileInfo.ast).filter(({ call }) => !hasStableId(call)).forEach(({ call, varName }, i) => {
|
|
487
|
+
const id = "s-" + shortHash(filePath + ":" + (varName ?? String(i)));
|
|
488
|
+
call.arguments.push({
|
|
489
|
+
spread: void 0,
|
|
490
|
+
expression: {
|
|
491
|
+
type: "StringLiteral",
|
|
492
|
+
span: DUMMY_SPAN,
|
|
493
|
+
value: id,
|
|
494
|
+
raw: `'${id}'`
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
//#endregion
|
|
504
|
+
export { FullContext, TransformationPipeline, defineConfig, loadConfig, mergeArrays, mergeCallbacks, resolveConfig, styledIdPlugin };
|
|
505
|
+
//# sourceMappingURL=index.mjs.map
|