Skip to content
This repository has been archived by the owner on Jul 4, 2021. It is now read-only.

feat: sourcemap supporting #33

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 56 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
![Test](https://github.com/intlify/vite-plugin-vue-i18n/workflows/Test/badge.svg)
[![npm](https://img.shields.io/npm/v/@intlify/vite-plugin-vue-i18n.svg)](https://www.npmjs.com/package/@intlify/vite-plugin-vue-i18n)

Vite plugin for i18n resource pre-compilation and custom blocks
Vite plugin for Vue I18n


## :star: Features
- i18n resources pre-compilation
- i18n custom block

## :cd: Installation

Expand All @@ -19,9 +24,10 @@ $ npm i --save-dev @intlify/vite-plugin-vue-i18n
$ yarn add -D @intlify/vite-plugin-vue-i18n
```

## :rocket: Usages

### i18n resource pre-compilation
## :rocket: Usage

### i18n resources pre-compilation

Since [email protected], The locale messages are handled with message compiler, which converts them to javascript functions after compiling. After compiling, message compiler converts them into javascript functions, which can improve the performance of the application.

Expand All @@ -35,15 +41,15 @@ the below example that `examples/composition/vite.config.ts`:

```ts
import path from 'path'
import vue from '@vitejs/plugin-vue'
import { pluginI18n } from '@intlify/vite-plugin-vue-i18n'
import Vue from '@vitejs/plugin-vue'
import { VueI18n } from '@intlify/vite-plugin-vue-i18n'

import type { UserConfig } from 'vite'

const config: UserConfig = {
plugins: [
vue(), // you need to install `@vitejs/plugin-vue`
pluginI18n({
Vue(), // you need to install `@vitejs/plugin-vue`
VueI18n({
// you need to set i18n resource including paths !
include: path.resolve(__dirname, './path/to/src/locales/**')
})
Expand All @@ -53,9 +59,9 @@ const config: UserConfig = {
export default config
```

### `i18n` custom block
### i18n custom block

the below example that `examples/composition/App.vue` have `i18n` custom block:
the below example that `examples/composition/App.vue` have i18n custom block:

```vue
<template>
Expand Down Expand Up @@ -96,7 +102,6 @@ export default {
}
}
</i18n>

```

### Locale Messages formatting
Expand All @@ -118,30 +123,53 @@ ja:
</i18n>
```

### `forceStringify` options

Whether pre-compile number and boolean values as message functions that return the string value, default `false`
## :wrench: Options

```ts
import path from 'path'
import vue from '@vitejs/plugin-vue'
import { pluginI18n } from '@intlify/vite-plugin-vue-i18n'
### `include`

import type { UserConfig } from 'vite'
- **Type:** `string | string[] | undefined`
- **Default:** `undefined`

const config: UserConfig = {
plugins: [
vue(),
pluginI18n({
forceStringify: true,
include: path.resolve(__dirname, './path/to/src/locales/**')
})
]
}
A [minimatch](https://github.com/isaacs/minimatch) pattern, or array of patterns, you can specify a path to pre-compile i18n resources files. The extensions of i18n resources to be precompiled are as follows:

export default config
```
```
- `json`
- `json5`
- `yaml`
- `yml`
```

Note `json` resources matches this option, it will be handled **before the internal json plugin of Vite, and will not be processed afterwards**, else the option doesn't match, the Vite side will handle.

### `forceStringify`

- **Type:** `boolean`
- **Default:** `false`

Whether pre-compile number and boolean values as message functions that return the string value.

for example, the following json resources:

```json
{
"trueValue": true,
"falseValue": false,
"nullValue": null,
"numberValue": 1
}
```

after pre-compiled (development):

```js
export default {
"trueValue": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize(["true"])};fn.source="true";return fn;})(),
"falseValue": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize(["false"])};fn.source="false";return fn;})(),
"nullValue": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize(["null"])};fn.source="null";return fn;})(),
"numberValue": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize(["1"])};fn.source="1";return fn;})()
}
```

## :scroll: Changelog
Details changes for each release are documented in the [CHANGELOG.md](https://github.com/intlify/vite-plugin-vue-i18n/blob/master/CHANGELOG.md).
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"dependencies": {
"@intlify/cli": "^0.1.2",
"@intlify/shared": "^9.0.0-beta.16",
"@rollup/pluginutils": "^4.1.0"
"@rollup/pluginutils": "^4.1.0",
"source-map": "0.6.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
Expand Down Expand Up @@ -56,7 +57,7 @@
"ts-jest": "^26.4.0",
"typescript": "^4.1.3",
"typescript-eslint-language-service": "^4.1.2",
"vite": "2.0.0-beta.9",
"vite": "2.0.0-beta.10",
"vue": "^3.0.5",
"vue-i18n": "^9.0.0-beta.18"
},
Expand Down
135 changes: 105 additions & 30 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { promises as fs } from 'fs'
import path from 'path'
import { isEmptyObject, isString } from '@intlify/shared'
import { createFilter } from '@rollup/pluginutils'
import { generateJSON, generateYAML } from '@intlify/cli'
import { SourceMapGenerator, SourceMapConsumer, RawSourceMap } from 'source-map'
import { debug as Debug } from 'debug'
import { parseVueRequest } from './query'

Expand All @@ -17,52 +17,107 @@ export function pluginI18n(
): Plugin {
debug('plugin options:', options)

const forceStringify = !!options.forceStringify
const filter = createFilter(options.include)
let config: ResolvedConfig | null = null

return {
name: 'vite-plugin-vue-i18n',

configResolved(_config: ResolvedConfig) {
// store config
config = _config
debug('configResolved', config)

// json transform handling
const jsonPlugin = config.plugins.find(p => p.name === 'json')
if (jsonPlugin) {
const orgTransform = jsonPlugin.transform // backup @rollup/plugin-json
jsonPlugin.transform = async function (code: string, id: string) {
if (!/\.json$/.test(id)) {
return null
}
if (filter(id)) {
const map = this.getCombinedSourcemap()
debug('override json plugin', code, map)
return Promise.resolve({
code,
map
})
} else {
debug('org json plugin')
return orgTransform!.apply(this, [code, id])
}
}
}
},

async transform(code: string, id: string) {
const { filename, query } = parseVueRequest(id)
debug('transform', id, code, JSON.stringify(query))

const parseOptions = getOptions(
filename,
config != null ? config.isProduction : false,
query as Record<string, unknown>,
options.forceStringify
) as CodeGenOptions
debug('parseOptions', parseOptions)
debug('transform', id, JSON.stringify(query))
const sourceMap =
config != null
? config.isProduction
? isString(config.build.sourcemap)
? true
: config.build.sourcemap
: true
: false
let inSourceMap: RawSourceMap | undefined
debug('sourcemap', sourceMap, id)

let langInfo = 'json'
if (!query.vue) {
if (/\.(json5?|ya?ml)$/.test(id) && filter(id)) {
langInfo = path.parse(filename).ext
// NOTE:
// `.json` is handled default in vite, and it's transformed to JS object.
let _source = code
if (langInfo === '.json') {
_source = await getRawJSON(id)
if (sourceMap) {
const map = this.getCombinedSourcemap()
console.log(map)
inSourceMap = (map as unknown) as RawSourceMap
}
langInfo = path.parse(filename).ext

const generate = /\.?json5?/.test(langInfo)
? generateJSON
: generateYAML
const { code: generatedCode } = generate(_source, parseOptions)
debug('generated code', generatedCode)

const parseOptions = getOptions(
filename,
config != null ? config.isProduction : false,
query as Record<string, unknown>,
sourceMap,
inSourceMap,
forceStringify
) as CodeGenOptions
debug('parseOptions', parseOptions)

const { code: generatedCode, map } = generate(code, parseOptions)
debug('generated', map)

// TODO: error handling & sourcempa
return Promise.resolve(generatedCode)
return Promise.resolve({
code: generatedCode,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map: (sourceMap ? map : { mappings: '' }) as any
})
} else {
return Promise.resolve(code)
return Promise.resolve({
code,
map: sourceMap ? this.getCombinedSourcemap() : { mappings: '' }
})
}
} else {
// for Vue SFC
if (isCustomBlock(query as Record<string, unknown>)) {
if (sourceMap) {
const map = this.getCombinedSourcemap()
console.log(map)
// const s = new SourceMapConsumer((map as any).toJSON())
// const s = new SourceMapConsumer(map as any)
// s.eachMapping(m => {
// console.log('sourcemap json', m)
// })
inSourceMap = (map as unknown) as RawSourceMap
}

if ('src' in query) {
if (isString(query.lang)) {
langInfo = query.lang === 'i18n' ? 'json' : query.lang
Expand All @@ -72,25 +127,41 @@ export function pluginI18n(
langInfo = query.lang
}
}

const generate = /\.?json5?/.test(langInfo)
? generateJSON
: generateYAML
const { code: generatedCode } = generate(code, parseOptions)
debug('generated code', generatedCode)

const parseOptions = getOptions(
filename,
config != null ? config.isProduction : false,
query as Record<string, unknown>,
sourceMap,
inSourceMap,
forceStringify
) as CodeGenOptions
debug('parseOptions', parseOptions)

const { code: generatedCode, map } = generate(code, parseOptions)
debug('generated', map)

// TODO: error handling & sourcempa
return Promise.resolve(generatedCode)
return Promise.resolve({
code: generatedCode,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map: (sourceMap ? map : { mappings: '' }) as any
})
} else {
return Promise.resolve(code)
return Promise.resolve({
code,
map: sourceMap ? this.getCombinedSourcemap() : { mappings: '' }
})
}
}
}
}
}

async function getRawJSON(path: string): Promise<string> {
return fs.readFile(path, { encoding: 'utf-8' })
}

function isCustomBlock(query: Record<string, unknown>): boolean {
// NOTE: should be more improvement. difference query type and blocktype in some environment ...
return (
Expand All @@ -106,12 +177,16 @@ function getOptions(
filename: string,
isProduction: boolean,
query: Record<string, unknown>,
forceStringify = false
sourceMap: boolean,
inSourceMap: RawSourceMap | undefined,
forceStringify: boolean
): Record<string, unknown> {
const mode: DevEnv = isProduction ? 'production' : 'production'

const baseOptions = {
filename,
sourceMap,
inSourceMap,
forceStringify,
env: mode,
onWarn: (msg: string): void => {
Expand Down
5 changes: 5 additions & 0 deletions test/__snapshots__/sourcemap.test.ts.snap

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions test/sourcemap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { bundleAndRun } from './utils'

test('i18n custom block only', async () => {
const { map } = await bundleAndRun('basic.vue', { sourcemap: true })
expect(map.mappings).toMatchSnapshot()
})

test('fully blocks', async () => {
const { map } = await bundleAndRun('fully-block.vue', { sourcemap: true })
expect(map.mappings).toMatchSnapshot()
})
Loading