mirror of
https://github.com/vuejs/vue.git
synced 2024-11-21 12:18:54 +00:00
wip: port @vue/component-compiler-utils
This commit is contained in:
parent
50f2870ff0
commit
06594f68b7
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
- name: Run unit tests
|
||||
run: pnpm run test:unit
|
||||
|
||||
ssr-test:
|
||||
ssr-sfc-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -45,6 +45,9 @@ jobs:
|
||||
- name: Run SSR tests
|
||||
run: pnpm run test:ssr
|
||||
|
||||
- name: Run compiler-sfc tests
|
||||
run: pnpm run test:sfc
|
||||
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
@ -3,6 +3,5 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./temp",
|
||||
"types": []
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
}
|
||||
|
37
package.json
37
package.json
@ -41,10 +41,11 @@
|
||||
"dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:compiler ",
|
||||
"build": "node scripts/build.js",
|
||||
"build:ssr": "npm run build -- runtime-cjs,server-renderer",
|
||||
"build:types": "rimraf temp && tsc --declaration --emitDeclarationOnly --outDir temp && api-extractor run",
|
||||
"test": "npm run ts-check && npm run test:types && npm run test:unit && npm run test:e2e && npm run test:ssr",
|
||||
"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 && npm run test:sfc",
|
||||
"test:unit": "vitest run test/unit",
|
||||
"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:transition": "karma start test/transition/karma.conf.js",
|
||||
"test:types": "npm run build:types && tsc -p ./types/tsconfig.json",
|
||||
@ -85,45 +86,45 @@
|
||||
"csstype": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "^7.24.2",
|
||||
"@microsoft/api-extractor": "^7.25.0",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@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",
|
||||
"@types/he": "^1.1.2",
|
||||
"@types/node": "^17.0.30",
|
||||
"chalk": "^4.0.0",
|
||||
"@types/node": "^17.0.41",
|
||||
"chalk": "^4.1.2",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"de-indent": "^1.0.2",
|
||||
"enquirer": "^2.3.6",
|
||||
"esbuild": "^0.14.39",
|
||||
"execa": "^4.0.0",
|
||||
"esbuild": "^0.14.43",
|
||||
"execa": "^4.1.0",
|
||||
"he": "^1.2.0",
|
||||
"jasmine-core": "^4.1.1",
|
||||
"jasmine-core": "^4.2.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"karma": "^6.3.20",
|
||||
"karma-chrome-launcher": "^3.1.1",
|
||||
"karma-cli": "^2.0.0",
|
||||
"karma-esbuild": "^2.2.4",
|
||||
"karma-jasmine": "^5.0.1",
|
||||
"lint-staged": "^12.4.1",
|
||||
"lint-staged": "^12.5.0",
|
||||
"postcss": "^8.4.14",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^4.0.6",
|
||||
"marked": "^4.0.16",
|
||||
"minimist": "^1.2.6",
|
||||
"prettier": "^2.6.2",
|
||||
"puppeteer": "^14.1.1",
|
||||
"puppeteer": "^14.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup": "^2.75.6",
|
||||
"rollup-plugin-typescript2": "^0.31.2",
|
||||
"semver": "^7.3.7",
|
||||
"shelljs": "^0.8.5",
|
||||
"terser": "^5.13.1",
|
||||
"terser": "^5.14.0",
|
||||
"todomvc-app-css": "^2.4.2",
|
||||
"ts-node": "^10.7.0",
|
||||
"ts-node": "^10.8.1",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vitest": "^0.12.6",
|
||||
"typescript": "^4.7.3",
|
||||
"vitest": "^0.12.10",
|
||||
"yorkie": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
64
packages/compiler-sfc/api-extractor.json
Normal file
64
packages/compiler-sfc/api-extractor.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,23 +8,28 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"source-map": "^0.6.1",
|
||||
"postcss": "^8.1.10"
|
||||
"@babel/parser": "^7.18.4",
|
||||
"postcss": "^8.4.14",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.18.4",
|
||||
"@types/estree": "^0.0.48",
|
||||
"@babel/types": "^7.16.0",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7",
|
||||
"pug": "^3.0.1",
|
||||
"sass": "^1.26.9",
|
||||
"@types/hash-sum": "^1.0.0",
|
||||
"@types/lru-cache": "^5.1.1",
|
||||
"@vue/consolidate": "^0.17.3",
|
||||
"de-indent": "^1.0.2",
|
||||
"estree-walker": "^2.0.2",
|
||||
"hash-sum": "^2.0.0",
|
||||
"less": "^4.1.3",
|
||||
"lru-cache": "^5.1.1",
|
||||
"magic-string": "^0.25.9",
|
||||
"merge-source-map": "^1.1.0",
|
||||
"postcss-modules": "^4.0.0",
|
||||
"postcss-selector-parser": "^6.0.4"
|
||||
"postcss-modules": "^4.3.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
143
packages/compiler-sfc/src/compileStyle.ts
Normal file
143
packages/compiler-sfc/src/compileStyle.ts
Normal 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
|
||||
)
|
||||
)
|
||||
}
|
203
packages/compiler-sfc/src/compileTemplate.ts
Normal file
203
packages/compiler-sfc/src/compileTemplate.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
12
packages/compiler-sfc/src/index.ts
Normal file
12
packages/compiler-sfc/src/index.ts
Normal 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'
|
112
packages/compiler-sfc/src/parse.ts
Normal file
112
packages/compiler-sfc/src/parse.ts
Normal 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())
|
||||
}
|
@ -1,23 +1,49 @@
|
||||
import deindent from 'de-indent'
|
||||
import { parseHTML } from 'compiler/parser/html-parser'
|
||||
import { makeMap } from 'shared/util'
|
||||
import {
|
||||
ASTAttr,
|
||||
SFCBlock,
|
||||
SFCDescriptor,
|
||||
WarningMessage
|
||||
} from 'types/compiler'
|
||||
import { ASTAttr, WarningMessage } from 'types/compiler'
|
||||
import { RawSourceMap } from './types'
|
||||
|
||||
const splitRE = /\r?\n/g
|
||||
const replaceRE = /./g
|
||||
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.
|
||||
*/
|
||||
export function parseComponent(
|
||||
content: string,
|
||||
options: Record<string, any> = {}
|
||||
options: VueTemplateCompilerParseOptions = {}
|
||||
): SFCDescriptor {
|
||||
const sfc: SFCDescriptor = {
|
||||
template: null,
|
||||
@ -48,7 +74,7 @@ export function parseComponent(
|
||||
|
||||
function start(
|
||||
tag: string,
|
||||
attrs: Array<ASTAttr>,
|
||||
attrs: ASTAttr[],
|
||||
unary: boolean,
|
||||
start: 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++) {
|
||||
const attr = attrs[i]
|
||||
if (attr.name === 'lang') {
|
207
packages/compiler-sfc/src/stylePlugins/scoped.ts
Normal file
207
packages/compiler-sfc/src/stylePlugins/scoped.ts
Normal 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
|
18
packages/compiler-sfc/src/stylePlugins/trim.ts
Normal file
18
packages/compiler-sfc/src/stylePlugins/trim.ts
Normal 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
|
135
packages/compiler-sfc/src/stylePreprocessors.ts
Normal file
135
packages/compiler-sfc/src/stylePreprocessors.ts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
73
packages/compiler-sfc/src/templateCompilerModules/srcset.ts
Normal file
73
packages/compiler-sfc/src/templateCompilerModules/srcset.ts
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
74
packages/compiler-sfc/src/templateCompilerModules/utils.ts
Normal file
74
packages/compiler-sfc/src/templateCompilerModules/utils.ts
Normal 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
|
||||
}
|
52
packages/compiler-sfc/src/types.ts
Normal file
52
packages/compiler-sfc/src/types.ts
Normal 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)[]
|
||||
}
|
16
packages/compiler-sfc/src/warn.ts
Normal file
16
packages/compiler-sfc/src/warn.ts
Normal 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`
|
||||
)
|
||||
}
|
203
packages/compiler-sfc/test/compileStyle.spec.ts
Normal file
203
packages/compiler-sfc/test/compileStyle.spec.ts
Normal 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}'
|
||||
)
|
||||
})
|
230
packages/compiler-sfc/test/compileTemplate.spec.ts
Normal file
230
packages/compiler-sfc/test/compileTemplate.spec.ts
Normal 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'
|
||||
)
|
||||
})
|
@ -1,4 +1,5 @@
|
||||
import { parseComponent } from 'sfc/parser'
|
||||
import { WarningMessage } from 'types/compiler'
|
||||
import { parseComponent } from '../src/parseComponent'
|
||||
|
||||
describe('Single File Component parser', () => {
|
||||
it('should parse', () => {
|
137
packages/compiler-sfc/test/stylePluginScoped.spec.ts
Normal file
137
packages/compiler-sfc/test/stylePluginScoped.spec.ts
Normal 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 {')
|
||||
})
|
7
packages/compiler-sfc/test/tsconfig.json
Normal file
7
packages/compiler-sfc/test/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "vitest/globals"]
|
||||
},
|
||||
"include": ["../src", "."]
|
||||
}
|
@ -24,7 +24,7 @@
|
||||
"url": "https://github.com/vuejs/vue/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"hash-sum": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"lodash.template": "^4.5.0",
|
||||
|
@ -26,8 +26,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/vue-template-compiler#readme",
|
||||
"dependencies": {
|
||||
"he": "^1.2.0",
|
||||
"de-indent": "^1.0.2"
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vue": "file:../.."
|
||||
|
477
pnpm-lock.yaml
477
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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 = {
|
||||
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
|
||||
'runtime-cjs-dev': {
|
||||
@ -202,12 +208,27 @@ const builds = {
|
||||
)
|
||||
},
|
||||
'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'),
|
||||
format: 'cjs',
|
||||
external: Object.keys(
|
||||
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'
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
export function compile() {}
|
@ -1,4 +1,4 @@
|
||||
export { parseComponent } from 'sfc/parser'
|
||||
export { parseComponent } from 'sfc/parseComponent'
|
||||
export { compile, compileToFunctions } from './compiler/index'
|
||||
export { ssrCompile, ssrCompileToFunctions } from 'server/compiler'
|
||||
export { generateCodeFrame } from 'compiler/codeframe'
|
||||
|
@ -199,26 +199,3 @@ export type ASTText = {
|
||||
start?: 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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user