compileStyle.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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
  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({ ...options, isAsync: true }) as Promise<
  77. SFCStyleCompileResults
  78. >
  79. }
  80. export function doCompileStyle(
  81. options: SFCAsyncStyleCompileOptions
  82. ): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
  83. const {
  84. filename,
  85. id,
  86. scoped = false,
  87. trim = true,
  88. isProd = false,
  89. modules = false,
  90. modulesOptions = {},
  91. preprocessLang,
  92. postcssOptions,
  93. postcssPlugins
  94. } = options
  95. const preprocessor = preprocessLang && processors[preprocessLang]
  96. const preProcessedSource = preprocessor && preprocess(options, preprocessor)
  97. const map = preProcessedSource
  98. ? preProcessedSource.map
  99. : options.inMap || options.map
  100. const source = preProcessedSource ? preProcessedSource.code : options.source
  101. const shortId = id.replace(/^data-v-/, '')
  102. const longId = `data-v-${shortId}`
  103. const plugins = (postcssPlugins || []).slice()
  104. plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
  105. if (trim) {
  106. plugins.push(trimPlugin())
  107. }
  108. if (scoped) {
  109. plugins.push(scopedPlugin(longId))
  110. }
  111. let cssModules: Record<string, string> | undefined
  112. if (modules) {
  113. if (__GLOBAL__ || __ESM_BROWSER__) {
  114. throw new Error(
  115. '[@vue/compiler-sfc] `modules` option is not supported in the browser build.'
  116. )
  117. }
  118. if (!options.isAsync) {
  119. throw new Error(
  120. '[@vue/compiler-sfc] `modules` option can only be used with compileStyleAsync().'
  121. )
  122. }
  123. plugins.push(
  124. require('postcss-modules')({
  125. ...modulesOptions,
  126. getJSON: (_cssFileName: string, json: Record<string, string>) => {
  127. cssModules = json
  128. }
  129. })
  130. )
  131. }
  132. const postCSSOptions: ProcessOptions = {
  133. ...postcssOptions,
  134. to: filename,
  135. from: filename
  136. }
  137. if (map) {
  138. postCSSOptions.map = {
  139. inline: false,
  140. annotation: false,
  141. prev: map
  142. }
  143. }
  144. let result: LazyResult | undefined
  145. let code: string | undefined
  146. let outMap: SourceMap | undefined
  147. // stylus output include plain css. so need remove the repeat item
  148. const dependencies = new Set(
  149. preProcessedSource ? preProcessedSource.dependencies : []
  150. )
  151. // sass has filename self when provided filename option
  152. dependencies.delete(filename)
  153. const errors: Error[] = []
  154. if (preProcessedSource && preProcessedSource.errors.length) {
  155. errors.push(...preProcessedSource.errors)
  156. }
  157. const recordPlainCssDependencies = (messages: Message[]) => {
  158. messages.forEach(msg => {
  159. if (msg.type === 'dependency') {
  160. // postcss output path is absolute position path
  161. dependencies.add(msg.file)
  162. }
  163. })
  164. return dependencies
  165. }
  166. try {
  167. result = postcss(plugins).process(source, postCSSOptions)
  168. // In async mode, return a promise.
  169. if (options.isAsync) {
  170. return result
  171. .then(result => ({
  172. code: result.css || '',
  173. map: result.map && (result.map.toJSON() as any),
  174. errors,
  175. modules: cssModules,
  176. rawResult: result,
  177. dependencies: recordPlainCssDependencies(result.messages)
  178. }))
  179. .catch(error => ({
  180. code: '',
  181. map: undefined,
  182. errors: [...errors, error],
  183. rawResult: undefined,
  184. dependencies
  185. }))
  186. }
  187. recordPlainCssDependencies(result.messages)
  188. // force synchronous transform (we know we only have sync plugins)
  189. code = result.css
  190. outMap = result.map
  191. } catch (e) {
  192. errors.push(e)
  193. }
  194. return {
  195. code: code || ``,
  196. map: outMap && (outMap.toJSON() as any),
  197. errors,
  198. rawResult: result,
  199. dependencies
  200. }
  201. }
  202. function preprocess(
  203. options: SFCStyleCompileOptions,
  204. preprocessor: StylePreprocessor
  205. ): StylePreprocessorResults {
  206. if ((__ESM_BROWSER__ || __GLOBAL__) && !options.preprocessCustomRequire) {
  207. throw new Error(
  208. `[@vue/compiler-sfc] Style preprocessing in the browser build must ` +
  209. `provide the \`preprocessCustomRequire\` option to return the in-browser ` +
  210. `version of the preprocessor.`
  211. )
  212. }
  213. return preprocessor(
  214. options.source,
  215. options.inMap || options.map,
  216. {
  217. filename: options.filename,
  218. ...options.preprocessOptions
  219. },
  220. options.preprocessCustomRequire
  221. )
  222. }