diff options
Diffstat (limited to 'node_modules/@vue/component-compiler-utils/lib')
11 files changed, 918 insertions, 0 deletions
diff --git a/node_modules/@vue/component-compiler-utils/lib/compileStyle.ts b/node_modules/@vue/component-compiler-utils/lib/compileStyle.ts new file mode 100644 index 00000000..bbd39c3f --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/compileStyle.ts @@ -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 './styleProcessors' + +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.render( + options.source, + options.map, + Object.assign( + { + filename: options.filename + }, + options.preprocessOptions + ) + ) +} diff --git a/node_modules/@vue/component-compiler-utils/lib/compileTemplate.ts b/node_modules/@vue/component-compiler-utils/lib/compileTemplate.ts new file mode 100644 index 00000000..13bb3c80 --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/compileTemplate.ts @@ -0,0 +1,176 @@ +import { VueTemplateCompiler, VueTemplateCompilerOptions } from './types' + +import assetUrlsModule, { + AssetURLOptions +} from './templateCompilerModules/assetUrl' +import srcsetModule from './templateCompilerModules/srcset' + +const prettier = require('prettier') +const consolidate = require('consolidate') +const transpile = require('vue-template-es2015-compiler') + +export interface TemplateCompileOptions { + source: string + filename: string + compiler: VueTemplateCompiler + compilerOptions?: VueTemplateCompilerOptions + transformAssetUrls?: AssetURLOptions | boolean + preprocessLang?: string + preprocessOptions?: any + transpileOptions?: any + isProduction?: boolean + isFunctional?: boolean + optimizeSSR?: boolean +} + +export interface TemplateCompileResult { + code: string + source: string + tips: string[] + errors: string[] +} + +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 { + 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, + compilerOptions = {}, + transpileOptions = {}, + transformAssetUrls, + isProduction = process.env.NODE_ENV === 'production', + isFunctional = false, + optimizeSSR = false + } = options + + const compile = + optimizeSSR && compiler.ssrCompile ? compiler.ssrCompile : compiler.compile + + let finalCompilerOptions = compilerOptions + if (transformAssetUrls) { + const builtInModules = [ + transformAssetUrls === true + ? assetUrlsModule() + : assetUrlsModule(transformAssetUrls), + srcsetModule() + ] + finalCompilerOptions = Object.assign({}, compilerOptions, { + modules: [...builtInModules, ...(compilerOptions.modules || [])] + }) + } + + const { render, staticRenderFns, tips, errors } = compile( + source, + finalCompilerOptions + ) + + if (errors && errors.length) { + return { + code: `var render = function () {}\n` + `var staticRenderFns = []\n`, + source, + tips, + errors + } + } else { + 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` + code = prettier.format(code, { semi: false, parser: 'babylon' }) + } + + return { + code, + source, + tips, + errors + } + } +} diff --git a/node_modules/@vue/component-compiler-utils/lib/index.ts b/node_modules/@vue/component-compiler-utils/lib/index.ts new file mode 100644 index 00000000..79d22ece --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/index.ts @@ -0,0 +1,28 @@ +import { parse, SFCBlock, SFCCustomBlock, SFCDescriptor } from './parse' + +import { + compileTemplate, + TemplateCompileOptions, + TemplateCompileResult +} from './compileTemplate' + +import { + compileStyle, + compileStyleAsync, + StyleCompileOptions, + StyleCompileResults +} from './compileStyle' + +// API +export { parse, compileTemplate, compileStyle, compileStyleAsync } + +// types +export { + SFCBlock, + SFCCustomBlock, + SFCDescriptor, + TemplateCompileOptions, + TemplateCompileResult, + StyleCompileOptions, + StyleCompileResults +} diff --git a/node_modules/@vue/component-compiler-utils/lib/parse.ts b/node_modules/@vue/component-compiler-utils/lib/parse.ts new file mode 100644 index 00000000..8fbd70ee --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/parse.ts @@ -0,0 +1,112 @@ +import { + RawSourceMap, + VueTemplateCompiler, + VueTemplateCompilerParseOptions +} from './types' + +const hash = require('hash-sum') +const cache = require('lru-cache')(100) +const { SourceMapGenerator } = require('source-map') + +const splitRE = /\r?\n/g +const emptyRE = /^(?:\/\/)?\s*$/ + +export interface ParseOptions { + source: string + filename?: string + compiler: VueTemplateCompiler + compilerParseOptions?: VueTemplateCompilerParseOptions + sourceRoot?: string + needMap?: boolean +} + +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[] +} + +export function parse(options: ParseOptions): SFCDescriptor { + const { + source, + filename = '', + compiler, + compilerParseOptions = { pad: 'line' }, + sourceRoot = process.cwd(), + needMap = true + } = options + const cacheKey = hash(filename + source) + let output: SFCDescriptor = cache.get(cacheKey) + if (output) return output + output = compiler.parseComponent(source, compilerParseOptions) + if (needMap) { + if (output.script && !output.script.src) { + output.script.map = generateSourceMap( + filename, + source, + output.script.content, + sourceRoot + ) + } + if (output.styles) { + output.styles.forEach(style => { + if (!style.src) { + style.map = generateSourceMap( + filename, + source, + style.content, + sourceRoot + ) + } + }) + } + } + cache.set(cacheKey, output) + return output +} + +function generateSourceMap( + filename: string, + source: string, + generated: string, + sourceRoot: string +): RawSourceMap { + const map = new SourceMapGenerator({ + file: filename, + sourceRoot + }) + map.setSourceContent(filename, source) + generated.split(splitRE).forEach((line, index) => { + if (!emptyRE.test(line)) { + map.addMapping({ + source: filename, + original: { + line: index + 1, + column: 0 + }, + generated: { + line: index + 1, + column: 0 + } + }) + } + }) + return map.toJSON() +} diff --git a/node_modules/@vue/component-compiler-utils/lib/stylePlugins/scoped.ts b/node_modules/@vue/component-compiler-utils/lib/stylePlugins/scoped.ts new file mode 100644 index 00000000..c0ce6c2a --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/stylePlugins/scoped.ts @@ -0,0 +1,98 @@ +import { Root } from 'postcss' +import * as postcss from 'postcss' +// postcss-selector-parser does have typings but it's problematic to work with. +const selectorParser = require('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 + + selector.each((n: any) => { + // ">>>" combinator + if (n.type === 'combinator' && n.value === '>>>') { + n.value = ' ' + n.spaces.before = n.spaces.after = '' + return false + } + // /deep/ alias for >>>, since >>> doesn't work in SASS + if (n.type === 'tag' && n.value === '/deep/') { + const prev = n.prev() + if (prev && prev.type === 'combinator' && prev.value === ' ') { + prev.remove() + } + n.remove() + 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 + }) + ) + }) + }).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(',') + } + }) + } +}) diff --git a/node_modules/@vue/component-compiler-utils/lib/stylePlugins/trim.ts b/node_modules/@vue/component-compiler-utils/lib/stylePlugins/trim.ts new file mode 100644 index 00000000..a7e9a0db --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/stylePlugins/trim.ts @@ -0,0 +1,10 @@ +import { Root } from 'postcss' +import * as postcss from 'postcss' + +export default postcss.plugin('trim', () => (css: Root) => { + css.walk(({ type, raws }) => { + if (type === 'rule' || type === 'atrule') { + raws.before = raws.after = '\n' + } + }) +}) diff --git a/node_modules/@vue/component-compiler-utils/lib/styleProcessors/index.ts b/node_modules/@vue/component-compiler-utils/lib/styleProcessors/index.ts new file mode 100644 index 00000000..b458c52e --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/styleProcessors/index.ts @@ -0,0 +1,133 @@ +const merge = require('merge-source-map') + +export interface StylePreprocessor { + render( + source: string, + map: any | null, + options: any + ): StylePreprocessorResults +} + +export interface StylePreprocessorResults { + code: string + map?: any + errors: Array<Error> +} + +// .scss/.sass processor +const scss: StylePreprocessor = { + render( + source: string, + map: any | null, + options: any + ): StylePreprocessorResults { + const nodeSass = require('node-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 = { + render( + source: string, + map: any | null, + options: any + ): StylePreprocessorResults { + return scss.render( + source, + map, + Object.assign({}, options, { indentedSyntax: true }) + ) + } +} + +// .less +const less = { + render( + source: string, + map: any | null, + options: any + ): StylePreprocessorResults { + 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 = { + render( + source: string, + map: any | null, + options: any + ): StylePreprocessorResults { + 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: { [key: string]: StylePreprocessor } = { + less, + sass, + scss, + styl, + stylus: styl +} diff --git a/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/assetUrl.ts b/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/assetUrl.ts new file mode 100644 index 00000000..b74b0596 --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/assetUrl.ts @@ -0,0 +1,51 @@ +// vue compiler module for transforming `<tag>:<attribute>` to `require` + +import { urlToRequire, ASTNode, Attr } from './utils' + +export interface AssetURLOptions { + [name: string]: string | string[] +} + +const defaultOptions: AssetURLOptions = { + video: ['src', 'poster'], + source: 'src', + img: 'src', + image: ['xlink:href', 'href'] +} + +export default (userOptions?: AssetURLOptions) => { + const options = userOptions + ? Object.assign({}, defaultOptions, userOptions) + : defaultOptions + + return { + postTransformNode: (node: ASTNode) => { + transform(node, options) + } + } +} + +function transform(node: ASTNode, options: AssetURLOptions) { + 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)) + } else if (Array.isArray(attributes)) { + attributes.forEach(item => node.attrs.some(attr => rewrite(attr, item))) + } + } + } +} + +function rewrite(attr: Attr, name: string) { + 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)) + return true + } + } + return false +} diff --git a/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/srcset.ts b/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/srcset.ts new file mode 100644 index 00000000..7cb7544e --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/srcset.ts @@ -0,0 +1,66 @@ +// vue compiler module for transforming `img:srcset` to a number of `require`s + +import { urlToRequire, ASTNode } from './utils' + +interface ImageCandidate { + require: string + descriptor: string +} + +export default () => ({ + postTransformNode: (node: ASTNode) => { + transform(node) + } +}) + +// 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) { + 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), 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 + } + }) + } +} diff --git a/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/utils.ts b/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/utils.ts new file mode 100644 index 00000000..893d8a41 --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/templateCompilerModules/utils.ts @@ -0,0 +1,54 @@ +export interface Attr { + name: string + value: string +} + +export interface ASTNode { + tag: string + attrs: Attr[] +} + +import { UrlWithStringQuery, parse as uriParse } from 'url' + +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 +} diff --git a/node_modules/@vue/component-compiler-utils/lib/types.ts b/node_modules/@vue/component-compiler-utils/lib/types.ts new file mode 100644 index 00000000..b5305201 --- /dev/null +++ b/node_modules/@vue/component-compiler-utils/lib/types.ts @@ -0,0 +1,47 @@ +import { SFCDescriptor } from './parse' + +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[] +} + +export interface VueTemplateCompilerParseOptions { + pad?: 'line' | 'space' +} + +export interface VueTemplateCompilerResults { + ast: Object | void + render: string + staticRenderFns: string[] + errors: string[] + tips: string[] +} |
