Skip to content

Commit

Permalink
Mark ftl imports side-effect free (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
Demivan committed Aug 4, 2023
1 parent 666d3a0 commit 10bc3cf
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 91 deletions.
Binary file modified __tests__/frameworks/vite/__snapshots__/external.spec.ts.snap
Binary file not shown.
4 changes: 1 addition & 3 deletions src/loader-query.ts
@@ -1,5 +1,3 @@
import type { SFCPluginOptions } from './types'

export interface VueQuery {
vue?: boolean
type?: 'script' | 'template' | 'style' | 'custom' | 'fluent'
Expand Down Expand Up @@ -33,7 +31,7 @@ export function parseVueRequest(id: string) {
}
}

export function isCustomBlock(query: VueQuery, options: SFCPluginOptions): boolean {
export function isCustomBlock(query: VueQuery, options: { blockType: string }): boolean {
return (
'vue' in query
&& (query.type === 'custom' // for vite (@vite-plugin-vue)
Expand Down
124 changes: 41 additions & 83 deletions src/plugins/external-plugin.ts
Expand Up @@ -5,34 +5,18 @@ import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import { createFilter, makeLegalIdentifier } from '@rollup/pluginutils'

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

function getInsertInfo(source: string): InsertInfo {
let target = null

// vite-plugin-vue2
if (source.includes('__component__'))
target = '__component__'

// rollup-plugin-vue
if (source.includes('export default script'))
target = 'script'

// @vitejs/plugin-vue
if (source.includes('_sfc_main'))
target = '_sfc_main'

// vue-loader
if (source.includes('__exports__'))
target = '__exports__'

const insertPos = source.indexOf('export default')

if (insertPos === -1 || target === null)
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.')
const isVue = createFilter(['**/*.vue'])
const isFtl = createFilter(['**/*.ftl'])

return { insertPos, target }
interface Dependency {
locale: string
ftlPath: string
relativeFtlPath: string
importVariable: string
}

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

const isVue = createFilter(['**/*.vue'])
const isFtl = createFilter(['**/*.ftl'])

interface Dependency {
locale: string
ftlPath: string
relativeFtlPath: string
importVariable: string
}

export const unplugin = createUnplugin((options: ExternalPluginOptions, meta) => {
export const unplugin = createUnplugin((options: ExternalPluginOptions) => {
const resolvedOptions = {
checkSyntax: true,
virtualModuleName: 'virtual:ftl-for-file',
Expand All @@ -76,38 +50,6 @@ export const unplugin = createUnplugin((options: ExternalPluginOptions, meta) =>
}
}

const insertFtlImports = (magic: MagicString, translations: Dependency[]) => {
for (const dep of translations)
magic.prepend(`import ${dep.importVariable} from '${dep.relativeFtlPath}';\n`)
}

const insertHotCode = (magic: MagicString, translations: Dependency[], target: string, insertPos: number) => {
const __HOT_API__ = meta.framework === 'webpack' ? 'import.meta.webpackHot' : 'import.meta.hot'

magic.appendLeft(insertPos, `
if (${__HOT_API__}) {
${__HOT_API__}.accept([${translations.map(dep => `'${dep.relativeFtlPath}'`).join(', ')}], (mods) => {
${translations.map(({ locale, importVariable }) => `${target}.fluent['${locale}'] = ${importVariable}`).join('\n')}
if (mods) {
${translations.map(({ locale }, index) => `if (mods['${index}']) ${target}.fluent['${locale}'] = mods['${index}'].default`).join('\n')}
}
delete ${target}._fluent
if (typeof __VUE_HMR_RUNTIME__ !== 'undefined') {
// Vue 3
__VUE_HMR_RUNTIME__.reload(${target}.__hmrId, ${target})
} else {
// Vue 2
// There is no proper api to access HMR for component from custom block
// so use this magic
delete ${target}._Ctor
}
})
}
`)
}

const getTranslationsForFile = async (id: string) => {
const dependencies: Dependency[] = []
for (const locale of options.locales) {
Expand All @@ -130,13 +72,21 @@ if (${__HOT_API__}) {
return dependencies
}

const isFluentCustomBlock = (id: string) => {
const request = parseVueRequest(id)
return isCustomBlock(request.query, { blockType: 'fluent' })
}

return {
name: 'unplugin-fluent-vue-external',
enforce: meta.framework === 'webpack' ? 'post' : undefined,
enforce: 'pre',
resolveId(id, importer) {
if (id === resolvedOptions.virtualModuleName)
return `${id}?importer=${importer}`
},
loadInclude(id: string) {
return id.startsWith(resolvedOptions.virtualModuleName)
},
async load(id) {
if (!id.startsWith(resolvedOptions.virtualModuleName))
return
Expand All @@ -159,29 +109,19 @@ if (${__HOT_API__}) {
return code
},
transformInclude(id: string) {
return isVue(id) || isFtl(id)
return isVue(id) || isFtl(id) || isFluentCustomBlock(id)
},
async transform(source: string, id: string) {
if (isVue(id)) {
const magic = new MagicString(source, { filename: id })

const { insertPos, target } = getInsertInfo(source)

const translations = await getTranslationsForFile(id)

if (translations.length === 0)
return

for (const { ftlPath } of translations)
this.addWatchFile(ftlPath)

insertFtlImports(magic, translations)

magic.appendLeft(insertPos, `${target}.fluent = ${target}.fluent || {};\n`)
for (const dep of translations)
magic.appendLeft(insertPos, `${target}.fluent['${dep.locale}'] = ${dep.importVariable}\n`)

insertHotCode(magic, translations, target, insertPos)
for (const { relativeFtlPath, locale } of translations)
magic.append(`<fluent locale="${locale}" src="${relativeFtlPath}"></fluent>\n`)

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

return `
import { FluentResource } from '@fluent/bundle'
export default new FluentResource(${JSON.stringify(source)})
export default /*#__PURE__*/ new FluentResource(${JSON.stringify(source)})
`
}

const query = parseVueRequest(id).query
if (isFluentCustomBlock(id)) {
if (options.checkSyntax) {
const errorsText = getSyntaxErrors(source)
if (errorsText)
this.error(errorsText)
}

return `
import { FluentResource } from '@fluent/bundle'
export default function (Component) {
const target = Component.options || Component
target.fluent = target.fluent || {}
target.fluent['${query.locale}'] = new FluentResource(${JSON.stringify(source)})
}`
}

return undefined
},
}
Expand Down
5 changes: 0 additions & 5 deletions src/types.ts
Expand Up @@ -19,8 +19,3 @@ export interface SFCPluginOptions {
blockType?: string
checkSyntax?: boolean
}

export interface InsertInfo {
insertPos: number
target: string
}

0 comments on commit 10bc3cf

Please sign in to comment.