compileStyle.spec.ts 13 KB

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