vue3-yuanma/packages/compiler-sfc/src/compileTemplate.ts

221 lines
5.9 KiB
TypeScript
Raw Normal View History

import {
CompilerOptions,
CodegenResult,
CompilerError,
NodeTransform
} from '@vue/compiler-core'
import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map'
import {
transformAssetUrl,
AssetURLOptions,
createAssetUrlTransformWithOptions
} from './templateTransformAssetUrl'
import { transformSrcset } from './templateTransformSrcset'
import { isObject } from '@vue/shared'
import consolidate from 'consolidate'
2019-12-13 16:24:09 +00:00
export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult
}
export interface SFCTemplateCompileResults {
code: string
source: string
tips: string[]
errors: (string | CompilerError)[]
map?: RawSourceMap
}
2019-12-13 16:24:09 +00:00
export interface SFCTemplateCompileOptions {
source: string
filename: string
inMap?: RawSourceMap
compiler?: TemplateCompiler
compilerOptions?: CompilerOptions
preprocessLang?: string
preprocessOptions?: any
transformAssetUrls?: AssetURLOptions | boolean
}
function preprocess(
2019-12-13 16:24:09 +00:00
{ source, filename, preprocessOptions }: SFCTemplateCompileOptions,
preprocessor: any
): string {
// Consolidate exposes a callback based API, but the callback is in fact
// called synchronously for most templating engines. In our case, we have to
// expose a synchronous API so that it is usable in Jest transforms (which
// have to be sync because they are applied via Node.js require hooks)
let res: any, err
preprocessor.render(
source,
{ filename, ...preprocessOptions },
(_err: Error | null, _res: string) => {
if (_err) err = _err
res = _res
}
)
if (err) throw err
return res
}
export function compileTemplate(
2019-12-13 16:24:09 +00:00
options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
const { preprocessLang } = options
const preprocessor =
preprocessLang && consolidate[preprocessLang as keyof typeof consolidate]
if (preprocessor) {
2019-12-11 14:26:14 +00:00
try {
return doCompileTemplate({
...options,
source: preprocess(options, preprocessor)
})
} catch (e) {
return {
code: `export default function render() {}`,
source: options.source,
tips: [],
errors: [e]
}
}
} else if (preprocessLang) {
return {
code: `export default function render() {}`,
source: options.source,
tips: [
`Component ${
options.filename
} uses lang ${preprocessLang} for template. Please install the language preprocessor.`
],
errors: [
`Component ${
options.filename
} uses lang ${preprocessLang} for template, however it is not installed.`
]
}
} else {
return doCompileTemplate(options)
}
}
function doCompileTemplate({
filename,
inMap,
source,
compiler = require('@vue/compiler-dom'),
compilerOptions = {},
transformAssetUrls
2019-12-13 16:24:09 +00:00
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
const errors: CompilerError[] = []
2019-12-10 22:41:56 +00:00
let nodeTransforms: NodeTransform[] = []
if (isObject(transformAssetUrls)) {
2019-12-10 22:41:56 +00:00
nodeTransforms = [
createAssetUrlTransformWithOptions(transformAssetUrls),
transformSrcset
]
} else if (transformAssetUrls !== false) {
2019-12-10 22:41:56 +00:00
nodeTransforms = [transformAssetUrl, transformSrcset]
}
let { code, map } = compiler.compile(source, {
2019-12-10 22:41:56 +00:00
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
cacheHandlers: true,
2019-12-10 22:41:56 +00:00
...compilerOptions,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,
sourceMap: true,
onError: e => errors.push(e)
})
// inMap should be the map produced by ./parse.ts which is a simple line-only
// mapping. If it is present, we need to adjust the final map and errors to
// reflect the original line numbers.
if (inMap) {
if (map) {
map = mapLines(inMap, map)
}
if (errors.length) {
patchErrors(errors, source, inMap)
}
}
return { code, source, errors, tips: [], map }
2019-11-07 02:58:15 +00:00
}
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
if (!oldMap) return newMap
if (!newMap) return oldMap
const oldMapConsumer = new SourceMapConsumer(oldMap)
const newMapConsumer = new SourceMapConsumer(newMap)
const mergedMapGenerator = new SourceMapGenerator()
newMapConsumer.eachMapping(m => {
if (m.originalLine == null) {
return
}
const origPosInOldMap = oldMapConsumer.originalPositionFor({
line: m.originalLine,
column: m.originalColumn
})
if (origPosInOldMap.source == null) {
return
}
mergedMapGenerator.addMapping({
generated: {
line: m.generatedLine,
column: m.generatedColumn
},
original: {
line: origPosInOldMap.line, // map line
// use current column, since the oldMap produced by @vue/compiler-sfc
// does not
column: m.originalColumn
},
source: origPosInOldMap.source,
name: origPosInOldMap.name
})
})
// source-map's type definition is incomplete
const generator = mergedMapGenerator as any
;(oldMapConsumer as any).sources.forEach((sourceFile: string) => {
generator._sources.add(sourceFile)
const sourceContent = oldMapConsumer.sourceContentFor(sourceFile)
if (sourceContent != null) {
mergedMapGenerator.setSourceContent(sourceFile, sourceContent)
}
})
generator._sourceRoot = oldMap.sourceRoot
generator._file = oldMap.file
return generator.toJSON()
}
function patchErrors(
errors: CompilerError[],
source: string,
inMap: RawSourceMap
) {
const originalSource = inMap.sourcesContent![0]
const offset = originalSource.indexOf(source)
const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
errors.forEach(err => {
if (err.loc) {
err.loc.start.line += lineOffset
err.loc.start.offset += offset
if (err.loc.end !== err.loc.start) {
err.loc.end.line += lineOffset
err.loc.end.offset += offset
}
}
})
}