diff --git a/README.md b/README.md index 4ad8f7e3..dbb4d2c3 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,14 @@ export default defineConfig({ SFCFluentPlugin({ // define messages in SFCs blockType: 'fluent', // default 'fluent' - name of the block in SFCs checkSyntax: true, // default true - whether to check syntax of the messages + parseFtl: false, // default false - whether to parse ftl files during build }), ExternalFluentPlugin({ // define messages in external ftl files baseDir: path.resolve('src'), // required - base directory for Vue files ftlDir: path.resolve('src/locales'), // required - directory with ftl files locales: ['en', 'da'], // required - list of locales checkSyntax: true, // default true - whether to check syntax of the messages + parseFtl: false, // default false - whether to parse ftl files during build }), ], }) diff --git a/__tests__/fixtures/errors.vue b/__tests__/fixtures/errors.vue index dbf44ec3..ec225486 100644 --- a/__tests__/fixtures/errors.vue +++ b/__tests__/fixtures/errors.vue @@ -19,4 +19,6 @@ shared-photos = [female] her stream *[other] their stream }. + +entry-without-error = Hello, World! diff --git a/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap b/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap index 7c33fa79..15fab514 100644 Binary files a/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap and b/__tests__/frameworks/vite/__snapshots__/external.spec.ts.snap differ diff --git a/__tests__/frameworks/vite/__snapshots__/sfc.spec.ts.snap b/__tests__/frameworks/vite/__snapshots__/sfc.spec.ts.snap index 01355e3d..8ea879fa 100644 Binary files a/__tests__/frameworks/vite/__snapshots__/sfc.spec.ts.snap and b/__tests__/frameworks/vite/__snapshots__/sfc.spec.ts.snap differ diff --git a/__tests__/frameworks/vite/external.spec.ts b/__tests__/frameworks/vite/external.spec.ts index bb3d478c..968fd99d 100644 --- a/__tests__/frameworks/vite/external.spec.ts +++ b/__tests__/frameworks/vite/external.spec.ts @@ -74,6 +74,29 @@ describe('Vite external', () => { expect(code).toMatchSnapshot() }) + describe('parseFtl', () => { + it('parses ftl syntax during compilation', async () => { + // Arrange + // Act + const code = await compile({ + plugins: [ + vue3({ + compiler, + }), + ExternalFluentPlugin({ + baseDir: resolve(baseDir, 'fixtures'), + ftlDir: resolve(baseDir, 'fixtures/ftl'), + locales: ['en', 'da'], + parseFtl: true, + }), + ], + }, '/fixtures/components/external.vue') + + // Assert + expect(code).toMatchSnapshot() + }) + }) + it('virtual:ftl-for-file', async () => { // Arrange // Act @@ -106,4 +129,57 @@ describe('Vite external', () => { " `) }) + + it('can import FTL files', async () => { + // Arrange + // Act + const code = await compile({ + plugins: [ + vue3({ + compiler, + }), + ExternalFluentPlugin({ + baseDir: resolve(baseDir, 'fixtures'), + ftlDir: resolve(baseDir, 'fixtures/ftl'), + locales: ['en', 'da'], + }), + ], + }, '/fixtures/ftl/en/importer.js.ftl') + + // Assert + expect(code).toMatchInlineSnapshot(` + "=== /fixtures/ftl/en/importer.js.ftl === + + import { FluentResource } from "/@id/virtual:empty:fluent-bundle" + + export default /*#__PURE__*/ new FluentResource("key = Translations for js file") + " + `) + }) + + it('can parse FTL files', async () => { + // Arrange + // Act + const code = await compile({ + plugins: [ + vue3({ + compiler, + }), + ExternalFluentPlugin({ + baseDir: resolve(baseDir, 'fixtures'), + ftlDir: resolve(baseDir, 'fixtures/ftl'), + locales: ['en', 'da'], + parseFtl: true, + }), + ], + }, '/fixtures/ftl/en/importer.js.ftl') + + // Assert + expect(code).toMatchInlineSnapshot(` + "=== /fixtures/ftl/en/importer.js.ftl === + + export default {"body":[{"id":"key","value":"Translations for js file","attributes":{}}]} + " + `) + }) }) diff --git a/__tests__/frameworks/vite/sfc.spec.ts b/__tests__/frameworks/vite/sfc.spec.ts index b343da05..0cec72e9 100644 --- a/__tests__/frameworks/vite/sfc.spec.ts +++ b/__tests__/frameworks/vite/sfc.spec.ts @@ -23,6 +23,46 @@ describe('Vite SFC', () => { expect(code).toMatchSnapshot() }) + describe('parseFtl', () => { + it('parses ftl syntax during compilation', async () => { + // Arrange + // Act + const code = await compile({ + plugins: [ + vue3({ + compiler, + }), + SFCFluentPlugin({ + parseFtl: true, + checkSyntax: false, + }), + ], + }, '/fixtures/test.vue') + + // Assert + expect(code).toMatchSnapshot() + }) + + it('generates block code even if it has errors', async () => { + // Arrange + // Act + const code = await compile({ + plugins: [ + vue3({ + compiler, + }), + SFCFluentPlugin({ + parseFtl: true, + checkSyntax: false, + }), + ], + }, '/fixtures/errors.vue') + + // Assert + expect(code).toMatchSnapshot() + }) + }) + it('supports custom blockType', async () => { // Arrange // Act diff --git a/package.json b/package.json index 4e66b37e..4a16bb02 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "release": "dotenv release-it" }, "peerDependencies": { + "@fluent/bundle": "*", "@nuxt/kit": "^3" }, "peerDependenciesMeta": { @@ -111,6 +112,7 @@ }, "devDependencies": { "@antfu/eslint-config": "^4.3.0", + "@fluent/bundle": "^0.18.0", "@nuxt/kit": "^3.15.4", "@nuxt/schema": "^3.15.4", "@release-it-plugins/lerna-changelog": "7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63ed9de1..7984497a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,10 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + version: 4.3.0(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + '@fluent/bundle': + specifier: ^0.18.0 + version: 0.18.0 '@nuxt/kit': specifier: ^3.15.4 version: 3.15.4(magicast@0.3.5) @@ -44,7 +47,7 @@ importers: version: 5.2.1(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)) '@vitest/coverage-istanbul': specifier: ^3.0.6 - version: 3.0.7(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + version: 3.0.7(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) '@vue/compiler-sfc': specifier: 3.5.13 version: 3.5.13 @@ -80,7 +83,7 @@ importers: version: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) vitest: specifier: 3.0.6 - version: 3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + version: 3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) vue: specifier: 3.5.13 version: 3.5.13(typescript@5.7.3) @@ -434,6 +437,10 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fluent/bundle@0.18.0': + resolution: {integrity: sha512-8Wfwu9q8F9g2FNnv82g6Ch/E1AW1wwljsUOolH5NEtdJdv0sZTuWvfCM7c3teB9dzNaJA8rn4khpidpozHWYEA==} + engines: {node: '>=14.0.0', npm: '>=7.0.0'} + '@fluent/syntax@0.19.0': resolution: {integrity: sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==} engines: {node: '>=14.0.0', npm: '>=7.0.0'} @@ -1985,6 +1992,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + happy-dom@15.11.6: + resolution: {integrity: sha512-elX7iUTu+5+3b2+NGQc0L3eWyq9jKhuJJ4GpOMxxT/c2pg9O3L5H3ty2VECX0XXZgRmmRqXyOK8brA2hDI6LsQ==} + engines: {node: '>=18.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3770,6 +3781,10 @@ packages: webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -3787,6 +3802,10 @@ packages: webpack-cli: optional: true + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -3902,7 +3921,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@4.3.0(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': + '@antfu/eslint-config@4.3.0(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': dependencies: '@antfu/install-pkg': 1.0.0 '@clack/prompts': 0.10.0 @@ -3911,7 +3930,7 @@ snapshots: '@stylistic/eslint-plugin': 4.0.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) - '@vitest/eslint-plugin': 1.1.31(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + '@vitest/eslint-plugin': 1.1.31(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) ansis: 3.16.0 cac: 6.7.14 eslint: 9.21.0(jiti@2.4.2) @@ -4220,6 +4239,8 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 + '@fluent/bundle@0.18.0': {} + '@fluent/syntax@0.19.0': {} '@gar/promisify@1.1.3': {} @@ -4763,7 +4784,7 @@ snapshots: vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) vue: 3.5.13(typescript@5.7.3) - '@vitest/coverage-istanbul@3.0.7(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': + '@vitest/coverage-istanbul@3.0.7(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.0 @@ -4775,17 +4796,17 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vitest: 3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.1.31(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': + '@vitest/eslint-plugin@1.1.31(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': dependencies: '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) eslint: 9.21.0(jiti@2.4.2) optionalDependencies: typescript: 5.7.3 - vitest: 3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vitest: 3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) '@vitest/expect@3.0.6': dependencies: @@ -5970,6 +5991,13 @@ snapshots: graphemer@1.4.0: {} + happy-dom@15.11.6: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + optional: true + has-flag@4.0.0: {} hash-sum@2.0.0: {} @@ -7944,7 +7972,7 @@ snapshots: terser: 5.39.0 yaml: 2.7.0 - vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): + vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.13.5)(happy-dom@15.11.6)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.6 '@vitest/mocker': 3.0.6(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) @@ -7969,6 +7997,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.13.5 + happy-dom: 15.11.6 transitivePeerDependencies: - jiti - less @@ -8023,6 +8052,9 @@ snapshots: webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: + optional: true + webpack-sources@3.2.3: {} webpack-virtual-modules@0.6.2: {} @@ -8057,6 +8089,9 @@ snapshots: - esbuild - uglify-js + whatwg-mimetype@3.0.0: + optional: true + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 diff --git a/src/loader-query.ts b/src/loader-query.ts index 91d08251..20cde329 100644 --- a/src/loader-query.ts +++ b/src/loader-query.ts @@ -17,13 +17,13 @@ export function parseVueRequest(id: string) { ret.type = params.get('type') as VueQuery['type'] if (params.has('blockType')) - ret.blockType = params.get('blockType') + ret.blockType = params.get('blockType') ?? undefined if (params.has('index')) ret.index = Number(params.get('index')) if (params.has('locale')) - ret.locale = params.get('locale') + ret.locale = params.get('locale') ?? undefined return { filename, diff --git a/src/plugins/external-plugin.ts b/src/plugins/external-plugin.ts index 8e496bb4..89ef683c 100644 --- a/src/plugins/external-plugin.ts +++ b/src/plugins/external-plugin.ts @@ -7,7 +7,7 @@ import MagicString from 'magic-string' import { createUnplugin } from 'unplugin' import { isCustomBlock, parseVueRequest } from '../loader-query' -import { getSyntaxErrors } from './ftl/parse' +import { getInjectFtl } from './ftl/inject' const isVue = createFilter(['**/*.vue']) const isFtl = createFilter(['**/*.ftl']) @@ -41,6 +41,7 @@ function isFluentCustomBlock(id: string) { export const unplugin = createUnplugin((options: ExternalPluginOptions) => { const resolvedOptions = { checkSyntax: true, + parseFtl: false, virtualModuleName: 'virtual:ftl-for-file', getFtlPath: undefined as ((locale: string, vuePath: string) => string) | undefined, ...options, @@ -131,55 +132,32 @@ export const unplugin = createUnplugin((options: ExternalPluginOptions) => { } if (isFtl(id)) { - if (options.checkSyntax) { - const errorsText = getSyntaxErrors(source) - if (errorsText) - this.error(errorsText) - } - - const magic = new MagicString(source, { filename: id }) + const injectFtl = getInjectFtl(resolvedOptions, true) + const result = injectFtl` +export default ${source} +` - if (source.length > 0) - magic.update(0, source.length, JSON.stringify(source)) - else - magic.append('""') - magic.prepend(` -import { FluentResource } from '@fluent/bundle' -export default /*#__PURE__*/ new FluentResource(`) - magic.append(')\n') + if (result.error) + this.error(result.error) - return { - code: magic.toString(), - map: magic.generateMap(), - } + return result.code } const query = parseVueRequest(id).query if (isFluentCustomBlock(id)) { - if (options.checkSyntax) { - const errorsText = getSyntaxErrors(source) - if (errorsText) - this.error(errorsText) - } - - const magic = new MagicString(source, { filename: id }) - if (source.length > 0) - magic.update(0, source.length, JSON.stringify(source)) - else - magic.append('""') - magic.prepend(` -import { FluentResource } from '@fluent/bundle' - + const injectFtl = getInjectFtl(resolvedOptions) + const result = injectFtl` export default function (Component) { const target = Component.options || Component target.fluent = target.fluent || {} - target.fluent['${query.locale}'] = new FluentResource(`) - magic.append(')\n}') + target.fluent['${query.locale}'] = ${source} +} +` - return { - code: magic.toString(), - map: magic.generateMap(), - } + if (result.error) + this.error(result.error) + + return result.code } return undefined diff --git a/src/plugins/ftl/inject.ts b/src/plugins/ftl/inject.ts new file mode 100644 index 00000000..e4907ced --- /dev/null +++ b/src/plugins/ftl/inject.ts @@ -0,0 +1,64 @@ +import type { SourceMap } from 'magic-string' +import { FluentResource } from '@fluent/bundle' +import MagicString from 'magic-string' +import { getSyntaxErrors } from './parse' + +type InjectFtlFn = (template: TemplateStringsArray, locale?: string, source?: string) => { code?: { code: string, map: SourceMap }, error?: string } + +function normalize(str: string) { + return str.replace(/\r\n/g, '\n').trim() +} + +export function getInjectFtl(options: { checkSyntax: boolean, parseFtl: boolean }, addPureAnotation = false): InjectFtlFn { + return (template, locale, source) => { + if (source == null) { + source = locale + locale = undefined + } + + const pureAnotation = addPureAnotation ? '/*#__PURE__*/ ' : '' + + if (source == null) + throw new Error('Missing source') + + if (options.checkSyntax) { + const errorsText = getSyntaxErrors(normalize(source)) + + if (errorsText) { + return { + error: errorsText, + } + } + } + + const magic = new MagicString(source) + const importString = options.parseFtl === true ? '' : '\nimport { FluentResource } from \'@fluent/bundle\'\n' + + if (source.length === 0) { + magic.append('{"body":[]}') + } + else if (options.parseFtl === true) { + const resource = new FluentResource(normalize(source)) + magic.overwrite(0, source.length, JSON.stringify(resource)) + } + else { + magic.overwrite(0, source.length, `${pureAnotation}new FluentResource(${JSON.stringify(normalize(source))})`) + } + + if (template.length === 2) { + magic.prepend(importString + template[0]) + magic.append(template[1]) + } + else if (template.length === 3) { + magic.prepend(importString + template[0] + locale + template[1]) + magic.append(template[2]) + } + + return { + code: { + code: magic.toString(), + map: magic.generateMap(), + }, + } + } +} diff --git a/src/plugins/sfc-plugin.ts b/src/plugins/sfc-plugin.ts index 2a5fd84a..2a7c9396 100644 --- a/src/plugins/sfc-plugin.ts +++ b/src/plugins/sfc-plugin.ts @@ -1,15 +1,16 @@ import type { VitePlugin } from 'unplugin' import type { SFCPluginOptions } from '../types' -import MagicString from 'magic-string' import { createUnplugin } from 'unplugin' + import { isCustomBlock, parseVueRequest } from '../loader-query' -import { getSyntaxErrors } from './ftl/parse' +import { getInjectFtl } from './ftl/inject' export const unplugin = createUnplugin((options: SFCPluginOptions, meta) => { const resolvedOptions = { blockType: 'fluent', checkSyntax: true, + parseFtl: false, ...options, } @@ -22,48 +23,33 @@ export const unplugin = createUnplugin((options: SFCPluginOptions, meta) => { }, async transform(source: string, id: string) { const { query } = parseVueRequest(id) + if (!isCustomBlock(query, resolvedOptions)) { + return undefined + } - if (isCustomBlock(query, resolvedOptions)) { - const originalSource = source - - const magic = new MagicString(source, { filename: id }) - - source = source.replace(/\r\n/g, '\n').trim() - - if (query.locale == null) - this.error('Custom block does not have locale attribute') - - // I have no idea why webpack processes this file multiple times - if (source.includes('FluentResource') || source.includes('unplugin-fluent-vue-sfc')) - return undefined - - if (resolvedOptions.checkSyntax) { - const errorsText = getSyntaxErrors(source) - if (errorsText) - this.error(errorsText) - } - - if (originalSource.length > 0) - magic.update(0, originalSource.length, JSON.stringify(source)) - else - magic.append('""') + const locale = query.locale + if (locale == null) { + this.error('Custom block does not have locale attribute') + return + } - magic.prepend(` -import { FluentResource } from '@fluent/bundle' + // I have no idea why webpack processes this file multiple times + if (source.includes('FluentResource') || source.includes('unplugin-fluent-vue-sfc') || source.includes('target.fluent')) + return undefined + const injectFtl = getInjectFtl(resolvedOptions) + const result = injectFtl` export default function (Component) { const target = Component.options || Component target.fluent = target.fluent || {} - target.fluent['${query.locale}'] = new FluentResource(`) - magic.append(')\n}\n') + target.fluent['${locale}'] = ${source} +} +` - return { - code: magic.toString(), - map: magic.generateMap(), - } - } + if (result.error) + this.error(result.error) - return undefined + return result.code }, } }) diff --git a/src/types.ts b/src/types.ts index 20d5b129..c9da0442 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,9 +13,24 @@ export interface ExternalPluginOptionsFunction extends ExternalPluginOptionsBase getFtlPath: (locale: string, vuePath: string) => string } -export type ExternalPluginOptions = ExternalPluginOptionsFolder | ExternalPluginOptionsFunction +export type ExternalPluginOptions = (ExternalPluginOptionsFolder | ExternalPluginOptionsFunction) & { + /** + * Whether to parse the ftl syntax before injecting it into component + */ + parseFtl?: boolean +} export interface SFCPluginOptions { + /** + * Whether to parse the ftl syntax before injecting it into component + */ + parseFtl?: boolean + /** + * Vue custom block name + */ blockType?: string + /** + * Whether to check for syntax errors in the ftl source + */ checkSyntax?: boolean }