Skip to content

Mark ftl imports side-effect free #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 4, 2023
Merged
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
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,3 @@ export interface SFCPluginOptions {
blockType?: string
checkSyntax?: boolean
}

export interface InsertInfo {
insertPos: number
target: string
}