From 7031e6a07a4d87bbeccea06e40de011a9fa623ed Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 6 Nov 2019 21:58:15 -0500 Subject: [PATCH] feat: (wip) setup compiler-sfc --- packages/compiler-sfc/README.md | 17 ++ packages/compiler-sfc/api-extractor.json | 7 + packages/compiler-sfc/package.json | 39 +++++ packages/compiler-sfc/src/compileStyle.ts | 145 ++++++++++++++++++ packages/compiler-sfc/src/compileTemplate.ts | 3 + packages/compiler-sfc/src/index.ts | 16 ++ packages/compiler-sfc/src/parse.ts | 137 +++++++++++++++++ packages/compiler-sfc/src/shims.d.ts | 3 + .../compiler-sfc/src/stylePluginScoped.ts | 101 ++++++++++++ packages/compiler-sfc/src/stylePluginTrim.ts | 10 ++ .../compiler-sfc/src/stylePreprocessors.ts | 113 ++++++++++++++ .../src/templatePluginAssetUrl.ts | 5 + .../compiler-sfc/src/templatePluginSrcset.ts | 5 + .../compiler-sfc/src/templatePluginUtils.ts | 55 +++++++ rollup.config.js | 6 +- yarn.lock | 57 +++++++ 16 files changed, 717 insertions(+), 2 deletions(-) create mode 100644 packages/compiler-sfc/README.md create mode 100644 packages/compiler-sfc/api-extractor.json create mode 100644 packages/compiler-sfc/package.json create mode 100644 packages/compiler-sfc/src/compileStyle.ts create mode 100644 packages/compiler-sfc/src/compileTemplate.ts create mode 100644 packages/compiler-sfc/src/index.ts create mode 100644 packages/compiler-sfc/src/parse.ts create mode 100644 packages/compiler-sfc/src/shims.d.ts create mode 100644 packages/compiler-sfc/src/stylePluginScoped.ts create mode 100644 packages/compiler-sfc/src/stylePluginTrim.ts create mode 100644 packages/compiler-sfc/src/stylePreprocessors.ts create mode 100644 packages/compiler-sfc/src/templatePluginAssetUrl.ts create mode 100644 packages/compiler-sfc/src/templatePluginSrcset.ts create mode 100644 packages/compiler-sfc/src/templatePluginUtils.ts diff --git a/packages/compiler-sfc/README.md b/packages/compiler-sfc/README.md new file mode 100644 index 000000000..66a6dea34 --- /dev/null +++ b/packages/compiler-sfc/README.md @@ -0,0 +1,17 @@ +# @vue/compiler-sfc + +> Lower level utilities for compiling Vue single file components + +This package contains lower level utilities that you can use if you are writing a plugin / transform for a bundler or module system that compiles Vue single file components into JavaScript. It is used in [vue-loader](https://github.com/vuejs/vue-loader). + +The API surface is intentionally minimal - the goal is to reuse as much as possible while being as flexible as possible. + +## Why isn't `@vue/compiler-dom` a peerDependency? + +Since this package is more often used as a low-level utility, it is usually a transitive dependency in an actual Vue project. It is therefore the responsibility of the higher-level package (e.g. `vue-loader`) to inject `@vue/compiler-dom` via options when calling the `compileTemplate` methods. + +Not listing it as a peer depedency also allows tooling authors to use a custom template compiler (built on top of `@vue/compiler-core`) instead of `@vue/compiler-dom`, without having to include it just to fullfil the peer dep requirement. + +## API + +TODO diff --git a/packages/compiler-sfc/api-extractor.json b/packages/compiler-sfc/api-extractor.json new file mode 100644 index 000000000..305e85ffe --- /dev/null +++ b/packages/compiler-sfc/api-extractor.json @@ -0,0 +1,7 @@ +{ + "extends": "../../api-extractor.json", + "mainEntryPointFilePath": "./dist/packages//src/index.d.ts", + "dtsRollup": { + "untrimmedFilePath": "./dist/.d.ts" + } +} \ No newline at end of file diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json new file mode 100644 index 000000000..cf842880c --- /dev/null +++ b/packages/compiler-sfc/package.json @@ -0,0 +1,39 @@ +{ + "name": "@vue/compiler-sfc", + "version": "3.0.0-alpha.1", + "description": "@vue/compiler-sfc", + "main": "dist/compiler-sfc.cjs.js", + "files": [ + "dist" + ], + "types": "dist/compiler-sfc.d.ts", + "buildOptions": { + "prod": false, + "formats": [ + "cjs" + ] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/vue.git" + }, + "keywords": [ + "vue" + ], + "author": "Evan You", + "license": "MIT", + "bugs": { + "url": "https://github.com/vuejs/vue/issues" + }, + "homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-sfc#readme", + "dependencies": { + "@vue/compiler-core": "3.0.0-alpha.1", + "consolidate": "^0.15.1", + "hash-sum": "^2.0.0", + "lru-cache": "^5.1.1", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.21", + "postcss-selector-parser": "^6.0.2", + "source-map": "^0.7.3" + } +} diff --git a/packages/compiler-sfc/src/compileStyle.ts b/packages/compiler-sfc/src/compileStyle.ts new file mode 100644 index 000000000..0780cf694 --- /dev/null +++ b/packages/compiler-sfc/src/compileStyle.ts @@ -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 { + return doCompileStyle({ ...options, isAsync: true }) as Promise< + StyleCompileResults + > +} + +export function doCompileStyle( + options: AsyncStyleCompileOptions +): StyleCompileResults | Promise { + 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 + ) + ) +} diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts new file mode 100644 index 000000000..b970d9155 --- /dev/null +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -0,0 +1,3 @@ +export function compileTemplate() { + // TODO +} diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts new file mode 100644 index 000000000..7c8918bee --- /dev/null +++ b/packages/compiler-sfc/src/index.ts @@ -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' diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts new file mode 100644 index 000000000..9cfded626 --- /dev/null +++ b/packages/compiler-sfc/src/parse.ts @@ -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 + 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 = {} + 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 +} diff --git a/packages/compiler-sfc/src/shims.d.ts b/packages/compiler-sfc/src/shims.d.ts new file mode 100644 index 000000000..99a3b6688 --- /dev/null +++ b/packages/compiler-sfc/src/shims.d.ts @@ -0,0 +1,3 @@ +declare module 'merge-source-map' { + export default function merge(oldMap: object, newMap: object): object +} diff --git a/packages/compiler-sfc/src/stylePluginScoped.ts b/packages/compiler-sfc/src/stylePluginScoped.ts new file mode 100644 index 000000000..32c132aed --- /dev/null +++ b/packages/compiler-sfc/src/stylePluginScoped.ts @@ -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