wip: port @vue/component-compiler-utils

This commit is contained in:
Evan You 2022-06-10 16:35:46 +08:00
parent 50f2870ff0
commit 06594f68b7
30 changed files with 2164 additions and 251 deletions

View File

@ -26,7 +26,7 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: pnpm run test:unit run: pnpm run test:unit
ssr-test: ssr-sfc-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -45,6 +45,9 @@ jobs:
- name: Run SSR tests - name: Run SSR tests
run: pnpm run test:ssr run: pnpm run test:ssr
- name: Run compiler-sfc tests
run: pnpm run test:sfc
e2e-test: e2e-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -3,6 +3,5 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "./temp", "baseUrl": "./temp",
"types": [] "types": []
}, }
"include": ["src"]
} }

View File

@ -41,10 +41,11 @@
"dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:compiler ", "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:compiler ",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"build:ssr": "npm run build -- runtime-cjs,server-renderer", "build:ssr": "npm run build -- runtime-cjs,server-renderer",
"build:types": "rimraf temp && tsc --declaration --emitDeclarationOnly --outDir temp && api-extractor run", "build:types": "rimraf temp && tsc --declaration --emitDeclarationOnly --outDir temp && api-extractor run && api-extractor run -c packages/compiler-sfc/api-extractor.json",
"test": "npm run ts-check && npm run test:types && npm run test:unit && npm run test:e2e && npm run test:ssr", "test": "npm run ts-check && npm run test:types && npm run test:unit && npm run test:e2e && npm run test:ssr && npm run test:sfc",
"test:unit": "vitest run test/unit", "test:unit": "vitest run test/unit",
"test:ssr": "npm run build:ssr && vitest run server-renderer", "test:ssr": "npm run build:ssr && vitest run server-renderer",
"test:sfc": "vitest run compiler-sfc",
"test:e2e": "npm run build -- full-prod,server-renderer-basic && vitest run test/e2e", "test:e2e": "npm run build -- full-prod,server-renderer-basic && vitest run test/e2e",
"test:transition": "karma start test/transition/karma.conf.js", "test:transition": "karma start test/transition/karma.conf.js",
"test:types": "npm run build:types && tsc -p ./types/tsconfig.json", "test:types": "npm run build:types && tsc -p ./types/tsconfig.json",
@ -85,45 +86,45 @@
"csstype": "^3.1.0" "csstype": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/api-extractor": "^7.24.2", "@microsoft/api-extractor": "^7.25.0",
"@rollup/plugin-alias": "^3.1.9", "@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.2.1", "@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
"@types/he": "^1.1.2", "@types/he": "^1.1.2",
"@types/node": "^17.0.30", "@types/node": "^17.0.41",
"chalk": "^4.0.0", "chalk": "^4.1.2",
"conventional-changelog-cli": "^2.2.2", "conventional-changelog-cli": "^2.2.2",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"de-indent": "^1.0.2",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"esbuild": "^0.14.39", "esbuild": "^0.14.43",
"execa": "^4.0.0", "execa": "^4.1.0",
"he": "^1.2.0", "he": "^1.2.0",
"jasmine-core": "^4.1.1", "jasmine-core": "^4.2.0",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
"karma": "^6.3.20", "karma": "^6.3.20",
"karma-chrome-launcher": "^3.1.1", "karma-chrome-launcher": "^3.1.1",
"karma-cli": "^2.0.0", "karma-cli": "^2.0.0",
"karma-esbuild": "^2.2.4", "karma-esbuild": "^2.2.4",
"karma-jasmine": "^5.0.1", "karma-jasmine": "^5.0.1",
"lint-staged": "^12.4.1", "lint-staged": "^12.5.0",
"postcss": "^8.4.14",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^4.0.6", "marked": "^4.0.16",
"minimist": "^1.2.6", "minimist": "^1.2.6",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"puppeteer": "^14.1.1", "puppeteer": "^14.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.70.2", "rollup": "^2.75.6",
"rollup-plugin-typescript2": "^0.31.2", "rollup-plugin-typescript2": "^0.31.2",
"semver": "^7.3.7", "semver": "^7.3.7",
"shelljs": "^0.8.5", "shelljs": "^0.8.5",
"terser": "^5.13.1", "terser": "^5.14.0",
"todomvc-app-css": "^2.4.2", "todomvc-app-css": "^2.4.2",
"ts-node": "^10.7.0", "ts-node": "^10.8.1",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.4", "typescript": "^4.7.3",
"vitest": "^0.12.6", "vitest": "^0.12.10",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
} }

View File

@ -0,0 +1,64 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"projectFolder": ".",
"mainEntryPointFilePath": "../../temp/packages/compiler-sfc/src/index.d.ts",
"compiler": {
"tsconfigFilePath": "../../api-extractor.tsconfig.json"
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "",
"publicTrimmedFilePath": "./dist/compiler-sfc.d.ts"
},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning",
"addToApiReportFile": true
},
"ae-missing-release-tag": {
"logLevel": "none"
},
"ae-internal-missing-underscore": {
"logLevel": "none"
},
"ae-forgotten-export": {
"logLevel": "none"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
},
"tsdoc-undefined-tag": {
"logLevel": "none"
}
}
}
}

View File

@ -8,23 +8,28 @@
"dist" "dist"
], ],
"dependencies": { "dependencies": {
"@babel/parser": "^7.16.4", "@babel/parser": "^7.18.4",
"source-map": "^0.6.1", "postcss": "^8.4.14",
"postcss": "^8.1.10" "source-map": "^0.6.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.18.4",
"@types/estree": "^0.0.48", "@types/estree": "^0.0.48",
"@babel/types": "^7.16.0", "@types/hash-sum": "^1.0.0",
"@types/lru-cache": "^5.1.0", "@types/lru-cache": "^5.1.1",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7",
"pug": "^3.0.1",
"sass": "^1.26.9",
"@vue/consolidate": "^0.17.3", "@vue/consolidate": "^0.17.3",
"de-indent": "^1.0.2",
"estree-walker": "^2.0.2",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"less": "^4.1.3",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"magic-string": "^0.25.9",
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"postcss-modules": "^4.0.0", "postcss-modules": "^4.3.1",
"postcss-selector-parser": "^6.0.4" "postcss-selector-parser": "^6.0.10",
"pug": "^3.0.2",
"sass": "^1.52.3",
"stylus": "^0.58.1",
"vue-template-es2015-compiler": "^1.9.1"
} }
} }

View File

