316 lines
8.4 KiB
TypeScript
316 lines
8.4 KiB
TypeScript
import {
|
|
CompilerOptions,
|
|
CodegenResult,
|
|
CompilerError,
|
|
NodeTransform,
|
|
ParserOptions,
|
|
RootNode
|
|
} from '@vue/compiler-core'
|
|
import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map'
|
|
import {
|
|
transformAssetUrl,
|
|
AssetURLOptions,
|
|
createAssetUrlTransformWithOptions,
|
|
AssetURLTagConfig,
|
|
normalizeOptions
|
|
} from './templateTransformAssetUrl'
|
|
import {
|
|
transformSrcset,
|
|
createSrcsetTransformWithOptions
|
|
} from './templateTransformSrcset'
|
|
import { generateCodeFrame, isObject } from '@vue/shared'
|
|
import * as CompilerDOM from '@vue/compiler-dom'
|
|
import * as CompilerSSR from '@vue/compiler-ssr'
|
|
import consolidate from '@vue/consolidate'
|
|
import { warnOnce } from './warn'
|
|
import { genCssVarsFromList } from './cssVars'
|
|
|
|
export interface TemplateCompiler {
|
|
compile(template: string, options: CompilerOptions): CodegenResult
|
|
parse(template: string, options: ParserOptions): RootNode
|
|
}
|
|
|
|
export interface SFCTemplateCompileResults {
|
|
code: string
|
|
ast?: RootNode
|
|
preamble?: string
|
|
source: string
|
|
tips: string[]
|
|
errors: (string | CompilerError)[]
|
|
map?: RawSourceMap
|
|
}
|
|
|
|
export interface SFCTemplateCompileOptions {
|
|
source: string
|
|
filename: string
|
|
id: string
|
|
scoped?: boolean
|
|
slotted?: boolean
|
|
isProd?: boolean
|
|
ssr?: boolean
|
|
ssrCssVars?: string[]
|
|
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 transform into asset url imports,
|
|
* or disable the transform altogether with `false`.
|
|
*/
|
|
transformAssetUrls?: AssetURLOptions | AssetURLTagConfig | boolean
|
|
}
|
|
|
|
interface PreProcessor {
|
|
render(
|
|
source: string,
|
|
options: any,
|
|
cb: (err: Error | null, res: string) => void
|
|
): void
|
|
}
|
|
|
|
function preprocess(
|
|
{ source, filename, preprocessOptions }: SFCTemplateCompileOptions,
|
|
preprocessor: PreProcessor
|
|
): 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: string = ''
|
|
let err: Error | null = null
|
|
|
|
preprocessor.render(
|
|
source,
|
|
{ filename, ...preprocessOptions },
|
|
(_err, _res) => {
|
|
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)
|
|
: __ESM_BROWSER__
|
|
? undefined
|
|
: consolidate[preprocessLang as keyof typeof consolidate]
|
|
: false
|
|
if (preprocessor) {
|
|
try {
|
|
return doCompileTemplate({
|
|
...options,
|
|
source: preprocess(options, preprocessor)
|
|
})
|
|
} catch (e: any) {
|
|
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,
|
|
id,
|
|
scoped,
|
|
slotted,
|
|
inMap,
|
|
source,
|
|
ssr = false,
|
|
ssrCssVars,
|
|
isProd = false,
|
|
compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
|
|
compilerOptions = {},
|
|
transformAssetUrls
|
|
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
|
|
const errors: CompilerError[] = []
|
|
const warnings: CompilerError[] = []
|
|
|
|
let nodeTransforms: NodeTransform[] = []
|
|
if (isObject(transformAssetUrls)) {
|
|
const assetOptions = normalizeOptions(transformAssetUrls)
|
|
nodeTransforms = [
|
|
createAssetUrlTransformWithOptions(assetOptions),
|
|
createSrcsetTransformWithOptions(assetOptions)
|
|
]
|
|
} else if (transformAssetUrls !== false) {
|
|
nodeTransforms = [transformAssetUrl, transformSrcset]
|
|
}
|
|
|
|
if (ssr && !ssrCssVars) {
|
|
warnOnce(
|
|
`compileTemplate is called with \`ssr: true\` but no ` +
|
|
`corresponding \`cssVars\` option.\`.`
|
|
)
|
|
}
|
|
if (!id) {
|
|
warnOnce(`compileTemplate now requires the \`id\` option.\`.`)
|
|
id = ''
|
|
}
|
|
|
|
const shortId = id.replace(/^data-v-/, '')
|
|
const longId = `data-v-${shortId}`
|
|
|
|
let { code, ast, preamble, map } = compiler.compile(source, {
|
|
mode: 'module',
|
|
prefixIdentifiers: true,
|
|
hoistStatic: true,
|
|
cacheHandlers: true,
|
|
ssrCssVars:
|
|
ssr && ssrCssVars && ssrCssVars.length
|
|
? genCssVarsFromList(ssrCssVars, shortId, isProd)
|
|
: '',
|
|
scopeId: scoped ? longId : undefined,
|
|
slotted,
|
|
sourceMap: true,
|
|
...compilerOptions,
|
|
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
|
|
filename,
|
|
onError: e => errors.push(e),
|
|
onWarn: w => warnings.push(w)
|
|
})
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
const tips = warnings.map(w => {
|
|
let msg = w.message
|
|
if (w.loc) {
|
|
msg += `\n${generateCodeFrame(
|
|
source,
|
|
w.loc.start.offset,
|
|
w.loc.end.offset
|
|
)}`
|
|
}
|
|
return msg
|
|
})
|
|
|
|
return { code, ast, preamble, 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
|
|
}
|
|
}
|
|
})
|
|
}
|