next-yak 0.0.28 → 0.0.30

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.
@@ -13,6 +13,18 @@ const loaderContext = {
13
13
  xl: "@media (min-width: 1280px)",
14
14
  xxl: "@media (min-width: 1536px)",
15
15
  },
16
+ spacing: {
17
+ 0.5: "4px",
18
+ 1: "8px",
19
+ 2: "16px",
20
+ 4: "32px",
21
+ },
22
+ typography: {
23
+ "letter spacing": "0.05em",
24
+ primary: {
25
+ "font weight": 800,
26
+ },
27
+ },
16
28
  };
17
29
  },
18
30
  getOptions: () => ({
@@ -289,6 +301,40 @@ const headline = css\`
289
301
  `);
290
302
  });
291
303
 
304
+ it("should replace breakpoint references with actual media queries when using square brackets", async () => {
305
+ expect(
306
+ await cssloader.call(
307
+ loaderContext,
308
+ `
309
+ import { css } from "next-yak";
310
+ import { queries } from '@/theme.yak';
311
+
312
+ const headline = css\`
313
+ color: blue;
314
+ \${queries["sm"]} {
315
+ color: red;
316
+ }
317
+ transition: color \${duration} \${easing};
318
+ display: block;
319
+ \${css\`color: orange\`}
320
+ \`;
321
+ `
322
+ )
323
+ ).toMatchInlineSnapshot(`
324
+ ".headline_0 {
325
+ color: blue;
326
+ @media (min-width: 640px) {
327
+ color: red;
328
+ }
329
+ transition: color var(--🦬18fi82j0) var(--🦬18fi82j1);
330
+ display: block;
331
+ &:where(.headline_1) {
332
+ color: orange
333
+ }
334
+ }"
335
+ `);
336
+ });
337
+
292
338
  it("should prevent double escaped chars", async () => {
293
339
  // in styled-components \\ is replaced with \
294
340
  // this test verifies that yak provides the same behavior
@@ -466,7 +512,7 @@ const Component = styled.div\`
466
512
  background-color: brown;
467
513
  \`}
468
514
  \`}
469
-
515
+
470
516
  border: 2px solid pink;
471
517
  }
472
518
  \`;
@@ -574,4 +620,33 @@ const Component = styled.div\`
574
620
  }"
575
621
  `);
576
622
  });
623
+
624
+ it("should replace all array like constants", async () => {
625
+ expect(
626
+ await cssloader.call(
627
+ loaderContext,
628
+ `
629
+ import { css } from "next-yak";
630
+ import { queries, spacing, typography } from "@/theme.yak";
631
+
632
+ const headline = css\`
633
+ \${queries["xl"]} {
634
+ color: red;
635
+ }
636
+ margin: -\${spacing[2]};
637
+ font-weight: \${typography.primary["font weight"]};
638
+ letter-spacing: \${typography["letter spacing"]};
639
+ \``
640
+ )
641
+ ).toMatchInlineSnapshot(`
642
+ ".headline_0 {
643
+ @media (min-width: 1280px) {
644
+ color: red;
645
+ }
646
+ margin: -16px;
647
+ font-weight: 800;
648
+ letter-spacing: 0.05em;
649
+ }"
650
+ `);
651
+ });
577
652
  });
@@ -25,43 +25,24 @@
25
25
  * @param {import("@babel/types")} t
26
26
  */
