next-yak 0.0.27 → 0.0.28

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.
@@ -503,6 +503,29 @@ const Component2 = styled.div\`
503
503
  `);
504
504
  });
505
505
 
506
+ it("should inline constants", async () => {
507
+ expect(
508
+ await cssloader.call(
509
+ loaderContext,
510
+ `
511
+ import { styled, css } from "next-yak";
512
+
513
+ const red = "#E50914";
514
+ const zIndex = 14;
515
+ const headline = css\`
516
+ color: \${red};
517
+ z-index: \${zIndex};
518
+ \`
519
+ `
520
+ )
521
+ ).toMatchInlineSnapshot(`
522
+ ".headline_0 {
523
+ color: #E50914;
524
+ z-index: 14;
525
+ }"
526
+ `);
527
+ });
528
+
506
529
  it("should support conditional nested expressions", async () => {
507
530
  expect(
508
531
  await cssloader.call(
@@ -510,9 +533,9 @@ const Component2 = styled.div\`
510
533
  `
511
534
  import { styled, keyframes, css } from "next-yak";
512
535
 
513
- const color = "green";
514
- const duration = "1s";
515
- const easing = "ease-out";
536
+ const color = new Token("green");
537
+ const duration = new Token("1s");
538
+ const easing = new Token("ease-out");
516
539
 
517
540
  const Component = styled.div\`
518
541
  background-color: red;
@@ -461,6 +461,30 @@ const Icon = styled.div\`
461
461
  `);
462
462
  });
463
463
 
464
+ it("should remove string and number constants from the ts code", async () => {
465
+ expect(
466
+ await tsloader.call(
467
+ loaderContext,
468
+ `
469
+ import { styled, css } from "next-yak";
470
+
471
+ const red = "#E50914";
472
+ const zIndex = 14;
473
+ const headline = css\`
474
+ color: \${red};
475
+ z-index: \${zIndex};
476
+ \`
477
+ `
478
+ )
479
+ ).toMatchInlineSnapshot(`
480
+ "import { styled, css } from \\"next-yak\\";
481
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
482
+ const red = \\"#E50914\\";
483
+ const zIndex = 14;
484
+ const headline = css(__styleYak.headline_0);"
485
+ `)
486
+ });
487
+
464
488
  it("should show error when using a runtime value from top level", async () => {
465
489
  await expect(() =>
466
490
  tsloader.call(
@@ -491,7 +515,7 @@ const headline = css\`
491
515
  `
492
516
  import { styled, css } from "next-yak";
493
517
 
494
- const $red = "#E50914";
518
+ const $red = new Token("#E50914");
495
519
  const Button = styled.button\`
496
520
  \${({ $primary, $digits }) => {
497
521
  return $primary && css\`
@@ -6,6 +6,10 @@ const { relative, resolve, basename } = require("path");
6
6
  const localIdent = require("./lib/localIdent.cjs");
7
7
  const getStyledComponentName = require("./lib/getStyledComponentName.cjs");
8
8
  const getCssName = require("./lib/getCssName.cjs");
9
+ const {
10
+ getConstantName,
11
+ getConstantValues,
12
+ } = require("./lib/getConstantValues.cjs");
9
13
 
10
14
  /** @typedef {{replaces: Record<string, unknown>, rootContext?: string}} YakBabelPluginOptions */
11
15
  /** @typedef {{ css: string | undefined, styled: string | undefined, keyframes: string | undefined }} YakLocalIdentifierNames */
@@ -21,7 +25,7 @@ const getCssName = require("./lib/getCssName.cjs");
21
25
  * localVarNames: YakLocalIdentifierNames,
22
26
  * isImportedInCurrentFile: boolean,
23
27
  * classNameCount: number,
24
- * topLevelConstBindings: Set<string>,
28
+ * topLevelConstBindings: Map<string, number | string | null>,
25
29
  * varIndex: number,
26
30
  * variableNameToStyledCall: Map<string, {
27
31
  * wasAdded: boolean,
@@ -123,32 +127,11 @@ module.exports = function (babel, options) {
123
127
  this.classNameCount = 0;
124
128
  this.varIndex = 0;
125
129
  this.variableNameToStyledCall = new Map();
126
- this.topLevelConstBindings = new Set();
130
+ this.topLevelConstBindings = new Map();
127
131
  },
128
132
  visitor: {
129
- Program(path, state) {
130
- const bindings = Object.entries(path.scope.bindings);
131
- const constBindings = bindings.filter(([_name, binding]) => {
132
- if (binding.kind === "module") {
133
- // it is unclear if this is a const binding
134
- // however to be safe, all module bindings are considered consts
135
- return true;
136
- }
137
- return (
138
- // let and var might change but this would be very strange react code
139
- // as states must use useState or useExternalStore
140
- (binding.kind === "let" ||
141
- binding.kind === "var" ||
142
- binding.kind === "const") &&
143
- // don't consider functions or arrow functions as constants
144
- (!("init" in binding.path.node) ||
145
- (!t.isFunctionDeclaration(binding.path.node.init) &&
146
- !t.isArrowFunctionExpression(binding.path.node.init)))
147
- );
148
- });
149
- constBindings.forEach(([name]) => {
150
- this.topLevelConstBindings.add(name);
151
- });
133
+ Program(path) {
134
+ this.topLevelConstBindings = getConstantValues(path, t);
152
135
  },
153
136
  /**
154
137
  * Store the name of the imported 'css' and 'styled' variables e.g.:
@@ -354,26 +337,14 @@ module.exports = function (babel, options) {
354
337
  wasInsideCssValue = true;
355
338
  // to prevent overuse of css variables, we only allow expressions
356
339
  // for css variables for arrow function expressions
357
- if (
358
- // e.g. styled.div`color: ${x};`
359
- (t.isIdentifier(expression) &&
360
- this.topLevelConstBindings.has(expression.name)) ||
361
- // e.g. styled.div`color: ${x.y};`
362
- (t.isMemberExpression(expression) &&
363
- t.isIdentifier(expression.object) &&
364
- this.topLevelConstBindings.has(expression.object.name)) ||
365
- // e.g. styled.div`color: ${x()};`
366
- (t.isCallExpression(expression) &&
367
- // x()
368
- ((t.isIdentifier(expression.callee) &&
369
- this.topLevelConstBindings.has(expression.callee.name)) ||
370
- // x.y()
371
- (t.isMemberExpression(expression.callee) &&
372
- t.isIdentifier(expression.callee.object) &&
373
- this.topLevelConstBindings.has(
374
- expression.callee.object.name
375
- ))))
376
- ) {
340
+ const variableName = getConstantName(expression, t);
341
+ if (variableName && this.topLevelConstBindings.has(variableName)) {
342
+ // Ignore constants that have a static string or number value
343
+ const value = this.topLevelConstBindings.get(variableName);
344
+ if (value !== null) {
345
+ continue;
346
+ }
347
+
377
348
  throw new InvalidPositionError(
378
349
  "Possible constant used as runtime value for a css variable\n" +
379
350
  "Please move the constant to a .yak import or use an arrow function\n" +
@@ -8,6 +8,10 @@ const getStyledComponentName = require("./lib/getStyledComponentName.cjs");
8
8
  const getCssName = require("./lib/getCssName.cjs");
9
9
  const murmurhash2_32_gc = require("./lib/hash.cjs");
10
10
  const { relative } = require("path");
11
+ const {
12
+ getConstantName,
13
+ getConstantValues,
14
+ } = require("./lib/getConstantValues.cjs");
11
15
 
12
16
  /**
13
17
  * @typedef {{ selector: string, hasParent: boolean, quasiCode: Array<{ insideCssValue: boolean, code: string }>, cssPartExpressions: { [key: number]: CssPartExpression[]} }} CssPartExpression
@@ -86,7 +90,14 @@ module.exports = async function cssLoader(source) {
86
90
  /** @type {Map<string, string>} */
87
91
  const variableNameToStyledClassName = new Map();
88
92
 
93
+ /** @type {Map<string, number | string | null>} */
94
+ let topLevelConstBindings = new Map();
95
+
96
+
89
97
  babel.traverse(ast, {
98
+ Program(path) {
99
+ topLevelConstBindings = getConstantValues(path, t);
100
+ },
90
101
  /**
91
102
  * @param {import("@babel/core").NodePath<import("@babel/types").ImportDeclaration>} path
92
103
  */
@@ -240,13 +251,22 @@ module.exports = async function cssLoader(source) {
240
251
  const relativePath = relative(rootContext, resourcePath);
241
252
  hashedFile = murmurhash2_32_gc(relativePath);
242
253
  }
243
- currentCssParts.quasiCode.push({
244
- insideCssValue: true,
245
- code:
246
- unEscapeCssCode(quasi.value.raw) +
247
- // replace the expression with a css variable
248
- `var(--🦬${hashedFile}${varIndex++})`,
249
- });
254
+ const variableName = t.isExpression(expression) && getConstantName(expression, t);
255
+ const variableConstValue = variableName && topLevelConstBindings.get(variableName);
256
+ if (variableConstValue === null || variableConstValue === undefined) {
257
+ currentCssParts.quasiCode.push({
258
+ insideCssValue: true,
259
+ code:
260
+ unEscapeCssCode(quasi.value.raw) +
261
+ // replace the expression with a css variable
262
+ `var(--🦬${hashedFile}${varIndex++})`,
263
+ });
264
+ } else {
265
+ currentCssParts.quasiCode.push({
266
+ insideCssValue: true,
267
+ code: unEscapeCssCode(quasi.value.raw) + String(variableConstValue),
268
+ });
269
+ }
250
270
  } else {
251
271
  wasInsideCssValue = false;
252
272
  // code is added
@@ -0,0 +1,83 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {import("@babel/types")} babel */
4
+
5
+ /**
6
+ * Returns the name of the expression
7
+ * @param {babel.types.Expression} expression
8
+ * @param {import("@babel/types")} t
9
+ */
10
+ const getConstantName = (expression, t) => {
11
+ // e.g. styled.div`color: ${x};`
12
+ if (t.isIdentifier(expression)) {
13
+ // e.g. x
14
+ return expression.name;
15
+ } else if (
16
+ t.isMemberExpression(expression) &&
17
+ t.isIdentifier(expression.object)
18
+ ) {
19
+ // e.g. x for x.y
20
+ return expression.object.name;
21
+ } else if (
22
+ t.isCallExpression(expression) &&
23
+ t.isIdentifier(expression.callee)
24
+ ) {
25
+ // e.g. x for x()
26
+ return expression.callee.name;
27
+ } else if (
28
+ t.isCallExpression(expression) &&
29
+ t.isMemberExpression(expression.callee) &&
30
+ t.isIdentifier(expression.callee.object)
31
+ ) {
32
+ // e.g. x for x.y()
33
+ return expression.callee.object.name;
34
+ } else {
35
+ return null;
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Extracts all top level constant values from a program path
41
+ *
42
+ * @param {babel.NodePath<babel.types.Program>} path
43
+ * @param {import("@babel/types")} t
44
+ */
45
+ function getConstantValues(path, t) {
46
+ /** @type {Map<string, string | number | null>} */
47
+ const topLevelConstBindings = new Map();
48
+ const bindings = Object.entries(path.scope.bindings);
49
+ for (const [name, binding] of bindings) {
50
+ if (binding.kind === "module") {
51
+ topLevelConstBindings.set(name, null);
52
+ continue;
53
+ }
54
+ if (
55
+ binding.kind === "let" ||
56
+ binding.kind === "var" ||
57
+ binding.kind === "const"
58
+ ) {
59
+ // don't consider function declarations or arrow functions as constants
60
+ if (
61
+ !("init" in binding.path.node) ||
62
+ t.isFunctionDeclaration(binding.path.node.init) ||
63
+ t.isArrowFunctionExpression(binding.path.node.init)
64
+ ) {
65
+ topLevelConstBindings.set(name, null);
66
+ continue;
67
+ }
68
+ const value = binding.path.node.init;
69
+ topLevelConstBindings.set(
70
+ name,
71
+ t.isStringLiteral(value) || t.isNumericLiteral(value)
72
+ ? value.value
73
+ : null
74
+ );
75
+ }
76
+ }
77
+ return topLevelConstBindings;
78
+ }
79
+
80
+ module.exports = {
81
+ getConstantValues,
82
+ getConstantName,
83
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-yak",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "type": "module",
5
5
  "types": "./dist/",
6
6
  "exports": {
@@ -30,6 +30,22 @@
30
30
  }
31
31
  }
32
32
  },
33
+ "scripts": {
34
+ "prepublishOnly": "npm run build && npm run test && npm run test:types:code && npm run test:types:test",
35
+ "build": "pnpm run --filter . \"/^build:/\"",
36
+ "build:runtime": "tsup runtime/index.ts --target es2022 --clean --external react --external next-yak/context --format cjs,esm --minify --sourcemap --out-dir dist",
37
+ "build:runtime:types": "tsup runtime/index.ts --target es2022 --clean --external react --dts-only --format cjs,esm --minify --sourcemap --out-dir dist",
38
+ "build:static": "tsup static/index.ts --target es2022 --clean --external react --external next-yak/context --format cjs,esm --minify --sourcemap --out-dir dist/static",
39
+ "build:static:types": "tsup static/index.ts --target es2022 --clean --external react --dts-only --format cjs,esm --minify --sourcemap --out-dir dist/static",
40
+ "build:baseContext": "tsup runtime/context/baseContext.tsx --target es2022 --external react --dts --format cjs,esm --sourcemap --out-dir dist/context/",
41
+ "build:context:client": "tsup runtime/context/index.tsx --target es2022 --external react --external next-yak/context/baseContext --dts --format cjs,esm --sourcemap --out-dir dist/context/",
42
+ "build:context:server": "tsup runtime/context/index.server.tsx --target es2022 --external react --external next-yak/context/baseContext --external ./index.js --format cjs,esm --sourcemap --out-dir dist/context/",
43
+ "build:withYak": "tsup withYak/index.ts --target es2022 --dts --format cjs --sourcemap --out-dir withYak --external ../loaders/tsloader.cjs --external ../loaders/cssloader.cjs",
44
+ "test": "vitest run",
45
+ "test:types:code": "tsc -p tsconfig.json",
46
+ "test:types:test": "tsc -p ./runtime/__tests__/tsconfig.json",
47
+ "test:watch": "vitest --watch"
48
+ },
33
49
  "dependencies": {
34
50
  "@babel/core": "7.23.2",
35
51
  "@babel/plugin-syntax-typescript": "7.22.5",
@@ -58,20 +74,5 @@
58
74
  "loaders",
59
75
  "runtime",
60
76
  "withYak"
61
- ],
62
- "scripts": {
63
- "build": "pnpm run --filter . \"/^build:/\"",
64
- "build:runtime": "tsup runtime/index.ts --target es2022 --clean --external react --external next-yak/context --format cjs,esm --minify --sourcemap --out-dir dist",
65
- "build:runtime:types": "tsup runtime/index.ts --target es2022 --clean --external react --dts-only --format cjs,esm --minify --sourcemap --out-dir dist",
66
- "build:static": "tsup static/index.ts --target es2022 --clean --external react --external next-yak/context --format cjs,esm --minify --sourcemap --out-dir dist/static",
67
- "build:static:types": "tsup static/index.ts --target es2022 --clean --external react --dts-only --format cjs,esm --minify --sourcemap --out-dir dist/static",
68
- "build:baseContext": "tsup runtime/context/baseContext.tsx --target es2022 --external react --dts --format cjs,esm --sourcemap --out-dir dist/context/",
69
- "build:context:client": "tsup runtime/context/index.tsx --target es2022 --external react --external next-yak/context/baseContext --dts --format cjs,esm --sourcemap --out-dir dist/context/",
70
- "build:context:server": "tsup runtime/context/index.server.tsx --target es2022 --external react --external next-yak/context/baseContext --external ./index.js --format cjs,esm --sourcemap --out-dir dist/context/",
71
- "build:withYak": "tsup withYak/index.ts --target es2022 --dts --format cjs --sourcemap --out-dir withYak --external ../loaders/tsloader.cjs --external ../loaders/cssloader.cjs",
72
- "test": "vitest run",
73
- "test:types:code": "tsc -p tsconfig.json",
74
- "test:types:test": "tsc -p ./runtime/__tests__/tsconfig.json",
75
- "test:watch": "vitest --watch"
76
- }
77
- }
77
+ ]
78
+ }