next-yak 0.0.18 → 0.0.19

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.
@@ -0,0 +1,133 @@
1
+ import quasiClassifier from '../lib/quasiClassifier.cjs';
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ describe('quasiClassifier', () => {
5
+ it('should classify empty quasi', () => {
6
+ expect(quasiClassifier('', [])).toEqual({
7
+ empty: true,
8
+ unknownSelector: false,
9
+ insideCssValue: false,
10
+ currentNestingScopes: [],
11
+ });
12
+ });
13
+ it("should recognize a comment as empty", () => {
14
+ expect(quasiClassifier("/* comment */", [])).toEqual({
15
+ empty: true,
16
+ unknownSelector: false,
17
+ insideCssValue: false,
18
+ currentNestingScopes: [],
19
+ });
20
+ });
21
+ it("should find incomplete css - unknownSelector", () => {
22
+ expect(quasiClassifier("{ color: blue;", [])).toEqual({
23
+ empty: false,
24
+ unknownSelector: true,
25
+ insideCssValue: false,
26
+ currentNestingScopes: [],
27
+ });
28
+ });
29
+ it("should find incomplete css - insideCssValue", () => {
30
+ expect(quasiClassifier("color: ", [
31
+ ".foo"
32
+ ])).toEqual({
33
+ empty: false,
34
+ unknownSelector: false,
35
+ insideCssValue: true,
36
+ currentNestingScopes: [".foo"],
37
+ });
38
+ });
39
+ it("should find incomplete css - insideCssValue after a media query", () => {
40
+ expect(quasiClassifier("} .foo { color: ", [
41
+ "@media (min-width: 640px)"
42
+ ])).toEqual({
43
+ empty: false,
44
+ unknownSelector: false,
45
+ insideCssValue: true,
46
+ currentNestingScopes: [".foo"],
47
+ });
48
+ });
49
+ it("should find incomplete css - insideCssValue in a multi value", () => {
50
+ expect(quasiClassifier("} .foo { transition: 200ms ", [
51
+ "@media (min-width: 640px)"
52
+ ])).toEqual({
53
+ empty: false,
54
+ unknownSelector: false,
55
+ insideCssValue: true,
56
+ currentNestingScopes: [".foo"],
57
+ });
58
+ });
59
+ it("should find nesting scopes", () => {
60
+ expect(quasiClassifier(`
61
+ .foo {
62
+ .bar {
63
+ @supports (display: grid) {
64
+ `, [
65
+ "@media (min-width: 640px)"
66
+ ])).toEqual({
67
+ empty: false,
68
+ unknownSelector: false,
69
+ insideCssValue: false,
70
+ currentNestingScopes: [
71
+ "@media (min-width: 640px)",
72
+ ".foo",
73
+ ".bar",
74
+ "@supports (display: grid)",
75
+ ],
76
+ });
77
+ });
78
+ it("should find nesting scopes with properties", () => {
79
+ expect(quasiClassifier(`
80
+ .foo {
81
+ color: purple;
82
+ .x {
83
+ color: green;
84
+ }
85
+ .bar {
86
+ color: orange;
87
+ @supports (display: grid) {
88
+ color: blue;
89
+ `, [
90
+ "@media (min-width: 640px)"
91
+ ])).toEqual({
92
+ empty: false,
93
+ unknownSelector: false,
94
+ insideCssValue: false,
95
+ currentNestingScopes: [
96
+ "@media (min-width: 640px)",
97
+ ".foo",
98
+ ".bar",
99
+ "@supports (display: grid)",
100
+ ],
101
+ });
102
+ });
103
+
104
+ it("should find nesting scopes with properties and closing nestings", () => {
105
+ expect(quasiClassifier(`
106
+ .foo {
107
+ /* }}}}} */
108
+ color: purple;
109
+ .x {
110
+ color: green;
111
+ }
112
+ .bar {
113
+ :before {
114
+ content: '{';
115
+ }
116
+ color: orange;
117
+ @supports (display: grid) {
118
+ color: blue;
119
+ }
120
+ }
121
+ `, [
122
+ "@media (min-width: 640px)"
123
+ ])).toEqual({
124
+ empty: false,
125
+ unknownSelector: false,
126
+ insideCssValue: false,
127
+ currentNestingScopes: [
128
+ "@media (min-width: 640px)",
129
+ ".foo",
130
+ ],
131
+ });
132
+ });
133
+ });
@@ -166,13 +166,12 @@ const FancyButton = styled(Button)\`
166
166
  const FancyButton = styled(Button)(__styleYak.FancyButton_2);"
167
167
  `);
168
168
  });
169
- });
170
169
 
