From 10bc3cf72f36d9a6677ddc5785a26b5d65c5a1bf Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 4 Aug 2023 13:21:31 +0300 Subject: [PATCH] Mark ftl imports side-effect free (#55) --- .../vite/__snapshots__/external.spec.ts.snap | Bin 9997 -> 9193 bytes src/loader-query.ts | 4 +- src/plugins/external-plugin.ts | 124 ++++++------------ src/types.ts | 5 - 4 files changed, 42 insertions(+), 91 deletions(-) diff --git a/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap b/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap index 9be675fdf78148654b53219c5ee53d80d91a9495..2570737b59b6f43ba318e2ac27673e28c28dea61 100644 GIT binary patch literal 9193 zcmeGhYj4{&aG&)nE^S~*fg;67w-yQ973+| zek5+%VnC5j`QV7;@p!x^?=FhqW2UH7aKhq3PPl?uKY)o+g*=F&nOO@{I*ZQBETxgG zp~8up=8i+>1s6&VPc;&tb43M#EzoH`E%g8@Y2Y_3W{Wt8w~ z!5L*rM$Nu(R#HGHC&MM7;2IJ^V?|$b^_(+>U=T|nL{mk`jkOBKf=}Vorw7q94H6}J zhWK%OXc2}}s^U=Jaj+4Kr?=llB*OTI31`SQBF71t#u-DL*VTe@#sAD?k;hjjN@VQZ z5WY4S)Ym)8=@jYw_=cy{z}cHEPY1vY%KpfCa$z9a(Hf2g#0m~Oz0991McE2f#`yuA z7V7E%S&z4JoN>t#m2qY-CMZj(@JOQO0gxn4Ce(*(2S8AiA_I@Klz6^|F!HJ?H>YZT zrr8XC17KyI2Qb9#um)=GeiM|eam)}BZY$57&^+h6rKG|QP*3#7n|MkO5U-kt%@8Ud zsp>jl!L=VN@Q&$BYjPnw8u$Q5{=YzeT6v;pd zT*M>doDL}K$;VLBq3zpk^vCt&P}@e%XB;K9YR7TQ}HTAYeyzLPWSeo{Pt*H^P}bkAs;^+k(aNJ$?=Cb?_a$> zA_TVd0VQ&FoH3fZ(CeZ5xd9eh<2j}Y7wO{k=0vov#?>^8Z2egnCkZW-H|0p!Q)8f7 zJX<*SYg0w#u@37RkdC;_Q6I}9Memv-W{^He|kl!+_ zv3KyEHv!$Mh}AX~3w#QoKDDbMFLZp7@cHv%5*5Ni)(!?Ytd0>Fqw5;f(51}Pj|)aG z*KVX^$wN3nFPBqL6O6zT&}V}*GvO!vV~ZGqC|>G-f}v!)i5=CJ0Wv=i4W&7qFQY2B zl-XHMEwYu600XlPmeJw&!u7-M(;5O`h_9d##A!-Xz)u@7n$?{lR}(6l^<^BDxwgSs zLsyE|C)P1!`IITT@exci2~uH{o;D}Ia7^npD%RH8(~*X3ZLzZVMn0R6bW@G*aOAhE z@m(JL=Tze~{(>4WbNH7{j?D7K`nJ|KJ#>cn0uj8BeiIkg%n~8cOV5(zZJ~@w;>01^ z*_aFPEbQ=tUIj3V^ATXIud(SjU}KFMKwBY#=&jmJ>*t}h;X{0~JaA4A8{ctDx^T(G zh3qngE>q|-g)URbSo^9onMIc=tR)m(rm&n|g@SgO!g?9a$O;7#OVP!*PfUKwT);86uY5DqEwmBhSawtSJwgUJ#Zm^?$7P*c6ap4N%wnX^PcM>~nyD zs+_lH|IkEXdq@{H-{Bcls4F~f0UZDq#WHzfM0KJiWy{6DMmPgmzxLj$4 NRPckl8~3~R{x8VWeJTI| literal 9997 zcmeHNZExE)5Z-6~ifbEGcA!|v(yc{;bj7lC?T5Y~Ns9rl!6*_PtCdATq+AE7{`(%1 zlKc`oZH9K(S_OtJk;milUU_%%eR>-!E)|`mK_*8+(PYP?ky4r5^ZkjL>qUIxUleh~ zeOW>EMrxcm4xeTsSF(Smkr1CMo~J=VkLiM|rz&|Js1fa)4d{T*oVy1H2jmaqsVZ_V z{ZNcEk@8eYzh2jy6r2F%#F)wzT~V0xK=Efn9g0)|i~>oFX{s2zHdoRx7h`(&?ymnt zgZN<*13%vPEyCWItDvXPc(4{rM=$>zk$~w|I4KI)P$hP2ceWB^eiW=bWe?N8S)$zi z`nWt@i;1UShA9*&M9JhZWaA)Can7rFHtnV&ag;B z6^qm!Eaz#&^9~Eu)FT!K;fQzX$|1@*G%KYJYbfb-HH6WjigL57mT?qM@a<7rB#B3T zET<(8urklu#sxFeny(b;G+NhV zY&6cFN6sPyIPX!GUH+h+k5-wh&_98%#Po@JbnJ+h%veY=inBQ>@ZGw z9B)}k^voU&#GG3%~1k7|GQ1V+c$bbFhzEGaJHbPGneXW_P>&>56@ zPO`(KfLGA&v*vTJ8v67Ju4exlXy4_jtNp)qZY$=az|31W>NuL4OWxYDb7`;^XCHdC z=yh-QmB#3=t-0A#1AO3-)*RWbbq4}jV!^GCs$GsguEnc8*!)P}-F64v^}Fa*1_sgD z=&E6z9o<>IvSM`u)2!6{!i4ot#1oECK06$<0ydMH+Ap`^+6;(%+%gc_cdH|=whN=m zbNgB+vS8AN&UvF~Q2$(^#!jar7%Ih*QbAdcSddeY#Rz_+J%E%1$3sJr!`7=Ch&6_= zkP6p_a~;rT*rd=Nk< z;>?dB5cOSa|E_0PVP6Jb-(_qVC*1WGbhkOe;w4He?4av7i#M*%UrEg6Yj$6FYOMX3 zOHt$@hdVO&E7#XfV=+=&9oagE0|$-E2T%D&TEwNpDn1}_1&ucw@V`pqJvxEkNsRo{ z3rPsEB+W?g9sbf;$cKx2rsF>%|3)3^%R2_heBGl%s9VCPT9s;4nUFF*Pq;<45W(rd zETegJc%DUS+In0;5cP499#9ZPJR&^W0AJ5;_Jx8;)bsN=O46b+I4kH9W?~Oeh5Y$l z#zBH@v?WM|OVzmEf%-#U&B53f=bGI?L%cLu+H=n-!cXghkd?ZPON~Kw;DK?QfsN1n zvJK!W$dQ3SCV)VMorK}5Oi4nJ!oWW4Q`>*X!uCJ#OOGZ&G9cVdDs0vpTQnL#hcJNX zrP{o!tEQdbhpFFn&h~Fehdzq@gKRUQ75QG938^*{YBQlW6N=hQsLg~-hSg?5I;n3n zp*9n0Ga=M6+e~Po9QR#hLLXx^hGdw>nIeg@eRiAN$SnAO^okl%b&gwijBelOY0xZY zODnoP$W0kbP1nI~+G=LGtGf*5se@Xt&Y5+SLaP?aTcxel*|$y?scc;eH@!iW=Jl`Z zIMtS@ck5mYk5IWskBJOa>AX6B&qJlliYVg0Hnlf(iS1_49-)vG%M&B|6D@hus^Doc z=6Qfp$Q}&?6unJCj!HDC9EC$v)2fVn9+&Pq{0QAGk2WfKZ5a9C9X*KvKO)m@F%&=a zm8s%T+y44bwZC>BJ=}iq=)o^=0B!q={pjtlZ`a;3oDTDev?m)4{!%}TABZTm);|GfA{?Hk?ZZ2LEL=lPcXo4)}==|@KZ diff --git a/src/loader-query.ts b/src/loader-query.ts index 5a521f0..91d0825 100644 --- a/src/loader-query.ts +++ b/src/loader-query.ts @@ -1,5 +1,3 @@ -import type { SFCPluginOptions } from './types' - export interface VueQuery { vue?: boolean type?: 'script' | 'template' | 'style' | 'custom' | 'fluent' @@ -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) diff --git a/src/plugins/external-plugin.ts b/src/plugins/external-plugin.ts index a11d1ed..d77f5da 100644 --- a/src/plugins/external-plugin.ts +++ b/src/plugins/external-plugin.ts @@ -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 { @@ -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', @@ -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) { @@ -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 @@ -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(`\n`) return { code: magic.toString(), @@ -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 }, } diff --git a/src/types.ts b/src/types.ts index e87391a..ccf3eb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,8 +19,3 @@ export interface SFCPluginOptions { blockType?: string checkSyntax?: boolean } - -export interface InsertInfo { - insertPos: number - target: string -}