@mochi-css/config 3.0.0 → 3.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/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import { createJiti } from "jiti";
4
- import clsx from "clsx";
4
+ import { shortHash } from "@mochi-css/core";
5
5
  import * as SWC from "@swc/core";
6
6
 
7
7
  //#region src/merge.ts
@@ -126,20 +126,51 @@ const fileFilter = ({ filter }, { filePath }) => {
126
126
  function makeFilePipeline() {
127
127
  return new FilteredTransformationPipeline(fileFilter);
128
128
  }
129
- var AnalysisHookCollector = class {
129
+ var SourceTransformCollector = class {
130
130
  hooks = [];
131
131
  register(hook) {
132
132
  this.hooks.push(hook);
133
133
  }
134
- getHooks() {
134
+ getAll() {
135
135
  return [...this.hooks];
136
136
  }
137
137
  };
138
+ var StageCollector = class {
139
+ stageList = [];
140
+ register(stage) {
141
+ this.stageList.push(stage);
142
+ }
143
+ getAll() {
144
+ return [...this.stageList];
145
+ }
146
+ };
147
+ var EmitHookCollector = class {
148
+ hooks = [];
149
+ register(hook) {
150
+ this.hooks.push(hook);
151
+ }
152
+ getAll() {
153
+ return [...this.hooks];
154
+ }
155
+ };
156
+ var CleanupCollector = class {
157
+ fns = [];
158
+ register(fn) {
159
+ this.fns.push(fn);
160
+ }
161
+ runAll() {
162
+ for (const fn of this.fns) fn();
163
+ }
164
+ };
138
165
  var FullContext = class {
139
- sourceTransform = makeFilePipeline();
140
- analysisTransform = new AnalysisHookCollector();
141
- getAnalysisHooks() {
142
- return this.analysisTransform.getHooks();
166
+ filePreProcess = makeFilePipeline();
167
+ sourceTransforms = new SourceTransformCollector();
168
+ stages = new StageCollector();
169
+ emitHooks = new EmitHookCollector();
170
+ cleanup = new CleanupCollector();
171
+ onDiagnostic;
172
+ constructor(onDiagnostic) {
173
+ this.onDiagnostic = onDiagnostic;
143
174
  }
144
175
  };
145
176
 
@@ -152,7 +183,6 @@ async function resolveConfig(fileConfig, inlineConfig, defaults) {
152
183
  const plugins = mergeArrays(fileConfig.plugins, inlineConfig?.plugins) ?? defaults?.plugins ?? [];
153
184
  const resolved = {
154
185
  roots: mergeArrays(fileConfig.roots, inlineConfig?.roots) ?? defaults?.roots ?? [],
155
- extractors: mergeArrays(fileConfig.extractors, inlineConfig?.extractors) ?? defaults?.extractors ?? [],
156
186
  splitCss: inlineConfig?.splitCss ?? fileConfig.splitCss ?? defaults?.splitCss ?? false,
157
187
  onDiagnostic: mergeCallbacks(fileConfig.onDiagnostic, inlineConfig?.onDiagnostic),
158
188
  plugins,
@@ -180,213 +210,15 @@ async function loadConfig(cwd) {
180
210
  return config;
181
211
  }
182
212
 
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
213
  //#endregion
382
214
  //#region src/styledIdTransform.ts
383
215
  const STABLE_ID_RE = /^s-[0-9A-Za-z_-]+$/;
384
- function collectStyledCalls(ast) {
216
+ function collectCallsBySymbol(ast, symbolNames) {
385
217
  const results = [];
386
218
  function visitExpr(expr, varName) {
387
219
  if (expr.type === "CallExpression") {
388
220
  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({
221
+ if (callee.type === "Identifier" && symbolNames.has(callee.value) || callee.type === "MemberExpression" && callee.property.type === "Identifier" && symbolNames.has(callee.property.value)) results.push({
390
222
  call: expr,
391
223
  varName
392
224
  });
@@ -406,22 +238,23 @@ function collectStyledCalls(ast) {
406
238
  return results;
407
239
  }
408
240
  /**
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.
241
+ * Transforms source code to inject stable `s-` class IDs into every call matching
242
+ * one of the given symbol names. Idempotent: skips calls that already have an `s-`
243
+ * string as the last argument.
411
244
  */
412
- function transformStyledIds(source, filePath) {
413
- if (!source.includes("styled")) return source;
245
+ function transformCallIds(source, filePath, symbolNames) {
246
+ if (![...symbolNames].some((name) => source.includes(name))) return source;
414
247
  let ast;
415
248
  try {
416
249
  ast = SWC.parseSync(source, {
417
250
  syntax: "typescript",
418
- tsx: filePath.endsWith(".tsx"),
251
+ tsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
419
252
  target: "es2022"
420
253
  });
421
254
  } catch {
422
255
  return source;
423
256
  }
424
- const calls = collectStyledCalls(ast);
257
+ const calls = collectCallsBySymbol(ast, symbolNames);
425
258
  if (calls.length === 0) return source;
426
259
  const toInject = calls.filter((entry) => {
427
260
  const args = entry.call.arguments;
@@ -435,7 +268,9 @@ function transformStyledIds(source, filePath) {
435
268
  let searchFrom = 0;
436
269
  const callsWithOffset = [];
437
270
  for (const entry of sortedAsc) {
438
- const idx = source.indexOf("styled(", searchFrom);
271
+ const symbolName = entry.call.callee.type === "Identifier" ? entry.call.callee.value : entry.call.callee.type === "MemberExpression" && entry.call.callee.property.type === "Identifier" ? entry.call.callee.property.value : null;
272
+ if (!symbolName) continue;
273
+ const idx = source.indexOf(symbolName + "(", searchFrom);
439
274
  if (idx < 0) continue;
440
275
  if (callsWithOffset.length === 0) {
441
276
  const callee = entry.call.callee;
@@ -473,28 +308,45 @@ function hasStableId(call) {
473
308
  return last?.expression.type === "StringLiteral" && STABLE_ID_RE.test(last.expression.value);
474
309
  }
475
310
  /**
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.
311
+ * Returns a MochiPlugin that injects stable `s-` class IDs into every call expression
312
+ * matched by the given extractors.
313
+ * - Registers a `filePreProcess` transformation for runtime source injection (Vite/Next `transform` hook).
314
+ * - Registers a `sourceTransforms` hook for CSS extraction via direct AST mutation,
315
+ * using data from the extractor pipeline stages (`extractedCallExpressions`).
479
316
  */
480
- function styledIdPlugin() {
317
+ function styledIdPlugin(extractors) {
318
+ const symbolNames = new Set(extractors.map((e) => e.symbolName));
481
319
  return {
482
320
  name: "mochi-styled-ids",
483
321
  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}'`
322
+ context.filePreProcess.registerTransformation((source, { filePath }) => transformCallIds(source, filePath, symbolNames), { filter: "*.{ts,tsx,js,jsx}" });
323
+ context.sourceTransforms.register((index) => {
324
+ for (const [filePath, fileInfo] of index.files) {
325
+ const callToVarName = /* @__PURE__ */ new Map();
326
+ for (const binding of fileInfo.moduleBindings.values()) {
327
+ if (binding.declarator.type !== "variable") continue;
328
+ const init = binding.declarator.declarator.init;
329
+ if (init?.type === "CallExpression") callToVarName.set(init, binding.identifier.value);
330
+ }
331
+ let fallbackIdx = 0;
332
+ for (const extractor of extractors) {
333
+ const calls = fileInfo.extractedCallExpressions.get(extractor) ?? [];
334
+ for (const call of calls) {
335
+ if (hasStableId(call)) continue;
336
+ const varName = callToVarName.get(call) ?? String(fallbackIdx++);
337
+ const id = "s-" + shortHash(filePath + ":" + varName);
338
+ call.arguments.push({
339
+ spread: void 0,
340
+ expression: {
341
+ type: "StringLiteral",
342
+ span: DUMMY_SPAN,
343
+ value: id,
344
+ raw: `'${id}'`
345
+ }
346
+ });
495
347
  }
496
- });
497
- });
348
+ }
349
+ }
498
350
  });
499
351
  }
500
352
  };