feat: (wip) setup compiler-sfc

This commit is contained in:
Evan You
2019-11-06 21:58:15 -05:00
parent 4b739e3bc0
commit 7031e6a07a
16 changed files with 717 additions and 2 deletions

View File

@@ -0,0 +1,145 @@
// const postcss = require('postcss')
import postcss, { ProcessOptions, LazyResult, Result, ResultMap } from 'postcss'
import trimPlugin from './stylePluginTrim'
import scopedPlugin from './stylePluginScoped'
import {
processors,
StylePreprocessor,
StylePreprocessorResults
} from './stylePreprocessors'
export interface StyleCompileOptions {
source: string
filename: string
id: string
map?: object
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: object | void
rawResult: LazyResult | Result | undefined
errors: string[]
}
export function compileStyle(
options: StyleCompileOptions
): StyleCompileResults {
return doCompileStyle({ ...options, isAsync: false }) as StyleCompileResults
}
export function compileStyleAsync(
options: StyleCompileOptions
): Promise<StyleCompileResults> {
return doCompileStyle({ ...options, isAsync: true }) as Promise<
StyleCompileResults
>
}
export function doCompileStyle(
options: AsyncStyleCompileOptions
): StyleCompileResults | Promise<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: LazyResult | undefined
let code: string | undefined
let outMap: ResultMap | undefined
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 => ({
code: result.css || '',
map: result.map && result.map.toJSON(),
errors,
rawResult: result
}))
.catch(error => ({
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.render(
options.source,
options.map,
Object.assign(
{
filename: options.filename
},
options.preprocessOptions
)
)
}

View File

@@ -0,0 +1,3 @@
export function compileTemplate() {
// TODO
}

View File

@@ -0,0 +1,16 @@
// API
export { parse } from './parse'
export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
// Types
export {
SFCParseOptions,
SFCDescriptor,
SFCBlock,
SFCTemplateBlock,
SFCScriptBlock,
SFCStyleBlock
} from './parse'
export { StyleCompileOptions, StyleCompileResults } from './compileStyle'

View File

@@ -0,0 +1,137 @@
import {
parse as baseParse,
TextModes,
NodeTypes,
TextNode,
ElementNode,
SourceLocation
} from '@vue/compiler-core'
import { RawSourceMap } from 'source-map'
export interface SFCParseOptions {
needMap?: boolean
filename?: string
sourceRoot?: string
}
export interface SFCBlock {
type: string
content: string
attrs: Record<string, string | true>
loc: SourceLocation
map?: RawSourceMap
lang?: string
src?: string
}
export interface SFCTemplateBlock extends SFCBlock {
type: 'template'
functional?: boolean
}
export interface SFCScriptBlock extends SFCBlock {
type: 'script'
}
export interface SFCStyleBlock extends SFCBlock {
type: 'style'
scoped?: boolean
module?: string | boolean
}
export interface SFCDescriptor {
filename: string
template: SFCTemplateBlock | null
script: SFCScriptBlock | null
styles: SFCStyleBlock[]
customBlocks: SFCBlock[]
}
export function parse(
source: string,
{
needMap = true,
filename = 'component.vue',
sourceRoot = ''
}: SFCParseOptions = {}
): SFCDescriptor {
// TODO check cache
const sfc: SFCDescriptor = {
filename,
template: null,
script: null,
styles: [],
customBlocks: []
}
const ast = baseParse(source, {
isNativeTag: () => true,
getTextMode: () => TextModes.RAWTEXT
})
ast.children.forEach(node => {
if (node.type !== NodeTypes.ELEMENT) {
return
}
switch (node.tag) {
case 'template':
if (!sfc.template) {
sfc.template = createBlock(node) as SFCTemplateBlock
} else {
// TODO warn duplicate template
}
break
case 'script':
if (!sfc.script) {
sfc.script = createBlock(node) as SFCScriptBlock
} else {
// TODO warn duplicate script
}
break
case 'style':
sfc.styles.push(createBlock(node) as SFCStyleBlock)
break
default:
sfc.customBlocks.push(createBlock(node))
break
}
})
if (needMap) {
// TODO source map
}
// TODO set cache
return sfc
}
function createBlock(node: ElementNode): SFCBlock {
const type = node.tag
const text = node.children[0] as TextNode
const attrs: Record<string, string | true> = {}
const block: SFCBlock = {
type,
content: text.content,
loc: text.loc,
attrs
}
node.props.forEach(p => {
if (p.type === NodeTypes.ATTRIBUTE) {
attrs[p.name] = p.value ? p.value.content || true : true
if (p.name === 'lang') {
block.lang = p.value && p.value.content
} else if (p.name === 'src') {
block.src = p.value && p.value.content
} else if (type === 'style') {
if (p.name === 'scoped') {
;(block as SFCStyleBlock).scoped = true
} else if (p.name === 'module') {
;(block as SFCStyleBlock).module = attrs[p.name]
}
} else if (type === 'template' && p.name === 'functional') {
;(block as SFCTemplateBlock).functional = true
}
}
})
return block
}

3
packages/compiler-sfc/src/shims.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module 'merge-source-map' {
export default function merge(oldMap: object, newMap: object): object
}

View File

@@ -0,0 +1,101 @@
import postcss, { Root } from 'postcss'
import selectorParser from 'postcss-selector-parser'
export default postcss.plugin('add-id', (options: any) => (root: Root) => {
const id: string = options
const keyframes = Object.create(null)
root.each(function rewriteSelector(node: any) {
if (!node.selector) {
// handle media queries
if (node.type === 'atrule') {
if (node.name === 'media' || node.name === 'supports') {
node.each(rewriteSelector)
} else if (/-?keyframes$/.test(node.name)) {
// register keyframes
keyframes[node.params] = node.params = node.params + '-' + id
}
}
return
}
node.selector = selectorParser((selectors: any) => {
selectors.each((selector: any) => {
let node: any = null
// find the last child node to insert attribute selector
selector.each((n: any) => {
// ">>>" combinator
// and /deep/ alias for >>>, since >>> doesn't work in SASS
if (
n.type === 'combinator' &&
(n.value === '>>>' || n.value === '/deep/')
) {
n.value = ' '
n.spaces.before = n.spaces.after = ''
return false
}
// in newer versions of sass, /deep/ support is also dropped, so add a ::v-deep alias
if (n.type === 'pseudo' && n.value === '::v-deep') {
n.value = n.spaces.before = n.spaces.after = ''
return false
}
if (n.type !== 'pseudo' && n.type !== 'combinator') {
node = n
}
})
if (node) {
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 = ''
}
selector.insertAfter(
node,
selectorParser.attribute({
attribute: id,
value: id,
raws: {}
})
)
})
}).processSync(node.selector)
})
// 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.
if (Object.keys(keyframes).length) {
root.walkDecls(decl => {
// individual animation-name declaration
if (/^(-\w+-)?animation-name$/.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => keyframes[v.trim()] || v.trim())
.join(',')
}
// shorthand
if (/^(-\w+-)?animation$/.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(',')
}
})
}
})

View File

@@ -0,0 +1,10 @@
import postcss, { Root } from 'postcss'
export default postcss.plugin('trim', () => (css: Root) => {
css.walk(({ type, raws }) => {
if (type === 'rule' || type === 'atrule') {
if (raws.before) raws.before = '\n'
if (raws.after) raws.after = '\n'
}
})
})

View File

@@ -0,0 +1,113 @@
import merge from 'merge-source-map'
export interface StylePreprocessor {
render(source: string, map?: object, options?: any): StylePreprocessorResults
}
export interface StylePreprocessorResults {
code: string
map?: object
errors: Array<Error>
}
// .scss/.sass processor
const scss: StylePreprocessor = {
render(source, map, options) {
const nodeSass = require('sass')
const finalOptions = Object.assign({}, options, {
data: source,
file: options.filename,
outFile: options.filename,
sourceMap: !!map
})
try {
const result = nodeSass.renderSync(finalOptions)
if (map) {
return {
code: result.css.toString(),
map: merge(map, JSON.parse(result.map.toString())),
errors: []
}
}
return { code: result.css.toString(), errors: [] }
} catch (e) {
return { code: '', errors: [e] }
}
}
}
const sass: StylePreprocessor = {
render(source, map, options) {
return scss.render(
source,
map,
Object.assign({}, options, { indentedSyntax: true })
)
}
}
// .less
const less: StylePreprocessor = {
render(source, map, options) {
const nodeLess = require('less')
let result: any
let error: Error | null = null
nodeLess.render(
source,
Object.assign({}, options, { syncImport: true }),
(err: Error | null, output: any) => {
error = err
result = output
}
)
if (error) return { code: '', errors: [error] }
if (map) {
return {
code: result.css.toString(),
map: merge(map, result.map),
errors: []
}
}
return { code: result.css.toString(), errors: [] }
}
}
// .styl
const styl: StylePreprocessor = {
render(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()
if (map) {
return {
code: result,
map: merge(map, ref.sourcemap),
errors: []
}
}
return { code: result, errors: [] }
} catch (e) {
return { code: '', errors: [e] }
}
}
}
export const processors: Record<string, StylePreprocessor> = {
less,
sass,
scss,
styl,
stylus: styl
}

View File

@@ -0,0 +1,5 @@
import { NodeTransform } from '@vue/compiler-core'
export const transformAssetUrl: NodeTransform = () => {
// TODO
}

View File

@@ -0,0 +1,5 @@
import { NodeTransform } from '@vue/compiler-core'
export const transformSrcset: NodeTransform = () => {
// TODO
}

View File

@@ -0,0 +1,55 @@
export interface Attr {
name: string
value: string
}
export interface ASTNode {
tag: string
attrs: Attr[]
}
import { UrlWithStringQuery, parse as uriParse } from 'url'
// TODO use imports instead
export function urlToRequire(url: string): string {
const returnValue = `"${url}"`
// same logic as in transform-require.js
const firstChar = url.charAt(0)
if (firstChar === '.' || firstChar === '~' || firstChar === '@') {
if (firstChar === '~') {
const secondChar = url.charAt(1)
url = url.slice(secondChar === '/' ? 2 : 1)
}
const uriParts = parseUriParts(url)
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
}