1 | import { SourceMapGenerator } from 'source-map'
|
2 | import {
|
3 | RawSourceMap,
|
4 | VueTemplateCompiler,
|
5 | VueTemplateCompilerParseOptions
|
6 | } from './types'
|
7 |
|
8 | const hash = require('hash-sum')
|
9 | const cache = new (require('lru-cache'))(100)
|
10 |
|
11 | const splitRE = /\r?\n/g
|
12 | const emptyRE = /^(?:\/\/)?\s*$/
|
13 |
|
14 | export interface ParseOptions {
|
15 | source: string
|
16 | filename?: string
|
17 | compiler: VueTemplateCompiler
|
18 | compilerParseOptions?: VueTemplateCompilerParseOptions
|
19 | sourceRoot?: string
|
20 | needMap?: boolean
|
21 | }
|
22 |
|
23 | export interface SFCCustomBlock {
|
24 | type: string
|
25 | content: string
|
26 | attrs: { [key: string]: string | true }
|
27 | start: number
|
28 | end: number
|
29 | map?: RawSourceMap
|
30 | }
|
31 |
|
32 | export interface SFCBlock extends SFCCustomBlock {
|
33 | lang?: string
|
34 | src?: string
|
35 | scoped?: boolean
|
36 | module?: string | boolean
|
37 | }
|
38 |
|
39 | export interface SFCDescriptor {
|
40 | template: SFCBlock | null
|
41 | script: SFCBlock | null
|
42 | styles: SFCBlock[]
|
43 | customBlocks: SFCCustomBlock[]
|
44 | }
|
45 |
|
46 | export function parse(options: ParseOptions): SFCDescriptor {
|
47 | const {
|
48 | source,
|
49 | filename = '',
|
50 | compiler,
|
51 | compilerParseOptions = { pad: 'line' } as VueTemplateCompilerParseOptions,
|
52 | sourceRoot = '',
|
53 | needMap = true
|
54 | } = options
|
55 | const cacheKey = hash(
|
56 | filename + source + JSON.stringify(compilerParseOptions)
|
57 | )
|
58 | let output: SFCDescriptor = cache.get(cacheKey)
|
59 | if (output) return output
|
60 | output = compiler.parseComponent(source, compilerParseOptions)
|
61 | if (needMap) {
|
62 | if (output.script && !output.script.src) {
|
63 | output.script.map = generateSourceMap(
|
64 | filename,
|
65 | source,
|
66 | output.script.content,
|
67 | sourceRoot,
|
68 | compilerParseOptions.pad
|
69 | )
|
70 | }
|
71 | if (output.styles) {
|
72 | output.styles.forEach(style => {
|
73 | if (!style.src) {
|
74 | style.map = generateSourceMap(
|
75 | filename,
|
76 | source,
|
77 | style.content,
|
78 | sourceRoot,
|
79 | compilerParseOptions.pad
|
80 | )
|
81 | }
|
82 | })
|
83 | }
|
84 | }
|
85 | cache.set(cacheKey, output)
|
86 | return output
|
87 | }
|
88 |
|
89 | function generateSourceMap(
|
90 | filename: string,
|
91 | source: string,
|
92 | generated: string,
|
93 | sourceRoot: string,
|
94 | pad?: 'line' | 'space'
|
95 | ): RawSourceMap {
|
96 | const map = new SourceMapGenerator({
|
97 | file: filename.replace(/\\/g, '/'),
|
98 | sourceRoot: sourceRoot.replace(/\\/g, '/')
|
99 | })
|
100 | let offset = 0
|
101 | if (!pad) {
|
102 | offset =
|
103 | source
|
104 | .split(generated)
|
105 | .shift()!
|
106 | .split(splitRE).length - 1
|
107 | }
|
108 | map.setSourceContent(filename, source)
|
109 | generated.split(splitRE).forEach((line, index) => {
|
110 | if (!emptyRE.test(line)) {
|
111 | map.addMapping({
|
112 | source: filename,
|
113 | original: {
|
114 | line: index + 1 + offset,
|
115 | column: 0
|
116 | },
|
117 | generated: {
|
118 | line: index + 1,
|
119 | column: 0
|
120 | }
|
121 | })
|
122 | }
|
123 | })
|
124 | return JSON.parse(map.toString())
|
125 | }
|