27
27
  module.exports = function replaceTokensInQuasiExpressions(quasi, replacer, t) {
28
- for (let i = 0; i < quasi.expressions.length; i++) {
28
+ // Iterate over the expressions in reverse order
29
+ // so removing items won't affect the index of the next item
30
+ for (let i = quasi.expressions.length - 1; i >= 0; i--) {
29
31
  const expression = quasi.expressions[i];
30
- // replace direct identifiers e.g. ${query}
31
- if (t.isIdentifier(expression)) {
32
- const replacement = replacer(expression.name);
33
- if (replacement === false) {
34
- continue;
35
- }
36
- replaceExpressionAndMergeQuasis(quasi, i, replacement);
37
- i--;
38
- }
39
- // replace member expressions e.g. ${query.xs}
40
- // replace deeply nested member expressions e.g. ${query.xs.min}
41
- else if (
42
- t.isMemberExpression(expression) &&
43
- t.isIdentifier(expression.object)
44
- ) {
45
- /** @type {any} */
46
- let replacement = replacer(expression.object.name);
47
- if (replacement === false) {
48
- continue;
49
- }
50
- /** @type {import("@babel/types").Expression} */
51
- let object = expression;
52
- while (t.isMemberExpression(object)) {
53
- if (!t.isIdentifier(object.property)) {
54
- break;
55
- }
56
- if (typeof replacement !== "object" || replacement === null) {
57
- break;
58
- }
59
- replacement = replacement[object.property.name];
60
- object = object.object;
61
- }
62
- replaceExpressionAndMergeQuasis(quasi, i, replacement);
63
- i--;
64
- }
32
+ // break the expression into parts
33
+ // e.g. x.y.z -> ["x", "y", "z"]
34
+ const parts = getExpressionParts(expression, t);
35
+ // find the replacement for the expression
36
+ const replacement = parts && replacer(parts[0]);
37
+ // if it is a nested value, find the value of the expression
38
+ // e.g. x.y.z -> find the value of z
39
+ const replacementValue = replacement && getReplacementValue(
40
+ replacement,
41
+ parts
42
+ );
43
+ if (replacementValue !== false && replacementValue !== null) {
44
+ replaceExpressionAndMergeQuasis(quasi, i, replacementValue);
45
+ }
65
46
  }
66
47
  };
67
48
 
@@ -85,3 +66,73 @@ function replaceExpressionAndMergeQuasis(quasi, expressionIndex, replacement) {
85
66
  stringReplacement + quasi.quasis[expressionIndex + 1].value.cooked;
86
67
  quasi.quasis.splice(expressionIndex + 1, 1);
87
68
  }
69
+
70
+ /**
71
+ * Find the replacement for the expression
72
+ *
73
+ * searches for:
74
+ * - `x` -> ["x"]
75
+ * - `x.y` -> ["x", "y"]
76
+ * - `x[0]` -> ["x", 0]
77
+ * - `x()` -> ["x"]
78
+ * - `x.y()` -> ["x", "y"]
79
+ * - (1 + 2) -> null
80
+ *
81
+ * @param {import("@babel/types").Expression | import("@babel/types").TSType} expression
82
+ * @param {import("@babel/types")} t
83
+ */
84
+ function getExpressionParts(expression, t) {
85
+ let currentExpression = expression;
86
+ /** @type {string[]} */
87
+ const tokens = [];
88
+ while (currentExpression) {
89
+ // e.g. x
90
+ if (t.isIdentifier(currentExpression)) {
91
+ tokens.unshift(currentExpression.name);
92
+ break;
93
+ }
94
+ // e.g. x.y
95
+ if (t.isMemberExpression(currentExpression)) {
96
+ if (currentExpression.computed === false && t.isIdentifier(currentExpression.property)) {
97
+ tokens.unshift(currentExpression.property.name);
98
+ } else if (t.isStringLiteral(currentExpression.property)) {
99
+ tokens.unshift(currentExpression.property.value);
100
+ } else if (t.isNumericLiteral(currentExpression.property)) {
101
+ tokens.unshift(String(currentExpression.property.value));
102
+ } else {
103
+ return null;
104
+ }
105
+ currentExpression = currentExpression.object;
106
+ } else if (t.isCallExpression(currentExpression)) {
107
+ if (!t.isExpression(currentExpression.callee)) {
108
+ return null;
109
+ }
110
+ currentExpression = currentExpression.callee;
111
+ } else {
112
+ return null;
113
+ }
114
+ }
115
+ return tokens;
116
+ }
117
+
118
+ /**
119
+ * Get the value of the replacement
120
+ *
121
+ * e.g. for `replacement.x.y[0]` and `replacement = { x: { y: [42] } }`
122
+ * parts = ["replacement", "x", "y", 0]
123
+ * --> 42
124
+ *
125
+ * @param {any} replacement
126
+ * @param {string[]} parts
127
+ */
128
+ function getReplacementValue(replacement, parts) {
129
+ let currentReplacement = replacement;
130
+ for (let i = 1; i < parts.length; i++) {
131
+ const part = parts[i];
132
+ if (currentReplacement == null || typeof currentReplacement !== "object") {
133
+ return false;
134
+ }
135
+ currentReplacement = currentReplacement[part];
136
+ }
137
+ return currentReplacement;
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-yak",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "type": "module",
5
5
  "types": "./dist/",
6
6
  "exports": {