compileStyle.ts 6.1 KB

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