compileStyle.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import {
  2. type SFCStyleCompileOptions,
  3. compileStyle,
  4. compileStyleAsync,
  5. } from '../src/compileStyle'
  6. import path from 'node:path'
  7. export function compileScoped(
  8. source: string,
  9. options?: Partial<SFCStyleCompileOptions>,
  10. ): string {
  11. const res = compileStyle({
  12. source,
  13. filename: 'test.css',
  14. id: 'data-v-test',
  15. scoped: true,
  16. ...options,
  17. })
  18. if (res.errors.length) {
  19. res.errors.forEach(err => {
  20. console.error(err)
  21. })
  22. expect(res.errors.length).toBe(0)
  23. }
  24. return res.code
  25. }
  26. describe('SFC scoped CSS', () => {
  27. test('simple selectors', () => {
  28. expect(compileScoped(`h1 { color: red; }`)).toMatch(
  29. `h1[data-v-test] { color: red;`,
  30. )
  31. expect(compileScoped(`.foo { color: red; }`)).toMatch(
  32. `.foo[data-v-test] { color: red;`,
  33. )
  34. })
  35. test('descendent selector', () => {
  36. expect(compileScoped(`h1 .foo { color: red; }`)).toMatch(
  37. `h1 .foo[data-v-test] { color: red;`,
  38. )
  39. // #13387
  40. expect(
  41. compileScoped(`main {
  42. width: 100%;
  43. > * {
  44. max-width: 200px;
  45. }
  46. }`),
  47. ).toMatchInlineSnapshot(`
  48. "main {
  49. &[data-v-test] {
  50. width: 100%;
  51. }
  52. > *[data-v-test] {
  53. max-width: 200px;
  54. }
  55. }"`)
  56. })
  57. test('nesting selector', () => {
  58. expect(compileScoped(`h1 { color: red; .foo { color: red; } }`)).toMatch(
  59. `h1 {\n&[data-v-test] { color: red;\n}\n.foo[data-v-test] { color: red;`,
  60. )
  61. })
  62. test('nesting selector with atrule and comment', () => {
  63. expect(
  64. compileScoped(
  65. `h1 {
  66. color: red;
  67. /*background-color: pink;*/
  68. @media only screen and (max-width: 800px) {
  69. background-color: green;
  70. .bar { color: white }
  71. }
  72. .foo { color: red; }
  73. }`,
  74. ),
  75. ).toMatch(
  76. `h1 {
  77. &[data-v-test] {
  78. color: red
  79. /*background-color: pink;*/
  80. }
  81. @media only screen and (max-width: 800px) {
  82. &[data-v-test] {
  83. background-color: green
  84. }
  85. .bar[data-v-test] { color: white
  86. }
  87. }
  88. .foo[data-v-test] { color: red;
  89. }
  90. }`,
  91. )
  92. })
  93. test('multiple selectors', () => {
  94. expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
  95. `h1 .foo[data-v-test], .bar[data-v-test], .baz[data-v-test] { color: red;`,
  96. )
  97. })
  98. test('pseudo class', () => {
  99. expect(compileScoped(`.foo:after { color: red; }`)).toMatch(
  100. `.foo[data-v-test]:after { color: red;`,
  101. )
  102. })
  103. test('pseudo element', () => {
  104. expect(compileScoped(`::selection { display: none; }`)).toMatch(
  105. '[data-v-test]::selection {',
  106. )
  107. })
  108. test('spaces before pseudo element', () => {
  109. const code = compileScoped(`.abc, ::selection { color: red; }`)
  110. expect(code).toMatch('.abc[data-v-test],')
  111. expect(code).toMatch('[data-v-test]::selection {')
  112. })
  113. test('::v-deep', () => {
  114. expect(compileScoped(`:deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
  115. "[data-v-test] .foo { color: red;
  116. }"
  117. `)
  118. expect(compileScoped(`::v-deep(.foo) { color: red; }`))
  119. .toMatchInlineSnapshot(`
  120. "[data-v-test] .foo { color: red;
  121. }"
  122. `)
  123. expect(compileScoped(`::v-deep(.foo .bar) { color: red; }`))
  124. .toMatchInlineSnapshot(`
  125. "[data-v-test] .foo .bar { color: red;
  126. }"
  127. `)
  128. expect(compileScoped(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
  129. .toMatchInlineSnapshot(`
  130. ".baz .qux[data-v-test] .foo .bar { color: red;
  131. }"
  132. `)
  133. expect(compileScoped(`:is(.foo :deep(.bar)) { color: red; }`))
  134. .toMatchInlineSnapshot(`
  135. ":is(.foo[data-v-test] .bar) { color: red;
  136. }"
  137. `)
  138. expect(compileScoped(`:where(.foo :deep(.bar)) { color: red; }`))
  139. .toMatchInlineSnapshot(`
  140. ":where(.foo[data-v-test] .bar) { color: red;
  141. }"
  142. `)
  143. expect(compileScoped(`:is(:deep(.foo)) .bar { color: red; }`))
  144. .toMatchInlineSnapshot(`
  145. ":is([data-v-test] .foo) .bar { color: red;
  146. }"
  147. `)
  148. expect(compileScoped(`:where(:deep(.foo)) .bar { color: red; }`))
  149. .toMatchInlineSnapshot(`
  150. ":where([data-v-test] .foo) .bar { color: red;
  151. }"
  152. `)
  153. expect(compileScoped(`:is(:deep(.foo), .bar) .baz { color: red; }`))
  154. .toMatchInlineSnapshot(`
  155. ":is([data-v-test] .foo) .baz, :is(.bar) .baz[data-v-test] { color: red;
  156. }"
  157. `)
  158. expect(compileScoped(`:where(:deep(.foo), .bar) .baz { color: red; }`))
  159. .toMatchInlineSnapshot(`
  160. ":where([data-v-test] .foo) .baz, :where(.bar) .baz[data-v-test] { color: red;
  161. }"
  162. `)
  163. expect(compileScoped(`:not(:deep(.foo)) .bar { color: red; }`))
  164. .toMatchInlineSnapshot(`
  165. ":not([data-v-test] .foo) .bar { color: red;
  166. }"
  167. `)
  168. expect(compileScoped(`:has(:deep(.foo)) .bar { color: red; }`))
  169. .toMatchInlineSnapshot(`
  170. ":has([data-v-test] .foo) .bar { color: red;
  171. }"
  172. `)
  173. expect(compileScoped(`:has(:deep(.foo), .bar) .baz { color: red; }`))
  174. .toMatchInlineSnapshot(`
  175. ":has([data-v-test] .foo) .baz, :has(.bar) .baz[data-v-test] { color: red;
  176. }"
  177. `)
  178. expect(compileScoped(`:deep(.foo) { color: red; .bar { color: red; } }`))
  179. .toMatchInlineSnapshot(`
  180. "[data-v-test] .foo { color: red;
  181. .bar { color: red;
  182. }
  183. }"
  184. `)
  185. })
  186. test('::v-slotted', () => {
  187. expect(compileScoped(`:slotted(.foo) { color: red; }`))
  188. .toMatchInlineSnapshot(`
  189. ".foo[data-v-test-s] { color: red;
  190. }"
  191. `)
  192. expect(compileScoped(`::v-slotted(.foo) { color: red; }`))
  193. .toMatchInlineSnapshot(`
  194. ".foo[data-v-test-s] { color: red;
  195. }"
  196. `)
  197. expect(compileScoped(`::v-slotted(.foo .bar) { color: red; }`))
  198. .toMatchInlineSnapshot(`
  199. ".foo .bar[data-v-test-s] { color: red;
  200. }"
  201. `)
  202. expect(compileScoped(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
  203. .toMatchInlineSnapshot(`
  204. ".baz .qux .foo .bar[data-v-test-s] { color: red;
  205. }"
  206. `)
  207. })
  208. test('::v-global', () => {
  209. expect(compileScoped(`:global(.foo) { color: red; }`))
  210. .toMatchInlineSnapshot(`
  211. ".foo { color: red;
  212. }"
  213. `)
  214. expect(compileScoped(`::v-global(.foo) { color: red; }`))
  215. .toMatchInlineSnapshot(`
  216. ".foo { color: red;
  217. }"
  218. `)
  219. expect(compileScoped(`::v-global(.foo .bar) { color: red; }`))
  220. .toMatchInlineSnapshot(`
  221. ".foo .bar { color: red;
  222. }"
  223. `)
  224. // global ignores anything before it
  225. expect(compileScoped(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
  226. .toMatchInlineSnapshot(`
  227. ".foo .bar { color: red;
  228. }"
  229. `)
  230. })
  231. test(':is() and :where() with multiple selectors', () => {
  232. expect(compileScoped(`:is(.foo) { color: red; }`)).toMatchInlineSnapshot(`
  233. ":is(.foo[data-v-test]) { color: red;
  234. }"
  235. `)
  236. expect(compileScoped(`:where(.foo, .bar) { color: red; }`))
  237. .toMatchInlineSnapshot(`
  238. ":where(.foo[data-v-test], .bar[data-v-test]) { color: red;
  239. }"
  240. `)
  241. expect(compileScoped(`:is(.foo, .bar) div { color: red; }`))
  242. .toMatchInlineSnapshot(`
  243. ":is(.foo, .bar) div[data-v-test] { color: red;
  244. }"
  245. `)
  246. })
  247. // #10511
  248. test(':is() and :where() in compound selectors', () => {
  249. expect(
  250. compileScoped(`.div { color: red; } .div:where(:hover) { color: blue; }`),
  251. ).toMatchInlineSnapshot(`
  252. ".div[data-v-test] { color: red;
  253. }
  254. .div[data-v-test]:where(:hover) { color: blue;
  255. }"
  256. `)
  257. expect(
  258. compileScoped(`.div { color: red; } .div:is(:hover) { color: blue; }`),
  259. ).toMatchInlineSnapshot(`
  260. ".div[data-v-test] { color: red;
  261. }
  262. .div[data-v-test]:is(:hover) { color: blue;
  263. }"
  264. `)
  265. expect(
  266. compileScoped(
  267. `.div { color: red; } .div:where(.foo:hover) { color: blue; }`,
  268. ),
  269. ).toMatchInlineSnapshot(`
  270. ".div[data-v-test] { color: red;
  271. }
  272. .div[data-v-test]:where(.foo:hover) { color: blue;
  273. }"
  274. `)
  275. expect(
  276. compileScoped(
  277. `.div { color: red; } .div:is(.foo:hover) { color: blue; }`,
  278. ),
  279. ).toMatchInlineSnapshot(`
  280. ".div[data-v-test] { color: red;
  281. }
  282. .div[data-v-test]:is(.foo:hover) { color: blue;
  283. }"
  284. `)
  285. })
  286. test('media query', () => {
  287. expect(compileScoped(`@media print { .foo { color: red }}`))
  288. .toMatchInlineSnapshot(`
  289. "@media print {
  290. .foo[data-v-test] { color: red
  291. }}"
  292. `)
  293. })
  294. test('supports query', () => {
  295. expect(compileScoped(`@supports(display: grid) { .foo { display: grid }}`))
  296. .toMatchInlineSnapshot(`
  297. "@supports(display: grid) {
  298. .foo[data-v-test] { display: grid
  299. }}"
  300. `)
  301. })
  302. test('scoped keyframes', () => {
  303. const style = compileScoped(
  304. `
  305. .anim {
  306. animation: color 5s infinite, other 5s;
  307. }
  308. .anim-2 {
  309. animation-name: color;
  310. animation-duration: 5s;
  311. }
  312. .anim-3 {
  313. animation: 5s color infinite, 5s other;
  314. }
  315. .anim-multiple {
  316. animation: color 5s infinite, opacity 2s;
  317. }
  318. .anim-multiple-2 {
  319. animation-name: color, opacity;
  320. animation-duration: 5s, 2s;
  321. }
  322. @keyframes color {
  323. from { color: red; }
  324. to { color: green; }
  325. }
  326. @-webkit-keyframes color {
  327. from { color: red; }
  328. to { color: green; }
  329. }
  330. @keyframes opacity {
  331. from { opacity: 0; }
  332. to { opacity: 1; }
  333. }
  334. @-webkit-keyframes opacity {
  335. from { opacity: 0; }
  336. to { opacity: 1; }
  337. }
  338. `,
  339. { id: 'data-v-test' },
  340. )
  341. expect(style).toContain(
  342. `.anim[data-v-test] {\n animation: color-test 5s infinite, other 5s;`,
  343. )
  344. expect(style).toContain(
  345. `.anim-2[data-v-test] {\n animation-name: color-test`,
  346. )
  347. expect(style).toContain(
  348. `.anim-3[data-v-test] {\n animation: 5s color-test infinite, 5s other;`,
  349. )
  350. expect(style).toContain(`@keyframes color-test {`)
  351. expect(style).toContain(`@-webkit-keyframes color-test {`)
  352. expect(style).toContain(
  353. `.anim-multiple[data-v-test] {\n animation: color-test 5s infinite,opacity-test 2s;`,
  354. )
  355. expect(style).toContain(
  356. `.anim-multiple-2[data-v-test] {\n animation-name: color-test,opacity-test;`,
  357. )
  358. expect(style).toContain(`@keyframes opacity-test {\nfrom { opacity: 0;`)
  359. expect(style).toContain(
  360. `@-webkit-keyframes opacity-test {\nfrom { opacity: 0;`,
  361. )
  362. })
  363. // vue-loader/#1370
  364. test('spaces after selector', () => {
  365. expect(compileScoped(`.foo , .bar { color: red; }`)).toMatchInlineSnapshot(`
  366. ".foo[data-v-test], .bar[data-v-test] { color: red;
  367. }"
  368. `)
  369. })
  370. describe('deprecated syntax', () => {
  371. test('::v-deep as combinator', () => {
  372. expect(compileScoped(`::v-deep .foo { color: red; }`))
  373. .toMatchInlineSnapshot(`
  374. "[data-v-test] .foo { color: red;
  375. }"
  376. `)
  377. expect(compileScoped(`.bar ::v-deep .foo { color: red; }`))
  378. .toMatchInlineSnapshot(`
  379. ".bar[data-v-test] .foo { color: red;
  380. }"
  381. `)
  382. expect(
  383. `::v-deep usage as a combinator has been deprecated.`,
  384. ).toHaveBeenWarned()
  385. })
  386. test('>>> (deprecated syntax)', () => {
  387. const code = compileScoped(`>>> .foo { color: red; }`)
  388. expect(code).toMatchInlineSnapshot(`
  389. "[data-v-test] .foo { color: red;
  390. }"
  391. `)
  392. expect(
  393. `the >>> and /deep/ combinators have been deprecated.`,
  394. ).toHaveBeenWarned()
  395. })
  396. test('/deep/ (deprecated syntax)', () => {
  397. const code = compileScoped(`/deep/ .foo { color: red; }`)
  398. expect(code).toMatchInlineSnapshot(`
  399. "[data-v-test] .foo { color: red;
  400. }"
  401. `)
  402. expect(
  403. `the >>> and /deep/ combinators have been deprecated.`,
  404. ).toHaveBeenWarned()
  405. })
  406. })
  407. })
  408. describe('SFC CSS modules', () => {
  409. test('should include resulting classes object in result', async () => {
  410. const result = await compileStyleAsync({
  411. source: `.red { color: red }\n.green { color: green }\n:global(.blue) { color: blue }`,
  412. filename: `test.css`,
  413. id: 'test',
  414. modules: true,
  415. })
  416. expect(result.modules).toBeDefined()
  417. expect(result.modules!.red).toMatch('_red_')
  418. expect(result.modules!.green).toMatch('_green_')
  419. expect(result.modules!.blue).toBeUndefined()
  420. })
  421. test('postcss-modules options', async () => {
  422. const result = await compileStyleAsync({
  423. source: `:local(.foo-bar) { color: red }\n.baz-qux { color: green }`,
  424. filename: `test.css`,
  425. id: 'test',
  426. modules: true,
  427. modulesOptions: {
  428. scopeBehaviour: 'global',
  429. generateScopedName: `[name]__[local]__[hash:base64:5]`,
  430. localsConvention: 'camelCaseOnly',
  431. },
  432. })
  433. expect(result.modules).toBeDefined()
  434. expect(result.modules!.fooBar).toMatch('__foo-bar__')
  435. expect(result.modules!.bazQux).toBeUndefined()
  436. })
  437. })
  438. describe('SFC style preprocessors', () => {
  439. test('scss @import', () => {
  440. const res = compileStyle({
  441. source: `
  442. @import "./import.scss";
  443. `,
  444. filename: path.resolve(__dirname, './fixture/test.scss'),
  445. id: '',
  446. preprocessLang: 'scss',
  447. })
  448. expect([...res.dependencies]).toStrictEqual([
  449. path.join(__dirname, './fixture/import.scss'),
  450. ])
  451. })
  452. test('scss respect user-defined string options.additionalData', () => {
  453. const res = compileStyle({
  454. preprocessOptions: {
  455. additionalData: `
  456. @mixin square($size) {
  457. width: $size;
  458. height: $size;
  459. }`,
  460. },
  461. source: `
  462. .square {
  463. @include square(100px);
  464. }
  465. `,
  466. filename: path.resolve(__dirname, './fixture/test.scss'),
  467. id: '',
  468. preprocessLang: 'scss',
  469. })
  470. expect(res.errors.length).toBe(0)
  471. })
  472. test('scss respect user-defined function options.additionalData', () => {
  473. const source = `
  474. .square {
  475. @include square(100px);
  476. }
  477. `
  478. const filename = path.resolve(__dirname, './fixture/test.scss')
  479. const res = compileStyle({
  480. preprocessOptions: {
  481. additionalData: (s: string, f: string) => {
  482. expect(s).toBe(source)
  483. expect(f).toBe(filename)
  484. return `
  485. @mixin square($size) {
  486. width: $size;
  487. height: $size;
  488. }`
  489. },
  490. },
  491. source,
  492. filename,
  493. id: '',
  494. preprocessLang: 'scss',
  495. })
  496. expect(res.errors.length).toBe(0)
  497. })
  498. test('should mount scope on correct selector when have universal selector', () => {
  499. expect(compileScoped(`* { color: red; }`)).toMatchInlineSnapshot(`
  500. "[data-v-test] { color: red;
  501. }"
  502. `)
  503. expect(compileScoped('* .foo { color: red; }')).toMatchInlineSnapshot(`
  504. ".foo[data-v-test] { color: red;
  505. }"
  506. `)
  507. expect(compileScoped(`*.foo { color: red; }`)).toMatchInlineSnapshot(`
  508. ".foo[data-v-test] { color: red;
  509. }"
  510. `)
  511. expect(compileScoped(`.foo * { color: red; }`)).toMatchInlineSnapshot(`
  512. ".foo[data-v-test] * { color: red;
  513. }"
  514. `)
  515. })
  516. })