ember-scoped-css 2.0.4 → 2.2.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.
@@ -22,7 +22,8 @@ it('should use a custom layer', function () {
22
22
  @layer utils {
23
23
  .foo_postfix { color: red; }
24
24
  }
25
- "`);
25
+ "
26
+ `);
26
27
  });
27
28
 
28
29
  it(`understands nth-of-type syntax`, function () {
@@ -40,7 +41,8 @@ it(`understands nth-of-type syntax`, function () {
40
41
 
41
42
  li.postfix:nth-of-type(odd) {}
42
43
  li.postfix:nth-of-type(even) {}
43
- "`);
44
+ "
45
+ `);
44
46
  });
45
47
 
46
48
  describe('@container', () => {
@@ -64,7 +66,8 @@ describe('@container', () => {
64
66
  font-size: 1.5em;
65
67
  }
66
68
  }
67
- "`);
69
+ "
70
+ `);
68
71
  });
69
72
 
70
73
  it('handles parameters', () => {
@@ -120,7 +123,8 @@ describe('@container', () => {
120
123
  color: white;
121
124
  }
122
125
  }
123
- "`);
126
+ "
127
+ `);
124
128
  });
125
129
  });
126
130
 
@@ -142,8 +146,9 @@ describe('@media', () => {
142
146
  @media (height >= 680px), screen and (orientation: portrait) {
143
147
  .foo_postfix { color: red; }
144
148
  }
145
- }
146
- "`);
149
+ }
150
+ "
151
+ `);
147
152
  });
148
153
  });
149
154
 
@@ -170,8 +175,9 @@ describe('@keyframe', () => {
170
175
  padding-top: 1rem;
171
176
  }
172
177
  }
173
- }
174
- "`);
178
+ }
179
+ "
180
+ `);
175
181
  });
176
182
 
177
183
  it(`references are also scoped`, function () {
@@ -200,7 +206,7 @@ describe('@keyframe', () => {
200
206
 
201
207
  expect(rewritten).toMatchInlineSnapshot(`
202
208
  "/* foo.css */
