Skip to content

Commit 10bc3cf

Browse files
authored
Mark ftl imports side-effect free (#55)
1 parent 666d3a0 commit 10bc3cf

File tree

4 files changed

+42
-91
lines changed

4 files changed

+42
-91
lines changed
Binary file not shown.

src/loader-query.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { SFCPluginOptions } from './types'
2-
31
export interface VueQuery {
42
vue?: boolean
53
type?: 'script' | 'template' | 'style' | 'custom' | 'fluent'
@@ -33,7 +31,7 @@ export function parseVueRequest(id: string) {
3331
}
3432
}
3533

36-
export function isCustomBlock(query: VueQuery, options: SFCPluginOptions): boolean {
34+
export function isCustomBlock(query: VueQuery, options: { blockType: string }): boolean {
3735
return (
3836
'vue' in query
3937
&& (query.type === 'custom' // for vite (@vite-plugin-vue)

src/plugins/external-plugin.ts

+41-83
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,18 @@ import { createUnplugin } from 'unplugin'
55
import MagicString from 'magic-string'
66
import { createFilter, makeLegalIdentifier } from '@rollup/pluginutils'
77

8-
import type { ExternalPluginOptions, InsertInfo } from '../types'
8+
import type { ExternalPluginOptions } from '../types'
9+
import { isCustomBlock, parseVueRequest } from '../loader-query'
910
import { getSyntaxErrors } from './ftl/parse'
1011

11-
function getInsertInfo(source: string): InsertInfo {
12-
let target = null
13-
14-
// vite-plugin-vue2
15-
if (source.includes('__component__'))
16-
target = '__component__'
17-
18-
// rollup-plugin-vue
19-
if (source.includes('export default script'))
20-
target = 'script'
21-
22-
// @vitejs/plugin-vue
23-
if (source.includes('_sfc_main'))
24-
target = '_sfc_main'
25-
26-
// vue-loader
27-
if (source.includes('__exports__'))
28-
target = '__exports__'
29-
30-
const insertPos = source.indexOf('export default')
31-
32-
if (insertPos === -1 || target === null)
33-
throw new Error('Could not parse vue component. This is the issue with unplugin-fluent-vue.\nPlease report this issue to the unplugin-fluent-vue repository.')
12+
const isVue = createFilter(['**/*.vue'])
13+
const isFtl = createFilter(['**/*.ftl'])
3414

35-
return { insertPos, target }
15+
interface Dependency {
16+
locale: string
17+
ftlPath: string
18+
relativeFtlPath: string
19+
importVariable: string
3620
}
3721

3822
async function fileExists(filename: string): Promise<boolean> {
@@ -49,17 +33,7 @@ function normalizePath(path: string) {
4933
return path.replace(/\\/g, '/')
5034
}
5135

52-
const isVue = createFilter(['**/*.vue'])
53-
const isFtl = createFilter(['**/*.ftl'])
54-
55-
interface Dependency {
56-
locale: string
57-
ftlPath: string
58-
relativeFtlPath: string
59-
importVariable: string
60-
}
61-
62-
export const unplugin = createUnplugin((options: ExternalPluginOptions, meta) => {
36+
export const unplugin = createUnplugin((options: ExternalPluginOptions) => {
6337
const resolvedOptions = {
6438
checkSyntax: true,
6539
virtualModuleName: 'virtual:ftl-for-file',
@@ -76,38 +50,6 @@ export const unplugin = createUnplugin((options: ExternalPluginOptions, meta) =>
7650
}
7751
}
7852

79-
const insertFtlImports = (magic: MagicString, translations: Dependency[]) => {
80-
for (const dep of translations)
81-
magic.prepend(`import ${dep.importVariable} from '${dep.relativeFtlPath}';\n`)
82-
}
83-
84-
const insertHotCode = (magic: MagicString, translations: Dependency[], target: string, insertPos: number) => {
85-
const __HOT_API__ = meta.framework === 'webpack' ? 'import.meta.webpackHot' : 'import.meta.hot'
86-
87-
magic.appendLeft(insertPos, `
88-
if (${__HOT_API__}) {
89-
${__HOT_API__}.accept([${translations.map(dep => `'${dep.relativeFtlPath}'`).join(', ')}], (mods) => {
90-
${translations.map(({ locale, importVariable }) => `${target}.fluent['${locale}'] = ${importVariable}`).join('\n')}
91-
92-
if (mods) {
93-
${translations.map(({ locale }, index) => `if (mods['${index}']) ${target}.fluent['${locale}'] = mods['${index}'].default`).join('\n')}
94-
}
95-
96-
delete ${target}._fluent
97-
if (typeof __VUE_HMR_RUNTIME__ !== 'undefined') {
98-
// Vue 3
99-
__VUE_HMR_RUNTIME__.reload(${target}.__hmrId, ${target})
100-
} else {
101-
// Vue 2
102-
// There is no proper api to access HMR for component from custom block
103-
// so use this magic
104-
delete ${target}._Ctor
105-
}
106-
})
107-
}
108-
`)
109-
}
110-
11153
const getTranslationsForFile = async (id: string) => {
11254
const dependencies: Dependency[] = []
11355
for (const locale of options.locales) {
@@ -130,13 +72,21 @@ if (${__HOT_API__}) {
13072
return dependencies
13173
}
13274

75+
const isFluentCustomBlock = (id: string) => {
76+
const request = parseVueRequest(id)
77+
return isCustomBlock(request.query, { blockType: 'fluent' })
78+
}
79+
13380
return {
13481
name: 'unplugin-fluent-vue-external',
135-
enforce: meta.framework === 'webpack' ? 'post' : undefined,
82+
enforce: 'pre',
13683
resolveId(id, importer) {
13784
if (id === resolvedOptions.virtualModuleName)
13885
return `${id}?importer=${importer}`
13986
},
87+
loadInclude(id: string) {
88+
return id.startsWith(resolvedOptions.virtualModuleName)
89+
},
14090
async load(id) {
14191
if (!id.startsWith(resolvedOptions.virtualModuleName))
14292
return
@@ -159,29 +109,19 @@ if (${__HOT_API__}) {
159109
return code
160110
},
161111
transformInclude(id: string) {
162-
return isVue(id) || isFtl(id)
112+
return isVue(id) || isFtl(id) || isFluentCustomBlock(id)
163113
},
164114
async transform(source: string, id: string) {
165115
if (isVue(id)) {
166116
const magic = new MagicString(source, { filename: id })
167117

168-
const { insertPos, target } = getInsertInfo(source)
169-
170118
const translations = await getTranslationsForFile(id)
171119

172120
if (translations.length === 0)
173121
return
174122

175-
for (const { ftlPath } of translations)
176-
this.addWatchFile(ftlPath)
177-
178-
insertFtlImports(magic, translations)
179-
180-
magic.appendLeft(insertPos, `${target}.fluent = ${target}.fluent || {};\n`)
181-
for (const dep of translations)
182-
magic.appendLeft(insertPos, `${target}.fluent['${dep.locale}'] = ${dep.importVariable}\n`)
183-
184-
insertHotCode(magic, translations, target, insertPos)
123+
for (const { relativeFtlPath, locale } of translations)
124+
magic.append(`<fluent locale="${locale}" src="${relativeFtlPath}"></fluent>\n`)
185125

186126
return {
187127
code: magic.toString(),
@@ -198,10 +138,28 @@ if (${__HOT_API__}) {
198138

199139
return `
200140
import { FluentResource } from '@fluent/bundle'
201-
export default new FluentResource(${JSON.stringify(source)})
141+
export default /*#__PURE__*/ new FluentResource(${JSON.stringify(source)})
202142
`
203143
}
204144

145+
const query = parseVueRequest(id).query
146+
if (isFluentCustomBlock(id)) {
147+
if (options.checkSyntax) {
148+
const errorsText = getSyntaxErrors(source)
149+
if (errorsText)
150+
this.error(errorsText)
151+
}
152+
153+
return `
154+
import { FluentResource } from '@fluent/bundle'
155+
156+
export default function (Component) {
157+
const target = Component.options || Component
158+
target.fluent = target.fluent || {}
159+
target.fluent['${query.locale}'] = new FluentResource(${JSON.stringify(source)})
160+
}`
161+
}
162+
205163
return undefined
206164
},
207165
}

src/types.ts

-5
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,3 @@ export interface SFCPluginOptions {
1919
blockType?: string
2020
checkSyntax?: boolean
2121
}
22-
23-
export interface InsertInfo {
24-
insertPos: number
25-
target: string
26-
}

0 commit comments

Comments
 (0)