@hypergood/css-core 0.0.3 → 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/package.json CHANGED
@@ -1,47 +1,57 @@
1
1
  {
2
2
  "name": "@hypergood/css-core",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
- "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js"
9
+ "import": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "require": {
14
+ "types": "./dist/index.d.cts",
15
+ "default": "./dist/index.cjs"
16
+ }
11
17
  },
12
18
  "./plugin": {
13
- "types": "./dist/plugin.d.ts",
14
- "import": "./dist/plugin.cjs"
19
+ "import": {
20
+ "types": "./dist/plugin.d.ts",
21
+ "default": "./dist/plugin.js"
22
+ },
23
+ "require": {
24
+ "types": "./dist/plugin.d.cts",
25
+ "default": "./dist/plugin.cjs"
26
+ }
15
27
  }
16
28
  },
17
- "scripts": {
18
- "dev": "tsup src/index.ts src/plugin.ts --watch",
19
- "build": "tsup src/index.ts src/plugin.ts",
20
- "test": "vitest"
21
- },
22
29
  "publishConfig": {
23
30
  "access": "public"
24
31
  },
25
32
  "author": "",
26
33
  "license": "MIT",
27
34
  "description": "",
28
- "devDependencies": {
29
- "@babel/core": "^7.28.5",
30
- "@babel/helper-module-imports": "^7.27.1",
35
+ "dependencies": {
36
+ "@babel/core": "^7.29.0",
37
+ "@babel/helper-module-imports": "^7.28.6",
31
38
  "@babel/parser": "^7.29.3",
32
39
  "@babel/preset-typescript": "^7.28.5",
40
+ "css-tree": "^3.2.1",
41
+ "csstype": "^3.2.3",
42
+ "es-module-lexer": "^2.1.0"
43
+ },
44
+ "devDependencies": {
33
45
  "@types/babel__core": "^7.20.5",
34
46
  "@types/babel__helper-module-imports": "^7.18.3",
35
- "@types/css-tree": "^2.3.11",
36
- "css-tree": "^3.1.0",
37
- "es-module-lexer": "^2.0.0",
38
- "prettier": "^3.7.4",
39
- "tsup": "^8.5.1",
40
- "typescript": "^5.9.3",
41
- "vite": "^7.2.4",
42
- "vitest": "^4.0.13"
47
+ "@types/css-tree": "^2.3.11"
43
48
  },
44
49
  "peerDependencies": {
45
- "vite": "^7.2.4"
50
+ "vite": "^8"
51
+ },
52
+ "scripts": {
53
+ "dev": "tsup src/index.ts src/plugin.ts --watch",
54
+ "build": "tsup src/index.ts src/plugin.ts",
55
+ "test": "vitest"
46
56
  }
47
- }
57
+ }
@@ -134,7 +134,7 @@ const NAMED_COLOR_MAP: Record<string, string> = {
134
134
  lime: "#0f0",
135
135
  limegreen: "#32cd32",
136
136
  "#faf0e6": "linen",
137
- magenta: "#ff00f",
137
+ magenta: "#ff00ff",
138
138
  "#800000": "maroon",
139
139
  mediumaquamarine: "#66cdaa",
140
140
  mediumblue: "#0000cd",
@@ -0,0 +1,43 @@
1
+ import type * as CSS from "csstype";
2
+
3
+ /**
4
+ * A value allowed inside a {@link CSSProperties} object: a string, a number, an
5
+ * array of strings/numbers (e.g. for fallback values), or a nested
6
+ * {@link CSSProperties} object (e.g. for selectors, media queries, or
7
+ * at-rules).
8
+ */
9
+ export type CSSValue =
10
+ | string
11
+ | number
12
+ | Array<string | number>
13
+ | CSSProperties;
14
+
15
+ /**
16
+ * The value type for a *known* CSS property.
17
+ *
18
+ * We deliberately do NOT union in a bare `string` here: csstype's own property
19
+ * types already permit any string via the `(string & {})` trick, which keeps
20
+ * the literal value suggestions (e.g. `"center"` for `alignItems`) visible in
21
+ * IntelliSense. Adding a bare `string` would collapse that union and suppress
22
+ * the autocomplete. We still allow numbers, fallback arrays, and nested objects.
23
+ */
24
+ type KnownCSSValue<K extends keyof CSS.Properties> =
25
+ | CSS.Properties[K]
26
+ | number
27
+ | Array<string | number>
28
+ | CSSProperties;
29
+
30
+ /**
31
+ * The shape of the style objects passed to `css()`, `variants()`, and
32
+ * `styled()`.
33
+ *
34
+ * Known camelCase CSS properties (sourced from `csstype`) offer autocomplete
35
+ * for both the property name and its value, but any other key is accepted
36
+ * without a type error so that selectors, media queries, custom properties, and
37
+ * other arbitrary keys can be used freely.
38
+ */
39
+ export type CSSProperties = {
40
+ [K in keyof CSS.Properties]?: KnownCSSValue<K>;
41
+ } & {
42
+ [key: string]: CSSValue;
43
+ };
package/src/evaluate.ts CHANGED
@@ -192,8 +192,21 @@ function evaluateObjectExpression(
192
192
  switch (property.type) {
193
193
  case "ObjectMethod":
194
194
  return UNKNOWN(property);
195
- case "SpreadElement":
196
- return UNKNOWN(property);
195
+ case "SpreadElement": {
196
+ const values = evaluateBabelExpression(
197
+ property.argument,
198
+ knownConstants,
199
+ );
200
+ if (values.confident) {
201
+ result = {
202
+ ...result,
203
+ ...values.value,
204
+ };
205
+ continue;
206
+ } else {
207
+ return UNKNOWN(property);
208
+ }
209
+ }
197
210
  case "ObjectProperty": {
198
211
  const keyNode = property.key;
199
212
  let key: EvaluationResult;
package/src/plugin.ts CHANGED
@@ -45,16 +45,22 @@ function babelPluginGetCssJsImports({
45
45
  }
46
46
 
47
47
  function babelPluginReplaceCssProp({
48
+ config,
48
49
  initialKnownConstants,
49
50
  styleObjectToClassNames,
50
51
  keyframesObjectToAnimationName,
51
52
  }: {
53
+ config: PluginConfig;
52
54
  initialKnownConstants: Record<string, any>;
53
55
  styleObjectToClassNames: (styleObject: any) => string;
54
56
  keyframesObjectToAnimationName: (keyframesObject: any) => string;
55
57
  }): babel.PluginItem {
56
58
  let localCssFunctionName: string | undefined;
57
59
  let localKeyframesFunctionName: string | undefined;
60
+ let localStyledFunctionName: string | undefined;
61
+ let localVariantsFunctionName: string | undefined;
62
+
63
+ const jsxClassProp = config.jsxClassProp ?? "className";
58
64
 
59
65
  let knownConstants = {
60
66
  ...initialKnownConstants,
@@ -95,7 +101,7 @@ function babelPluginReplaceCssProp({
95
101
  },
96
102
  },
97
103
  ImportDeclaration(path) {
98
- if (path.node.source.value !== "@hypergood/css-core") return;
104
+ if (path.node.source.value !== config.moduleName) return;
99
105
  const cssFunctionSpecifier = path.node.specifiers.find((specifier) => {
100
106
  return (
101
107
  specifier.type === "ImportSpecifier" &&
@@ -118,6 +124,30 @@ function babelPluginReplaceCssProp({
118
124
  if (keyframesFunctionSpecifier) {
119
125
  localKeyframesFunctionName = keyframesFunctionSpecifier.local.name;
120
126
  }
127
+ const styledFunctionSpecifier = path.node.specifiers.find(
128
+ (specifier) => {
129
+ return (
130
+ specifier.type === "ImportSpecifier" &&
131
+ specifier.imported.type === "Identifier" &&
132
+ specifier.imported.name === "styled"
133
+ );
134
+ },
135
+ );
136
+ if (styledFunctionSpecifier) {
137
+ localStyledFunctionName = styledFunctionSpecifier.local.name;
138
+ }
139
+ const variantsFunctionSpecifier = path.node.specifiers.find(
140
+ (specifier) => {
141
+ return (
142
+ specifier.type === "ImportSpecifier" &&
143
+ specifier.imported.type === "Identifier" &&
144
+ specifier.imported.name === "variants"
145
+ );
146
+ },
147
+ );
148
+ if (variantsFunctionSpecifier) {
149
+ localVariantsFunctionName = variantsFunctionSpecifier.local.name;
150
+ }
121
151
  },
122
152
  CallExpression: {
123
153
  exit(path) {
@@ -178,6 +208,182 @@ function babelPluginReplaceCssProp({
178
208
  value: classNames,
179
209
  });
180
210
  }
211
+
212
+ if (
213
+ babel.types.isIdentifier(path.node.callee, {
214
+ name: localStyledFunctionName,
215
+ })
216
+ ) {
217
+ const args = path.node.arguments;
218
+ if (args.length !== 2) return;
219
+ const secondArg = args[1];
220
+ if (secondArg.type !== "ObjectExpression") return;
221
+
222
+ const getPropertyKeyName = (
223
+ prop: babel.types.ObjectExpression["properties"][number],
224
+ ): string | undefined => {
225
+ if (prop.type !== "ObjectProperty") return undefined;
226
+ const key = prop.key;
227
+ if (key.type === "Identifier") return key.name;
228
+ if (key.type === "StringLiteral") return key.value;
229
+ return undefined;
230
+ };
231
+
232
+ const evaluateToClassNames = (
233
+ node: babel.types.Expression,
234
+ what: string,
235
+ ) => {
236
+ const result = evaluateBabelExpression(node, knownConstants);
237
+ if (!result.confident) {
238
+ throw new Error(
239
+ `COULD NOT EVALUATE ${what}: L` + node.loc?.start.line,
240
+ );
241
+ }
242
+ return styleObjectToClassNames(result.value);
243
+ };
244
+
245
+ // Separate the base styles from the `variants` / `defaultVariants`
246
+ // keys, which get transformed independently.
247
+ const baseProperties: babel.types.ObjectExpression["properties"] =
248
+ [];
249
+ let variantsProperty: babel.types.ObjectProperty | undefined;
250
+ let defaultVariantsProperty:
251
+ | babel.types.ObjectExpression["properties"][number]
252
+ | undefined;
253
+
254
+ for (const prop of secondArg.properties) {
255
+ const keyName = getPropertyKeyName(prop);
256
+ if (keyName === "variants" && prop.type === "ObjectProperty") {
257
+ variantsProperty = prop;
258
+ } else if (keyName === "defaultVariants") {
259
+ defaultVariantsProperty = prop;
260
+ } else {
261
+ baseProperties.push(prop);
262
+ }
263
+ }
264
+
265
+ const newProperties: babel.types.ObjectExpression["properties"] =
266
+ [];
267
+
268
+ // base: everything that isn't `variants` / `defaultVariants`
269
+ const baseClassNames = evaluateToClassNames(
270
+ babel.types.objectExpression(baseProperties),
271
+ "STYLED",
272
+ );
273
+ newProperties.push(
274
+ babel.types.objectProperty(
275
+ babel.types.identifier("base"),
276
+ babel.types.stringLiteral(baseClassNames),
277
+ ),
278
+ );
279
+
280
+ // variants: { variant: { value: styleObject } } -> { variant: { value: classNames } }
281
+ if (variantsProperty) {
282
+ if (variantsProperty.value.type === "ObjectExpression") {
283
+ const newVariantGroups: babel.types.ObjectExpression["properties"] =
284
+ [];
285
+ for (const group of variantsProperty.value.properties) {
286
+ if (
287
+ group.type !== "ObjectProperty" ||
288
+ group.value.type !== "ObjectExpression"
289
+ ) {
290
+ newVariantGroups.push(group);
291
+ continue;
292
+ }
293
+ const newVariantValues: babel.types.ObjectExpression["properties"] =
294
+ [];
295
+ for (const variant of group.value.properties) {
296
+ if (
297
+ variant.type !== "ObjectProperty" ||
298
+ variant.value.type !== "ObjectExpression"
299
+ ) {
300
+ newVariantValues.push(variant);
301
+ continue;
302
+ }
303
+ const variantClassNames = evaluateToClassNames(
304
+ variant.value,
305
+ "STYLED VARIANT",
306
+ );
307
+ newVariantValues.push(
308
+ babel.types.objectProperty(
309
+ variant.key,
310
+ babel.types.stringLiteral(variantClassNames),
311
+ variant.computed,
312
+ ),
313
+ );
314
+ }
315
+ newVariantGroups.push(
316
+ babel.types.objectProperty(
317
+ group.key,
318
+ babel.types.objectExpression(newVariantValues),
319
+ group.computed,
320
+ ),
321
+ );
322
+ }
323
+ newProperties.push(
324
+ babel.types.objectProperty(
325
+ babel.types.identifier("variants"),
326
+ babel.types.objectExpression(newVariantGroups),
327
+ variantsProperty.computed,
328
+ ),
329
+ );
330
+ } else {
331
+ newProperties.push(variantsProperty);
332
+ }
333
+ }
334
+
335
+ // defaultVariants: passed through unchanged
336
+ if (defaultVariantsProperty) {
337
+ newProperties.push(defaultVariantsProperty);
338
+ }
339
+
340
+ args[1] = babel.types.objectExpression(newProperties);
341
+ }
342
+
343
+ if (
344
+ babel.types.isIdentifier(path.node.callee, {
345
+ name: localVariantsFunctionName,
346
+ })
347
+ ) {
348
+ const args = path.node.arguments;
349
+ if (args.length !== 1) return;
350
+ const firstArg = args[0];
351
+ if (firstArg.type !== "ObjectExpression") return;
352
+
353
+ // { name: styleObject } -> { name: classNames }
354
+ const newProperties: babel.types.ObjectExpression["properties"] =
355
+ [];
356
+ for (const prop of firstArg.properties) {
357
+ if (
358
+ prop.type !== "ObjectProperty" ||
359
+ prop.value.type !== "ObjectExpression"
360
+ ) {
361
+ newProperties.push(prop);
362
+ continue;
363
+ }
364
+ const styleObjectResult = evaluateBabelExpression(
365
+ prop.value,
366
+ knownConstants,
367
+ );
368
+ if (!styleObjectResult.confident) {
369
+ throw new Error(
370
+ "COULD NOT EVALUATE VARIANTS: L" + prop.value.loc?.start.line,
371
+ );
372
+ }
373
+ const classNames = styleObjectToClassNames(
374
+ styleObjectResult.value,
375
+ );
376
+ newProperties.push(
377
+ babel.types.objectProperty(
378
+ prop.key,
379
+ babel.types.stringLiteral(classNames),
380
+ prop.computed,
381
+ ),
382
+ );
383
+ }
384
+
385
+ path.replaceWith(babel.types.objectExpression(newProperties));
386
+ }
181
387
  },
182
388
  },
183
389
  JSXAttribute(path, state) {
@@ -214,7 +420,7 @@ function babelPluginReplaceCssProp({
214
420
  }
215
421
  const classNames = styleObjectToClassNames(styleObject);
216
422
 
217
- path.node.name.name = "className";
423
+ path.node.name.name = jsxClassProp;
218
424
  path.node.value = {
219
425
  type: "StringLiteral",
220
426
  value: classNames,
@@ -226,7 +432,7 @@ function babelPluginReplaceCssProp({
226
432
  const mergeClassNamesId = addNamed(
227
433
  path,
228
434
  "mergeClassNames",
229
- "@hypergood/css-core",
435
+ config.moduleName || "",
230
436
  );
231
437
 
232
438
  const cleanedElements: babel.types.Expression[] = [];
@@ -305,7 +511,7 @@ function babelPluginReplaceCssProp({
305
511
  }
306
512
  }
307
513
 
308
- path.node.name.name = "className";
514
+ path.node.name.name = jsxClassProp;
309
515
  value.expression = babel.types.callExpression(mergeClassNamesId, [
310
516
  babel.types.arrayExpression(cleanedElements),
311
517
  ]);
@@ -313,7 +519,7 @@ function babelPluginReplaceCssProp({
313
519
  break;
314
520
  }
315
521
  default: {
316
- path.node.name.name = "className";
522
+ path.node.name.name = jsxClassProp;
317
523
  }
318
524
  }
319
525
  }
@@ -323,13 +529,16 @@ function babelPluginReplaceCssProp({
323
529
  }
324
530
 
325
531
  export type PluginConfig = {
532
+ moduleName: string;
533
+ jsxClassProp?: string;
326
534
  media?: Record<string, string>;
327
535
  macros?: Record<string, (value: string) => any>;
328
536
  };
329
537
 
330
- export function vitePluginCss(config: PluginConfig = {}): Plugin[] {
331
- const styleCache = createStyleCache();
332
-
538
+ const styleCache = createStyleCache();
539
+ export function vitePluginCss(
540
+ config: PluginConfig = { moduleName: "@hypergood/css-core" },
541
+ ): Plugin[] {
333
542
  const { plugin: generationPlugin, reload: reloadStyles } =
334
543
  createGenerationPlugin(styleCache);
335
544
 
@@ -345,6 +554,29 @@ export function vitePluginCss(config: PluginConfig = {}): Plugin[] {
345
554
  configureServer(server) {
346
555
  devServer = server;
347
556
  },
557
+
558
+ // `css={...}` is compiled to a literal `class="..."` string at transform
559
+ // time, so the markup is only as fresh as the transformed module the
560
+ // renderer used. `hotUpdate` runs per-environment, but a non-standard SSR
561
+ // environment (e.g. Nitro) may not re-transform on its own — leaving the
562
+ // server-rendered class stale until a restart. Explicitly invalidate the
563
+ // changed file in *every* environment so client and SSR copies refresh
564
+ // together.
565
+ hotUpdate(ctx) {
566
+ const cleanFile = ctx.file.split("?")[0]!;
567
+ if (!/\.[jt]sx?$/.test(cleanFile)) return;
568
+
569
+ const server = ctx.server ?? devServer;
570
+ if (!server?.environments) return;
571
+
572
+ for (const env of Object.values(server.environments)) {
573
+ for (const mod of env.moduleGraph.getModulesByFile(cleanFile) ??
574
+ []) {
575
+ env.moduleGraph.invalidateModule(mod);
576
+ }
577
+ }
578
+ },
579
+
348
580
  // Run before other plugins (like the default JSX transform)
349
581
  enforce: "pre",
350
582
 
@@ -427,6 +659,7 @@ export function vitePluginCss(config: PluginConfig = {}): Plugin[] {
427
659
  },
428
660
  plugins: [
429
661
  babelPluginReplaceCssProp({
662
+ config,
430
663
  initialKnownConstants: importedConstants,
431
664
  keyframesObjectToAnimationName: (keyframes) => {
432
665
  const newKeyframes = Object.fromEntries(
@@ -501,15 +734,33 @@ function createGenerationPlugin(styleCache: StyleCache) {
501
734
  return;
502
735
  }
503
736
 
504
- for (const [id] of modulesToInvalidate.entries()) {
505
- const module = server.moduleGraph.getModuleById(id);
737
+ // In Vite 6+/8 every environment (client, ssr, and any custom ones such
738
+ // as Nitro's) keeps its own module graph. The deprecated
739
+ // `server.moduleGraph` / `server.reloadModule` only ever touch the client
740
+ // graph, so a stale SSR copy of the generated stylesheet would survive
741
+ // until a full dev-server restart. Invalidate in every environment.
742
+ const environments = server.environments
743
+ ? Object.values(server.environments)
744
+ : [];
506
745
 
507
- if (!module) {
508
- return;
746
+ for (const [id] of modulesToInvalidate.entries()) {
747
+ if (environments.length > 0) {
748
+ for (const env of environments) {
749
+ const mod = env.moduleGraph.getModuleById(id);
750
+ if (mod) env.moduleGraph.invalidateModule(mod);
751
+ }
752
+ } else {
753
+ // Legacy (Vite <6) fallback.
754
+ const mod = server.moduleGraph.getModuleById(id);
755
+ if (mod) server.moduleGraph.invalidateModule(mod);
509
756
  }
510
757
 
511
- server.moduleGraph.invalidateModule(module);
512
- await server.reloadModule(module);
758
+ // Push the regenerated CSS to the browser. HMR only targets the client,
759
+ // so use the legacy client module node `reloadModule` expects.
760
+ const clientMod = server.moduleGraph.getModuleById(id);
761
+ if (clientMod) {
762
+ await server.reloadModule(clientMod);
763
+ }
513
764
  }
514
765
  }
515
766
 
@@ -537,7 +788,7 @@ function createGenerationPlugin(styleCache: StyleCache) {
537
788
  configureServer(_server) {
538
789
  server = _server;
539
790
  },
540
- async transform(code, id, { ssr: isSSR = false } = {}) {
791
+ async transform(code, id, { ssr: isSSR = false } = { moduleType: "jsx" }) {
541
792
  if (!/\.css/.test(id)) {
542
793
  return null;
543
794
  }
package/src/runtime.ts CHANGED
@@ -1,8 +1,31 @@
1
- export function css(styleObject: any): string {
1
+ import type { CSSProperties } from "./css-properties.js";
2
+
3
+ export type { CSSProperties, CSSValue } from "./css-properties.js";
4
+
5
+ /**
6
+ * The style object accepted by `styled()`: top-level CSS properties plus the
7
+ * optional `variants` and `defaultVariants` keys.
8
+ */
9
+ export type StyledStyleObject = CSSProperties & {
10
+ variants?: Record<string, Record<string, CSSProperties>>;
11
+ defaultVariants?: Record<string, string | number | boolean>;
12
+ };
13
+
14
+ export function css(styleObject: CSSProperties): string {
15
+ throw new Error("RAHHH");
16
+ }
17
+
18
+ export function keyframes(styleObject: Record<string, CSSProperties>): string {
19
+ throw new Error("RAHHH");
20
+ }
21
+
22
+ export function styled(component: any, styleObject: StyledStyleObject): any {
2
23
  throw new Error("RAHHH");
3
24
  }
4
25
 
5
- export function keyframes(styleObject: any): string {
26
+ export function variants<
27
+ T extends Record<string, Record<string, CSSProperties>>,
28
+ >(variantsObject: T): { [K in keyof T]: string } {
6
29
  throw new Error("RAHHH");
7
30
  }
8
31
 
@@ -49,6 +49,9 @@ describe("evaluateBabelExpression", () => {
49
49
  it("handles ObjectExpression", () => {
50
50
  expect(evaluateString("{a:1}")).toStrictEqual(KNOWN({ a: 1 }));
51
51
  expect(evaluateString('{["a"]:1}')).toStrictEqual(KNOWN({ a: 1 }));
52
+ expect(evaluateString("{a:1, ...({a:2,b:2}) }")).toStrictEqual(
53
+ KNOWN({ a: 2, b: 2 }),
54
+ );
52
55
  });
53
56
  it("handles TemplateLiteral", () => {
54
57
  expect(evaluateString("`Something`")).toStrictEqual(KNOWN("Something"));
package/tsconfig.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "module": "nodenext",
4
4
  "strict": true,
5
- "moduleResolution": "nodenext"
5
+ "moduleResolution": "nodenext",
6
+ "ignoreDeprecations": "6.0"
6
7
  }
7
8
  }
package/tsup.config.ts CHANGED
@@ -8,5 +8,7 @@ export const tsup: Options = {
8
8
  splitting: false,
9
9
  format: ["cjs", "esm"],
10
10
  target: "node20",
11
- external: ["fs", "css-tree"],
11
+ // Don't bundle dependencies — @babel/core (and friends) use dynamic
12
+ // require() of node builtins, which is illegal in an ESM bundle.
13
+ skipNodeModulesBundle: true,
12
14
  };