Skip to content

Commit 1e6fc21

Browse files
authored
improve directive compilation (#59)
* improve directive compilation * fix lint errors
1 parent dfddbc1 commit 1e6fc21

File tree

10 files changed

+3153
-383
lines changed

10 files changed

+3153
-383
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
},
2727
rules: {
2828
'object-curly-spacing': 'off',
29+
'@typescript-eslint/no-non-null-assertion': 'off',
2930
'@typescript-eslint/explicit-function-return-type': 'off',
3031
'@typescript-eslint/member-delimiter-style': 'off',
3132
'@typescript-eslint/no-use-before-define': 'off'

package.json

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,22 @@
2525
},
2626
"dependencies": {
2727
"@babel/parser": "^7.9.6",
28-
"@vue/compiler-dom": "^3.0.0-beta.15",
29-
"@vue/shared": "^3.0.0-beta.15",
30-
"vue-i18n": "^9.0.0-alpha.11"
28+
"@vue/compiler-dom": "^3.0.0-rc.5",
29+
"@vue/shared": "^3.0.0-rc.5",
30+
"vue-i18n": "^9.0.0-alpha.12"
3131
},
3232
"devDependencies": {
3333
"@types/jest": "^26.0.0",
3434
"@types/node": "^13.1.4",
3535
"@types/webpack": "^4.41.1",
3636
"@types/webpack-merge": "^4.1.5",
37-
"@typescript-eslint/eslint-plugin": "^3.0.0",
38-
"@typescript-eslint/parser": "^3.0.0",
39-
"@typescript-eslint/typescript-estree": "^3.0.0",
40-
"@vue/compiler-sfc": "^3.0.0-beta.15",
41-
"@vue/runtime-dom": "^3.0.0-beta.15",
42-
"@vue/server-renderer": "^3.0.0-beta.15",
37+
"@typescript-eslint/eslint-plugin": "^3.7.0",
38+
"@typescript-eslint/parser": "^3.7.0",
39+
"@vue/compiler-sfc": "^3.0.0-rc.5",
40+
"@vue/runtime-dom": "^3.0.0-rc.5",
41+
"@vue/server-renderer": "^3.0.0-rc.5",
4342
"babel-loader": "^8.1.0",
44-
"eslint": "^7.1.0",
43+
"eslint": "^7.6.0",
4544
"eslint-config-prettier": "^6.10.1",
4645
"eslint-plugin-prettier": "^3.1.2",
4746
"eslint-plugin-vue-libs": "^4.0.0",
@@ -54,10 +53,10 @@
5453
"puppeteer": "^2.1.1",
5554
"shipjs": "^0.20.0",
5655
"ts-jest": "^26.0.0",
57-
"typescript": "^3.9.5",
56+
"typescript": "^3.9.7",
5857
"typescript-eslint-language-service": "^3.0.0",
59-
"vue": "^3.0.0-beta.15",
60-
"vue-loader": "^16.0.0-beta.3",
58+
"vue": "^3.0.0-rc.5",
59+
"vue-loader": "^16.0.0-beta.4",
6160
"webpack": "^4.42.1",
6261
"webpack-cli": "^3.3.11",
6362
"webpack-dev-server": "^3.10.3",
@@ -81,7 +80,7 @@
8180
"license": "MIT",
8281
"main": "lib/index.js",
8382
"peerDependencies": {
84-
"vue": "^3.0.0-beta.5"
83+
"vue": "^3.0.0-rc.5"
8584
},
8685
"repository": {
8786
"type": "git",

src/index.ts

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import {
44
createSimpleExpression,
55
SimpleExpressionNode
66
} from '@vue/compiler-dom'
7-
import { isPlainObject, isString, toDisplayString } from '@vue/shared'
7+
import { isObject, isString, toDisplayString } from '@vue/shared'
88
import { I18n, Locale } from 'vue-i18n'
9-
import { evaluateValue } from './utils'
9+
import { evaluateValue } from './transpiler'
10+
import { report, ReportCodes } from './report'
1011

1112
// TODO: should be imported from vue-i18n
1213
type VTDirectiveValue = {
@@ -16,58 +17,94 @@ type VTDirectiveValue = {
1617
choice?: number
1718
}
1819

20+
// TODO: should be used from shared libs
1921
export const isNumber = (val: unknown): val is number =>
2022
typeof val === 'number' && isFinite(val)
2123

2224
export function defineTransformVT(i18n: I18n): DirectiveTransform {
23-
return (dir, node, context) => {
25+
return (dir, node /*, context*/) => {
2426
const { exp, loc } = dir
25-
// console.log('v-t trans', dir)
26-
// console.log('v-t trans node', node)
2727
if (!exp) {
28-
// TODO: throw error
28+
// TODO: throw error with context.OnError
29+
// NOTE: We need to support from @vue/compiler-core
30+
// https://github.com/vuejs/vue-next/issues/1147
31+
report(ReportCodes.UNEXPECTED_DIRECTIVE_EXPRESSION, {
32+
mode: 'error',
33+
args: [node.loc.source || ''],
34+
loc: node.loc
35+
})
2936
}
30-
if (node.children.length) {
31-
// TODO: throw error
37+
if (node.children.length > 0) {
38+
// TODO: throw error with context.OnError
39+
// NOTE: We need to support from @vue/compiler-core
40+
// https://github.com/vuejs/vue-next/issues/1147
41+
report(ReportCodes.ORVERRIDE_ELEMENT_CHILDREN, {
42+
mode: 'error',
43+
args: [node.loc.source || ''],
44+
loc: node.loc
45+
})
46+
node.children.length = 0
3247
}
3348

34-
const { status, value } = evaluateValue(
35-
(exp as SimpleExpressionNode).content
36-
)
37-
if (status === 'ng') {
38-
// TODO: throw error
39-
}
49+
const simpleExp = exp as SimpleExpressionNode
50+
if (simpleExp.isConstant) {
51+
if (dir.modifiers.includes('preserve')) {
52+
report(ReportCodes.NOT_SUPPORTED_PRESERVE, {
53+
args: [node.loc.source || ''],
54+
loc: node.loc
55+
})
56+
}
4057

41-
const parsedValue = parseValue(value)
42-
const translated = i18n.global.t(...makeParams(parsedValue))
43-
if (translated) {
44-
;(exp as SimpleExpressionNode).content = toDisplayString(
45-
`${JSON.stringify(translated)}`
46-
)
47-
}
58+
const { status, value } = evaluateValue(simpleExp.content)
59+
if (status === 'ng') {
60+
report(ReportCodes.FAILED_VALUE_EVALUATION, {
61+
args: [node.loc.source || ''],
62+
loc: node.loc
63+
})
64+
return { props: [] }
65+
}
66+
67+
const [parsedValue, parseStatus] = parseValue(value)
68+
if (parseStatus !== ReportCodes.SUCCESS) {
69+
report(parseStatus, { args: [node.loc.source || ''], loc: node.loc })
70+
return { props: [] }
71+
}
72+
73+
const translated = i18n.global.t(...makeParams(parsedValue!))
74+
if (translated) {
75+
simpleExp.content = toDisplayString(`${JSON.stringify(translated)}`)
76+
}
4877

49-
return {
50-
props: [
51-
createObjectProperty(
52-
createSimpleExpression(`textContent`, true, loc),
53-
exp || createSimpleExpression('', true)
54-
)
55-
]
78+
// success!
79+
return {
80+
props: [
81+
createObjectProperty(
82+
createSimpleExpression(`textContent`, true, loc),
83+
exp || createSimpleExpression('', true)
84+
)
85+
]
86+
}
87+
} else {
88+
report(ReportCodes.NOT_SUPPORTED_BINDING_PRE_TRANSLATION, {
89+
args: [node.loc.source || ''],
90+
loc: node.loc
91+
})
92+
return { props: [] }
5693
}
5794
}
5895
}
5996

6097
// TODO: should be defined typing ...
61-
function parseValue(value: unknown): VTDirectiveValue {
98+
function parseValue(value: unknown): [VTDirectiveValue | null, ReportCodes] {
6299
if (isString(value)) {
63-
return { path: value }
64-
} else if (isPlainObject(value)) {
100+
return [{ path: value }, ReportCodes.SUCCESS]
101+
} else if (isObject(value)) {
65102
if (!('path' in value)) {
66-
throw new Error('TODO')
103+
return [null, ReportCodes.REQUIRED_PARAMETER]
67104
}
68-
return value as VTDirectiveValue
105+
return [value as VTDirectiveValue, ReportCodes.SUCCESS]
69106
} else {
70-
throw new Error('TODO')
107+
return [null, ReportCodes.INVALID_PARAMETER_TYPE]
71108
}
72109
}
73110

src/report.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { format } from './utils'
2+
import { SourceLocation } from '@vue/compiler-core'
3+
import { isString } from '@vue/shared'
4+
5+
export interface ExtensionsError extends SyntaxError {
6+
code: number
7+
loc?: SourceLocation
8+
}
9+
10+
export interface ReportOptions {
11+
mode?: 'error' | 'warn'
12+
loc?: SourceLocation
13+
args?: unknown[]
14+
}
15+
16+
export const enum ReportCodes {
17+
// Special value for higher-order compilers to pick up the last code
18+
// to avoid collision of error codes. This should always be kept as the last
19+
// item.
20+
SUCCESS,
21+
UNEXPECTED_DIRECTIVE_EXPRESSION,
22+
NOT_SUPPORTED_BINDING_PRE_TRANSLATION,
23+
FAILED_VALUE_EVALUATION,
24+
REQUIRED_PARAMETER,
25+
INVALID_PARAMETER_TYPE,
26+
NOT_SUPPORTED_PRESERVE,
27+
ORVERRIDE_ELEMENT_CHILDREN,
28+
__EXTEND_POINT__
29+
}
30+
31+
// TODO: should be extracted as i18n resources (intlify project self hosting!)
32+
const ReportMessages: { [code: number]: string } = {
33+
[ReportCodes.UNEXPECTED_DIRECTIVE_EXPRESSION]: `Unexpected directive expression: {0}`,
34+
[ReportCodes.NOT_SUPPORTED_BINDING_PRE_TRANSLATION]: `Not support binding in pre-localization: {0}`,
35+
[ReportCodes.FAILED_VALUE_EVALUATION]: `Failed valu evaluation: {0}`,
36+
[ReportCodes.REQUIRED_PARAMETER]: `Required parameter: {0}`,
37+
[ReportCodes.INVALID_PARAMETER_TYPE]: `Required parameter: {0}`,
38+
[ReportCodes.NOT_SUPPORTED_PRESERVE]: `Not supportted 'preserve': {0}`,
39+
[ReportCodes.ORVERRIDE_ELEMENT_CHILDREN]: `v-t will override element children: {0}`
40+
}
41+
42+
export function getReportMessage(
43+
code: ReportCodes,
44+
...args: unknown[]
45+
): string {
46+
return format(ReportMessages[code], ...args)
47+
}
48+
49+
function createExtensionsError(
50+
code: ReportCodes,
51+
msg: string,
52+
loc?: SourceLocation
53+
): ExtensionsError {
54+
const error = new SyntaxError(msg) as ExtensionsError
55+
error.code = code
56+
if (loc) {
57+
error.loc = loc
58+
}
59+
return error
60+
}
61+
62+
export function report(
63+
code: ReportCodes,
64+
optinos: ReportOptions = {}
65+
): void | ExtensionsError {
66+
const mode =
67+
optinos.mode &&
68+
isString(optinos.mode) &&
69+
['warn', 'error'].includes(optinos.mode)
70+
? optinos.mode
71+
: 'warn'
72+
73+
const msg = getReportMessage(code, optinos.args)
74+
if (mode === 'warn') {
75+
console.warn('[vue-i18n-extensions] ' + msg)
76+
} else {
77+
// error
78+
throw createExtensionsError(code, msg, optinos.loc)
79+
}
80+
}

src/transpiler.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { parse } from '@babel/parser'
2+
3+
interface EvaluateReturn {
4+
status: 'ok' | 'ng'
5+
value?: unknown
6+
}
7+
8+
export function evaluateValue(expression: string): EvaluateReturn {
9+
const ret = { status: 'ng', value: undefined } as EvaluateReturn
10+
11+
try {
12+
const ast = parse(`const a = ${expression.trim()}`)
13+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14+
const node = (ast.program.body[0] as any).declarations[0].init
15+
if (node.type === 'StringLiteral' || node.type === 'ObjectExpression') {
16+
const val = new Function(`return ${expression.trim()}`)()
17+
ret.status = 'ok'
18+
ret.value = val
19+
}
20+
} catch (e) {}
21+
22+
return ret
23+
}

src/utils.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
1-
import { parse } from '@babel/parser'
1+
import { isObject } from '@vue/shared'
22

3-
interface EvaluateReturn {
4-
status: 'ok' | 'ng'
5-
value?: unknown
6-
}
7-
8-
export function evaluateValue(expression: string): EvaluateReturn {
9-
const ret = { status: 'ng', value: undefined } as EvaluateReturn
3+
const RE_ARGS = /\{([0-9a-zA-Z]+)\}/g
104

11-
try {
12-
const ast = parse(`const a = ${expression.trim()}`)
13-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14-
const node = (ast.program.body[0] as any).declarations[0].init
15-
if (node.type === 'StringLiteral' || node.type === 'ObjectExpression') {
16-
const val = new Function(`return ${expression.trim()}`)()
17-
ret.status = 'ok'
18-
ret.value = val
5+
// eslint-disable-next-line
6+
export function format(message: string, ...args: any): string {
7+
if (args.length === 1 && isObject(args[0])) {
8+
args = args[0]
9+
}
10+
if (!args || !args.hasOwnProperty) {
11+
args = {}
12+
}
13+
return message.replace(
14+
RE_ARGS,
15+
(match: string, identifier: string): string => {
16+
return args.hasOwnProperty(identifier) ? args[identifier] : ''
1917
}
20-
} catch (e) {}
21-
22-
return ret
18+
)
2319
}

0 commit comments

Comments
 (0)