| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- import {
- type BindingMetadata,
- type CodegenSourceMapGenerator,
- type CompilerError,
- type ElementNode,
- NodeTypes,
- type ParserOptions,
- type RawSourceMap,
- type RootNode,
- type SourceLocation,
- createRoot,
- } from '@vue/compiler-core'
- import * as CompilerDOM from '@vue/compiler-dom'
- import { SourceMapGenerator } from 'source-map-js'
- import type { TemplateCompiler } from './compileTemplate'
- import { parseCssVars } from './style/cssVars'
- import { createCache } from './cache'
- import type { ImportBinding } from './compileScript'
- import { isImportUsed } from './script/importUsageCheck'
- import type { LRUCache } from 'lru-cache'
- import { genCacheKey } from '@vue/shared'
- export const DEFAULT_FILENAME = 'anonymous.vue'
- export interface SFCParseOptions {
- filename?: string
- sourceMap?: boolean
- sourceRoot?: string
- pad?: boolean | 'line' | 'space'
- ignoreEmpty?: boolean
- compiler?: TemplateCompiler
- templateParseOptions?: ParserOptions
- }
- export interface SFCBlock {
- type: string
- content: string
- attrs: Record<string, string | true>
- loc: SourceLocation
- map?: RawSourceMap
- lang?: string
- src?: string
- }
- export interface SFCTemplateBlock extends SFCBlock {
- type: 'template'
- ast?: RootNode
- }
- export interface SFCScriptBlock extends SFCBlock {
- type: 'script'
- setup?: string | boolean
- bindings?: BindingMetadata
- imports?: Record<string, ImportBinding>
- scriptAst?: import('@babel/types').Statement[]
- scriptSetupAst?: import('@babel/types').Statement[]
- warnings?: string[]
- /**
- * Fully resolved dependency file paths (unix slashes) with imported types
- * used in macros, used for HMR cache busting in @vitejs/plugin-vue and
- * vue-loader.
- */
- deps?: string[]
- }
- export interface SFCStyleBlock extends SFCBlock {
- type: 'style'
- scoped?: boolean
- module?: string | boolean
- }
- export interface SFCDescriptor {
- filename: string
- source: string
- template: SFCTemplateBlock | null
- script: SFCScriptBlock | null
- scriptSetup: SFCScriptBlock | null
- styles: SFCStyleBlock[]
- customBlocks: SFCBlock[]
- cssVars: string[]
- /**
- * whether the SFC uses :slotted() modifier.
- * this is used as a compiler optimization hint.
- */
- slotted: boolean
- vapor: boolean
- /**
- * compare with an existing descriptor to determine whether HMR should perform
- * a reload vs. re-render.
- *
- * Note: this comparison assumes the prev/next script are already identical,
- * and only checks the special case where <script setup lang="ts"> unused import
- * pruning result changes due to template changes.
- */
- shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
- }
- export interface SFCParseResult {
- descriptor: SFCDescriptor
- errors: (CompilerError | SyntaxError)[]
- }
- export const parseCache:
- | Map<string, SFCParseResult>
- | LRUCache<string, SFCParseResult> = createCache<SFCParseResult>()
- export function parse(
- source: string,
- options: SFCParseOptions = {},
- ): SFCParseResult {
- const sourceKey = genCacheKey(source, {
- ...options,
- compiler: { parse: options.compiler?.parse },
- })
- const cache = parseCache.get(sourceKey)
- if (cache) {
- return cache
- }
- const {
- sourceMap = true,
- filename = DEFAULT_FILENAME,
- sourceRoot = '',
- pad = false,
- ignoreEmpty = true,
- compiler = CompilerDOM,
- templateParseOptions = {},
- } = options
- const descriptor: SFCDescriptor = {
- filename,
- source,
- template: null,
- script: null,
- scriptSetup: null,
- styles: [],
- customBlocks: [],
- cssVars: [],
- slotted: false,
- vapor: false,
- shouldForceReload: prevImports => hmrShouldReload(prevImports, descriptor),
- }
- const errors: (CompilerError | SyntaxError)[] = []
- const ast = compiler.parse(source, {
- parseMode: 'sfc',
- prefixIdentifiers: true,
- ...templateParseOptions,
- onError: e => {
- errors.push(e)
- },
- })
- ast.children.forEach(node => {
- if (node.type !== NodeTypes.ELEMENT) {
- return
- }
- // we only want to keep the nodes that are not empty
- // (when the tag is not a template)
- if (
- ignoreEmpty &&
- node.tag !== 'template' &&
- isEmpty(node) &&
- !hasSrc(node)
- ) {
- return
- }
- switch (node.tag) {
- case 'template':
- if (!descriptor.template) {
- const templateBlock = (descriptor.template = createBlock(
- node,
- source,
- false,
- ) as SFCTemplateBlock)
- descriptor.vapor ||= !!templateBlock.attrs.vapor
- if (!templateBlock.attrs.src) {
- templateBlock.ast = createRoot(node.children, source)
- }
- // warn against 2.x <template functional>
- if (templateBlock.attrs.functional) {
- const err = new SyntaxError(
- `<template functional> is no longer supported in Vue 3, since ` +
- `functional components no longer have significant performance ` +
- `difference from stateful ones. Just use a normal <template> ` +
- `instead.`,
- ) as CompilerError
- err.loc = node.props.find(
- p => p.type === NodeTypes.ATTRIBUTE && p.name === 'functional',
- )!.loc
- errors.push(err)
- }
- } else {
- errors.push(createDuplicateBlockError(node))
- }
- break
- case 'script':
- const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
- descriptor.vapor ||= !!scriptBlock.attrs.vapor
- const isSetup = !!scriptBlock.attrs.setup
- if (isSetup && !descriptor.scriptSetup) {
- descriptor.scriptSetup = scriptBlock
- break
- }
- if (!isSetup && !descriptor.script) {
- descriptor.script = scriptBlock
- break
- }
- errors.push(createDuplicateBlockError(node, isSetup))
- break
- case 'style':
- const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
- if (styleBlock.attrs.vars) {
- errors.push(
- new SyntaxError(
- `<style vars> has been replaced by a new proposal: ` +
- `https://github.com/vuejs/rfcs/pull/231`,
- ),
- )
- }
- descriptor.styles.push(styleBlock)
- break
- default:
- descriptor.customBlocks.push(createBlock(node, source, pad))
- break
- }
- })
- if (!descriptor.template && !descriptor.script && !descriptor.scriptSetup) {
- errors.push(
- new SyntaxError(
- `At least one <template> or <script> is required in a single file component. ${descriptor.filename}`,
- ),
- )
- }
- if (descriptor.scriptSetup) {
- if (descriptor.scriptSetup.src) {
- errors.push(
- new SyntaxError(
- `<script setup> cannot use the "src" attribute because ` +
- `its syntax will be ambiguous outside of the component.`,
- ),
- )
- descriptor.scriptSetup = null
- }
- if (descriptor.script && descriptor.script.src) {
- errors.push(
- new SyntaxError(
- `<script> cannot use the "src" attribute when <script setup> is ` +
- `also present because they must be processed together.`,
- ),
- )
- descriptor.script = null
- }
- }
- // dedent pug/jade templates
- let templateColumnOffset = 0
- if (
- descriptor.template &&
- (descriptor.template.lang === 'pug' || descriptor.template.lang === 'jade')
- ) {
- ;[descriptor.template.content, templateColumnOffset] = dedent(
- descriptor.template.content,
- )
- }
- if (sourceMap) {
- const genMap = (block: SFCBlock | null, columnOffset = 0) => {
- if (block && !block.src) {
- block.map = generateSourceMap(
- filename,
- source,
- block.content,
- sourceRoot,
- !pad || block.type === 'template' ? block.loc.start.line - 1 : 0,
- columnOffset,
- )
- }
- }
- genMap(descriptor.template, templateColumnOffset)
- genMap(descriptor.script)
- descriptor.styles.forEach(s => genMap(s))
- descriptor.customBlocks.forEach(s => genMap(s))
- }
- // parse CSS vars
- descriptor.cssVars = parseCssVars(descriptor)
- // check if the SFC uses :slotted
- const slottedRE = /(?:::v-|:)slotted\(/
- descriptor.slotted = descriptor.styles.some(
- s => s.scoped && slottedRE.test(s.content),
- )
- const result = {
- descriptor,
- errors,
- }
- parseCache.set(sourceKey, result)
- return result
- }
- function createDuplicateBlockError(
- node: ElementNode,
- isScriptSetup = false,
- ): CompilerError {
- const err = new SyntaxError(
- `Single file component can contain only one <${node.tag}${
- isScriptSetup ? ` setup` : ``
- }> element`,
- ) as CompilerError
- err.loc = node.loc
- return err
- }
- function createBlock(
- node: ElementNode,
- source: string,
- pad: SFCParseOptions['pad'],
- ): SFCBlock {
- const type = node.tag
- const loc = node.innerLoc!
- const attrs: Record<string, string | true> = {}
- const block: SFCBlock = {
- type,
- content: source.slice(loc.start.offset, loc.end.offset),
- loc,
- attrs,
- }
- if (pad) {
- block.content = padContent(source, block, pad) + block.content
- }
- node.props.forEach(p => {
- if (p.type === NodeTypes.ATTRIBUTE) {
- const name = p.name
- attrs[name] = p.value ? p.value.content || true : true
- if (name === 'lang') {
- block.lang = p.value && p.value.content
- } else if (name === 'src') {
- block.src = p.value && p.value.content
- } else if (type === 'style') {
- if (name === 'scoped') {
- ;(block as SFCStyleBlock).scoped = true
- } else if (name === 'module') {
- ;(block as SFCStyleBlock).module = attrs[name]
- }
- } else if (type === 'script' && name === 'setup') {
- ;(block as SFCScriptBlock).setup = attrs.setup
- }
- }
- })
- return block
- }
- const splitRE = /\r?\n/g
- const emptyRE = /^(?:\/\/)?\s*$/
- const replaceRE = /./g
- function generateSourceMap(
- filename: string,
- source: string,
- generated: string,
- sourceRoot: string,
- lineOffset: number,
- columnOffset: number,
- ): RawSourceMap {
- const map = new SourceMapGenerator({
- file: filename.replace(/\\/g, '/'),
- sourceRoot: sourceRoot.replace(/\\/g, '/'),
- }) as unknown as CodegenSourceMapGenerator
- map.setSourceContent(filename, source)
- map._sources.add(filename)
- generated.split(splitRE).forEach((line, index) => {
- if (!emptyRE.test(line)) {
- const originalLine = index + 1 + lineOffset
- const generatedLine = index + 1
- for (let i = 0; i < line.length; i++) {
- if (!/\s/.test(line[i])) {
- map._mappings.add({
- originalLine,
- originalColumn: i + columnOffset,
- generatedLine,
- generatedColumn: i,
- source: filename,
- name: null,
- })
- }
- }
- }
- })
- return map.toJSON()
- }
- function padContent(
- content: string,
- block: SFCBlock,
- pad: SFCParseOptions['pad'],
- ): string {
- content = content.slice(0, block.loc.start.offset)
- if (pad === 'space') {
- return content.replace(replaceRE, ' ')
- } else {
- const offset = content.split(splitRE).length
- const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
- return Array(offset).join(padChar)
- }
- }
- function hasSrc(node: ElementNode) {
- return node.props.some(p => {
- if (p.type !== NodeTypes.ATTRIBUTE) {
- return false
- }
- return p.name === 'src'
- })
- }
- /**
- * Returns true if the node has no children
- * once the empty text nodes (trimmed content) have been filtered out.
- */
- function isEmpty(node: ElementNode) {
- for (let i = 0; i < node.children.length; i++) {
- const child = node.children[i]
- if (child.type !== NodeTypes.TEXT || child.content.trim() !== '') {
- return false
- }
- }
- return true
- }
- /**
- * Note: this comparison assumes the prev/next script are already identical,
- * and only checks the special case where <script setup lang="ts"> unused import
- * pruning result changes due to template changes.
- */
- export function hmrShouldReload(
- prevImports: Record<string, ImportBinding>,
- next: SFCDescriptor,
- ): boolean {
- if (
- !next.scriptSetup ||
- (next.scriptSetup.lang !== 'ts' && next.scriptSetup.lang !== 'tsx')
- ) {
- return false
- }
- // for each previous import, check if its used status remain the same based on
- // the next descriptor's template
- for (const key in prevImports) {
- // if an import was previous unused, but now is used, we need to force
- // reload so that the script now includes that import.
- if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
- return true
- }
- }
- return false
- }
- /**
- * Dedent a string.
- *
- * This removes any whitespace that is common to all lines in the string from
- * each line in the string.
- */
- function dedent(s: string): [string, number] {
- const lines = s.split('\n')
- const minIndent = lines.reduce(function (minIndent, line) {
- if (line.trim() === '') {
- return minIndent
- }
- const indent = line.match(/^\s*/)?.[0]?.length || 0
- return Math.min(indent, minIndent)
- }, Infinity)
- if (minIndent === 0) {
- return [s, minIndent]
- }
- return [
- lines
- .map(function (line) {
- return line.slice(minIndent)
- })
- .join('\n'),
- minIndent,
- ]
- }
|