From f9dd11925392da4179cf4aaa2c2085ad26f003e3 Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Thu, 23 Mar 2023 10:47:58 +0200 Subject: [PATCH 1/4] Make ftl imports side-effect free --- .../vite/__snapshots__/external.spec.ts.snap | Bin 9997 -> 9807 bytes src/plugins/external-plugin.ts | 22 +++++++++++++----- src/types.ts | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap b/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap index 9be675fdf78148654b53219c5ee53d80d91a9495..09b44d19bf1435f1ca11f0059ef4499f6892fdc9 100644 GIT binary patch delta 154 zcmeD6JMXhWm2L9`HfCn!w4Bn^yb^VtYW37Sb*t38__UH79rct%b*q#_5W6;3XY)kP wd7PxEzQeq^Txu=b5zz3-6FHrLig-4gsa$6Q00~1i5C8xG delta 198 zcmX@_)9bfEl}$UoI4wCoH!(9$FD<7uHLpa$Rsln(rbeN<)|zYb1vbacuY{PGHw%j0 qV&1IIdx(`>?d@D7>QtEgQYdP2jxy)w-wF~;lLf^bHs>fmW&!~DCP#b# diff --git a/src/plugins/external-plugin.ts b/src/plugins/external-plugin.ts index a11d1ed..c26ba4c 100644 --- a/src/plugins/external-plugin.ts +++ b/src/plugins/external-plugin.ts @@ -20,8 +20,13 @@ function getInsertInfo(source: string): InsertInfo { target = 'script' // @vitejs/plugin-vue - if (source.includes('_sfc_main')) - target = '_sfc_main' + if (source.includes('_sfc_main')) { + return { + target: '_sfc_main', + insertPos: source.indexOf('export default'), + usePos: source.indexOf('_export_sfc(_sfc_main, [') + 24, + } + } // vue-loader if (source.includes('__exports__')) @@ -165,7 +170,7 @@ if (${__HOT_API__}) { if (isVue(id)) { const magic = new MagicString(source, { filename: id }) - const { insertPos, target } = getInsertInfo(source) + const { insertPos, target, usePos } = getInsertInfo(source) const translations = await getTranslationsForFile(id) @@ -177,9 +182,14 @@ if (${__HOT_API__}) { 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`) + if (usePos == null) { + magic.appendLeft(insertPos, `${target}.fluent = ${target}.fluent || {};\n`) + for (const dep of translations) + magic.appendLeft(insertPos, `${target}.fluent['${dep.locale}'] = ${dep.importVariable}\n`) + } + else { + magic.appendRight(usePos, `['fluent',{${translations.map(dep => `'${dep.locale}':${dep.importVariable}`).join(',')}}],`) + } insertHotCode(magic, translations, target, insertPos) diff --git a/src/types.ts b/src/types.ts index e87391a..5eeee05 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,5 +22,6 @@ export interface SFCPluginOptions { export interface InsertInfo { insertPos: number + usePos?: number target: string } From 2c725b433720fb7ce65802b32eb4d4cc7da844e7 Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Thu, 23 Mar 2023 11:33:40 +0200 Subject: [PATCH 2/4] Mark FluentResource constructor as pure --- src/plugins/external-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/external-plugin.ts b/src/plugins/external-plugin.ts index c26ba4c..2df2217 100644 --- a/src/plugins/external-plugin.ts +++ b/src/plugins/external-plugin.ts @@ -208,7 +208,7 @@ if (${__HOT_API__}) { return ` import { FluentResource } from '@fluent/bundle' -export default new FluentResource(${JSON.stringify(source)}) +export default /*#__PURE__*/ new FluentResource(${JSON.stringify(source)}) ` } From b75d2e69888683892a494fa199d102ef1ab8b86b Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Thu, 23 Mar 2023 11:36:05 +0200 Subject: [PATCH 3/4] Update snapshots --- .../vite/__snapshots__/external.spec.ts.snap | Bin 9807 -> 9877 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap b/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap index 09b44d19bf1435f1ca11f0059ef4499f6892fdc9..32491a25a1bc791fecf1209aec17cbbf84c3bebb 100644 GIT binary patch delta 75 zcmX@_Gu3y4JlEtmO1zWhxRyd`@yQA*pEmbP#4v7F=iS4JL$;b5rhK!4iarwnD)1U` delta 48 rcmbR0d){Y*JlEs^6|u?ATz#7ZR5%znKj)U Date: Thu, 23 Mar 2023 13:17:02 +0200 Subject: [PATCH 4/4] Greatly simplify external-plugin --- .../vite/__snapshots__/external.spec.ts.snap | Bin 9877 -> 9193 bytes src/loader-query.ts | 4 +- src/plugins/external-plugin.ts | 132 ++++++------------ src/types.ts | 6 - 4 files changed, 41 insertions(+), 101 deletions(-) diff --git a/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap b/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap index 32491a25a1bc791fecf1209aec17cbbf84c3bebb..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 9877 zcmeHNZExE)5Z-6~ifbEGcA!|v(yfJqbj7lC?T5Y~Ns9rl!6*_PYn4Soq+AE7{`(%1 zlKc`oZHHj%Fa?1vk;milUU_%+eR>}&E)|`oK_L+}htD#RD>*pVNQloA&(k2Gr*z5HbCtXe)R^|phjd8i&i%u~L-I%QOcgno zekdlHNO`KHU$5&;3r>J?Voc?Vt|`oUp!f@+jzp>eMu8;8G*yhCYq!PQeJ70-0KMFRTvWMwkEK%-3 zeO#Vy#KhCD!xV}XqGWOuvPlr9IOla)qrWC4ev4(61XrgjkJC%T%#|^(V)=2xC*a-t z*COHu&T@Nk*dvkg^w&g$9}PtFwt{00v4q18FUl;Apl`V=csct_R)K53hp>?*p%z_3BqDyP#^1{B1fr{^N|c z{jPTxyUTHq^Jpi7dJB0Q0UjBcBT=NPqHR9l8M1#jEM6eo4y+*^ln=XIr*s^r(YhXE z<4OK9g7$&MkE7lFCqF;h*OF9M85bki-urjQ?8U1ScJl7^*~?eQjL|(f2e*J^M{&v{ zmmWS`2iCK~JmI0pqt1;TA?;fAyOr8EHs1@vkY}nh5g_cTQJ8o{oqKriR;q);SOdBy z&=C{~_)}yNJWvG8Ik%d}M4DVtiPK>zmS`-?lU5y}PdCrmtFd${#5J?Pc)JrIw0?Ji zxg?%QZb;pPED=Gp5tYH=lr<}eS$C~@RQs!EFgnI&ySt2KNpTscJ21Mwh1-EbXHaIx z(MMcyq4ta`gEfx_5caYwyxoBrOT4k>cJ~SJp+{PrvRexjT&m^Y8zluC*udqftK*M# zt_w<9&qslow{F}4*R4b?UhP&EM+WY$JM3=WMXxe22$@EK4D0OZ-uji*{tZmCQtu19 z(q9ozIO6O4aLiKQQ2X_URqG~^kI;{JyI&n~y|NnpTG-cG_L7|$Iu|vyLH%Qe8athh zVLcQ}N(E&(9CS`W79%2&HcKf9-hf6ThXGdC0;{26Ar)f!g^n_FY*J`9MQRL$li)HA ztEroOI#MDxlO;Z331xLY-<`k*ZTkxbGIuVB4g=Tn+x4!sKJ~3Kc`jtv zw~A8^y@6{j*Yym)?28^|Y!oNl^_CR8Ip^~AM{D)4>p07Ir7vG5%(ZDA>7N_rKjBgo zdC1{N%+1L4wYe|HSbMOY3%Fj;cq?}(<)3I7vks5=2f=G7zTM{kMT#HLDI8Z~6ezuv zguqGCjPyR>FP#T+IF1)Osw19n)d9S`XMoJd13H4fCVZxKu||~%DdUTTTVzXdnGVe| zT11EEdHkiFrxgUz0N3Lo1yRH!!lMn__3ZXQDA-gzzlftWf-*QO=n{5j4^Rd2#a$*r zf^D=ZNrkJ@q~3uBBVNtHT9@aV-9baVGFjSlFQ~;2n?j4#0UK=^i|oLKGNkjZ~z0>4y0pY2Os&BN7En~5^fw7HtWtU8V#TW13>gr z?H{TOq@6#2uek4=AKa4Ud=mLb+2%KEvbZ+CQEh(H<~MDA6SesbE^mm1o~cVUg{3dw z=&+{EZ**>uhWpKGuOJi^X)yE2F-E~w5BtIjFYj{bO_w01!k7Jx+7qoI;hL)OjtJ*be@c= zS7}Rl_N~)Jf?AiYO&0&t*ZSek9QwEdGr$;K-;QfZL4bOw+Y6a zbtKb%{qH4bF#Nl1E@^yii|9LU5#32!sL?#Fb?ci$8(J;RO0BbP!^Qp{qx>ftF1oYX Nc3kRa^EY-}{sNg*6x09! 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 2df2217..d77f5da 100644 --- a/src/plugins/external-plugin.ts +++ b/src/plugins/external-plugin.ts @@ -5,39 +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')) { - return { - target: '_sfc_main', - insertPos: source.indexOf('export default'), - usePos: source.indexOf('_export_sfc(_sfc_main, [') + 24, - } - } - - // 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 { @@ -54,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', @@ -81,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) { @@ -135,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 @@ -164,34 +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, usePos } = getInsertInfo(source) - const translations = await getTranslationsForFile(id) if (translations.length === 0) return - for (const { ftlPath } of translations) - this.addWatchFile(ftlPath) - - insertFtlImports(magic, translations) - - if (usePos == null) { - magic.appendLeft(insertPos, `${target}.fluent = ${target}.fluent || {};\n`) - for (const dep of translations) - magic.appendLeft(insertPos, `${target}.fluent['${dep.locale}'] = ${dep.importVariable}\n`) - } - else { - magic.appendRight(usePos, `['fluent',{${translations.map(dep => `'${dep.locale}':${dep.importVariable}`).join(',')}}],`) - } - - insertHotCode(magic, translations, target, insertPos) + for (const { relativeFtlPath, locale } of translations) + magic.append(`\n`) return { code: magic.toString(), @@ -212,6 +142,24 @@ 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 5eeee05..ccf3eb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,9 +19,3 @@ export interface SFCPluginOptions { blockType?: string checkSyntax?: boolean } - -export interface InsertInfo { - insertPos: number - usePos?: number - target: string -}