@ -0,0 +1,143 @@
const postcss = require('postcss')
import { ProcessOptions, LazyResult } from 'postcss'
import trimPlugin from './stylePlugins/trim'
import scopedPlugin from './stylePlugins/scoped'
import {
processors,
StylePreprocessor,
StylePreprocessorResults
} from './stylePreprocessors'
export interface StyleCompileOptions {
source: string
filename: string
id: string
map?: any
scoped?: boolean
trim?: boolean
preprocessLang?: string
preprocessOptions?: any
postcssOptions?: any
postcssPlugins?: any[]
}
export interface AsyncStyleCompileOptions extends StyleCompileOptions {
isAsync?: boolean
}
export interface StyleCompileResults {
code: string
map: any | void
rawResult: LazyResult | void
errors: string[]
}
export function compileStyle(
options: StyleCompileOptions
): StyleCompileResults {
return doCompileStyle({ ...options, isAsync: false })
}
export function compileStyleAsync(
options: StyleCompileOptions
): Promise<StyleCompileResults> {
return Promise.resolve(doCompileStyle({ ...options, isAsync: true }))
}
export function doCompileStyle(
options: AsyncStyleCompileOptions
): StyleCompileResults {
const {
filename,
id,
scoped = true,
trim = true,
preprocessLang,
postcssOptions,
postcssPlugins
} = options
const preprocessor = preprocessLang && processors[preprocessLang]
const preProcessedSource = preprocessor && preprocess(options, preprocessor)
const map = preProcessedSource ? preProcessedSource.map : options.map
const source = preProcessedSource ? preProcessedSource.code : options.source
const plugins = (postcssPlugins || []).slice()
if (trim) {
plugins.push(trimPlugin())
}
if (scoped) {
plugins.push(scopedPlugin(id))
}
const postCSSOptions: ProcessOptions = {
...postcssOptions,
to: filename,
from: filename
}
if (map) {
postCSSOptions.map = {
inline: false,
annotation: false,
prev: map
}
}
let result, code, outMap
const errors: any[] = []
if (preProcessedSource && preProcessedSource.errors.length) {
errors.push(...preProcessedSource.errors)
}
try {
result = postcss(plugins).process(source, postCSSOptions)
// In async mode, return a promise.
if (options.isAsync) {
return result
.then(
(result: LazyResult): StyleCompileResults => ({
code: result.css || '',
map: result.map && result.map.toJSON(),
errors,
rawResult: result
})
)
.catch(
(error: Error): StyleCompileResults => ({
code: '',
map: undefined,
errors: [...errors, error.message],
rawResult: undefined
})
)
}
// force synchronous transform (we know we only have sync plugins)
code = result.css
outMap = result.map
} catch (e) {
errors.push(e)
}
return {
code: code || ``,
map: outMap && outMap.toJSON(),
errors,
rawResult: result
}
}
function preprocess(
options: StyleCompileOptions,
preprocessor: StylePreprocessor
): StylePreprocessorResults {
return preprocessor(
options.source,
options.map,
Object.assign(
{
filename: options.filename
},
options.preprocessOptions
)
)
}

View File

@ -0,0 +1,203 @@
import {
VueTemplateCompiler,
VueTemplateCompilerOptions,
ErrorWithRange
} from './types'
import assetUrlsModule, {
AssetURLOptions,
TransformAssetUrlsOptions
} from './templateCompilerModules/assetUrl'
import srcsetModule from './templateCompilerModules/srcset'
import consolidate from '@vue/consolidate'
import * as _compiler from 'web/entry-compiler'
import transpile from 'vue-template-es2015-compiler'
export interface TemplateCompileOptions {
source: string
filename: string
compiler?: VueTemplateCompiler
compilerOptions?: VueTemplateCompilerOptions
transformAssetUrls?: AssetURLOptions | boolean
transformAssetUrlsOptions?: TransformAssetUrlsOptions
preprocessLang?: string
preprocessOptions?: any
transpileOptions?: any
isProduction?: boolean
isFunctional?: boolean
optimizeSSR?: boolean
prettify?: boolean
}
export interface TemplateCompileResult {
ast: Object | undefined
code: string
source: string
tips: (string | ErrorWithRange)[]
errors: (string | ErrorWithRange)[]
}
export function compileTemplate(
options: TemplateCompileOptions
): TemplateCompileResult {
const { preprocessLang } = options
const preprocessor = preprocessLang && consolidate[preprocessLang]
if (preprocessor) {
return actuallyCompile(
Object.assign({}, options, {
source: preprocess(options, preprocessor)
})
)
} else if (preprocessLang) {
return {
ast: {},
code: `var render = function () {}\n` + `var staticRenderFns = []\n`,
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 actuallyCompile(options)
}
}
function preprocess(
options: TemplateCompileOptions,
preprocessor: any
): string {
const { source, filename, preprocessOptions } = options
const finalPreprocessOptions = Object.assign(
{
filename
},
preprocessOptions
)
// 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,
finalPreprocessOptions,
(_err: Error | null, _res: string) => {
if (_err) err = _err
res = _res
}
)
if (err) throw err
return res
}
function actuallyCompile(
options: TemplateCompileOptions
): TemplateCompileResult {
const {
source,
compiler = _compiler,
compilerOptions = {},
transpileOptions = {},
transformAssetUrls,
transformAssetUrlsOptions,
isProduction = process.env.NODE_ENV === 'production',
isFunctional = false,
optimizeSSR = false,
prettify = true
} = options
const compile =
optimizeSSR && compiler.ssrCompile ? compiler.ssrCompile : compiler.compile
let finalCompilerOptions = compilerOptions
if (transformAssetUrls) {
const builtInModules = [
transformAssetUrls === true
? assetUrlsModule(undefined, transformAssetUrlsOptions)
: assetUrlsModule(transformAssetUrls, transformAssetUrlsOptions),
srcsetModule(transformAssetUrlsOptions)
]
finalCompilerOptions = Object.assign({}, compilerOptions, {
modules: [...builtInModules, ...(compilerOptions.modules || [])],
filename: options.filename
})
}
const { ast, render, staticRenderFns, tips, errors } = compile(
source,
finalCompilerOptions
)
if (errors && errors.length) {
return {
ast,
code: `var render = function () {}\n` + `var staticRenderFns = []\n`,
source,
tips,
errors
}
} else {
// TODO better transpile
const finalTranspileOptions = Object.assign({}, transpileOptions, {
transforms: Object.assign({}, transpileOptions.transforms, {
stripWithFunctional: isFunctional
})
})
const toFunction = (code: string): string => {
return `function (${isFunctional ? `_h,_vm` : ``}) {${code}}`
}
// transpile code with vue-template-es2015-compiler, which is a forked
// version of Buble that applies ES2015 transforms + stripping `with` usage
let code =
transpile(
`var __render__ = ${toFunction(render)}\n` +
`var __staticRenderFns__ = [${staticRenderFns.map(toFunction)}]`,
finalTranspileOptions
) + `\n`
// #23 we use __render__ to avoid `render` not being prefixed by the
// transpiler when stripping with, but revert it back to `render` to
// maintain backwards compat
code = code.replace(/\s__(render|staticRenderFns)__\s/g, ' $1 ')
if (!isProduction) {
// mark with stripped (this enables Vue to use correct runtime proxy
// detection)
code += `render._withStripped = true`
if (prettify) {
try {
code = require('prettier').format(code, {
semi: false,
parser: 'babel'
})
} catch (e: any) {
if (e.code === 'MODULE_NOT_FOUND') {
tips.push(
'The `prettify` option is on, but the dependency `prettier` is not found.\n' +
'Please either turn off `prettify` or manually install `prettier`.'
)
}
tips.push(
`Failed to prettify component ${options.filename} template source after compilation.`
)
}
}
}
return {
ast,
code,
source,
tips,
errors
}
}
}