203
- @layer components {
209
+ @layer components {
204
210
 
205
211
  p.postfix {
206
212
  animation-duration: 3s;
@@ -218,8 +224,9 @@ describe('@keyframe', () => {
218
224
  scale: 100% 1;
219
225
  }
220
226
  }
221
- }
222
- "`);
227
+ }
228
+ "
229
+ `);
223
230
  });
224
231
 
225
232
  it('handles multiple references and keyframes', () => {
@@ -268,7 +275,7 @@ describe('@keyframe', () => {
268
275
 
269
276
  expect(rewritten).toMatchInlineSnapshot(`
270
277
  "/* foo.css */
271
- @layer components {
278
+ @layer components {
272
279
 
273
280
  p.postfix {
274
281
  animation-duration: 3s;
@@ -307,8 +314,9 @@ describe('@keyframe', () => {
307
314
  color: magenta;
308
315
  }
309
316
  }
310
- }
311
- "`);
317
+ }
318
+ "
319
+ `);
312
320
  });
313
321
 
314
322
  it('works in shorthand combo-declarations', () => {
@@ -333,7 +341,7 @@ describe('@keyframe', () => {
333
341
 
334
342
  expect(rewritten).toMatchInlineSnapshot(`
335
343
  "/* foo.css */
336
- @layer components {
344
+ @layer components {
337
345
 
338
346
  div.postfix {
339
347
  width: 100px;
@@ -347,8 +355,9 @@ describe('@keyframe', () => {
347
355
  from {top: 0px;}
348
356
  to {top: 200px;}
349
357
  }
350
- }
351
- "`);
358
+ }
359
+ "
360
+ `);
352
361
  });
353
362
  });
354
363
 
@@ -375,8 +384,9 @@ describe('@counter-style', () => {
375
384
  symbols: Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ;
376
385
  suffix: " ";
377
386
  }
378
- }
379
- "`);
387
+ }
388
+ "
389
+ `);
380
390
  });
381
391
 
382
392
  it('updates references', () => {
@@ -398,19 +408,20 @@ describe('@counter-style', () => {
398
408
 
399
409
  expect(rewritten).toMatchInlineSnapshot(`
400
410
  "/* foo.css */
401
- @layer components {
411
+ @layer components {
402
412
 
403
- @counter-style circled-alpha__postfix {
404
- system: fixed;
405
- symbols: Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ;
406
- suffix: " ";
407
- }
413
+ @counter-style circled-alpha__postfix {
414
+ system: fixed;
415
+ symbols: Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ;
416
+ suffix: " ";
417
+ }
408
418
 
409
- .items_postfix {
410
- list-style: circled-alpha__postfix;
411
- }
412
- }
413
- "`);
419
+ .items_postfix {
420
+ list-style: circled-alpha__postfix;
421
+ }
422
+ }
423
+ "
424
+ `);
414
425
  });
415
426
  });
416
427
 
@@ -430,15 +441,16 @@ describe('@position-try', () => {
430
441
 
431
442
  expect(rewritten).toMatchInlineSnapshot(`
432
443
  "/* foo.css */
433
- @layer components {
444
+ @layer components {
434
445
 
435
446
  @position-try --custom-left__postfix {
436
447
  position-area: left;
437
448
  width: 100px;
438
449
  margin-right: 10px;
439
450
  }
440
- }
441
- "`);
451
+ }
452
+ "
453
+ `);
442
454
  });
443
455
 
444
456
  it('updates references', () => {
@@ -461,20 +473,21 @@ describe('@position-try', () => {
461
473
 
462
474
  expect(rewritten).toMatchInlineSnapshot(`
463
475
  "/* foo.css */
464
- @layer components {
476
+ @layer components {
465
477
 
466
- @position-try --custom-left__postfix {
467
- position-area: left;
468
- width: 100px;
469
- margin-right: 10px;
470
- }
478
+ @position-try --custom-left__postfix {
479
+ position-area: left;
480
+ width: 100px;
481
+ margin-right: 10px;
482
+ }
471
483
 
472
- .infobox_postfix {
473
- position-try-fallbacks:
474
- --custom-left__postfix;
475
- }
476
- }
477
- "`);
484
+ .infobox_postfix {
485
+ position-try-fallbacks:
486
+ --custom-left__postfix;
487
+ }
488
+ }
489
+ "
490
+ `);
478
491
  });
479
492
  });
480
493
 
@@ -494,15 +507,16 @@ describe('@property', () => {
494
507
 
495
508
  expect(rewritten).toMatchInlineSnapshot(`
496
509
  "/* foo.css */
497
- @layer components {
510
+ @layer components {
498
511
 
499
512
  @property --item-size__postfix {
500
513
  syntax: "<percentage>";
501
514
  inherits: true;
502
515
  initial-value: 40%;
503
516
  }
504
- }
505
- "`);
517
+ }
518
+ "
519
+ `);
506
520
  });
507
521
 
508
522
  it('updates references', () => {
@@ -537,7 +551,7 @@ describe('@property', () => {
537
551
 
538
552
  expect(rewritten).toMatchInlineSnapshot(`
539
553
  "/* foo.css */
540
- @layer components {
554
+ @layer components {
541
555
 
542
556
  @property --item-size__postfix {
543
557
  syntax: "<percentage>";
@@ -561,8 +575,9 @@ describe('@property', () => {
561
575
  height: var(--item-size__postfix);
562
576
  background-color: var(--item-color);
563
577
  }
564
- }
565
- "`);
578
+ }
579
+ "
580
+ `);
566
581
  });
567
582
  });
568
583
 
@@ -584,6 +599,7 @@ describe('@supports', () => {
584
599
  @supports (transform-origin: 5% 5%) {}
585
600
  @supports selector(h2 > p) {}
586
601
  }
587
- "`);
602
+ "
603
+ `);
588
604
  });
589
605
  });
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import postcss from 'postcss';
3
+ import scssSyntax from 'postcss-scss';
3
4
  import parser from 'postcss-selector-parser';
4
5
 
5
6
  import { md5 } from '../path/md5.js';
@@ -39,17 +40,25 @@ export function getCSSInfo(cssPath) {
39
40
  * to see if we need to leave it alone or transform it
40
41
  *
41
42
  * @param {string} css the CSS's contents
43
+ * @param {string} [lang] optional language hint (e.g. 'scss', 'sass', 'less')
42
44
  * @return {{ classes: Set<string>, tags: Set<string>, css: string, id: string }}
43
45
  */
44
- export function getCSSContentInfo(css) {
46
+ export function getCSSContentInfo(css, lang) {
45
47
  const classes = new Set();
46
48
  const tags = new Set();
47
49
 
48
- const ast = postcss.parse(css);
50
+ const parseOptions =
51
+ lang === 'scss' || lang === 'sass' ? { syntax: scssSyntax } : {};
52
+
53
+ const ast = postcss.parse(css, parseOptions);
54
+
55
+ const isScss = lang === 'scss' || lang === 'sass';
49
56
 
50
57
  ast.walk((node) => {
51
58
  if (node.type === 'rule') {
52
- getClassesAndTags(node.selector, classes, tags);
59
+ const selector = isScss ? resolveNestedSassSelector(node) : node.selector;
60
+
61
+ getClassesAndTags(selector, classes, tags);
53
62
  }
54
63
  });
55
64
 
@@ -58,6 +67,36 @@ export function getCSSContentInfo(css) {
58
67
  return { classes, tags, css, id };
59
68
  }
60
69
 
70
+ /**
71
+ * Resolves a nested SCSS selector by substituting `&` with the fully-resolved
72
+ * parent selector, recursively. This converts e.g. `&--modifier` (child of
73
+ * `.block`) into `.block--modifier`, and handles arbitrary nesting depth so
74
+ * that `&--modifier` inside `&--modifier` inside `.block` yields
75
+ * `.block--modifier--modifier`.
76
+ *
77
+ * @param {import('postcss').Rule} node
78
+ * @return {string}
79
+ */
80
+ function resolveNestedSassSelector(node) {
81
+ const { selector } = node;
82
+
83
+ if (!selector.includes('&')) {
84
+ return selector;
85
+ }
86
+
87
+ const parent = node.parent;
88
+
89
+ if (!parent || parent.type !== 'rule') {
90
+ // No parent rule — `&` has nothing to substitute, return as-is
91
+ return selector;
92
+ }
93
+
94
+ // Recursively resolve the parent first, then substitute into this selector
95
+ const resolvedParent = resolveNestedSassSelector(parent);
96
+
97
+ return selector.replace(/&/g, resolvedParent);
98
+ }
99
+
61
100
  function getClassesAndTags(sel, classes, tags) {
62
101
  const transform = (sls) => {
63
102
  sls.walk((selector) => {
@@ -85,4 +124,69 @@ if (import.meta.vitest) {
85
124
  expect(tags.size).to.equal(1);
86
125
  expect([...tags]).to.have.members(['div']);
87
126
  });
127
+
128
+ it('should parse SCSS nesting syntax without crashing when lang=scss', function () {
129
+ const scss = `
130
+ $base-color: #c6538c;
131
+ $border-dark: rgba($base-color, 0.88);
132
+
133
+ .parent {
134
+ &:hover { color: $base-color; }
135
+ .child { border: 1px solid $border-dark; }
136
+ color: red;
137
+ }
138
+ `;
139
+ const { classes } = getCSSContentInfo(scss, 'scss');
140
+
141
+ expect([...classes]).toMatchInlineSnapshot(`
142
+ [
143
+ "parent",
144
+ "child",
145
+ ]
146
+ `);
147
+ });
148
+
149
+ it('should parse SCSS nesting syntax without crashing when lang=sass', function () {
150
+ const scss = `
151
+ $base-color: green;
152
+ .block {
153
+ &--modifier { color: $base-color; }
154
+ }
155
+ `;
156
+ const { classes } = getCSSContentInfo(scss, 'sass');
157
+
158
+ expect([...classes]).toMatchInlineSnapshot(`
159
+ [
160
+ "block",
161
+ "block--modifier",
162
+ ]
163
+ `);
164
+ });
165
+
166
+ it('should parse SCSS deeply nested BEM when lang=sass', function () {
167
+ const scss = `
168
+ $base-color: green;
169
+ .block {
170
+ &--modifier {
171
+ color: $base-color;
172
+ &--modifier {
173
+ color: $base-color;
174
+ &--modifier {
175
+ color: $base-color;
176
+ }
177
+ }
178
+ }
179
+ }
180
+ `;
181
+ const { classes } = getCSSContentInfo(scss, 'sass');
182
+
183
+ expect([...classes]).toMatchInlineSnapshot(`
184
+ [
185
+ "block",
186
+ "block--modifier",
187
+ "block--modifier--modifier",
188
+ "block--modifier--modifier--modifier",
189
+ ]
190
+ `);
191
+ });
88
192
  }
@@ -9,6 +9,7 @@ export const leadingSlashPath = {
9
9
  atEmbroider: path.join('/@embroider'),
10
10
  componentsDir: path.join('/components/'),
11
11
  templatesDir: path.join('/templates/'),
12
+ routesDir: path.join('/routes/'),
12
13
  testem: path.join('/testem'),
13
14
  src: path.join('/src/'),
14
15
  app: path.join('/app/'),
@@ -20,6 +20,13 @@ describe('isRelevantFile()', () => {
20
20
 
21
21
  expect(result).toBeTruthy();
22
22
  });
23
+
24
+ it('for /routes/', () => {
25
+ let file = path.join(paths.viteApp, 'app/routes/application.gts');
26
+ let result = isRelevantFile(file, { cwd: paths.viteApp });
27
+
28
+ expect(result).toBeTruthy();
29
+ });
23
30
  });
24
31
 
25
32
  describe('the file is not relevant', () => {
@@ -259,6 +259,7 @@ export function isRelevantFile(fileName, { additionalRoots, cwd }) {
259
259
  let roots = [
260
260
  leadingSlashPath.componentsDir,
261
261
  leadingSlashPath.templatesDir,
262
+ leadingSlashPath.routesDir,
262
263
  ...(additionalRoots || []),
263
264
  ];
264
265
 
@@ -19,9 +19,16 @@ export const request = {
19
19
  * @param {string} cssHash the hash of the CSS contents
20
20
  * @param {string} postfix the hash of the file that _includes_ the linked file
21
21
  * @param {string} cssContents the contents of the CSS file
22
+ * @param {string} [lang] optional preprocessor language (e.g. 'scss', 'sass', 'less')
22
23
  */
23
- create(cssHash, postfix, cssContents) {
24
- return `./${postfix}${SEP}${cssHash}.${KEY}?css=${encodeURIComponent(cssContents)}`;
24
+ create(cssHash, postfix, cssContents, lang) {
25
+ let url = `./${postfix}${SEP}${cssHash}.${KEY}?css=${encodeURIComponent(cssContents)}`;
26
+
27
+ if (lang) {
28
+ url += `&lang=${encodeURIComponent(lang)}`;
29
+ }
30
+
31
+ return url;
25
32
  },
26
33
  decode(request) {
27
34
  let [left, qps] = request.split('?');
@@ -37,6 +44,7 @@ export const request = {
37
44
  postfix,
38
45
  css: search.get('css'),
39
46
  from: search.get('from'),
47
+ lang: search.get('lang'),
40
48
  };
41
49
  },
42
50
  },