Skip to content

Commit 2d3209b

Browse files
authored
feat: support source map for message functions (#147)
* support source map * update tasks
1 parent 5754319 commit 2d3209b

29 files changed

+5298
-1036
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Note: the replacement value **must be boolean literals** and cannot be strings,
209209

210210
- Intlify message format compiler
211211
- [x] vue-i18n message format
212-
- [ ] sourcemap
212+
- [x] sourcemap
213213
- [x] HTML format handling
214214
- [ ] more unit (fuzzing) tests
215215
- [x] performance tests (benchmark)
@@ -324,14 +324,15 @@ Note: the replacement value **must be boolean literals** and cannot be strings,
324324
- [x] vite-plugin-vue-i18n
325325
- [ ] vue-cli-plugin-i18n
326326
- [ ] eslint-plugin-vue-i18n
327-
- [ ] message format pre-compilation tools
328327
- Others
329328
- [ ] documentation
330329
- [x] fallback localization (bubble up)
331330
- [x] SSR
332331
- General tasks
333332
- [x] error handlings
333+
- Next Tasks (v9.1)
334334
- [ ] monorepo
335+
- [ ] message format pre-compilation tools
335336

336337
</details>
337338

devtools-test/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,6 +1865,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
18651865
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
18661866
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
18671867

1868+
source-map@^0.7.3:
1869+
version "0.7.3"
1870+
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
1871+
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
1872+
18681873
sourcemap-codec@^1.4.4:
18691874
version "1.4.8"
18701875
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"

format-explorer/package.json

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,28 @@
77
"license": "MIT",
88
"scripts": {
99
"build": "webpack",
10-
"dev": "webpack-dev-server",
10+
"dev": "webpack serve",
1111
"clean": "rm -rf ./dist"
1212
},
1313
"devDependencies": {
1414
"@vue/compiler-sfc": "^3.0.0",
1515
"css-loader": "^4.3.0",
1616
"file-loader": "^6.1.0",
17+
"html-webpack-plugin": "^4.3.0",
18+
"monaco-editor-webpack-plugin": "^2.0.0",
19+
"style-loader": "^2.0.0",
1720
"ts-loader": "^8.0.0",
18-
"url-loader": "^4.1.0",
1921
"typescript": "^4.0.3",
22+
"url-loader": "^4.1.0",
2023
"vue-loader": "^16.0.0-beta.8",
21-
"webpack": "^4.44.0",
22-
"webpack-cli": "^3.3.12",
23-
"webpack-dev-server": "^3.11.0",
24-
"html-webpack-plugin": "^4.3.0",
25-
"monaco-editor-webpack-plugin": "^2.0.0",
26-
"style-loader": "^2.0.0"
24+
"webpack": "^5.1.0",
25+
"webpack-cli": "^4.0.0",
26+
"webpack-dev-server": "^3.11.0"
2727
},
2828
"dependencies": {
29+
"monaco-editor": "^0.21.2",
30+
"source-map": "^0.6.1",
2931
"vue": "^3.0.0",
30-
"vue-i18n": "link:..",
31-
"monaco-editor": "^0.21.2"
32+
"vue-i18n": "link:.."
3233
}
3334
}