View File

@ -0,0 +1,12 @@
// API
export { parse } from './parse'
export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
// types
export { SFCBlock, SFCCustomBlock, SFCDescriptor } from './parseComponent'
export {
TemplateCompileOptions,
TemplateCompileResult
} from './compileTemplate'
export { StyleCompileOptions, StyleCompileResults } from './compileStyle'

View File

@ -0,0 +1,112 @@
import { SourceMapGenerator } from 'source-map'
import { RawSourceMap, VueTemplateCompiler } from './types'
import {
parseComponent,
VueTemplateCompilerParseOptions,
SFCDescriptor
} from './parseComponent'
import hash from 'hash-sum'
import LRU from 'lru-cache'
const cache = new LRU<string, SFCDescriptor>(100)
const splitRE = /\r?\n/g
const emptyRE = /^(?:\/\/)?\s*$/
export interface ParseOptions {
source: string
filename?: string
compiler?: VueTemplateCompiler
compilerParseOptions?: VueTemplateCompilerParseOptions
sourceRoot?: string
needMap?: boolean
}
export function parse(options: ParseOptions): SFCDescriptor {
const {
source,
filename = '',
compiler,
compilerParseOptions = { pad: 'line' } as VueTemplateCompilerParseOptions,
sourceRoot = '',
needMap = true
} = options
const cacheKey = hash(
filename + source + JSON.stringify(compilerParseOptions)
)
let output = cache.get(cacheKey)
if (output) {
return output
}
if (compiler) {
// user-provided compiler
output = compiler.parseComponent(source, compilerParseOptions)
} else {
// use built-in compiler
output = parseComponent(source, compilerParseOptions)
}
if (needMap) {
if (output.script && !output.script.src) {
output.script.map = generateSourceMap(
filename,
source,
output.script.content,
sourceRoot,
compilerParseOptions.pad
)
}
if (output.styles) {
output.styles.forEach(style => {
if (!style.src) {
style.map = generateSourceMap(
filename,
source,
style.content,
sourceRoot,
compilerParseOptions.pad
)
}
})
}
}
cache.set(cacheKey, output)
return output
}
function generateSourceMap(
filename: string,
source: string,
generated: string,
sourceRoot: string,
pad?: 'line' | 'space' | boolean
): RawSourceMap {
const map = new SourceMapGenerator({
file: filename.replace(/\\/g, '/'),
sourceRoot: sourceRoot.replace(/\\/g, '/')
})
let offset = 0
if (!pad) {
offset = source.split(generated).shift()!.split(splitRE).length - 1
}
map.setSourceContent(filename, source)
generated.split(splitRE).forEach((line, index) => {
if (!emptyRE.test(line)) {
map.addMapping({
source: filename,
original: {
line: index + 1 + offset,
column: 0
},
generated: {
line: index + 1,
column: 0
}
})
}
})
return JSON.parse(map.toString())
}

View File

