mirror of
https://gitee.com/vuejs/vue.git
synced 2024-11-30 11:07:51 +08:00
wip: strip with
This commit is contained in:
parent
ed4bbed990
commit
22c457fe24
@ -10,7 +10,7 @@ import assetUrlsModule, {
|
||||
import srcsetModule from './templateCompilerModules/srcset'
|
||||
import consolidate from '@vue/consolidate'
|
||||
import * as _compiler from 'web/entry-compiler'
|
||||
import transpile from 'vue-template-es2015-compiler'
|
||||
import { stripWith } from './stripWith'
|
||||
|
||||
export interface TemplateCompileOptions {
|
||||
source: string
|
||||
@ -26,6 +26,7 @@ export interface TemplateCompileOptions {
|
||||
isFunctional?: boolean
|
||||
optimizeSSR?: boolean
|
||||
prettify?: boolean
|
||||
isTS?: boolean
|
||||
}
|
||||
|
||||
export interface TemplateCompileResult {
|
||||
@ -108,7 +109,8 @@ function actuallyCompile(
|
||||
isProduction = process.env.NODE_ENV === 'production',
|
||||
isFunctional = false,
|
||||
optimizeSSR = false,
|
||||
prettify = true
|
||||
prettify = true,
|
||||
isTS = false
|
||||
} = options
|
||||
|
||||
const compile =
|
||||
@ -142,25 +144,20 @@ function actuallyCompile(
|
||||
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`
|
||||
`var __render__ = ${stripWith(
|
||||
render,
|
||||
`render`,
|
||||
isFunctional,
|
||||
isTS,
|
||||
transpileOptions
|
||||
)}\n` +
|
||||
`var __staticRenderFns__ = [${staticRenderFns.map(code =>
|
||||
stripWith(code, ``, isFunctional, isTS, transpileOptions)
|
||||
)}]` +
|
||||
`\n`
|
||||
|
||||
// #23 we use __render__ to avoid `render` not being prefixed by the
|
||||
// transpiler when stripping with, but revert it back to `render` to
|
||||
|
452
packages/compiler-sfc/src/stripWith.ts
Normal file
452
packages/compiler-sfc/src/stripWith.ts
Normal file
@ -0,0 +1,452 @@
|
||||
import MagicString from 'magic-string'
|
||||
import { parseExpression, ParserOptions, ParserPlugin } from '@babel/parser'
|
||||
import { walk } from 'estree-walker'
|
||||
import { makeMap } from 'shared/util'
|
||||
|
||||
import type {
|
||||
Identifier,
|
||||
Node,
|
||||
Function,
|
||||
ObjectProperty,
|
||||
BlockStatement,
|
||||
Program
|
||||
} from '@babel/types'
|
||||
|
||||
const doNotPrefix = makeMap(
|
||||
'Infinity,undefined,NaN,isFinite,isNaN,' +
|
||||
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
|
||||
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
|
||||
'require,' + // for webpack
|
||||
'arguments,' + // parsed as identifier but is a special keyword...
|
||||
'_c' // cached to save property access
|
||||
)
|
||||
|
||||
/**
|
||||
* The input is expected to be the render function code directly returned from
|
||||
* `compile()` calls, e.g. `with(this){return ...}`
|
||||
*/
|
||||
export function stripWith(
|
||||
source: string,
|
||||
fnName = '',
|
||||
isFunctional = false,
|
||||
isTS = false,
|
||||
babelOptions: ParserOptions = {}
|
||||
) {
|
||||
source = `function ${fnName}(${isFunctional ? `_c,_vm` : ``}){${source}\n}`
|
||||
|
||||
const s = new MagicString(source)
|
||||
|
||||
const plugins: ParserPlugin[] = [
|
||||
...(isTS ? (['typescript'] as const) : []),
|
||||
...(babelOptions?.plugins || [])
|
||||
]
|
||||
|
||||
const ast = parseExpression(source, {
|
||||
...babelOptions,
|
||||
plugins
|
||||
})
|
||||
|
||||
const parentStack: Node[] = []
|
||||
const knownIds: Record<string, number> = Object.create(null)
|
||||
|
||||
// based on https://github.com/vuejs/core/blob/main/packages/compiler-core/src/babelUtils.ts
|
||||
;(walk as any)(ast, {
|
||||
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
|
||||
parent && parentStack.push(parent)
|
||||
if (
|
||||
parent &&
|
||||
parent.type.startsWith('TS') &&
|
||||
parent.type !== 'TSAsExpression' &&
|
||||
parent.type !== 'TSNonNullExpression' &&
|
||||
parent.type !== 'TSTypeAssertion'
|
||||
) {
|
||||
return this.skip()
|
||||
}
|
||||
|
||||
if (node.type === 'WithStatement') {
|
||||
s.remove(node.start!, node.body.start! + 1)
|
||||
s.remove(node.end! - 1, node.end!)
|
||||
if (!isFunctional) {
|
||||
s.prependRight(node.start!, `var _vm=this;var _c=_vm._self._c;`)
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'Identifier') {
|
||||
const isLocal = !!knownIds[node.name]
|
||||
const isRefed = isReferencedIdentifier(node, parent!, parentStack)
|
||||
if (isRefed && !isLocal) {
|
||||
if (doNotPrefix(node.name)) {
|
||||
return
|
||||
}
|
||||
s.prependRight(node.start!, '_vm.')
|
||||
}
|
||||
} else if (
|
||||
node.type === 'ObjectProperty' &&
|
||||
parent!.type === 'ObjectPattern'
|
||||
) {
|
||||
// mark property in destructure pattern
|
||||
;(node as any).inPattern = true
|
||||
} else if (isFunctionType(node)) {
|
||||
// walk function expressions and add its arguments to known identifiers
|
||||
// so that we don't prefix them
|
||||
walkFunctionParams(node, id => markScopeIdentifier(node, id, knownIds))
|
||||
} else if (node.type === 'BlockStatement') {
|
||||
// #3445 record block-level local variables
|
||||
walkBlockDeclarations(node, id =>
|
||||
markScopeIdentifier(node, id, knownIds)
|
||||
)
|
||||
}
|
||||
},
|
||||
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
|
||||
parent && parentStack.pop()
|
||||
if (node !== ast && node.scopeIds) {
|
||||
for (const id of node.scopeIds) {
|
||||
knownIds[id]--
|
||||
if (knownIds[id] === 0) {
|
||||
delete knownIds[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return s.toString()
|
||||
}
|
||||
|
||||
export function isReferencedIdentifier(
|
||||
id: Identifier,
|
||||
parent: Node | null,
|
||||
parentStack: Node[]
|
||||
) {
|
||||
if (!parent) {
|
||||
return true
|
||||
}
|
||||
|
||||
// is a special keyword but parsed as identifier
|
||||
if (id.name === 'arguments') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isReferenced(id, parent)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// babel's isReferenced check returns false for ids being assigned to, so we
|
||||
// need to cover those cases here
|
||||
switch (parent.type) {
|
||||
case 'AssignmentExpression':
|
||||
case 'AssignmentPattern':
|
||||
return true
|
||||
case 'ObjectPattern':
|
||||
case 'ArrayPattern':
|
||||
return isInDestructureAssignment(parent, parentStack)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function isInDestructureAssignment(
|
||||
parent: Node,
|
||||
parentStack: Node[]
|
||||
): boolean {
|
||||
if (
|
||||
parent &&
|
||||
(parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
|
||||
) {
|
||||
let i = parentStack.length
|
||||
while (i--) {
|
||||
const p = parentStack[i]
|
||||
if (p.type === 'AssignmentExpression') {
|
||||
return true
|
||||
} else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function walkFunctionParams(
|
||||
node: Function,
|
||||
onIdent: (id: Identifier) => void
|
||||
) {
|
||||
for (const p of node.params) {
|
||||
for (const id of extractIdentifiers(p)) {
|
||||
onIdent(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function walkBlockDeclarations(
|
||||
block: BlockStatement | Program,
|
||||
onIdent: (node: Identifier) => void
|
||||
) {
|
||||
for (const stmt of block.body) {
|
||||
if (stmt.type === 'VariableDeclaration') {
|
||||
if (stmt.declare) continue
|
||||
for (const decl of stmt.declarations) {
|
||||
for (const id of extractIdentifiers(decl.id)) {
|
||||
onIdent(id)
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
stmt.type === 'FunctionDeclaration' ||
|
||||
stmt.type === 'ClassDeclaration'
|
||||
) {
|
||||
if (stmt.declare || !stmt.id) continue
|
||||
onIdent(stmt.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function extractIdentifiers(
|
||||
param: Node,
|
||||
nodes: Identifier[] = []
|
||||
): Identifier[] {
|
||||
switch (param.type) {
|
||||
case 'Identifier':
|
||||
nodes.push(param)
|
||||
break
|
||||
|
||||
case 'MemberExpression':
|
||||
let object: any = param
|
||||
while (object.type === 'MemberExpression') {
|
||||
object = object.object
|
||||
}
|
||||
nodes.push(object)
|
||||
break
|
||||
|
||||
case 'ObjectPattern':
|
||||
for (const prop of param.properties) {
|
||||
if (prop.type === 'RestElement') {
|
||||
extractIdentifiers(prop.argument, nodes)
|
||||
} else {
|
||||
extractIdentifiers(prop.value, nodes)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'ArrayPattern':
|
||||
param.elements.forEach(element => {
|
||||
if (element) extractIdentifiers(element, nodes)
|
||||
})
|
||||
break
|
||||
|
||||
case 'RestElement':
|
||||
extractIdentifiers(param.argument, nodes)
|
||||
break
|
||||
|
||||
case 'AssignmentPattern':
|
||||
extractIdentifiers(param.left, nodes)
|
||||
break
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
export function markScopeIdentifier(
|
||||
node: Node & { scopeIds?: Set<string> },
|
||||
child: Identifier,
|
||||
knownIds: Record<string, number>
|
||||
) {
|
||||
const { name } = child
|
||||
if (node.scopeIds && node.scopeIds.has(name)) {
|
||||
return
|
||||
}
|
||||
if (name in knownIds) {
|
||||
knownIds[name]++
|
||||
} else {
|
||||
knownIds[name] = 1
|
||||
}
|
||||
;(node.scopeIds || (node.scopeIds = new Set())).add(name)
|
||||
}
|
||||
|
||||
export const isFunctionType = (node: Node): node is Function => {
|
||||
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
|
||||
}
|
||||
|
||||
export const isStaticProperty = (node: Node): node is ObjectProperty =>
|
||||
node &&
|
||||
(node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
|
||||
!node.computed
|
||||
|
||||
export const isStaticPropertyKey = (node: Node, parent: Node) =>
|
||||
isStaticProperty(parent) && parent.key === node
|
||||
|
||||
/**
|
||||
* Copied from https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/isReferenced.ts
|
||||
* To avoid runtime dependency on @babel/types (which includes process references)
|
||||
* This file should not change very often in babel but we may need to keep it
|
||||
* up-to-date from time to time.
|
||||
*
|
||||
* https://github.com/babel/babel/blob/main/LICENSE
|
||||
*
|
||||
*/
|
||||
function isReferenced(node: Node, parent: Node, grandparent?: Node): boolean {
|
||||
switch (parent.type) {
|
||||
// yes: PARENT[NODE]
|
||||
// yes: NODE.child
|
||||
// no: parent.NODE
|
||||
case 'MemberExpression':
|
||||
case 'OptionalMemberExpression':
|
||||
if (parent.property === node) {
|
||||
return !!parent.computed
|
||||
}
|
||||
return parent.object === node
|
||||
|
||||
case 'JSXMemberExpression':
|
||||
return parent.object === node
|
||||
// no: let NODE = init;
|
||||
// yes: let id = NODE;
|
||||
case 'VariableDeclarator':
|
||||
return parent.init === node
|
||||
|
||||
// yes: () => NODE
|
||||
// no: (NODE) => {}
|
||||
case 'ArrowFunctionExpression':
|
||||
return parent.body === node
|
||||
|
||||
// no: class { #NODE; }
|
||||
// no: class { get #NODE() {} }
|
||||
// no: class { #NODE() {} }
|
||||
// no: class { fn() { return this.#NODE; } }
|
||||
case 'PrivateName':
|
||||
return false
|
||||
|
||||
// no: class { NODE() {} }
|
||||
// yes: class { [NODE]() {} }
|
||||
// no: class { foo(NODE) {} }
|
||||
case 'ClassMethod':
|
||||
case 'ClassPrivateMethod':
|
||||
case 'ObjectMethod':
|
||||
if (parent.key === node) {
|
||||
return !!parent.computed
|
||||
}
|
||||
return false
|
||||
|
||||
// yes: { [NODE]: "" }
|
||||
// no: { NODE: "" }
|
||||
// depends: { NODE }
|
||||
// depends: { key: NODE }
|
||||
case 'ObjectProperty':
|
||||
if (parent.key === node) {
|
||||
return !!parent.computed
|
||||
}
|
||||
// parent.value === node
|
||||
return !grandparent || grandparent.type !== 'ObjectPattern'
|
||||
// no: class { NODE = value; }
|
||||
// yes: class { [NODE] = value; }
|
||||
// yes: class { key = NODE; }
|
||||
case 'ClassProperty':
|
||||
if (parent.key === node) {
|
||||
return !!parent.computed
|
||||
}
|
||||
return true
|
||||
case 'ClassPrivateProperty':
|
||||
return parent.key !== node
|
||||
|
||||
// no: class NODE {}
|
||||
// yes: class Foo extends NODE {}
|
||||
case 'ClassDeclaration':
|
||||
case 'ClassExpression':
|
||||
return parent.superClass === node
|
||||
|
||||
// yes: left = NODE;
|
||||
// no: NODE = right;
|
||||
case 'AssignmentExpression':
|
||||
return parent.right === node
|
||||
|
||||
// no: [NODE = foo] = [];
|
||||
// yes: [foo = NODE] = [];
|
||||
case 'AssignmentPattern':
|
||||
return parent.right === node
|
||||
|
||||
// no: NODE: for (;;) {}
|
||||
case 'LabeledStatement':
|
||||
return false
|
||||
|
||||
// no: try {} catch (NODE) {}
|
||||
case 'CatchClause':
|
||||
return false
|
||||
|
||||
// no: function foo(...NODE) {}
|
||||
case 'RestElement':
|
||||
return false
|
||||
|
||||
case 'BreakStatement':
|
||||
case 'ContinueStatement':
|
||||
return false
|
||||
|
||||
// no: function NODE() {}
|
||||
// no: function foo(NODE) {}
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression':
|
||||
return false
|
||||
|
||||
// no: export NODE from "foo";
|
||||
// no: export * as NODE from "foo";
|
||||
case 'ExportNamespaceSpecifier':
|
||||
case 'ExportDefaultSpecifier':
|
||||
return false
|
||||
|
||||
// no: export { foo as NODE };
|
||||
// yes: export { NODE as foo };
|
||||
// no: export { NODE as foo } from "foo";
|
||||
case 'ExportSpecifier':
|
||||
// @ts-expect-error
|
||||
if (grandparent?.source) {
|
||||
return false
|
||||
}
|
||||
return parent.local === node
|
||||
|
||||
// no: import NODE from "foo";
|
||||
// no: import * as NODE from "foo";
|
||||
// no: import { NODE as foo } from "foo";
|
||||
// no: import { foo as NODE } from "foo";
|
||||
// no: import NODE from "bar";
|
||||
case 'ImportDefaultSpecifier':
|
||||
case 'ImportNamespaceSpecifier':
|
||||
case 'ImportSpecifier':
|
||||
return false
|
||||
|
||||
// no: import "foo" assert { NODE: "json" }
|
||||
case 'ImportAttribute':
|
||||
return false
|
||||
|
||||
// no: <div NODE="foo" />
|
||||
case 'JSXAttribute':
|
||||
return false
|
||||
|
||||
// no: [NODE] = [];
|
||||
// no: ({ NODE }) = [];
|
||||
case 'ObjectPattern':
|
||||
case 'ArrayPattern':
|
||||
return false
|
||||
|
||||
// no: new.NODE
|
||||
// no: NODE.target
|
||||
case 'MetaProperty':
|
||||
return false
|
||||
|
||||
// yes: type X = { someProperty: NODE }
|
||||
// no: type X = { NODE: OtherType }
|
||||
case 'ObjectTypeProperty':
|
||||
return parent.key !== node
|
||||
|
||||
// yes: enum X { Foo = NODE }
|
||||
// no: enum X { NODE }
|
||||
case 'TSEnumMember':
|
||||
return parent.id !== node
|
||||
|
||||
// yes: { [NODE]: value }
|
||||
// no: { NODE: value }
|
||||
case 'TSPropertySignature':
|
||||
if (parent.key === node) {
|
||||
return !!parent.computed
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { parse } from '../src/parse'
|
||||
import { compileStyle, compileStyleAsync } from '../src/compileStyle'
|
||||
|
||||
test.only('preprocess less', () => {
|
||||
test('preprocess less', () => {
|
||||
const style = parse({
|
||||
source:
|
||||
'<style lang="less">\n' +
|
||||
|
@ -115,7 +115,7 @@ test('warn missing preprocessor', () => {
|
||||
expect(result.errors.length).toBe(1)
|
||||
})
|
||||
|
||||
test.only('transform assetUrls', () => {
|
||||
test('transform assetUrls', () => {
|
||||
const source = `
|
||||
<div>
|
||||
<img src="./logo.png">
|
||||
|
55
packages/compiler-sfc/test/stripWith.spec.ts
Normal file
55
packages/compiler-sfc/test/stripWith.spec.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { stripWith } from '../src/stripWith'
|
||||
import { compile } from 'web/entry-compiler'
|
||||
import { format } from 'prettier'
|
||||
|
||||
it('should work', () => {
|
||||
const { render } = compile(`<div id="app">
|
||||
<div>{{ foo }}</div>
|
||||
<p v-for="i in list">{{ i }}</p>
|
||||
<foo inline-template>
|
||||
<div>{{ bar }}</div>
|
||||
</foo>
|
||||
</div>`)
|
||||
|
||||
const result = format(stripWith(render, `render`), {
|
||||
semi: false,
|
||||
parser: 'babel'
|
||||
})
|
||||
|
||||
expect(result).not.toMatch(`_vm._c`)
|
||||
expect(result).toMatch(`_vm.foo`)
|
||||
expect(result).toMatch(`_vm.list`)
|
||||
expect(result).not.toMatch(`_vm.i`)
|
||||
expect(result).not.toMatch(`with (this)`)
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"function render() {
|
||||
var _vm = this
|
||||
var _c = _vm._self._c
|
||||
return _c(
|
||||
\\"div\\",
|
||||
{ attrs: { id: \\"app\\" } },
|
||||
[
|
||||
_c(\\"div\\", [_vm._v(_vm._s(_vm.foo))]),
|
||||
_vm._v(\\" \\"),
|
||||
_vm._l(_vm.list, function (i) {
|
||||
return _c(\\"p\\", [_vm._v(_vm._s(i))])
|
||||
}),
|
||||
_vm._v(\\" \\"),
|
||||
_c(\\"foo\\", {
|
||||
inlineTemplate: {
|
||||
render: function () {
|
||||
var _vm = this
|
||||
var _c = _vm._self._c
|
||||
return _c(\\"div\\", [_vm._v(_vm._s(_vm.bar))])
|
||||
},
|
||||
staticRenderFns: [],
|
||||
},
|
||||
}),
|
||||
],
|
||||
2
|
||||
)
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
Loading…
Reference in New Issue
Block a user