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 * as CompilerDOM from '@vue/compiler-dom' import * as CompilerSSR from '@vue/compiler-ssr' 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 ssr?: boolean inMap?: RawSourceMap compiler?: TemplateCompiler compilerOptions?: CompilerOptions preprocessLang?: string preprocessOptions?: any /** * In some cases, compiler-sfc may not be inside the project root (e.g. when * linked or globally installed). In such cases a custom `require` can be * passed to correctly resolve the preprocessors. */ preprocessCustomRequire?: (id: string) => any /** * Configure what tags/attributes to trasnform into relative asset url imports * in the form of `{ [tag: string]: string[] }`, or disable the transform with * `false`. */ transformAssetUrls?: AssetURLOptions | boolean /** * If base is provided, instead of transforming relative asset urls into * imports, they will be directly rewritten to absolute urls. */ transformAssetUrlsBase?: string } 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, preprocessCustomRequire } = options if ( (__ESM_BROWSER__ || __GLOBAL__) && preprocessLang && !preprocessCustomRequire ) { throw new Error( `[@vue/compiler-sfc] Template preprocessing in the browser build must ` + `provide the \`preprocessCustomRequire\` option to return the in-browser ` + `version of the preprocessor in the shape of { render(): string }.` ) } const preprocessor = preprocessLang ? preprocessCustomRequire ? preprocessCustomRequire(preprocessLang) : require('consolidate')[preprocessLang as keyof typeof consolidate] : false 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, ssr = false, compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM, compilerOptions = {}, transformAssetUrls, transformAssetUrlsBase }: SFCTemplateCompileOptions): SFCTemplateCompileResults { const errors: CompilerError[] = [] let nodeTransforms: NodeTransform[] = [] if (transformAssetUrls !== false) { if (transformAssetUrlsBase || isObject(transformAssetUrls)) { nodeTransforms = [ createAssetUrlTransformWithOptions({ base: transformAssetUrlsBase, tags: isObject(transformAssetUrls) ? transformAssetUrls : undefined }), transformSrcset ] } else { 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 } } }) }