compileStyle.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import postcss, {
  2. ProcessOptions,
  3. Result,
  4. SourceMap,
  5. Message,
  6. LazyResult
  7. } from 'postcss'
  8. import trimPlugin from './stylePluginTrim'
  9. import scopedPlugin from './stylePluginScoped'
  10. import {
  11. processors,
  12. StylePreprocessor,
  13. StylePreprocessorResults,
  14. PreprocessLang
  15. } from './stylePreprocessors'
  16. import { RawSourceMap } from 'source-map'
  17. import { cssVarsPlugin } from './cssVars'
  18. export interface SFCStyleCompileOptions {
  19. source: string
  20. filename: string
  21. id: string
  22. scoped?: boolean
  23. trim?: boolean
  24. isProd?: boolean
  25. inMap?: RawSourceMap
  26. preprocessLang?: PreprocessLang
  27. preprocessOptions?: any
  28. preprocessCustomRequire?: (id: string) => any
  29. postcssOptions?: any
  30. postcssPlugins?: any[]
  31. /**
  32. * @deprecated use `inMap` instead.
  33. */
  34. map?: RawSourceMap
  35. }
  36. /**
  37. * Aligns with postcss-modules
  38. * https://github.com/css-modules/postcss-modules
  39. */
  40. export interface CSSModulesOptions {
  41. scopeBehaviour?: 'global' | 'local'
  42. generateScopedName?:
  43. | string
  44. | ((name: string, filename: string, css: string) => string)
  45. hashPrefix?: string
  46. localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
  47. exportGlobals?: boolean
  48. globalModulePaths?: string[]
  49. }
  50. export interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
  51. isAsync?: boolean
  52. // css modules support, note this requires async so that we can get the
  53. // resulting json
  54. modules?: boolean
  55. modulesOptions?: CSSModulesOptions
  56. }
  57. export interface SFCStyleCompileResults {
  58. code: string
  59. map: RawSourceMap | undefined
  60. rawResult: Result | LazyResult | undefined
  61. errors: Error[]
  62. modules?: Record<string, string>
  63. dependencies: Set<string>
  64. }
  65. export function compileStyle(
  66. options: SFCStyleCompileOptions
  67. ): SFCStyleCompileResults {
  68. return doCompileStyle({
  69. ...options,
  70. isAsync: false
  71. }) as SFCStyleCompileResults
  72. }
  73. export function compileStyleAsync(
  74. options: SFCAsyncStyleCompileOptions
  75. ): Promise<SFCStyleCompileResults> {
  76. return doCompileStyle({
  77. ...options,
  78. isAsync: true
  79. }) as Promise<SFCStyleCompileResults>
  80. }
  81. export function doCompileStyle(
  82. options: SFCAsyncStyleCompileOptions
  83. ): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
  84. const {
  85. filename,
  86. id,
  87. scoped = false,
  88. trim = true,
  89. isProd = false,
  90. modules = false,
  91. modulesOptions = {},
  92. preprocessLang,
  93. postcssOptions,
  94. postcssPlugins
  95. } = options
  96. const preprocessor = preprocessLang && processors[preprocessLang]
  97. const preProcessedSource = preprocessor && preprocess(options, preprocessor)
  98. const map = preProcessedSource
  99. ? preProcessedSource.map
  100. : options.inMap || options.map
  101. const source = preProcessedSource ? preProcessedSource.code : options.source
  102. const shortId = id.replace(/^data-v-/, '')
  103. const longId = `data-v-${shortId}`
  104. const plugins = (postcssPlugins || []).slice()
  105. plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
  106. if (trim) {
  107. plugins.push(trimPlugin())
  108. }
  109. if (scoped) {
  110. plugins.push(scopedPlugin(longId))
  111. }
  112. let cssModules: Record<string, string> | undefined
  113. if (modules) {
  114. if (__GLOBAL__ || __ESM_BROWSER__) {
  115. throw new Error(
  116. '[@vue/compiler-sfc] `modules` option is not supported in the browser build.'
  117. )
  118. }
  119. if (!options.isAsync) {
  120. throw new Error(
  121. '[@vue/compiler-sfc] `modules` option can only be used with compileStyleAsync().'
  122. )
  123. }
  124. plugins.push(
  125. require('postcss-modules')({
  126. ...modulesOptions,
  127. getJSON: (_cssFileName: string, json: Record<string, string>) => {
  128. cssModules = json
  129. }
  130. })
  131. )
  132. }
  133. const postCSSOptions: ProcessOptions = {
  134. ...postcssOptions,
  135. to: filename,
  136. from: filename
  137. }
  138. if (map) {
  139. postCSSOptions.map = {
  140. inline: false,
  141. annotation: false,
  142. prev: map
  143. }
  144. }
  145. let result: LazyResult | undefined
  146. let code: string | undefined
  147. let outMap: SourceMap | undefined
  148. // stylus output include plain css. so need remove the repeat item
  149. const dependencies = new Set(
  150. preProcessedSource ? preProcessedSource.dependencies : []
  151. )
  152. // sass has filename self when provided filename option
  153. dependencies.delete(filename)
  154. const errors: Error[] = []
  155. if (preProcessedSource && preProcessedSource.errors.length) {
  156. errors.push(...preProcessedSource.errors)
  157. }
  158. const recordPlainCssDependencies = (messages: Message[]) => {
  159. messages.forEach(msg => {
  160. if (msg.type === 'dependency') {
  161. // postcss output path is absolute position path
  162. dependencies.add(msg.file)
  163. }
  164. })
  165. return dependencies
  166. }
  167. try {
  168. result = postcss(plugins).process(source, postCSSOptions)
  169. // In async mode, return a promise.
  170. if (options.isAsync) {
  171. return result
  172. .then(result => ({
  173. code: result.css || '',
  174. map: result.map && result.map.toJSON(),
  175. errors,
  176. modules: cssModules,
  177. rawResult: result,
  178. dependencies: recordPlainCssDependencies(result.messages)
  179. }))
  180. .catch(error => ({
  181. code: '',
  182. map: undefined,
  183. errors: [...errors, error],
  184. rawResult: undefined,
  185. dependencies
  186. }))
  187. }
  188. recordPlainCssDependencies(result.messages)
  189. // force synchronous transform (we know we only have sync plugins)
  190. code = result.css
  191. outMap = result.map
  192. } catch (e: any) {
  193. errors.push(e)
  194. }
  195. return {
  196. code: code || ``,
  197. map: outMap && outMap.toJSON(),
  198. errors,
  199. rawResult: result,
  200. dependencies
  201. }
  202. }
  203. function preprocess(
  204. options: SFCStyleCompileOptions,
  205. preprocessor: StylePreprocessor
  206. ): StylePreprocessorResults {
  207. if ((__ESM_BROWSER__ || __GLOBAL__) && !options.preprocessCustomRequire) {
  208. throw new Error(
  209. `[@vue/compiler-sfc] Style preprocessing in the browser build must ` +
  210. `provide the \`preprocessCustomRequire\` option to return the in-browser ` +
  211. `version of the preprocessor.`
  212. )
  213. }
  214. return preprocessor(
  215. options.source,
  216. options.inMap || options.map,
  217. {
  218. filename: options.filename,
  219. ...options.preprocessOptions
  220. },
  221. options.preprocessCustomRequire
  222. )
  223. }