compileStyle.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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(`:deep(.foo) { color: red; .bar { color: red; } }`))
  144. .toMatchInlineSnapshot(`
  145. "[data-v-test] .foo { color: red;
  146. .bar { color: red;
  147. }
  148. }"
  149. `)
  150. })
  151. test('::v-slotted', () => {
  152. expect(compileScoped(`:slotted(.foo) { color: red; }`))
  153. .toMatchInlineSnapshot(`
  154. ".foo[data-v-test-s] { color: red;
  155. }"
  156. `)
  157. expect(compileScoped(`::v-slotted(.foo) { color: red; }`))
  158. .toMatchInlineSnapshot(`
  159. ".foo[data-v-test-s] { color: red;
  160. }"
  161. `)
  162. expect(compileScoped(`::v-slotted(.foo .bar) { color: red; }`))
  163. .toMatchInlineSnapshot(`
  164. ".foo .bar[data-v-test-s] { color: red;
  165. }"
  166. `)
  167. expect(compileScoped(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
  168. .toMatchInlineSnapshot(`
  169. ".baz .qux .foo .bar[data-v-test-s] { color: red;
  170. }"
  171. `)
  172. })
  173. test('::v-global', () => {
  174. expect(compileScoped(`:global(.foo) { color: red; }`))
  175. .toMatchInlineSnapshot(`
  176. ".foo { color: red;
  177. }"
  178. `)
  179. expect(compileScoped(`::v-global(.foo) { color: red; }`))
  180. .toMatchInlineSnapshot(`
  181. ".foo { color: red;
  182. }"
  183. `)
  184. expect(compileScoped(`::v-global(.foo .bar) { color: red; }`))
  185. .toMatchInlineSnapshot(`
  186. ".foo .bar { color: red;
  187. }"
  188. `)
  189. // global ignores anything before it
  190. expect(compileScoped(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
  191. .toMatchInlineSnapshot(`
  192. ".foo .bar { color: red;
  193. }"
  194. `)
  195. })
  196. test(':is() and :where() with multiple selectors', () => {
  197. expect(compileScoped(`:is(.foo) { color: red; }`)).toMatchInlineSnapshot(`
  198. ":is(.foo[data-v-test]) { color: red;
  199. }"
  200. `)
  201. expect(compileScoped(`:where(.foo, .bar) { color: red; }`))
  202. .toMatchInlineSnapshot(`
  203. ":where(.foo[data-v-test], .bar[data-v-test]) { color: red;
  204. }"
  205. `)
  206. expect(compileScoped(`:is(.foo, .bar) div { color: red; }`))
  207. .toMatchInlineSnapshot(`
  208. ":is(.foo, .bar) div[data-v-test] { color: red;
  209. }"
  210. `)
  211. })
  212. // #10511
  213. test(':is() and :where() in compound selectors', () => {
  214. expect(
  215. compileScoped(`.div { color: red; } .div:where(:hover) { color: blue; }`),
  216. ).toMatchInlineSnapshot(`
  217. ".div[data-v-test] { color: red;
  218. }
  219. .div[data-v-test]:where(:hover) { color: blue;
  220. }"
  221. `)
  222. expect(
  223. compileScoped(`.div { color: red; } .div:is(:hover) { color: blue; }`),
  224. ).toMatchInlineSnapshot(`
  225. ".div[data-v-test] { color: red;
  226. }
  227. .div[data-v-test]:is(:hover) { color: blue;
  228. }"
  229. `)
  230. expect(
  231. compileScoped(
  232. `.div { color: red; } .div:where(.foo:hover) { color: blue; }`,
  233. ),
  234. ).toMatchInlineSnapshot(`
  235. ".div[data-v-test] { color: red;
  236. }
  237. .div[data-v-test]:where(.foo:hover) { color: blue;
  238. }"
  239. `)
  240. expect(
  241. compileScoped(
  242. `.div { color: red; } .div:is(.foo:hover) { color: blue; }`,
  243. ),
  244. ).toMatchInlineSnapshot(`
  245. ".div[data-v-test] { color: red;
  246. }
  247. .div[data-v-test]:is(.foo:hover) { color: blue;
  248. }"
  249. `)
  250. })
  251. test('media query', () => {
  252. expect(compileScoped(`@media print { .foo { color: red }}`))
  253. .toMatchInlineSnapshot(`
  254. "@media print {
  255. .foo[data-v-test] { color: red
  256. }}"
  257. `)
  258. })
  259. test('supports query', () => {
  260. expect(compileScoped(`@supports(display: grid) { .foo { display: grid }}`))
  261. .toMatchInlineSnapshot(`
  262. "@supports(display: grid) {
  263. .foo[data-v-test] { display: grid
  264. }}"
  265. `)
  266. })
  267. test('scoped keyframes', () => {
  268. const style = compileScoped(
  269. `
  270. .anim {
  271. animation: color 5s infinite, other 5s;
  272. }
  273. .anim-2 {
  274. animation-name: color;
  275. animation-duration: 5s;
  276. }
  277. .anim-3 {
  278. animation: 5s color infinite, 5s other;
  279. }
  280. .anim-multiple {
  281. animation: color 5s infinite, opacity 2s;
  282. }
  283. .anim-multiple-2 {
  284. animation-name: color, opacity;
  285. animation-duration: 5s, 2s;
  286. }
  287. @keyframes color {
  288. from { color: red; }
  289. to { color: green; }
  290. }
  291. @-webkit-keyframes color {
  292. from { color: red; }
  293. to { color: green; }
  294. }
  295. @keyframes opacity {
  296. from { opacity: 0; }
  297. to { opacity: 1; }
  298. }
  299. @-webkit-keyframes opacity {
  300. from { opacity: 0; }
  301. to { opacity: 1; }
  302. }
  303. `,
  304. { id: 'data-v-test' },
  305. )
  306. expect(style).toContain(
  307. `.anim[data-v-test] {\n animation: color-test 5s infinite, other 5s;`,
  308. )
  309. expect(style).toContain(
  310. `.anim-2[data-v-test] {\n animation-name: color-test`,
  311. )
  312. expect(style).toContain(
  313. `.anim-3[data-v-test] {\n animation: 5s color-test infinite, 5s other;`,
  314. )
  315. expect(style).toContain(`@keyframes color-test {`)
  316. expect(style).toContain(`@-webkit-keyframes color-test {`)
  317. expect(style).toContain(
  318. `.anim-multiple[data-v-test] {\n animation: color-test 5s infinite,opacity-test 2s;`,
  319. )
  320. expect(style).toContain(
  321. `.anim-multiple-2[data-v-test] {\n animation-name: color-test,opacity-test;`,
  322. )
  323. expect(style).toContain(`@keyframes opacity-test {\nfrom { opacity: 0;`)
  324. expect(style).toContain(
  325. `@-webkit-keyframes opacity-test {\nfrom { opacity: 0;`,
  326. )
  327. })
  328. // vue-loader/#1370
  329. test('spaces after selector', () => {
  330. expect(compileScoped(`.foo , .bar { color: red; }`)).toMatchInlineSnapshot(`
  331. ".foo[data-v-test], .bar[data-v-test] { color: red;
  332. }"
  333. `)
  334. })
  335. describe('deprecated syntax', () => {
  336. test('::v-deep as combinator', () => {
  337. expect(compileScoped(`::v-deep .foo { color: red; }`))
  338. .toMatchInlineSnapshot(`
  339. "[data-v-test] .foo { color: red;
  340. }"
  341. `)
  342. expect(compileScoped(`.bar ::v-deep .foo { color: red; }`))
  343. .toMatchInlineSnapshot(`
  344. ".bar[data-v-test] .foo { color: red;
  345. }"
  346. `)
  347. expect(
  348. `::v-deep usage as a combinator has been deprecated.`,
  349. ).toHaveBeenWarned()
  350. })
  351. test('>>> (deprecated syntax)', () => {
  352. const code = compileScoped(`>>> .foo { color: red; }`)
  353. expect(code).toMatchInlineSnapshot(`
  354. "[data-v-test] .foo { color: red;
  355. }"
  356. `)
  357. expect(
  358. `the >>> and /deep/ combinators have been deprecated.`,
  359. ).toHaveBeenWarned()
  360. })
  361. test('/deep/ (deprecated syntax)', () => {
  362. const code = compileScoped(`/deep/ .foo { color: red; }`)
  363. expect(code).toMatchInlineSnapshot(`
  364. "[data-v-test] .foo { color: red;
  365. }"
  366. `)
  367. expect(
  368. `the >>> and /deep/ combinators have been deprecated.`,
  369. ).toHaveBeenWarned()
  370. })
  371. })
  372. })
  373. describe('SFC CSS modules', () => {
  374. test('should include resulting classes object in result', async () => {
  375. const result = await compileStyleAsync({
  376. source: `.red { color: red }\n.green { color: green }\n:global(.blue) { color: blue }`,
  377. filename: `test.css`,
  378. id: 'test',
  379. modules: true,
  380. })
  381. expect(result.modules).toBeDefined()
  382. expect(result.modules!.red).toMatch('_red_')
  383. expect(result.modules!.green).toMatch('_green_')
  384. expect(result.modules!.blue).toBeUndefined()
  385. })
  386. test('postcss-modules options', async () => {
  387. const result = await compileStyleAsync({
  388. source: `:local(.foo-bar) { color: red }\n.baz-qux { color: green }`,
  389. filename: `test.css`,
  390. id: 'test',
  391. modules: true,
  392. modulesOptions: {
  393. scopeBehaviour: 'global',
  394. generateScopedName: `[name]__[local]__[hash:base64:5]`,
  395. localsConvention: 'camelCaseOnly',
  396. },
  397. })
  398. expect(result.modules).toBeDefined()
  399. expect(result.modules!.fooBar).toMatch('__foo-bar__')
  400. expect(result.modules!.bazQux).toBeUndefined()
  401. })
  402. })
  403. describe('SFC style preprocessors', () => {
  404. test('scss @import', () => {
  405. const res = compileStyle({
  406. source: `
  407. @import "./import.scss";
  408. `,
  409. filename: path.resolve(__dirname, './fixture/test.scss'),
  410. id: '',
  411. preprocessLang: 'scss',
  412. })
  413. expect([...res.dependencies]).toStrictEqual([
  414. path.join(__dirname, './fixture/import.scss'),
  415. ])
  416. })
  417. test('scss respect user-defined string options.additionalData', () => {
  418. const res = compileStyle({
  419. preprocessOptions: {
  420. additionalData: `
  421. @mixin square($size) {
  422. width: $size;
  423. height: $size;
  424. }`,
  425. },
  426. source: `
  427. .square {
  428. @include square(100px);
  429. }
  430. `,
  431. filename: path.resolve(__dirname, './fixture/test.scss'),
  432. id: '',
  433. preprocessLang: 'scss',
  434. })
  435. expect(res.errors.length).toBe(0)
  436. })
  437. test('scss respect user-defined function options.additionalData', () => {
  438. const source = `
  439. .square {
  440. @include square(100px);
  441. }
  442. `
  443. const filename = path.resolve(__dirname, './fixture/test.scss')
  444. const res = compileStyle({
  445. preprocessOptions: {
  446. additionalData: (s: string, f: string) => {
  447. expect(s).toBe(source)
  448. expect(f).toBe(filename)
  449. return `
  450. @mixin square($size) {
  451. width: $size;
  452. height: $size;
  453. }`
  454. },
  455. },
  456. source,
  457. filename,
  458. id: '',
  459. preprocessLang: 'scss',
  460. })
  461. expect(res.errors.length).toBe(0)
  462. })
  463. test('should mount scope on correct selector when have universal selector', () => {
  464. expect(compileScoped(`* { color: red; }`)).toMatchInlineSnapshot(`
  465. "[data-v-test] { color: red;
  466. }"
  467. `)
  468. expect(compileScoped('* .foo { color: red; }')).toMatchInlineSnapshot(`
  469. ".foo[data-v-test] { color: red;
  470. }"
  471. `)
  472. expect(compileScoped(`*.foo { color: red; }`)).toMatchInlineSnapshot(`
  473. ".foo[data-v-test] { color: red;
  474. }"
  475. `)
  476. expect(compileScoped(`.foo * { color: red; }`)).toMatchInlineSnapshot(`
  477. ".foo[data-v-test] * { color: red;
  478. }"
  479. `)
  480. })
  481. })