@ -1,23 +1,49 @@
import deindent from 'de-indent' import deindent from 'de-indent'
import { parseHTML } from 'compiler/parser/html-parser' import { parseHTML } from 'compiler/parser/html-parser'
import { makeMap } from 'shared/util' import { makeMap } from 'shared/util'
import { import { ASTAttr, WarningMessage } from 'types/compiler'
ASTAttr, import { RawSourceMap } from './types'
SFCBlock,
SFCDescriptor,
WarningMessage
} from 'types/compiler'
const splitRE = /\r?\n/g const splitRE = /\r?\n/g
const replaceRE = /./g const replaceRE = /./g
const isSpecialTag = makeMap('script,style,template', true) const isSpecialTag = makeMap('script,style,template', true)
export interface SFCCustomBlock {
type: string
content: string
attrs: { [key: string]: string | true }
start: number
end?: number
map?: RawSourceMap
}
export interface SFCBlock extends SFCCustomBlock {
lang?: string
src?: string
scoped?: boolean
module?: string | boolean
}
export interface SFCDescriptor {
template: SFCBlock | null
script: SFCBlock | null
styles: SFCBlock[]
customBlocks: SFCCustomBlock[]
errors: WarningMessage[]
}
export interface VueTemplateCompilerParseOptions {
pad?: 'line' | 'space' | boolean
deindent?: boolean
outputSourceRange?: boolean
}
/** /**
* Parse a single-file component (*.vue) file into an SFC Descriptor Object. * Parse a single-file component (*.vue) file into an SFC Descriptor Object.
*/ */
export function parseComponent( export function parseComponent(
content: string, content: string,
options: Record<string, any> = {} options: VueTemplateCompilerParseOptions = {}
): SFCDescriptor { ): SFCDescriptor {
const sfc: SFCDescriptor = { const sfc: SFCDescriptor = {
template: null, template: null,
@ -48,7 +74,7 @@ export function parseComponent(
function start( function start(
tag: string, tag: string,
attrs: Array<ASTAttr>, attrs: ASTAttr[],
unary: boolean, unary: boolean,
start: number, start: number,
end: number end: number
@ -80,7 +106,7 @@ export function parseComponent(
} }
} }
function checkAttrs(block: SFCBlock, attrs: Array<ASTAttr>) { function checkAttrs(block: SFCBlock, attrs: ASTAttr[]) {
for (let i = 0; i < attrs.length; i++) { for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i] const attr = attrs[i]
if (attr.name === 'lang') { if (attr.name === 'lang') {

View File

@ -0,0 +1,207 @@
import { PluginCreator, Rule, AtRule } from 'postcss'
import selectorParser from 'postcss-selector-parser'
import { warn } from '../warn'
const animationNameRE = /^(-\w+-)?animation-name$/
const animationRE = /^(-\w+-)?animation$/
const scopedPlugin: PluginCreator<string> = (id = '') => {
const keyframes = Object.create(null)
const shortId = id.replace(/^data-v-/, '')
return {
postcssPlugin: 'vue-sfc-scoped',
Rule(rule) {
processRule(id, rule)
},
AtRule(node) {
if (
/-?keyframes$/.test(node.name) &&
!node.params.endsWith(`-${shortId}`)
) {
// register keyframes
keyframes[node.params] = node.params = node.params + '-' + shortId
}
},
OnceExit(root) {
if (Object.keys(keyframes).length) {
// If keyframes are found in this <style>, find and rewrite animation names
// in declarations.
// Caveat: this only works for keyframes and animation rules in the same
// <style> element.
// individual animation-name declaration
root.walkDecls(decl => {
if (animationNameRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => keyframes[v.trim()] || v.trim())
.join(',')
}
// shorthand
if (animationRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => {
const vals = v.trim().split(/\s+/)
const i = vals.findIndex(val => keyframes[val])
if (i !== -1) {
vals.splice(i, 1, keyframes[vals[i]])
return vals.join(' ')
} else {
return v
}
})
.join(',')
}
})
}
}
}
}
const processedRules = new WeakSet<Rule>()
function processRule(id: string, rule: Rule) {
if (
processedRules.has(rule) ||
(rule.parent &&
rule.parent.type === 'atrule' &&
/-?keyframes$/.test((rule.parent as AtRule).name))
) {
return
}
processedRules.add(rule)
rule.selector = selectorParser(selectorRoot => {
selectorRoot.each(selector => {
rewriteSelector(id, selector, selectorRoot)
})
}).processSync(rule.selector)
}
function rewriteSelector(
id: string,
selector: selectorParser.Selector,
selectorRoot: selectorParser.Root,
slotted = false
) {
let node: selectorParser.Node | null = null
let shouldInject = true
// find the last child node to insert attribute selector
selector.each(n => {
// DEPRECATED ">>>" and "/deep/" combinator
if (
n.type === 'combinator' &&
(n.value === '>>>' || n.value === '/deep/')
) {
n.value = ' '
n.spaces.before = n.spaces.after = ''
warn(
`the >>> and /deep/ combinators have been deprecated. ` +
`Use :deep() instead.`
)
return false
}
if (n.type === 'pseudo') {
const { value } = n
// deep: inject [id] attribute at the node before the ::v-deep
// combinator.
if (value === ':deep' || value === '::v-deep') {
if (n.nodes.length) {
// .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
// replace the current node with ::v-deep's inner selector
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// insert a space combinator before if it doesn't already have one
const prev = selector.at(selector.index(n) - 1)
if (!prev || !isSpaceCombinator(prev)) {
selector.insertAfter(
n,
selectorParser.combinator({
value: ' '
})
)
}
selector.removeChild(n)
} else {
// DEPRECATED usage
// .foo ::v-deep .bar -> .foo[xxxxxxx] .bar
warn(
`::v-deep usage as a combinator has ` +
`been deprecated. Use :deep(<inner-selector>) instead.`
)
const prev = selector.at(selector.index(n) - 1)
if (prev && isSpaceCombinator(prev)) {
selector.removeChild(prev)
}
selector.removeChild(n)
}
return false
}
// slot: use selector inside `::v-slotted` and inject [id + '-s']
// instead.
// ::v-slotted(.foo) -> .foo[xxxxxxx-s]
if (value === ':slotted' || value === '::v-slotted') {
rewriteSelector(id, n.nodes[0], selectorRoot, true /* slotted */)
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// selector.insertAfter(n, n.nodes[0])
selector.removeChild(n)
// since slotted attribute already scopes the selector there's no
// need for the non-slot attribute.
shouldInject = false
return false
}
// global: replace with inner selector and do not inject [id].
// ::v-global(.foo) -> .foo
if (value === ':global' || value === '::v-global') {
selectorRoot.insertAfter(selector, n.nodes[0])
selectorRoot.removeChild(selector)
return false
}
}
if (n.type !== 'pseudo' && n.type !== 'combinator') {
node = n
}
})
if (node) {
;(node as selectorParser.Node).spaces.after = ''
} else {
// For deep selectors & standalone pseudo selectors,
// the attribute selectors are prepended rather than appended.
// So all leading spaces must be eliminated to avoid problems.
selector.first.spaces.before = ''
}
if (shouldInject) {
const idToAdd = slotted ? id + '-s' : id
selector.insertAfter(
// If node is null it means we need to inject [id] at the start
// insertAfter can handle `null` here
node as any,
selectorParser.attribute({
attribute: idToAdd,
value: idToAdd,
raws: {},
quoteMark: `"`
})
)
}
}
function isSpaceCombinator(node: selectorParser.Node) {
return node.type === 'combinator' && /^\s+$/.test(node.value)
}
scopedPlugin.postcss = true
export default scopedPlugin

View File

@ -0,0 +1,18 @@
import { PluginCreator } from 'postcss'
const trimPlugin: PluginCreator<{}> = () => {
return {
postcssPlugin: 'vue-sfc-trim',
Once(root) {
root.walk(({ type, raws }) => {
if (type === 'rule' || type === 'atrule') {
if (raws.before) raws.before = '\n'
if ('after' in raws && raws.after) raws.after = '\n'
}
})
}
}
}
trimPlugin.postcss = true
export default trimPlugin

View File

@ -0,0 +1,135 @@
import merge from 'merge-source-map'
import { RawSourceMap } from 'source-map'
import { isFunction } from 'shared/util'
export type StylePreprocessor = (
source: string,
map: RawSourceMap | undefined,
options: {
[key: string]: any
additionalData?: string | ((source: string, filename: string) => string)
filename: string
}
) => StylePreprocessorResults
export interface StylePreprocessorResults {
code: string
map?: object
errors: Error[]
dependencies: string[]
}
// .scss/.sass processor
const scss: StylePreprocessor = (source, map, options) => {
const nodeSass = require('sass')
const finalOptions = {
...options,
data: getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
sourceMap: !!map
}
try {
const result = nodeSass.renderSync(finalOptions)
const dependencies = result.stats.includedFiles
if (map) {
return {
code: result.css.toString(),
map: merge(map, JSON.parse(result.map.toString())),
errors: [],
dependencies
}
}
return { code: result.css.toString(), errors: [], dependencies }
} catch (e: any) {
return { code: '', errors: [e], dependencies: [] }
}
}
const sass: StylePreprocessor = (source, map, options) =>
scss(source, map, {
...options,
indentedSyntax: true
})
// .less
const less: StylePreprocessor = (source, map, options) => {
const nodeLess = require('less')
let result: any
let error: Error | null = null
nodeLess.render(
getSource(source, options.filename, options.additionalData),
{ ...options, syncImport: true },
(err: Error | null, output: any) => {
error = err
result = output
}
)
if (error) return { code: '', errors: [error], dependencies: [] }
const dependencies = result.imports
if (map) {
return {
code: result.css.toString(),
map: merge(map, result.map),
errors: [],
dependencies: dependencies
}
}
return {
code: result.css.toString(),
errors: [],
dependencies: dependencies
}
}
// .styl
const styl: StylePreprocessor = (source, map, options) => {
const nodeStylus = require('stylus')
try {
const ref = nodeStylus(source)
Object.keys(options).forEach(key => ref.set(key, options[key]))
if (map) ref.set('sourcemap', { inline: false, comment: false })
const result = ref.render()
const dependencies = ref.deps()
if (map) {
return {
code: result,
map: merge(map, ref.sourcemap),
errors: [],
dependencies
}
}
return { code: result, errors: [], dependencies }
} catch (e: any) {
return { code: '', errors: [e], dependencies: [] }
}
}
function getSource(
source: string,
filename: string,
additionalData?: string | ((source: string, filename: string) => string)
) {
if (!additionalData) return source
if (isFunction(additionalData)) {
return additionalData(source, filename)
}
return additionalData + source
}
export type PreprocessLang = 'less' | 'sass' | 'scss' | 'styl' | 'stylus'
export const processors: Record<PreprocessLang, StylePreprocessor> = {
less,
sass,
scss,
styl,
stylus: styl
}

View File

@ -0,0 +1,76 @@
// vue compiler module for transforming `<tag>:<attribute>` to `require`
import { urlToRequire, ASTNode, Attr } from './utils'
export interface AssetURLOptions {
[name: string]: string | string[]
}
export interface TransformAssetUrlsOptions {
/**
* If base is provided, instead of transforming relative asset urls into
* imports, they will be directly rewritten to absolute urls.
*/
base?: string
}
const defaultOptions: AssetURLOptions = {
audio: 'src',
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href']
}
export default (
userOptions?: AssetURLOptions,
transformAssetUrlsOption?: TransformAssetUrlsOptions
) => {
const options = userOptions
? Object.assign({}, defaultOptions, userOptions)
: defaultOptions
return {
postTransformNode: (node: ASTNode) => {
transform(node, options, transformAssetUrlsOption)
}
}
}
function transform(
node: ASTNode,
options: AssetURLOptions,
transformAssetUrlsOption?: TransformAssetUrlsOptions
) {
for (const tag in options) {
if ((tag === '*' || node.tag === tag) && node.attrs) {
const attributes = options[tag]
if (typeof attributes === 'string') {
node.attrs.some(attr =>
rewrite(attr, attributes, transformAssetUrlsOption)
)
} else if (Array.isArray(attributes)) {
attributes.forEach(item =>
node.attrs.some(attr => rewrite(attr, item, transformAssetUrlsOption))
)
}
}
}
}
function rewrite(
attr: Attr,
name: string,
transformAssetUrlsOption?: TransformAssetUrlsOptions
) {
if (attr.name === name) {
const value = attr.value
// only transform static URLs
if (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') {
attr.value = urlToRequire(value.slice(1, -1), transformAssetUrlsOption)
return true
}
}
return false
}

View File

@ -0,0 +1,73 @@
// vue compiler module for transforming `img:srcset` to a number of `require`s
import { urlToRequire, ASTNode } from './utils'
import { TransformAssetUrlsOptions } from './assetUrl'
interface ImageCandidate {
require: string
descriptor: string
}
export default (transformAssetUrlsOptions?: TransformAssetUrlsOptions) => ({
postTransformNode: (node: ASTNode) => {
transform(node, transformAssetUrlsOptions)
}
})
// http://w3c.github.io/html/semantics-embedded-content.html#ref-for-image-candidate-string-5
const escapedSpaceCharacters = /( |\\t|\\n|\\f|\\r)+/g
function transform(
node: ASTNode,
transformAssetUrlsOptions?: TransformAssetUrlsOptions
) {
const tags = ['img', 'source']
if (tags.indexOf(node.tag) !== -1 && node.attrs) {
node.attrs.forEach(attr => {
if (attr.name === 'srcset') {
// same logic as in transform-require.js
const value = attr.value
const isStatic =
value.charAt(0) === '"' && value.charAt(value.length - 1) === '"'
if (!isStatic) {
return
}
const imageCandidates: ImageCandidate[] = value
.substr(1, value.length - 2)
.split(',')
.map(s => {
// The attribute value arrives here with all whitespace, except
// normal spaces, represented by escape sequences
const [url, descriptor] = s
.replace(escapedSpaceCharacters, ' ')
.trim()
.split(' ', 2)
return {
require: urlToRequire(url, transformAssetUrlsOptions),
descriptor
}
})
// "require(url1)"
// "require(url1) 1x"
// "require(url1), require(url2)"
// "require(url1), require(url2) 2x"
// "require(url1) 1x, require(url2)"
// "require(url1) 1x, require(url2) 2x"
const code = imageCandidates
.map(
({ require, descriptor }) =>
`${require} + "${descriptor ? ' ' + descriptor : ''}, " + `
)
.join('')
.slice(0, -6)
.concat('"')
.replace(/ \+ ""$/, '')
attr.value = code
}
})
}
}

View File

@ -0,0 +1,74 @@
import { TransformAssetUrlsOptions } from './assetUrl'
import { UrlWithStringQuery, parse as uriParse } from 'url'
import path from 'path'
export interface Attr {
name: string
value: string
}
export interface ASTNode {
tag: string
attrs: Attr[]
}
export function urlToRequire(
url: string,
transformAssetUrlsOption: TransformAssetUrlsOptions = {}
): string {
const returnValue = `"${url}"`
// same logic as in transform-require.js
const firstChar = url.charAt(0)
if (firstChar === '~') {
const secondChar = url.charAt(1)
url = url.slice(secondChar === '/' ? 2 : 1)
}
const uriParts = parseUriParts(url)
if (transformAssetUrlsOption.base) {
// explicit base - directly rewrite the url into absolute url
// does not apply to absolute urls or urls that start with `@`
// since they are aliases
if (firstChar === '.' || firstChar === '~') {
// when packaged in the browser, path will be using the posix-
// only version provided by rollup-plugin-node-builtins.
return `"${(path.posix || path).join(
transformAssetUrlsOption.base,
uriParts.path + (uriParts.hash || '')
)}"`
}
return returnValue
}
if (firstChar === '.' || firstChar === '~' || firstChar === '@') {
if (!uriParts.hash) {
return `require("${url}")`
} else {
// support uri fragment case by excluding it from
// the require and instead appending it as string;
// assuming that the path part is sufficient according to
// the above caseing(t.i. no protocol-auth-host parts expected)
return `require("${uriParts.path}") + "${uriParts.hash}"`
}
}
return returnValue
}
/**
* vuejs/component-compiler-utils#22 Support uri fragment in transformed require
* @param urlString an url as a string
*/
function parseUriParts(urlString: string): UrlWithStringQuery {
// initialize return value
const returnValue: UrlWithStringQuery = uriParse('')
if (urlString) {
// A TypeError is thrown if urlString is not a string
// @see https://nodejs.org/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost
if ('string' === typeof urlString) {
// check is an uri
return uriParse(urlString) // take apart the uri
}
}
return returnValue
}

View File

@ -0,0 +1,52 @@
import { SFCDescriptor } from './parseComponent'
export interface StartOfSourceMap {
file?: string
sourceRoot?: string
}
export interface RawSourceMap extends StartOfSourceMap {
version: string
sources: string[]
names: string[]
sourcesContent?: string[]
mappings: string
}
export interface VueTemplateCompiler {
parseComponent(source: string, options?: any): SFCDescriptor
compile(
template: string,
options: VueTemplateCompilerOptions
): VueTemplateCompilerResults
ssrCompile(
template: string,
options: VueTemplateCompilerOptions
): VueTemplateCompilerResults
}
// we'll just shim this much for now - in the future these types
// should come from vue-template-compiler directly, or this package should be
// part of the vue monorepo.
export interface VueTemplateCompilerOptions {
modules?: Object[]
outputSourceRange?: boolean
whitespace?: 'preserve' | 'condense'
directives?: { [key: string]: Function }
}
export interface ErrorWithRange {
msg: string
start: number
end: number
}
export interface VueTemplateCompilerResults {
ast: Object | undefined
render: string
staticRenderFns: string[]
errors: (string | ErrorWithRange)[]
tips: (string | ErrorWithRange)[]
}

View File

@ -0,0 +1,16 @@
const hasWarned: Record<string, boolean> = {}
export function warnOnce(msg: string) {
const isNodeProd =
typeof process !== 'undefined' && process.env.NODE_ENV === 'production'
if (!isNodeProd && !hasWarned[msg]) {
hasWarned[msg] = true
warn(msg)
}
}
export function warn(msg: string) {
console.warn(
`\x1b[1m\x1b[33m[@vue/compiler-sfc]\x1b[0m\x1b[33m ${msg}\x1b[0m\n`
)
}

View File

@ -0,0 +1,203 @@
import { parse } from '../src/parse'
import { compileStyle, compileStyleAsync } from '../src/compileStyle'
test.only('preprocess less', () => {
const style = parse({
source:
'<style lang="less">\n' +
'@red: rgb(255, 0, 0);\n' +
'.color { color: @red; }\n' +
'</style>\n',
filename: 'example.vue',
needMap: true
}).styles[0]
const result = compileStyle({
id: 'v-scope-xxx',
filename: 'example.vue',
source: style.content,
map: style.map,
scoped: false,
preprocessLang: style.lang
})
expect(result.errors.length).toBe(0)
expect(result.code).toEqual(expect.stringContaining('color: #ff0000;'))
expect(result.map).toBeTruthy()
})
test('preprocess scss', () => {
const style = parse({
source:
'<style lang="scss">\n' +
'$red: rgb(255, 0, 0);\n' +
'.color { color: $red; }\n' +
'</style>\n',
filename: 'example.vue',
needMap: true
}).styles[0]
const result = compileStyle({
id: 'v-scope-xxx',
filename: 'example.vue',
source: style.content,
map: style.map,
scoped: false,
preprocessLang: style.lang
})
expect(result.errors.length).toBe(0)
expect(result.code).toEqual(expect.stringContaining('color: red;'))
expect(result.map).toBeTruthy()
})
test('preprocess sass', () => {
const style = parse({
source:
'<style lang="sass">\n' +
'$red: rgb(255, 0, 0)\n' +
'.color\n' +
' color: $red\n' +
'</style>\n',
filename: 'example.vue',
needMap: true
}).styles[0]
const result = compileStyle({
id: 'v-scope-xxx',
filename: 'example.vue',
source: style.content,
map: style.map,
scoped: false,
preprocessLang: style.lang
})
expect(result.errors.length).toBe(0)
expect(result.code).toEqual(expect.stringContaining('color: red;'))
expect(result.map).toBeTruthy()
})
test('preprocess stylus', () => {
const style = parse({
source:
'<style lang="styl">\n' +
'red-color = rgb(255, 0, 0);\n' +
'.color\n' +
' color: red-color\n' +
'</style>\n',
filename: 'example.vue',
needMap: true
}).styles[0]
const result = compileStyle({
id: 'v-scope-xxx',
filename: 'example.vue',
source: style.content,
map: style.map,
scoped: false,
preprocessLang: style.lang
})
expect(result.errors.length).toBe(0)
expect(result.code).toEqual(expect.stringContaining('color: #f00;'))
expect(result.map).toBeTruthy()
})
test('custom postcss plugin', () => {
const spy = vi.fn()
compileStyle({
id: 'v-scope-xxx',
filename: 'example.vue',
source: '.foo { color: red }',
scoped: false,
postcssPlugins: [require('postcss').plugin('test-plugin', () => spy)()]
})
expect(spy).toHaveBeenCalled()
})
test('custom postcss options', () => {
const result = compileStyle({
id: 'v-scope-xxx',
filename: 'example.vue',
source: '.foo { color: red }',
scoped: false,
postcssOptions: { random: 'foo' }
})
expect((result.rawResult as any).opts.random).toBe('foo')
})
test('async postcss plugin in sync mode', () => {
const result = compileStyle({
id: 'v-scope-xxx',
filename: 'example.vue',
source: '.foo { color: red }',
scoped: false,
postcssPlugins: [
require('postcss').plugin(
'test-plugin',
() => async (result: any) => result
)
]
})
expect(result.errors).toHaveLength(1)
})
test('async postcss plugin', async () => {
const promise = compileStyleAsync({
id: 'v-scope-xxx',
filename: 'example.vue',
source: '.foo { color: red }',
scoped: false,
postcssPlugins: [
require('postcss').plugin(
'test-plugin',
() => async (result: any) => result
)
]
})
expect(promise instanceof Promise).toBe(true)
const result = await promise
expect(result.errors).toHaveLength(0)
expect(result.code).toEqual(expect.stringContaining('color: red'))
})
test('media query', () => {
const result = compileStyle({
id: 'v-scope-xxx',
scoped: true,
filename: 'example.vue',
source: `
@media print {
.foo {
color: #000;
}
}`
})
expect(result.errors).toHaveLength(0)
expect(result.code).toContain(
'@media print {\n.foo[v-scope-xxx] {\n color: #000;\n}\n}'
)
})
test('supports query', () => {
const result = compileStyle({
id: 'v-scope-xxx',
scoped: true,
filename: 'example.vue',
source: `
@supports ( color: #000 ) {
.foo {
color: #000;
}
}`
})
expect(result.errors).toHaveLength(0)
expect(result.code).toContain(
'@supports ( color: #000 ) {\n.foo[v-scope-xxx] {\n color: #000;\n}\n}'
)
})

View File

@ -0,0 +1,230 @@
import { parse } from '../src/parse'
import { SFCBlock } from '../src/parseComponent'
import { compileTemplate } from '../src/compileTemplate'
import Vue from 'vue'
function mockRender(code: string, mocks: Record<string, any> = {}) {
console.log(code)
const fn = new Function(
`require`,
`${code}; return { render, staticRenderFns }`
)
const vm = new Vue(
Object.assign(
{},
fn((id: string) => mocks[id])
)
)
vm.$mount()
return (vm as any)._vnode
}
test('should work', () => {
const source = `<div><p>{{ render }}</p></div>`
const result = compileTemplate({
filename: 'example.vue',
source
})
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
// should expose render fns
expect(result.code).toMatch(`var render = function`)
expect(result.code).toMatch(`var staticRenderFns = []`)
// should mark with stripped
expect(result.code).toMatch(`render._withStripped = true`)
// should prefix bindings
expect(result.code).toMatch(`_vm.render`)
expect(result.ast).not.toBeUndefined()
})
test('preprocess pug', () => {
const template = parse({
source:
'<template lang="pug">\n' +
'body\n' +
' h1 Pug Examples\n' +
' div.container\n' +
' p Cool Pug example!\n' +
'</template>\n',
filename: 'example.vue',
needMap: true
}).template as SFCBlock
const result = compileTemplate({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang
})
expect(result.errors.length).toBe(0)
})
/**
* vuejs/component-compiler-utils#22 Support uri fragment in transformed require
*/
test('supports uri fragment in transformed require', () => {
const source = '<svg>\
<use href="~@svg/file.svg#fragment"></use>\
</svg>' //
const result = compileTemplate({
filename: 'svgparticle.html',
source: source,
transformAssetUrls: {
use: 'href'
}
})
expect(result.errors.length).toBe(0)
expect(result.code).toMatch(
/href: require\("@svg\/file.svg"\) \+ "#fragment"/
)
})
/**
* vuejs/component-compiler-utils#22 Support uri fragment in transformed require
*/
test('when too short uri then empty require', () => {
const source = '<svg>\
<use href="~"></use>\
</svg>' //
const result = compileTemplate({
filename: 'svgparticle.html',
source: source,
transformAssetUrls: {
use: 'href'
}
})
expect(result.errors.length).toBe(0)
expect(result.code).toMatch(/href: require\(""\)/)
})
test('warn missing preprocessor', () => {
const template = parse({
source: '<template lang="unknownLang">\n' + '</template>\n',
filename: 'example.vue',
needMap: true
}).template as SFCBlock
const result = compileTemplate({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang
})
expect(result.errors.length).toBe(1)
})
test.only('transform assetUrls', () => {
const source = `
<div>
<img src="./logo.png">
<img src="~fixtures/logo.png">
<img src="~/fixtures/logo.png">
</div>
`
const result = compileTemplate({
filename: 'example.vue',
source,
transformAssetUrls: true
})
expect(result.errors.length).toBe(0)
const vnode = mockRender(result.code, {
'./logo.png': 'a',
'fixtures/logo.png': 'b'
})
expect(vnode.children[0].data.attrs.src).toBe('a')
expect(vnode.children[2].data.attrs.src).toBe('b')
expect(vnode.children[4].data.attrs.src).toBe('b')
})
test('transform srcset', () => {
// TODO:
const source = `
<div>
<img src="./logo.png">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
<image xlink:href="./logo.png" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
<use xlink:href="./logo.png"/>
</svg>
</svg>
<img src="./logo.png" srcset="./logo.png">
<img src="./logo.png" srcset="./logo.png 2x">
<img src="./logo.png" srcset="./logo.png, ./logo.png 2x">
<img src="./logo.png" srcset="./logo.png 2x, ./logo.png">
<img src="./logo.png" srcset="./logo.png 2x, ./logo.png 3x">
<img src="./logo.png" srcset="./logo.png, ./logo.png 2x, ./logo.png 3x">
<img
src="./logo.png"
srcset="
./logo.png 2x,
./logo.png 3x
">
</div>
`
const result = compileTemplate({
filename: 'example.vue',
source,
transformAssetUrls: true
})
expect(result.errors.length).toBe(0)
const vnode = mockRender(result.code, {
'./logo.png': 'test-url'
})
// img tag
expect(vnode.children[0].data.attrs.src).toBe('test-url')
// image tag (SVG)
expect(vnode.children[2].children[0].data.attrs['xlink:href']).toBe(
'test-url'
)
// use tag (SVG)
expect(vnode.children[4].children[0].data.attrs['xlink:href']).toBe(
'test-url'
)
// image tag with srcset
expect(vnode.children[6].data.attrs.srcset).toBe('test-url')
expect(vnode.children[8].data.attrs.srcset).toBe('test-url 2x')
// image tag with multiline srcset
expect(vnode.children[10].data.attrs.srcset).toBe('test-url, test-url 2x')
expect(vnode.children[12].data.attrs.srcset).toBe('test-url 2x, test-url')
expect(vnode.children[14].data.attrs.srcset).toBe('test-url 2x, test-url 3x')
expect(vnode.children[16].data.attrs.srcset).toBe(
'test-url, test-url 2x, test-url 3x'
)
expect(vnode.children[18].data.attrs.srcset).toBe('test-url 2x, test-url 3x')
})
test('transform assetUrls and srcset with base option', () => {
const source = `
<div>
<img src="./logo.png">
<img src="~fixtures/logo.png">
<img src="~/fixtures/logo.png">
<img src="./logo.png" srcset="./logo.png 2x, ./logo.png 3x">
</div>
`
const result = compileTemplate({
filename: 'example.vue',
source,
transformAssetUrls: true,
transformAssetUrlsOptions: { base: '/base/' }
})
expect(result.errors.length).toBe(0)
const vnode = mockRender(result.code)
expect(vnode.children[0].data.attrs.src).toBe('/base/logo.png')
expect(vnode.children[2].data.attrs.src).toBe('/base/fixtures/logo.png')
expect(vnode.children[4].data.attrs.src).toBe('/base/fixtures/logo.png')
expect(vnode.children[6].data.attrs.srcset).toBe(
'/base/logo.png 2x, /base/logo.png 3x'
)
})

View File

@ -1,4 +1,5 @@
import { parseComponent } from 'sfc/parser' import { WarningMessage } from 'types/compiler'
import { parseComponent } from '../src/parseComponent'
describe('Single File Component parser', () => { describe('Single File Component parser', () => {
it('should parse', () => { it('should parse', () => {

View File

@ -0,0 +1,137 @@
import { compileStyle } from '../src/compileStyle'
// vue-loader/#1370
test('spaces after selector', () => {
const { code } = compileStyle({
source: `.foo , .bar { color: red; }`,
filename: 'test.css',
id: 'test'
})
expect(code).toMatch(`.foo[test], .bar[test] { color: red;`)
})
test('leading deep selector', () => {
const { code } = compileStyle({
source: `>>> .foo { color: red; }`,
filename: 'test.css',
id: 'test'
})
expect(code).toMatch(`[test] .foo { color: red;`)
})
test('scoped css', () => {
const { code: style } = compileStyle({
id: 'v-scope-xxx',
scoped: true,
filename: 'example.vue',
source: `
.test {
color: yellow;
}
.test:after {
content: 'bye!';
}
h1 {
color: green;
}
.anim {
animation: color 5s infinite, other 5s;
}
.anim-2 {
animation-name: color;
animation-duration: 5s;
}
.anim-3 {
animation: 5s color infinite, 5s other;
}
.anim-multiple {
animation: color 5s infinite, opacity 2s;
}
.anim-multiple-2 {
animation-name: color, opacity;
animation-duration: 5s, 2s;
}
@keyframes color {
from { color: red; }
to { color: green; }
}
@-webkit-keyframes color {
from { color: red; }
to { color: green; }
}
@keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
.foo p >>> .bar {
color: red;
}
.foo div /deep/ .bar {
color: red;
}
.foo span ::v-deep .bar {
color: red;
}
`
})
expect(style).toContain(`.test[v-scope-xxx] {\n color: yellow;\n}`)
expect(style).toContain(`.test[v-scope-xxx]:after {\n content: \'bye!\';\n}`)
expect(style).toContain(`h1[v-scope-xxx] {\n color: green;\n}`)
// scoped keyframes
expect(style).toContain(
`.anim[v-scope-xxx] {\n animation: color-v-scope-xxx 5s infinite, other 5s;`
)
expect(style).toContain(
`.anim-2[v-scope-xxx] {\n animation-name: color-v-scope-xxx`
)
expect(style).toContain(
`.anim-3[v-scope-xxx] {\n animation: 5s color-v-scope-xxx infinite, 5s other;`
)
expect(style).toContain(`@keyframes color-v-scope-xxx {`)
expect(style).toContain(`@-webkit-keyframes color-v-scope-xxx {`)
expect(style).toContain(
`.anim-multiple[v-scope-xxx] {\n animation: color-v-scope-xxx 5s infinite,opacity-v-scope-xxx 2s;`
)
expect(style).toContain(
`.anim-multiple-2[v-scope-xxx] {\n animation-name: color-v-scope-xxx,opacity-v-scope-xxx;`
)
expect(style).toContain(`@keyframes opacity-v-scope-xxx {`)
expect(style).toContain(`@-webkit-keyframes opacity-v-scope-xxx {`)
// >>> combinator
expect(style).toContain(`.foo p[v-scope-xxx] .bar {\n color: red;\n}`)
// /deep/ alias for >>>
expect(style).toContain(`.foo div[v-scope-xxx] .bar {\n color: red;\n}`)
// ::-v-deep alias for >>>
expect(style).toContain(`.foo span[v-scope-xxx] .bar {\n color: red;\n}`)
})
test('pseudo element', () => {
const { code } = compileStyle({
source: '::selection { display: none; }',
filename: 'test.css',
id: 'test'
})
expect(code).toContain('[test]::selection {')
})
test('spaces before pseudo element', () => {
const { code } = compileStyle({
source: '.abc, ::selection { color: red; }',
filename: 'test.css',
id: 'test'
})
expect(code).toContain('.abc[test],')
expect(code).toContain('[test]::selection {')
})

View File

@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"types": ["node", "vitest/globals"]
},
"include": ["../src", "."]
}

View File

@ -24,7 +24,7 @@
"url": "https://github.com/vuejs/vue/issues" "url": "https://github.com/vuejs/vue/issues"
}, },
"dependencies": { "dependencies": {
"chalk": "^4.0.0", "chalk": "^4.1.2",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"he": "^1.2.0", "he": "^1.2.0",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",

View File

@ -26,8 +26,8 @@
}, },
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/vue-template-compiler#readme", "homepage": "https://github.com/vuejs/vue/tree/dev/packages/vue-template-compiler#readme",
"dependencies": { "dependencies": {
"he": "^1.2.0", "de-indent": "^1.0.2",
"de-indent": "^1.0.2" "he": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"vue": "file:../.." "vue": "file:../.."

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,12 @@ const resolve = p => {
} }
} }
// we are bundling forked consolidate.js in compiler-sfc which dynamically
// requires a ton of template engines which should be ignored.
const consolidatePath = require.resolve('@vue/consolidate/package.json', {
paths: [path.resolve(__dirname, '../packages/compiler-sfc')]
})
const builds = { const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'runtime-cjs-dev': { 'runtime-cjs-dev': {
@ -202,12 +208,27 @@ const builds = {
) )
}, },
'compiler-sfc': { 'compiler-sfc': {
entry: resolve('web/entry-compiler-sfc.ts'), entry: resolve('packages/compiler-sfc/src/index.ts'),
dest: resolve('packages/compiler-sfc/dist/compiler-sfc.js'), dest: resolve('packages/compiler-sfc/dist/compiler-sfc.js'),
format: 'cjs', format: 'cjs',
external: Object.keys( external: Object.keys(
require('../packages/compiler-sfc/package.json').dependencies require('../packages/compiler-sfc/package.json').dependencies
) ),
plugins: [
node({ preferBuiltins: true }),
cjs({
ignore: [
...Object.keys(require(consolidatePath).devDependencies),
'vm',
'crypto',
'react-dom/server',
'teacup/lib/express',
'arc-templates/dist/es5',
'then-pug',
'then-jade'
]
})
]
} }
} }

View File

@ -1 +0,0 @@
export function compile() {}

View File

@ -1,4 +1,4 @@
export { parseComponent } from 'sfc/parser' export { parseComponent } from 'sfc/parseComponent'
export { compile, compileToFunctions } from './compiler/index' export { compile, compileToFunctions } from './compiler/index'
export { ssrCompile, ssrCompileToFunctions } from 'server/compiler' export { ssrCompile, ssrCompileToFunctions } from 'server/compiler'
export { generateCodeFrame } from 'compiler/codeframe' export { generateCodeFrame } from 'compiler/codeframe'

View File

@ -199,26 +199,3 @@ export type ASTText = {
start?: number start?: number
end?: number end?: number
} }
// SFC-parser related declarations
// an object format describing a single-file component
export type SFCDescriptor = {
template: SFCBlock | null
script: SFCBlock | null
styles: Array<SFCBlock>
customBlocks: Array<SFCBlock>
errors: Array<string | WarningMessage>
}
export type SFCBlock = {
type: string
content: string
attrs: { [attribute: string]: string }
start?: number
end?: number
lang?: string
src?: string
scoped?: boolean
module?: string | boolean
}