format-explorer/src/App.vue

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { defineComponent, ref } from 'vue'
33
import Navigation from './components/Navigation.vue'
44
import Editor from './components/Editor.vue'
55
import { baseCompile } from 'vue-i18n'
6+
import * as monaco from 'monaco-editor'
7+
import { debounce } from './utils'
8+
import { SourceMapConsumer } from 'source-map'
69
import type { CompileError, CompileOptions } from 'vue-i18n'
710
811
interface PersistedState {
@@ -35,27 +38,33 @@ export default defineComponent({
3538
* utilties
3639
*/
3740
let lastSuccessCode: string
38-
function compile(message: string): string {
41+
let lastSuccessfulMap: SourceMapConsumer | undefined
42+
async function compile(message: string): Promise<string> {
3943
console.clear()
4044
4145
try {
4246
const start = performance.now()
4347
4448
const errors: CompileError[] = []
45-
const { code, ast } = baseCompile(message, {
46-
onError: err => errors.push(err)
47-
})
49+
const options = {
50+
sourceMap: true,
51+
onError: (err: CompileError) => errors.push(err)
52+
}
53+
const { code, ast, map } = baseCompile(message, options)
4854
if (errors.length > 0) {
4955
console.error(errors)
5056
}
5157
5258
console.log(`Compiled in ${(performance.now() - start).toFixed(2)}ms.`)
5359
compileErrors.value = errors
5460
console.log(`AST: `, ast)
61+
console.log('sourcemap', map)
5562
5663
const evalCode = new Function(`return ${code}`)()
5764
lastSuccessCode =
5865
evalCode.toString() + `\n\n// Check the console for the AST`
66+
lastSuccessfulMap = await new SourceMapConsumer(map!)
67+
lastSuccessfulMap!.computeColumnSpans()
5968
} catch (e) {
6069
lastSuccessCode = `/* ERROR: ${e.message} (see console for more info) */`
6170
console.error(e)
@@ -67,42 +76,157 @@ export default defineComponent({
6776
/**
6877
* envet handlers
6978
*/
70-
const onChange = (message: string): void => {
79+
80+
let inputEditor: monaco.editor.IStandaloneCodeEditor | null = null
81+
let outputEditor: monaco.editor.IStandaloneCodeEditor | null = null
82+
83+
// input editor model change event
84+
const onChangeModel = async (message: string): Promise<void> => {
7185
const state = JSON.stringify({ src: message } as PersistedState)
7286
localStorage.setItem('state', state)
7387
window.location.hash = encodeURIComponent(state)
74-
genCodes.value = compile(message)
88+
genCodes.value = await compile(message)
89+
}
90+
91+
// highlight output codes
92+
let prevOutputDecos: string[] = []
93+
function clearOutputDecos() {
94+
if (!outputEditor) {
95+
return
96+
}
97+
prevOutputDecos = outputEditor.deltaDecorations(prevOutputDecos, [])
98+
}
99+
100+
let prevInputDecos: string[] = []
101+
function clearInputDecos() {
102+
if (!inputEditor) {
103+
return
104+
}
105+
prevInputDecos = inputEditor.deltaDecorations(prevInputDecos, [])
106+
}
107+
108+
// input editor ready event
109+
const onReadyInput = (editor: monaco.editor.IStandaloneCodeEditor) => {
110+
inputEditor = editor
111+
inputEditor.onDidChangeCursorPosition(
112+
debounce(e => {
113+
clearInputDecos()
114+
if (lastSuccessfulMap) {
115+
const pos = lastSuccessfulMap.generatedPositionFor({
116+
source: 'message.intl',
117+
line: e.position.lineNumber,
118+
column: e.position.column
119+
})
120+
if (pos.line != null && pos.column != null) {
121+
prevOutputDecos = outputEditor!.deltaDecorations(
122+
prevOutputDecos,
123+
[
124+
{
125+
range: new monaco.Range(
126+
pos.line,
127+
pos.column + 1,
128+
pos.line,
129+
pos.lastColumn ? pos.lastColumn + 2 : pos.column + 2
130+
),
131+
options: {
132+
inlineClassName: `highlight`
133+
}
134+
}
135+
]
136+
)
137+
outputEditor!.revealPositionInCenter({
138+
lineNumber: pos.line,
139+
column: pos.column + 1
140+
})
141+
} else {
142+
clearOutputDecos()
143+
}
144+
}
145+
}, 100)
146+
)
147+
}
148+
149+
// output editor ready event
150+
const onReadyOutput = (editor: monaco.editor.IStandaloneCodeEditor) => {
151+
outputEditor = editor
152+
editor.onDidChangeCursorPosition(
153+
debounce(e => {
154+
clearOutputDecos()
155+
if (lastSuccessfulMap) {
156+
const pos = lastSuccessfulMap.originalPositionFor({
157+
line: e.position.lineNumber,
158+
column: e.position.column
159+
})
160+
console.log('onReadyOutput', e.position, pos)
161+
if (
162+
pos.line != null &&
163+
pos.column != null &&
164+
!(
165+
// ignore mock location
166+
(pos.line === 1 && pos.column === 0)
167+
)
168+
) {
169+
const translatedPos = {
170+
column: pos.column + 1,
171+
lineNumber: pos.line
172+
}
173+
prevInputDecos = inputEditor!.deltaDecorations(prevInputDecos, [
174+
{
175+
range: new monaco.Range(
176+
pos.line,
177+
pos.column + 1,
178+
pos.line,
179+
pos.column + 1
180+
),
181+
options: {
182+
isWholeLine: true,
183+
className: `highlight`
184+
}
185+
}
186+
])
187+
inputEditor!.revealPositionInCenter(translatedPos)
188+
} else {
189+
clearInputDecos()
190+
}
191+
}
192+
}, 100)
193+
)
75194
}
76195
77196
// setup context
78197
return {
79198
initialCodes,
80199
genCodes,
81200
compileErrors,
82-
onChange
201+
onChangeModel,
202+
onReadyInput,
203+
onReadyOutput
83204
}
84205
}
85206
})
86207
</script>
87208

88209
<template>
89210
<div class="container">
90-
<div class="navigation">
211+
<nav class="navigation">
212+
<!-- prettier-ignore -->
91213
<Navigation class="navigation" />
92-
</div>
214+
</nav>
93215
<div class="operation">
94216
<Editor
95217
class="input"
96218
language="intlify"
97219
:code="initialCodes"
98220
:errors="compileErrors"
99221
:debounce="true"
100-
@change="onChange"
222+
@change-model="onChangeModel"
223+
@ready="onReadyInput"
101224
/>
102225
<!-- prettier-ignore -->
103226
<Editor
104227
class="output"
105228
:code="genCodes"
229+
@ready="onReadyOutput"
106230
/>
107231
</div>
108232
</div>
@@ -119,6 +243,7 @@ export default defineComponent({
119243
box-sizing: border-box;
120244
background-color: var(--bg);
121245
border-bottom: 1px solid var(--border);
246+
display: contents;
122247
}
123248
.operation {
124249
width: 100%;

format-explorer/src/components/Editor.vue

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
11
<script lang="ts">
22
import { defineComponent, ref, onMounted, watchEffect } from 'vue'
33
import theme from '../theme'
4+
import { debounce } from '../utils'
45
import * as monaco from 'monaco-editor'
56
import type { PropType } from 'vue'
67
import type { CompileError } from 'vue-i18n'
78
8-
/* eslint-disable */
9-
function debounce<T extends (...args: any[]) => any>(
10-
fn: T,
11-
delay: number = 300
12-
): T {
13-
let prevTimer: number | null = null
14-
return ((...args: any[]) => {
15-
if (prevTimer) {
16-
clearTimeout(prevTimer)
17-
}
18-
prevTimer = window.setTimeout(() => {
19-
fn(...args)
20-
prevTimer = null
21-
}, delay)
22-
}) as any
23-
}
24-
/* eslint-enable */
25-
26-
export type Foo = number
27-
289
export default defineComponent({
2910
name: 'Editor',
3011
@@ -48,7 +29,8 @@ export default defineComponent({
4829
},
4930
5031
emits: {
51-
change: null
32+
ready: null,
33+
'change-model': null
5234
},
5335
5436
setup(props, { emit }) {
@@ -90,8 +72,8 @@ export default defineComponent({
9072
window.addEventListener('resize', () => editor.layout())
9173
9274
const changeEmitter = props.debounce
93-
? debounce(() => emit('change', editor.getValue()))
94-
: () => emit('change', editor.getValue())
75+
? debounce(() => emit('change-model', editor.getValue()))
76+
: () => emit('change-model', editor.getValue())
9577
9678
editor.onDidChangeModelContent(changeEmitter)
9779
@@ -103,6 +85,8 @@ export default defineComponent({
10385
props.errors.filter(e => e.location).map(formatError)
10486
)
10587
})
88+
89+
emit('ready', editor)
10690
})
10791
10892
return { container }
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
1+
<script lang="ts">
2+
import { defineComponent } from 'vue'
3+
4+
export default defineComponent({
5+
name: 'Navigation'
6+
})
7+
</script>
8+
19
<template>
2-
<div class="navigation-root">
3-
<h2>Intlify Message Format Explorer</h2>
4-
</div>
10+
<h2>Intlify Message Format Explorer</h2>
11+
<!-- eslint-disable-next-line vue/no-multiple-template-root -->
12+
<ul>
13+
<li>
14+
<!-- prettier-ignore -->
15+
</li>
16+
</ul>
517
</template>
618

719
<style scoped>
820
h2 {
921
margin: 0;
10-
padding: 0.4rem 0 0.4rem 2rem;
22+
margin-left: 2rem;
23+
display: inline-block;
24+
}
25+
ul {
26+
list-style-type: none;
27+
display: inline-block;
1128
}
1229
</style>

0 commit comments

Comments
 (0)