import { CompilerOptions, CodegenResult, CompilerError, NodeTransform, ParserOptions, RootNode } 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' export interface TemplateCompiler { compile(template: string, options: CompilerOptions): CodegenResult parse(template: string, options: ParserOptions): RootNode } export interface SFCTemplateCompileResults { code: string source: string tips: string[] errors: (string | CompilerError)[] map?: RawSourceMap } export interface SFCTemplateCompileOptions { source: string filename: string inMap?: RawSourceMap compiler?: TemplateCompiler compilerOptions?: CompilerOptions preprocessLang?: string preprocessOptions?: any transformAssetUrls?: AssetURLOptions | boolean } function preprocess( { 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( options: SFCTemplateCompileOptions ): SFCTemplateCompileResults { const { preprocessLang } = options const preprocessor = preprocessLang && consolidate[preprocessLang as keyof typeof consolidate] if (preprocessor) { 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 }: SFCTemplateCompileOptions): SFCTemplateCompileResults { const errors: CompilerError[] = [] let nodeTransforms: NodeTransform[] = [] if (isObject(transformAssetUrls)) { nodeTransforms = [ createAssetUrlTransformWithOptions(transformAssetUrls), transformSrcset ] } else if (transformAssetUrls !== false) { nodeTransforms = [transformAssetUrl, transformSrcset] } let { code, map } = compiler.compile(source, { mode: 'module', prefixIdentifiers: true, hoistStatic: true, cacheHandlers: true, ...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 } } 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 } } }) }