@hypergood/css-core 0.0.1

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/src/plugin.ts ADDED
@@ -0,0 +1,573 @@
1
+ import babel from "@babel/core";
2
+ import { addNamed } from "@babel/helper-module-imports";
3
+ import { Plugin, ViteDevServer } from "vite";
4
+ import { atomizeStyleRules } from "./atomizer/index.js";
5
+ import { cssJsLogger } from "./css-js-logger.js";
6
+ import { evaluateBabelExpression } from "./evaluate.js";
7
+ import { createStyleCache, StyleCache } from "./style-cache.js";
8
+ import {
9
+ evaluateMacros,
10
+ evaluateMedia,
11
+ flattenStyleObject,
12
+ } from "./style-object.js";
13
+
14
+ type ImportDeclaration = {
15
+ source: string;
16
+ specifiers: Array<{ imported: string; local: string }>;
17
+ };
18
+
19
+ function babelPluginGetCssJsImports({
20
+ onFoundPotentialImport,
21
+ }: {
22
+ onFoundPotentialImport: (potential: ImportDeclaration) => void;
23
+ }): babel.PluginItem {
24
+ return {
25
+ name: "find-css-js-imports",
26
+ visitor: {
27
+ ImportDeclaration(path) {
28
+ if (path.node.source.value.includes(".css")) {
29
+ onFoundPotentialImport({
30
+ source: path.node.source.value,
31
+ specifiers: path.node.specifiers.map((specifier) => ({
32
+ imported:
33
+ specifier.type === "ImportSpecifier"
34
+ ? specifier.imported.type === "StringLiteral"
35
+ ? specifier.imported.value
36
+ : specifier.imported.name
37
+ : specifier.local.name,
38
+ local: specifier.local.name,
39
+ })),
40
+ });
41
+ }
42
+ },
43
+ },
44
+ };
45
+ }
46
+
47
+ function babelPluginReplaceCssProp({
48
+ initialKnownConstants,
49
+ styleObjectToClassNames,
50
+ keyframesObjectToAnimationName,
51
+ }: {
52
+ initialKnownConstants: Record<string, any>;
53
+ styleObjectToClassNames: (styleObject: any) => string;
54
+ keyframesObjectToAnimationName: (keyframesObject: any) => string;
55
+ }): babel.PluginItem {
56
+ let localCssFunctionName: string | undefined;
57
+ let localKeyframesFunctionName: string | undefined;
58
+
59
+ let knownConstants = {
60
+ ...initialKnownConstants,
61
+ };
62
+
63
+ return {
64
+ name: "replace-css-prop",
65
+ visitor: {
66
+ // Statically analyze global constants and register them in case they are used in css() / keyframes()
67
+ VariableDeclaration: {
68
+ exit(path) {
69
+ const parent = path.parentPath;
70
+ const isTopLevel =
71
+ parent.isProgram() ||
72
+ (parent.isExportNamedDeclaration() &&
73
+ parent.parentPath.isProgram());
74
+
75
+ if (!isTopLevel) return;
76
+ const node = path.node;
77
+
78
+ if (node.kind !== "const") return;
79
+
80
+ for (let declarator of node.declarations) {
81
+ const init = evaluateBabelExpression(
82
+ declarator.init,
83
+ knownConstants,
84
+ );
85
+ if (init.confident) {
86
+ const id = declarator.id;
87
+ if (id.type === "Identifier") {
88
+ // console.log(
89
+ // `[FOUND] ${id.name} = ${JSON.stringify(init.value, null, 2)}`,
90
+ // );
91
+ knownConstants[id.name] = init.value;
92
+ }
93
+ }
94
+ }
95
+ },
96
+ },
97
+ ImportDeclaration(path) {
98
+ if (path.node.source.value !== "@hypergood/css-core") return;
99
+ const cssFunctionSpecifier = path.node.specifiers.find((specifier) => {
100
+ return (
101
+ specifier.type === "ImportSpecifier" &&
102
+ specifier.imported.type === "Identifier" &&
103
+ specifier.imported.name === "css"
104
+ );
105
+ });
106
+ if (cssFunctionSpecifier) {
107
+ localCssFunctionName = cssFunctionSpecifier.local.name;
108
+ }
109
+ const keyframesFunctionSpecifier = path.node.specifiers.find(
110
+ (specifier) => {
111
+ return (
112
+ specifier.type === "ImportSpecifier" &&
113
+ specifier.imported.type === "Identifier" &&
114
+ specifier.imported.name === "keyframes"
115
+ );
116
+ },
117
+ );
118
+ if (keyframesFunctionSpecifier) {
119
+ localKeyframesFunctionName = keyframesFunctionSpecifier.local.name;
120
+ }
121
+ },
122
+ CallExpression: {
123
+ exit(path) {
124
+ if (
125
+ babel.types.isIdentifier(path.node.callee, {
126
+ name: localKeyframesFunctionName,
127
+ })
128
+ ) {
129
+ const args = path.node.arguments;
130
+ if (args.length !== 1) return;
131
+ const firstArg = args[0];
132
+ if (firstArg.type !== "ObjectExpression") return;
133
+
134
+ const styleObjectResult = evaluateBabelExpression(
135
+ firstArg,
136
+ knownConstants,
137
+ );
138
+ let styleObject = styleObjectResult.value;
139
+ if (!styleObjectResult.confident) {
140
+ throw new Error(
141
+ "COULD NOT EVALUATE KEYFRAMES: L" + firstArg.loc?.start.line,
142
+ );
143
+ }
144
+
145
+ const animationName = keyframesObjectToAnimationName(styleObject);
146
+
147
+ path.replaceWith({
148
+ type: "StringLiteral",
149
+ value: animationName,
150
+ });
151
+ }
152
+
153
+ if (
154
+ babel.types.isIdentifier(path.node.callee, {
155
+ name: localCssFunctionName,
156
+ })
157
+ ) {
158
+ const args = path.node.arguments;
159
+ if (args.length !== 1) return;
160
+ const firstArg = args[0];
161
+ if (firstArg.type !== "ObjectExpression") return;
162
+
163
+ const styleObjectResult = evaluateBabelExpression(
164
+ firstArg,
165
+ knownConstants,
166
+ );
167
+ let styleObject = styleObjectResult.value;
168
+ if (!styleObjectResult.confident) {
169
+ throw new Error(
170
+ "COULD NOT EVALUATE CSS: L" + firstArg.loc?.start.line,
171
+ );
172
+ }
173
+
174
+ const classNames = styleObjectToClassNames(styleObject);
175
+
176
+ path.replaceWith({
177
+ type: "StringLiteral",
178
+ value: classNames,
179
+ });
180
+ }
181
+ },
182
+ },
183
+ JSXAttribute(path) {
184
+ if (
185
+ path.node.name &&
186
+ path.node.name.type === "JSXIdentifier" &&
187
+ path.node.name.name === "css"
188
+ ) {
189
+ const value = path.node.value;
190
+ if (value?.type !== "JSXExpressionContainer") return;
191
+
192
+ const expression = value.expression;
193
+
194
+ switch (expression.type) {
195
+ case "ObjectExpression": {
196
+ const styleObjectResult = evaluateBabelExpression(
197
+ expression,
198
+ knownConstants,
199
+ );
200
+ let styleObject = styleObjectResult.value;
201
+ if (!styleObjectResult.confident) {
202
+ throw new Error(
203
+ "COULD NOT EVALUATE CSS: L" + expression.loc?.start.line,
204
+ );
205
+ }
206
+ const classNames = styleObjectToClassNames(styleObject);
207
+
208
+ path.node.name.name = "className";
209
+ path.node.value = {
210
+ type: "StringLiteral",
211
+ value: classNames,
212
+ };
213
+ break;
214
+ }
215
+ case "ArrayExpression": {
216
+ const elements = expression.elements;
217
+ const mergeClassNamesId = addNamed(
218
+ path,
219
+ "mergeClassNames",
220
+ "@hypergood/css-core",
221
+ );
222
+
223
+ const cleanedElements: babel.types.Expression[] = [];
224
+
225
+ for (const element of elements) {
226
+ if (element) {
227
+ // if (element.type !== "SpreadElement") {
228
+ // cleanedElements.push(element);
229
+ // }
230
+ if (element.type === "ObjectExpression") {
231
+ const styleObjectResult = evaluateBabelExpression(
232
+ element,
233
+ knownConstants,
234
+ );
235
+ if (styleObjectResult.confident) {
236
+ let styleObject = styleObjectResult.value;
237
+ const classNames = styleObjectToClassNames(styleObject);
238
+ cleanedElements.push(
239
+ babel.types.stringLiteral(classNames),
240
+ );
241
+ }
242
+ } else if (
243
+ element.type === "LogicalExpression" &&
244
+ element.operator === "&&" &&
245
+ element.right.type === "ObjectExpression"
246
+ ) {
247
+ const styleObjectResult = evaluateBabelExpression(
248
+ element.right,
249
+ knownConstants,
250
+ );
251
+ if (styleObjectResult.confident) {
252
+ let styleObject = styleObjectResult.value;
253
+ const classNames = styleObjectToClassNames(styleObject);
254
+ cleanedElements.push(
255
+ babel.types.logicalExpression(
256
+ "&&",
257
+ element.left,
258
+ babel.types.stringLiteral(classNames),
259
+ ),
260
+ );
261
+ }
262
+ } else if (
263
+ element.type === "ConditionalExpression" &&
264
+ element.consequent.type === "ObjectExpression" &&
265
+ element.alternate.type === "ObjectExpression"
266
+ ) {
267
+ const consequentObject = evaluateBabelExpression(
268
+ element.consequent,
269
+ knownConstants,
270
+ );
271
+ const alternateObject = evaluateBabelExpression(
272
+ element.alternate,
273
+ knownConstants,
274
+ );
275
+ if (
276
+ consequentObject.confident &&
277
+ alternateObject.confident
278
+ ) {
279
+ const classNames = styleObjectToClassNames(
280
+ consequentObject.value,
281
+ );
282
+ const alternateClassNames = styleObjectToClassNames(
283
+ alternateObject.value,
284
+ );
285
+ cleanedElements.push(
286
+ babel.types.conditionalExpression(
287
+ element.test,
288
+ babel.types.stringLiteral(classNames),
289
+ babel.types.stringLiteral(alternateClassNames),
290
+ ),
291
+ );
292
+ }
293
+ } else if (element.type !== "SpreadElement") {
294
+ cleanedElements.push(element);
295
+ }
296
+ }
297
+ }
298
+
299
+ path.node.name.name = "className";
300
+ value.expression = babel.types.callExpression(mergeClassNamesId, [
301
+ babel.types.arrayExpression(cleanedElements),
302
+ ]);
303
+
304
+ break;
305
+ }
306
+ default: {
307
+ path.node.name.name = "className";
308
+ }
309
+ }
310
+ }
311
+ },
312
+ },
313
+ };
314
+ }
315
+
316
+ export type PluginConfig = {
317
+ media?: Record<string, string>;
318
+ macros?: Record<string, (value: string) => any>;
319
+ };
320
+
321
+ export function vitePluginCss(config: PluginConfig = {}): Plugin[] {
322
+ const styleCache = createStyleCache();
323
+
324
+ const { plugin: generationPlugin, reload: reloadStyles } =
325
+ createGenerationPlugin(styleCache);
326
+
327
+ let devServer: ViteDevServer | null = null;
328
+ const { processedFiles, transform: registerCssJsFiles } = cssJsLogger();
329
+
330
+ return [
331
+ generationPlugin,
332
+ {
333
+ name: "hypercss:process",
334
+
335
+ // Capture the dev server instance
336
+ configureServer(server) {
337
+ devServer = server;
338
+ },
339
+ // Run before other plugins (like the default JSX transform)
340
+ enforce: "pre",
341
+
342
+ async transform(code, id) {
343
+ const cleanId = id.split("?")[0]!;
344
+ await registerCssJsFiles.bind(this)(code, id, devServer);
345
+ const rulesCount = styleCache.getLength();
346
+ if (!/\.[jt]sx?$/.test(cleanId)) {
347
+ return null;
348
+ }
349
+ if (id.includes("node_modules")) {
350
+ return null;
351
+ }
352
+
353
+ if (!code.includes("css")) {
354
+ return null;
355
+ }
356
+
357
+ const isTypeScript =
358
+ cleanId.endsWith(".tsx") || cleanId.endsWith(".ts");
359
+
360
+ const cssJsImportDeclarations: ImportDeclaration[] = [];
361
+
362
+ const getImportedConstants = async () => {
363
+ await babel.transformAsync(code, {
364
+ filename: id,
365
+ sourceFileName: id,
366
+ sourceMaps: true,
367
+ parserOpts: {
368
+ sourceType: "module",
369
+ plugins: [
370
+ "jsx",
371
+ // Add TypeScript support for .tsx files
372
+ ...(isTypeScript ? ["typescript" as const] : []),
373
+ ],
374
+ },
375
+ plugins: [
376
+ babelPluginGetCssJsImports({
377
+ onFoundPotentialImport: (dec) => {
378
+ cssJsImportDeclarations.push(dec);
379
+ },
380
+ }),
381
+ ],
382
+ // Don't transform anything else, just run our plugin
383
+ configFile: false,
384
+ babelrc: false,
385
+ });
386
+ const importedConstants: Record<string, unknown> = {};
387
+
388
+ for (const declaration of cssJsImportDeclarations) {
389
+ const resolvedId = (await this.resolve(declaration.source, id))?.id;
390
+ const resolvedPath = resolvedId?.split("?")[0]!;
391
+
392
+ const module = processedFiles.get(resolvedPath);
393
+
394
+ if (module) {
395
+ for (const specifier of declaration.specifiers) {
396
+ importedConstants[specifier.local] = module[specifier.imported];
397
+ }
398
+ }
399
+ }
400
+ return importedConstants;
401
+ };
402
+
403
+ try {
404
+ const importedConstants: Record<string, unknown> =
405
+ await getImportedConstants();
406
+
407
+ let result = await babel.transformAsync(code, {
408
+ filename: id,
409
+ sourceFileName: id,
410
+ sourceMaps: true,
411
+ parserOpts: {
412
+ sourceType: "module",
413
+ plugins: [
414
+ "jsx",
415
+ // Add TypeScript support for .tsx files
416
+ ...(isTypeScript ? ["typescript" as const] : []),
417
+ ],
418
+ },
419
+ plugins: [
420
+ babelPluginReplaceCssProp({
421
+ initialKnownConstants: importedConstants,
422
+ keyframesObjectToAnimationName: (keyframes) => {
423
+ const newKeyframes = Object.fromEntries(
424
+ Object.entries(keyframes).map(
425
+ ([key, styleObject]: [key: string, value: any]) => {
426
+ if (config.macros) {
427
+ styleObject = evaluateMacros(
428
+ styleObject,
429
+ config.macros,
430
+ );
431
+ }
432
+ if (config.media) {
433
+ styleObject = evaluateMedia(
434
+ styleObject,
435
+ config.media,
436
+ );
437
+ }
438
+ const styleRules = flattenStyleObject(styleObject);
439
+ const optimized = atomizeStyleRules(styleRules);
440
+ return [key, optimized];
441
+ },
442
+ ),
443
+ );
444
+
445
+ return styleCache.getAnimationName(newKeyframes);
446
+ },
447
+ styleObjectToClassNames: (styleObject) => {
448
+ if (config.macros) {
449
+ styleObject = evaluateMacros(styleObject, config.macros);
450
+ }
451
+ if (config.media) {
452
+ styleObject = evaluateMedia(styleObject, config.media);
453
+ }
454
+ const styleRules = flattenStyleObject(styleObject);
455
+ const optimized = atomizeStyleRules(styleRules);
456
+ const classNames = styleCache.getClassNames(optimized);
457
+ return classNames;
458
+ },
459
+ }),
460
+ ],
461
+ // Don't transform anything else, just run our plugin
462
+ configFile: false,
463
+ babelrc: false,
464
+ });
465
+
466
+ if (result && result.code) {
467
+ if (styleCache.getLength() > rulesCount) {
468
+ void reloadStyles();
469
+ }
470
+ return {
471
+ code: result.code,
472
+ map: result.map,
473
+ };
474
+ }
475
+ } catch (error: any) {
476
+ this.error(`Failed to transform ${id}: ${error.message}`);
477
+ }
478
+
479
+ return null;
480
+ },
481
+ },
482
+ ];
483
+ }
484
+
485
+ const CSS_REPLACE_ME = "@hypercss;";
486
+ function createGenerationPlugin(styleCache: StyleCache) {
487
+ let modulesToInvalidate = new Map<string, string>();
488
+ let server: ViteDevServer;
489
+
490
+ async function reload() {
491
+ if (!server || modulesToInvalidate.size === 0) {
492
+ return;
493
+ }
494
+
495
+ for (const [id] of modulesToInvalidate.entries()) {
496
+ const module = server.moduleGraph.getModuleById(id);
497
+
498
+ if (!module) {
499
+ return;
500
+ }
501
+
502
+ server.moduleGraph.invalidateModule(module);
503
+ await server.reloadModule(module);
504
+ }
505
+ }
506
+
507
+ function pickCssAssetFromRollupBundle(bundle: any, choose: any) {
508
+ const assets = Object.values(bundle).filter(
509
+ (a: any) =>
510
+ a &&
511
+ a.type === "asset" &&
512
+ typeof a.fileName === "string" &&
513
+ a.fileName.endsWith(".css"),
514
+ );
515
+ if (assets.length === 0) return null;
516
+ if (typeof choose === "function") {
517
+ const chosen = assets.find((a: any) => choose(a.fileName));
518
+ if (chosen) return chosen;
519
+ }
520
+ const best =
521
+ assets.find((a: any) => /(^|\/)index\.css$/.test(a.fileName)) ||
522
+ assets.find((a: any) => /(^|\/)style\.css$/.test(a.fileName));
523
+ return best || assets[0];
524
+ }
525
+ const plugin: Plugin = {
526
+ name: "hypercss:generate",
527
+ enforce: "pre",
528
+ configureServer(_server) {
529
+ server = _server;
530
+ },
531
+ async transform(code, id, { ssr: isSSR = false } = {}) {
532
+ if (!/\.css/.test(id)) {
533
+ return null;
534
+ }
535
+ if (id.includes("node_modules")) {
536
+ return null;
537
+ }
538
+ if (!code.includes(CSS_REPLACE_ME)) {
539
+ return null;
540
+ }
541
+
542
+ modulesToInvalidate.set(id, code);
543
+ if (server) {
544
+ if (!isSSR) {
545
+ await server?.waitForRequestsIdle?.(id);
546
+ }
547
+
548
+ return code.replace(CSS_REPLACE_ME, styleCache.processStyleRules());
549
+ }
550
+ },
551
+ // Rollup/Vite: append to an existing CSS asset during generateBundle
552
+ // Note: some bundlers/plugins may mutate CSS assets later; we also guard in writeBundle.
553
+ generateBundle(_opts, bundle) {
554
+ const css = styleCache.processStyleRules();
555
+ if (!css) return;
556
+ const target = pickCssAssetFromRollupBundle(bundle, undefined) as any;
557
+ if (target) {
558
+ const current =
559
+ typeof target.source === "string"
560
+ ? target.source
561
+ : target.source?.toString() || "";
562
+ target.source = current ? current + "\n" + css : css;
563
+ } else {
564
+ // Defer to writeBundle to append or emit fallback file
565
+ }
566
+ },
567
+ };
568
+
569
+ return {
570
+ plugin,
571
+ reload,
572
+ };
573
+ }
package/src/print.ts ADDED
@@ -0,0 +1,101 @@
1
+ import { AnimationRules, StyleRule } from "./style-rule.js";
2
+
3
+ export function printStyleSheet({
4
+ animations,
5
+ getAnimationName,
6
+ styleRules,
7
+ styleRuleToClassName,
8
+ }: {
9
+ animations: AnimationRules[];
10
+ getAnimationName: (rule: AnimationRules) => string;
11
+ styleRules: StyleRule[];
12
+ styleRuleToClassName: (styleRule: StyleRule) => string;
13
+ }) {
14
+ const keyframesSection = animations.map((animation) => {
15
+ return `@keyframes ${getAnimationName(animation)} {
16
+ ${Object.entries(animation)
17
+ .map(
18
+ ([key, styleRules]) => ` ${key} {
19
+ ${styleRules.map((rule) => ` ${rule.property}: ${rule.value};`).join("\n")}
20
+ }`,
21
+ )
22
+ .join("\n")}
23
+ }`;
24
+ });
25
+
26
+ const tree = groupStyleRules(styleRules, []);
27
+ const rulesSection = renderStyleRuleNode(tree, styleRuleToClassName);
28
+ return (
29
+ keyframesSection +
30
+ "\n" +
31
+ rulesSection +
32
+ "\n" +
33
+ "/* " +
34
+ JSON.stringify(styleRules, null, 2) +
35
+ " */"
36
+ );
37
+ }
38
+
39
+ function renderStyleRuleNode(
40
+ node: StyleRuleNode,
41
+ styleRuleToClassName: (styleRule: StyleRule) => string,
42
+ indent = 0,
43
+ ): string {
44
+ const rulesSection = node.rules
45
+ .sort((a, b) => a.value.localeCompare(b.value))
46
+ .sort((a, b) => a.property.localeCompare(b.property))
47
+ .sort((a, b) => a.selector.length - b.selector.length)
48
+ .sort((a, b) => a.atRules.length - b.atRules.length)
49
+ .map((rule) => {
50
+ const className = styleRuleToClassName(rule);
51
+ return `${space(indent)}${rule.selector.replaceAll("&", "." + className)} { ${rule.property}: ${rule.value} }`;
52
+ })
53
+ .join("\n");
54
+
55
+ const childrenSection = node.children
56
+ .map((node) => {
57
+ return `${space(indent)}${node.atRule} {
58
+ ${renderStyleRuleNode(node, styleRuleToClassName, indent + 1)}
59
+ ${space(indent)}}`;
60
+ })
61
+ .join("\n");
62
+
63
+ return [rulesSection, childrenSection].filter(Boolean).join("\n\n");
64
+ }
65
+
66
+ function space(indent = 0) {
67
+ return "".padEnd(indent * 2, " ");
68
+ }
69
+
70
+ type StyleRuleNode = {
71
+ atRule: string | undefined;
72
+ rules: StyleRule[];
73
+ children: StyleRuleNode[];
74
+ };
75
+
76
+ function groupStyleRules(styleRules: StyleRule[], atRulesSoFar: string[]) {
77
+ const matchingStyleRules = styleRules.filter(
78
+ (r) => r.atRules.length === atRulesSoFar.length,
79
+ );
80
+
81
+ const nextAtRulesArr = styleRules
82
+ .map((r) => r.atRules[atRulesSoFar.length])
83
+ .filter(Boolean);
84
+
85
+ const nextAtRulesSet = new Set(nextAtRulesArr);
86
+
87
+ const node: StyleRuleNode = {
88
+ atRule: atRulesSoFar.length
89
+ ? atRulesSoFar[atRulesSoFar.length - 1]
90
+ : undefined,
91
+ rules: matchingStyleRules,
92
+ children: [...nextAtRulesSet].map((atRule) =>
93
+ groupStyleRules(
94
+ styleRules.filter((r) => r.atRules[atRulesSoFar.length] === atRule),
95
+ [...atRulesSoFar, atRule],
96
+ ),
97
+ ),
98
+ };
99
+
100
+ return node;
101
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,27 @@
1
+ export function css(styleObject: any): string {
2
+ throw new Error("RAHHH");
3
+ }
4
+
5
+ export function keyframes(styleObject: any): string {
6
+ throw new Error("RAHHH");
7
+ }
8
+
9
+ export function mergeClassNames(
10
+ classNames: Array<string | boolean | null | undefined>,
11
+ ): string {
12
+ let cleanNames = classNames
13
+ .filter((element) => typeof element === "string")
14
+ .join(" ")
15
+ .split(/\s+/);
16
+
17
+ let classMap = Object.create(null) as Record<string, string>;
18
+ for (let className of cleanNames) {
19
+ if (className.startsWith("x") && className.includes("-")) {
20
+ classMap[className.split("-")[0]!] = className;
21
+ } else {
22
+ classMap[className] = className;
23
+ }
24
+ }
25
+
26
+ return Object.values(classMap).join(" ");
27
+ }