171
- it("should support attrs on intrinsic elements", async () => {
172
- expect(
173
- await tsloader.call(
174
- loaderContext,
175
- `
170
+ it("should support attrs on intrinsic elements", async () => {
171
+ expect(
172
+ await tsloader.call(
173
+ loaderContext,
174
+ `
176
175
  import { styled } from "next-yak";
177
176
 
178
177
  const headline = styled.input.attrs({
@@ -181,21 +180,21 @@ const headline = styled.input.attrs({
181
180
  color: red;
182
181
  \`;
183
182
  `
184
- )
185
- ).toMatchInlineSnapshot(`
183
+ )
184
+ ).toMatchInlineSnapshot(`
186
185
  "import { styled } from \\"next-yak\\";
187
186
  import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
188
187
  const headline = styled.input.attrs({
189
188
  type: \\"text\\"
190
189
  })(__styleYak.headline_0);"
191
190
  `);
192
- });
191
+ });
193
192
 
194
- it("should support attrs on wrapped elements", async () => {
195
- expect(
196
- await tsloader.call(
197
- loaderContext,
198
- `
193
+ it("should support attrs on wrapped elements", async () => {
194
+ expect(
195
+ await tsloader.call(
196
+ loaderContext,
197
+ `
199
198
  import { styled } from "next-yak";
200
199
 
201
200
  const headline = styled.input\`
@@ -208,8 +207,8 @@ const newHeadline = styled(headline).attrs({
208
207
  color: black;
209
208
  \`;
210
209
  `
211
- )
212
- ).toMatchInlineSnapshot(`
210
+ )
211
+ ).toMatchInlineSnapshot(`
213
212
  "import { styled } from \\"next-yak\\";
214
213
  import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
215
214
  const headline = styled.input(__styleYak.headline_0);
@@ -217,13 +216,13 @@ const newHeadline = styled(headline).attrs({
217
216
  type: \\"text\\"
218
217
  })(__styleYak.newHeadline_1);"
219
218
  `);
220
- });
219
+ });
221
220
 
222
- it("should support css variables with spaces", async () => {
223
- expect(
224
- await tsloader.call(
225
- loaderContext,
226
- `
221
+ it("should support css variables with spaces", async () => {
222
+ expect(
223
+ await tsloader.call(
224
+ loaderContext,
225
+ `
227
226
  import styles from "./page.module.css";
228
227
  import { css } from "next-yak";
229
228
  import { easing } from "styleguide";
@@ -235,8 +234,8 @@ const headline = css\`
235
234
  \${css\`color: blue\`}
236
235
  \`;
237
236
  `
238
- )
239
- ).toMatchInlineSnapshot(`
237
+ )
238
+ ).toMatchInlineSnapshot(`
240
239
  "import styles from \\"./page.module.css\\";
241
240
  import { css } from \\"next-yak\\";
242
241
  import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
@@ -250,13 +249,13 @@ const headline = css\`
250
249
  }
251
250
  });"
252
251
  `);
253
- });
252
+ });
254
253
 
255
- it("should convert keyframes", async () => {
256
- expect(
257
- await tsloader.call(
258
- loaderContext,
259
- `
254
+ it("should convert keyframes", async () => {
255
+ expect(
256
+ await tsloader.call(
257
+ loaderContext,
258
+ `
260
259
  import styles from "./page.module.css";
261
260
  import { styled, keyframes } from "next-yak";
262
261
 
@@ -273,8 +272,8 @@ const FadeInButton = styled.button\`
273
272
  animation: 1s \${fadeIn} ease-out;
274
273
  \`
275
274
  `
276
- )
277
- ).toMatchInlineSnapshot(`
275
+ )
276
+ ).toMatchInlineSnapshot(`
278
277
  "import styles from \\"./page.module.css\\";
279
278
  import { styled, keyframes } from \\"next-yak\\";
280
279
  import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
@@ -285,13 +284,13 @@ const FadeInButton = styled.button\`
285
284
  }
286
285
  });"
287
286
  `);
288
- });
287
+ });
289
288
 
290
- it("should allow to target components", async () => {
291
- expect(
292
- await tsloader.call(
293
- loaderContext,
294
- `
289
+ it("should allow to target components", async () => {
290
+ expect(
291
+ await tsloader.call(
292
+ loaderContext,
293
+ `
295
294
  import { styled, keyframes } from "next-yak";
296
295
 
297
296
  const Link = styled.a\`
@@ -317,21 +316,21 @@ const Wrapper = styled.div\`
317
316
  \`
318
317
 
319
318
  `
320
- )
321
- ).toMatchInlineSnapshot(`
319
+ )
320
+ ).toMatchInlineSnapshot(`
322
321
  "import { styled, keyframes } from \\"next-yak\\";
323
322
  import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
324
323
  const Link = styled.a(__styleYak.Link_0);
325
324
  const Icon = styled.svg(__styleYak.Icon_1);
326
325
  const Wrapper = styled.div(__styleYak.Wrapper_2);"
327
326
  `);
328
- });
327
+ });
329
328
 
330
- it("should allow to target components even if they don't have styles", async () => {
331
- expect(
332
- await tsloader.call(
333
- loaderContext,
334
- `
329
+ it("should allow to target components even if they don't have styles", async () => {
330
+ expect(
331
+ await tsloader.call(
332
+ loaderContext,
333
+ `
335
334
  import { styled, keyframes } from "next-yak";
336
335
 
337
336
  const Link = styled.a\`
@@ -347,12 +346,65 @@ const Wrapper = styled.div\`
347
346
  \`
348
347
 
349
348
  `
350
- )
351
- ).toMatchInlineSnapshot(`
349
+ )
350
+ ).toMatchInlineSnapshot(`
352
351
  "import { styled, keyframes } from \\"next-yak\\";
353
352
  import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
354
353
  const Link = styled.a();
355
354
  const Icon = styled.svg(__styleYak.Icon_1);
356
355
  const Wrapper = styled.div(__styleYak.Wrapper_2);"
357
356
  `);
357
+ });
358
+
359
+ it("should show error when mixin is used in nested selector", async () => {
360
+ await expect(() =>
361
+ tsloader.call(
362
+ loaderContext,
363
+ `
364
+ import { styled, css } from "next-yak";
365
+
366
+ const bold = css\`
367
+ font-weight: bold;
368
+ \`
369
+
370
+ const Icon = styled.div\`
371
+ @media (min-width: 640px) {
372
+ .bar {
373
+ \${bold}
374
+ }
375
+ }
376
+ \`
377
+ `
378
+ )
379
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
380
+ "/some/special/path/page.tsx: Expressions are not allowed inside nested selectors:
381
+ line 11: found \\"bold\\" inside \\"@media (min-width: 640px) { .bar {\\""
382
+ `);
383
+ });
384
+
385
+ it("should show error when mixin is used in nested selector inside a css", async () => {
386
+ await expect(() =>
387
+ tsloader.call(
388
+ loaderContext,
389
+ `
390
+ import { styled, css } from "next-yak";
391
+
392
+ const bold = css\`
393
+ font-weight: bold;
394
+ \`
395
+
396
+ const Icon = styled.div\`
397
+ @media (min-width: 640px) {
398
+ .bar {
399
+ \${() => css\`\${bold}\`}
400
+ }
401
+ }
402
+ \`
403
+ `
404
+ )
405
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
406
+ "/some/special/path/page.tsx: Expressions are not allowed inside nested selectors:
407
+ line 11: found Expression inside \\"@media (min-width: 640px) { .bar {\\""
408
+ `);
409
+ });
358
410
  });
@@ -62,9 +62,9 @@ module.exports = function (babel, options) {
62
62
  t.importDeclaration(
63
63
  [t.importDefaultSpecifier(t.identifier("__styleYak"))],
64
64
  t.stringLiteral(
65
- `./${fileName}.yak.module.css!=!./${fileName}?./${fileName}.yak.module.css`
66
- )
67
- )
65
+ `./${fileName}.yak.module.css!=!./${fileName}?./${fileName}.yak.module.css`,
66
+ ),
67
+ ),
68
68
  );
69
69
 
70
70
  // Process import specifiers
@@ -115,7 +115,7 @@ module.exports = function (babel, options) {
115
115
  const isStyledLiteral =
116
116
  t.isMemberExpression(tag) &&
117
117
  t.isIdentifier(
118
- /** @type {babel.types.MemberExpression} */ (tag).object
118
+ /** @type {babel.types.MemberExpression} */ (tag).object,
119
119
  ) &&
120
120
  /** @type {babel.types.Identifier} */ (
121
121
  /** @type {babel.types.MemberExpression} */ (tag).object
@@ -123,7 +123,7 @@ module.exports = function (babel, options) {
123
123
  const isStyledCall =
124
124
  t.isCallExpression(tag) &&
125
125
  t.isIdentifier(
126
- /** @type {babel.types.CallExpression} */ (tag).callee
126
+ /** @type {babel.types.CallExpression} */ (tag).callee,
127
127
  ) &&
128
128
  /** @type {babel.types.Identifier} */ (
129
129
  /** @type {babel.types.CallExpression} */ (tag).callee
@@ -169,15 +169,15 @@ module.exports = function (babel, options) {
169
169
  astNode.arguments.push(
170
170
  t.memberExpression(
171
171
  t.identifier("__styleYak"),
172
- t.identifier(className)
173
- )
172
+ t.identifier(className),
173
+ ),
174
174
  );
175
175
  }
176
176
  return className;
177
177
  }
178
178
  return false;
179
179
  },
180
- t
180
+ t,
181
181
  );
182
182
 
183
183
  let literalSelectorWasUsed = false;
@@ -189,17 +189,21 @@ module.exports = function (babel, options) {
189
189
  localIdent(
190
190
  variableName,
191
191
  literalSelectorIndex,
192
- isKeyframesLiteral ? "animation" : "className"
193
- )
194
- )
192
+ isKeyframesLiteral ? "animation" : "className",
193
+ ),
194
+ ),
195
195
  );
196
196
 
197
197
  // Replace the tagged template expression with a call to the 'styled' function
198
198
  const newArguments = new Set();
199
199
  const quasis = path.node.quasi.quasis;
200
- const quasiTypes = quasis.map((quasi) =>
201
- quasiClassifier(quasi.value.raw)
202
- );
200
+ /** @type {string[]} */
201
+ let currentNestingScopes = [];
202
+ const quasiTypes = quasis.map((quasi) => {
203
+ const classification = quasiClassifier(quasi.value.raw, currentNestingScopes);
204
+ currentNestingScopes = classification.currentNestingScopes;
205
+ return classification;
206
+ });
203
207
  const expressions = path.node.quasi.expressions;
204
208
 
205
209
  let cssVariablesInlineStyle;
@@ -226,8 +230,8 @@ module.exports = function (babel, options) {
226
230
  const type = quasiTypes[i];
227
231
  // expressions after a partial css are converted into css variables
228
232
  if (
229
- type.partialStart ||
230
- type.partialEnd ||
233
+ type.unknownSelector ||
234
+ type.insideCssValue ||
231
235
  (isMerging && type.empty)
232
236
  ) {
233
237
  isMerging = true;
@@ -249,7 +253,7 @@ module.exports = function (babel, options) {
249
253
  }
250
254
  const relativePath = relative(
251
255
  rootContext,
252
- resolve(rootContext, resourcePath)
256
+ resolve(rootContext, resourcePath),
253
257
  );
254
258
  hashedFile = murmurhash2_32_gc(relativePath);
255
259
  }
@@ -257,14 +261,23 @@ module.exports = function (babel, options) {
257
261
  cssVariablesInlineStyle.properties.push(
258
262
  t.objectProperty(
259
263
  t.stringLiteral(`--🦬${hashedFile}${this.varIndex++}`),
260
- /** @type {babel.types.Expression} */ (expression)
261
- )
264
+ /** @type {babel.types.Expression} */ (expression),
265
+ ),
262
266
  );
263
267
  } else if (type.empty) {
264
- // empty quasis can be ignored
268
+ // empty quasis can be ignored in typescript
265
269
  // e.g. `transition: color ${duration} ${easing};`
270
+ // ^
266
271
  } else {
267
272
  if (expressions[i]) {
273
+ if (quasiTypes[i].currentNestingScopes.length > 0) {
274
+ const errorExpression = expressions[i];
275
+ const name = errorExpression.type === "Identifier" ? `"${errorExpression.name}"` : "Expression";
276
+ const line = errorExpression.loc?.start.line || -1
277
+ throw new InvalidPositionError(
278
+ `Expressions are not allowed inside nested selectors:\n${line !== -1 ? `line ${line}: ` : ""}found ${name} inside "${quasiTypes[i].currentNestingScopes.join(" { ")} {"`,
279
+ );
280
+ }
268
281
  newArguments.add(expressions[i]);
269
282
  }
270
283
  break;
@@ -277,9 +290,9 @@ module.exports = function (babel, options) {
277
290
  t.objectExpression([
278
291
  t.objectProperty(
279
292
  t.stringLiteral(`style`),
280
- cssVariablesInlineStyle
293
+ cssVariablesInlineStyle,
281
294
  ),
282
- ])
295
+ ]),
283
296
  );
284
297
  }
285
298
 
@@ -294,7 +307,7 @@ module.exports = function (babel, options) {
294
307
  className: localIdent(
295
308
  variableName,
296
309
  literalSelectorIndex,
297
- "className"
310
+ "className",
298
311
  ),
299
312
  astNode: styledCall,
300
313
  });
@@ -303,3 +316,14 @@ module.exports = function (babel, options) {
303
316
  },
304
317
  };
305
318
  };
319
+
320
+ class InvalidPositionError extends Error {
321
+ /**
322
+ * @param {string} message
323
+ */
324
+ constructor(message) {
325
+ super(message);
326
+ }
327
+ }
328
+
329
+ module.exports.InvalidPositionError = InvalidPositionError;
@@ -34,7 +34,7 @@ module.exports = async function cssLoader(source) {
34
34
  imports.forEach(({ localName, importedName }) => {
35
35
  replaces[localName] = constantValues[importedName];
36
36
  });
37
- })
37
+ }),
38
38
  );
39
39
 
40
40
  // parse source with babel
@@ -127,7 +127,7 @@ module.exports = async function cssLoader(source) {
127
127
  const isStyledLiteral =
128
128
  t.isMemberExpression(tag) &&
129
129
  t.isIdentifier(
130
- /** @type {babel.types.MemberExpression} */ (tag).object
130
+ /** @type {babel.types.MemberExpression} */ (tag).object,
131
131
  ) &&
132
132
  /** @type {babel.types.Identifier} */ (
133
133
  /** @type {babel.types.MemberExpression} */ (tag).object
@@ -136,7 +136,7 @@ module.exports = async function cssLoader(source) {
136
136
  const isStyledCall =
137
137
  t.isCallExpression(tag) &&
138
138
  t.isIdentifier(
139
- /** @type {babel.types.CallExpression} */ (tag).callee
139
+ /** @type {babel.types.CallExpression} */ (tag).callee,
140
140
  ) &&
141
141
  /** @type {babel.types.Identifier} */ (
142
142
  /** @type {babel.types.CallExpression} */ (tag).callee
@@ -176,7 +176,7 @@ module.exports = async function cssLoader(source) {
176
176
  }
177
177
  return false;
178
178
  },
179
- t
179
+ t,
180
180
  );
181
181
 
182
182
  // Keep the same selector for all quasis belonging to the same css block
@@ -184,14 +184,12 @@ module.exports = async function cssLoader(source) {
184
184
  const literalSelector = localIdent(
185
185
  variableName,
186
186
  literalSelectorIndex,
187
- isKeyFrameLiteral ? "keyframes" : "selector"
187
+ isKeyFrameLiteral ? "keyframes" : "selector",
188
188
  );
189
189
 
190
190
  // Replace the tagged template expression with a call to the 'styled' function
191
191
  const quasis = path.node.quasi.quasis;
192
- const quasiTypes = quasis.map((quasi) =>
193
- quasiClassifier(quasi.value.raw)
194
- );
192
+ const quasiTypes = quasis.map((quasi) => quasiClassifier(quasi.value.raw, []));
195
193
 
196
194
  for (let i = 0; i < quasis.length; i++) {
197
195
  const quasi = quasis[i];
@@ -206,8 +204,8 @@ module.exports = async function cssLoader(source) {
206
204
  const type = quasiTypes[i];
207
205
  // expressions after a partial css are converted into css variables
208
206
  if (
209
- type.partialStart ||
210
- type.partialEnd ||
207
+ type.unknownSelector ||
208
+ type.insideCssValue ||
211
209
  (isMerging && type.empty)
212
210
  ) {
213
211
  isMerging = true;
@@ -243,7 +241,7 @@ module.exports = async function cssLoader(source) {
243
241
  if (isStyledLiteral || isStyledCall || isAttrsCall) {
244
242
  variableNameToStyledClassName.set(
245
243
  variableName,
246
- localIdent(variableName, literalSelectorIndex, "selector")
244
+ localIdent(variableName, literalSelectorIndex, "selector"),
247
245
  );
248
246
  }
249
247
  },
@@ -13,7 +13,7 @@
13
13
  */
14
14
  const getStyledComponentName = (taggedTemplateExpressionPath) => {
15
15
  const variableDeclaratorPath = taggedTemplateExpressionPath.findParent(
16
- (path) => path.isVariableDeclarator()
16
+ (path) => path.isVariableDeclarator(),
17
17
  );
18
18
  if (
19
19
  !variableDeclaratorPath ||
@@ -22,7 +22,7 @@ const getStyledComponentName = (taggedTemplateExpressionPath) => {
22
22
  ) {
23
23
  throw new Error(
24
24
  "Could not find variable declaration for styled component at " +
25
- taggedTemplateExpressionPath.node.loc
25
+ taggedTemplateExpressionPath.node.loc,
26
26
  );
27
27
  }
28
28
  return variableDeclaratorPath.node.id.name;
@@ -1,38 +1,47 @@
1
1
  // @ts-check
2
2
  /**
3
3
  * Finds all imports in a given code string which import from a .yak file
4
- *
4
+ *
5
5
  * Uses regex to work with typescript and javascript
6
6
  * Does not support lazy imports
7
- *
7
+ *
8
8
  * @param {string} code
9
9
  * @returns { { imports: { localName: string, importedName: string }[], from: string }[] }
10
10
  */
11
11
  const getYakImports = (code) => {
12
- const codeWithoutComments = code.replace(/\/\*[\s\S]*?\*\//g, '');
13
- const allImports = codeWithoutComments.matchAll(/(^|\n|;)\s*import\s+(?:(\w+(?:\s+as\s+\w+)?)\s*,?\s*)?(?:{([^}]*)})?\s+from\s+["']([^'"]+\.yak)["'](;|\n)/g);
14
- return [...allImports].map(([, , defaultImport, namedImports, from,]) => {
15
- // parse named imports to { localName: string, importedName: string }[]
16
- const imports = namedImports?.split(',').map((namedImport) => {
17
- const [importedName, localName = importedName] = namedImport.replace(/^type\s+/, "").trim().split(/\s+as\s+/);
18
- return { localName, importedName };
19
- }) ?? [];
20
- // parse default import to { localName: string, importedName: string }[]
21
- if (defaultImport) {
22
- imports.push(parseDefaultImport(defaultImport));
23
- }
24
- return { imports, from };
25
- });
12
+ const codeWithoutComments = code.replace(/\/\*[\s\S]*?\*\//g, "");
13
+ const allImports = codeWithoutComments.matchAll(
14
+ /(^|\n|;)\s*import\s+(?:(\w+(?:\s+as\s+\w+)?)\s*,?\s*)?(?:{([^}]*)})?\s+from\s+["']([^'"]+\.yak)["'](;|\n)/g,
15
+ );
16
+ return [...allImports].map(([, , defaultImport, namedImports, from]) => {
17
+ // parse named imports to { localName: string, importedName: string }[]
18
+ const imports =
19
+ namedImports?.split(",").map((namedImport) => {
20
+ const [importedName, localName = importedName] = namedImport
21
+ .replace(/^type\s+/, "")
22
+ .trim()
23
+ .split(/\s+as\s+/);
24
+ return { localName, importedName };
25
+ }) ?? [];
26
+ // parse default import to { localName: string, importedName: string }[]
27
+ if (defaultImport) {
28
+ imports.push(parseDefaultImport(defaultImport));
29
+ }
30
+ return { imports, from };
31
+ });
26
32
  };
27
33
 
28
34
  /**
29
35
  * Parses a default import string
30
36
  * @param {string} defaultImport
31
37
  * @returns {{ localName: string, importedName: string }}
32
- */
38
+ */
33
39
  function parseDefaultImport(defaultImport) {
34
- const defaultImportArray = defaultImport.split(/\s+as\s+/);
35
- return { localName: defaultImportArray[1] ?? defaultImportArray[0], importedName: defaultImportArray[0] }
40
+ const defaultImportArray = defaultImport.split(/\s+as\s+/);
41
+ return {
42
+ localName: defaultImportArray[1] ?? defaultImportArray[0],
43
+ importedName: defaultImportArray[0],
44
+ };
36
45
  }
37
46
 
38
- module.exports = getYakImports;
47
+ module.exports = getYakImports;
@@ -3,31 +3,85 @@ const stripCssComments = require("./stripCssComments.cjs");
3
3
 
4
4
  /**
5
5
  * Checks a quasiValue and returns its type
6
- *
6
+ *
7
7
  * - empty: no expressions, no text
8
- * - partialStart: starts with a `{`
9
- * - partialEnd: does not end with a `}` or `;`
10
- *
11
- * @param {string} quasiValue
8
+ * - unknownSelector: starts with a `{` e.g. `{ opacity: 0.5; }`
9
+ * - insideCssValue: does not end with a `{` or `}` or `;` e.g. `color: `
10
+ *
11
+ * @param {string} quasiValue
12
+ * @param {string[]} currentNestingScopes - the current nesting scope
13
+ *
12
14
  * @returns {{
13
15
  * empty: boolean,
14
- * partialStart: boolean,
15
- * partialEnd: boolean,
16
+ * unknownSelector: boolean,
17
+ * insideCssValue: boolean,
18
+ * currentNestingScopes: string[],
16
19
  * }}
17
20
  */
18
- module.exports = function quasiClassifier(quasiValue) {
19
- const trimmed = stripCssComments(quasiValue).trim();
20
- if (trimmed === "") {
21
- return {
22
- empty: true,
23
- partialStart: false,
24
- partialEnd: false,
25
- }
26
- }
27
-
21
+ module.exports = function quasiClassifier(quasiValue, currentNestingScopes) {
22
+ // TODO - for better performance we could move the comment skipping logic
23
+ // directly in the for loop below instead of calling stripCssComments
24
+ const trimmedCssString = stripCssComments(quasiValue).trim();
25
+ if (trimmedCssString === "") {
28
26
  return {
29
- empty: false,
30
- partialStart: trimmed.startsWith("{"),
31
- partialEnd: !trimmed.endsWith("}") && !trimmed.endsWith(";"),
27
+ empty: true,
28
+ unknownSelector: false,
29
+ insideCssValue: false,
30
+ currentNestingScopes,
31
+ };
32
+ }
33
+ /** @type {'"' | "'" | false} */
34
+ let isInsideString = false;
35
+ let currentCharacter = "";
36
+ let newNestingLevel = [...currentNestingScopes];
37
+ let currentSelector = "";
38
+ for (let index = 0; index < trimmedCssString.length; index++) {
39
+ currentCharacter = trimmedCssString[index];
40
+ if (
41
+ trimmedCssString[index - 1] !== "\\" &&
42
+ (currentCharacter === '"' || currentCharacter === "'")
43
+ ) {
44
+ if (isInsideString === currentCharacter) {
45
+ isInsideString = false;
46
+ } else if (!isInsideString) {
47
+ isInsideString = currentCharacter;
48
+ }
49
+ }
50
+ if (isInsideString) {
51
+ continue;
32
52
  }
33
- }
53
+ if (currentCharacter === "{") {
54
+ const selector = currentSelector.trim();
55
+ if (selector !== "") {
56
+ newNestingLevel.push(selector);
57
+ }
58
+ // after an opening bracket a new selector might start e.g.:
59
+ // .class {
60
+ // .nested-class {
61
+ currentSelector = "";
62
+ } else if (currentCharacter === "}") {
63
+ newNestingLevel.pop();
64
+ // after a closing bracket a new selector might start e.g.:
65
+ // .class {
66
+ // color: red;
67
+ // }
68
+ // .other-class {
69
+ currentSelector = "";
70
+ } else if (currentCharacter === ";") {
71
+ // after a semi-colon a nested selector might start e.g.:
72
+ // .class {
73
+ // color: red;
74
+ // .nested-class {
75
+ currentSelector = "";
76
+ } else {
77
+ currentSelector += currentCharacter;
78
+ }
79
+ }
80
+
81
+ return {
82
+ empty: false,
83
+ unknownSelector: trimmedCssString[0] === "{",
84
+ insideCssValue: currentCharacter !== "{" && currentCharacter !== "}" && currentCharacter !== ";",
85
+ currentNestingScopes: newNestingLevel,
86
+ };
87
+ };
@@ -35,10 +35,13 @@ module.exports = function replaceTokensInQuasiExpressions(quasi, replacer, t) {
35
35
  }
36
36
  replaceExpressionAndMergeQuasis(quasi, i, replacement);
37
37
  i--;
38
- }
38
+ }
39
39
  // replace member expressions e.g. ${query.xs}
40
40
  // replace deeply nested member expressions e.g. ${query.xs.min}
41
- else if (t.isMemberExpression(expression) && t.isIdentifier(expression.object)) {
41
+ else if (
42
+ t.isMemberExpression(expression) &&
43
+ t.isIdentifier(expression.object)
44
+ ) {
42
45
  /** @type {any} */
43
46
  let replacement = replacer(expression.object.name);
44
47
  if (replacement === false) {
@@ -60,7 +63,7 @@ module.exports = function replaceTokensInQuasiExpressions(quasi, replacer, t) {
60
63
  i--;
61
64
  }
62
65
  }
63
- }
66
+ };
64
67
 
65
68
  /**
66
69
  * Replace tokens with predefined values
@@ -69,9 +72,16 @@ module.exports = function replaceTokensInQuasiExpressions(quasi, replacer, t) {
69
72
  * @param {unknown} replacement
70
73
  */
71
74
  function replaceExpressionAndMergeQuasis(quasi, expressionIndex, replacement) {
72
- const stringReplacement = typeof replacement === "string" ? replacement : replacement == null ? "" : JSON.stringify(replacement);
75
+ const stringReplacement =
76
+ typeof replacement === "string"
77
+ ? replacement
78
+ : replacement == null
79
+ ? ""
80
+ : JSON.stringify(replacement);
73
81
  quasi.expressions.splice(expressionIndex, 1);
74
- quasi.quasis[expressionIndex].value.raw += stringReplacement + quasi.quasis[expressionIndex + 1].value.raw;
75
- quasi.quasis[expressionIndex].value.cooked += stringReplacement + quasi.quasis[expressionIndex + 1].value.cooked;
82
+ quasi.quasis[expressionIndex].value.raw +=
83
+ stringReplacement + quasi.quasis[expressionIndex + 1].value.raw;
84
+ quasi.quasis[expressionIndex].value.cooked +=
85
+ stringReplacement + quasi.quasis[expressionIndex + 1].value.cooked;
76
86
  quasi.quasis.splice(expressionIndex + 1, 1);
77
- }
87
+ }
@@ -1,51 +1,58 @@
1
1
  /// @ts-check
2
2
  // from https://github.com/sindresorhus/strip-css-comments/tree/main
3
3
  /**
4
- *
5
- * @param {string} cssString
4
+ *
5
+ * @param {string} cssString
6
6
  */
7
7
  module.exports = function stripCssComments(cssString) {
8
- /** @type {string | false} */
9
- let isInsideString = false;
10
- let currentCharacter = '';
11
- let comment = '';
12
- let returnValue = '';
8
+ /** @type {string | false} */
9
+ let isInsideString = false;
10
+ let currentCharacter = "";
11
+ let comment = "";
12
+ let returnValue = "";
13
13
 
14
- for (let index = 0; index < cssString.length; index++) {
15
- currentCharacter = cssString[index];
14
+ for (let index = 0; index < cssString.length; index++) {
15
+ currentCharacter = cssString[index];
16
16
 
17
- if (cssString[index - 1] !== '\\' && (currentCharacter === '"' || currentCharacter === '\'')) {
18
- if (isInsideString === currentCharacter) {
19
- isInsideString = false;
20
- } else if (!isInsideString) {
21
- isInsideString = currentCharacter;
22
- }
23
- }
17
+ if (
18
+ cssString[index - 1] !== "\\" &&
19
+ (currentCharacter === '"' || currentCharacter === "'")
20
+ ) {
21
+ if (isInsideString === currentCharacter) {
22
+ isInsideString = false;
23
+ } else if (!isInsideString) {
24
+ isInsideString = currentCharacter;
25
+ }
26
+ }
24
27
 
25
- // Find beginning of `/*` type comment
26
- if (!isInsideString && currentCharacter === '/' && cssString[index + 1] === '*') {
27
- let index2 = index + 2;
28
+ // Find beginning of `/*` type comment
29
+ if (
30
+ !isInsideString &&
31
+ currentCharacter === "/" &&
32
+ cssString[index + 1] === "*"
33
+ ) {
34
+ let index2 = index + 2;
28
35
 
29
- // Iterate over comment
30
- for (; index2 < cssString.length; index2++) {
31
- // Find end of comment
32
- if (cssString[index2] === '*' && cssString[index2 + 1] === '/') {
33
- if (cssString[index2 + 2] === '\n') {
34
- index2++;
35
- } else if (cssString[index2 + 2] + cssString[index2 + 3] === '\r\n') {
36
- index2 += 2;
37
- }
38
- comment = '';
39
- break;
40
- }
41
- // Store comment text
42
- comment += cssString[index2];
43
- }
44
- // Resume iteration over CSS string from the end of the comment
45
- index = index2 + 1;
46
- continue;
47
- }
48
- returnValue += currentCharacter;
49
- }
50
- return returnValue;
51
- }
36
+ // Iterate over comment
37
+ for (; index2 < cssString.length; index2++) {
38
+ // Find end of comment
39
+ if (cssString[index2] === "*" && cssString[index2 + 1] === "/") {
40
+ if (cssString[index2 + 2] === "\n") {
41
+ index2++;
42
+ } else if (cssString[index2 + 2] + cssString[index2 + 3] === "\r\n") {
43
+ index2 += 2;
44
+ }
45
+ comment = "";
46
+ break;
47
+ }
48
+ // Store comment text
49
+ comment += cssString[index2];
50
+ }
51
+ // Resume iteration over CSS string from the end of the comment
52
+ index = index2 + 1;
53
+ continue;
54
+ }
55
+ returnValue += currentCharacter;
56
+ }
57
+ return returnValue;
58
+ };
@@ -1,6 +1,7 @@
1
1
  /// @ts-check
2
2
  const babel = require("@babel/core");
3
3
  const getYakImports = require("./lib/getYakImports.cjs");
4
+ const InvalidPositionError = require("./babel-yak-plugin.cjs").InvalidPositionError;
4
5
 
5
6
  /**
6
7
  * Loader for typescript files that use yak, it replaces the css template literal with a call to the 'styled' function
@@ -20,34 +21,49 @@ module.exports = async function tsloader(source) {
20
21
  const isYakFile = /\.yak\.(j|t)sx?$/.test(resourcePath.matches);
21
22
  // The user may import constants from a .yak file
22
23
  // e.g. import { primary } from './colors.yak'
23
- //
24
+ //
24
25
  // However .yak files inside .yak files are not be compiled
25
26
  // to avoid performance overhead
26
- const importedYakConstantNames = isYakFile ? [] : getYakImports(source).map(({ imports }) => imports.map(({ localName }) => localName)).flat(2);
27
- const replaces = Object.fromEntries(importedYakConstantNames.map((name) => [name, null]));
27
+ const importedYakConstantNames = isYakFile
28
+ ? []
29
+ : getYakImports(source)
30
+ .map(({ imports }) => imports.map(({ localName }) => localName))
31
+ .flat(2);
32
+ const replaces = Object.fromEntries(
33
+ importedYakConstantNames.map((name) => [name, null])
34
+ );
28
35
 
29
- // Compile the typescript file with babel - this will:
30
- // - inject the import to the css-module (with .yak.module.css extension)
31
- // - replace the css template literal with styles from the css-module
32
- const result = babel.transformSync(source, {
33
- filename: resourcePath,
34
- configFile: false,
35
- plugins: [
36
- [
37
- "@babel/plugin-syntax-typescript",
38
- { isTSX: this.resourcePath.endsWith(".tsx") },
36
+ /** @type {babel.BabelFileResult | null} */
37
+ let result = null;
38
+ try {
39
+ // Compile the typescript file with babel - this will:
40
+ // - inject the import to the css-module (with .yak.module.css extension)
41
+ // - replace the css template literal with styles from the css-module
42
+ result = babel.transformSync(source, {
43
+ filename: resourcePath,
44
+ configFile: false,
45
+ plugins: [
46
+ [
47
+ "@babel/plugin-syntax-typescript",
48
+ { isTSX: this.resourcePath.endsWith(".tsx") },
49
+ ],
50
+ [
51
+ require.resolve("./babel-yak-plugin.cjs"),
52
+ {
53
+ replaces,
54
+ rootContext,
55
+ },
56
+ ],
39
57
  ],
40
- [
41
- require.resolve("./babel-yak-plugin.cjs"),
42
- {
43
- replaces,
44
- rootContext,
45
- },
46
- ],
47
- ],
48
- });
58
+ });
59
+ } catch (error) {
60
+ if (error instanceof InvalidPositionError) {
61
+ return callback(new Error(error.message));
62
+ }
63
+ return callback(new Error("babel transform failed"));
64
+ }
49
65
  if (!result?.code) {
50
- throw new Error("babel transform failed");
66
+ return callback(new Error("babel transform failed"));
51
67
  }
52
68
  return callback(null, result.code, result.map);
53
69
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-yak",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "types": "./dist/",
6
6
  "exports": {