Skip to content

Commit

Permalink
fix: defineI18nRoute macro transformed inside <template> (#2887)
Browse files Browse the repository at this point in the history
  • Loading branch information
BobbieGoede committed Mar 29, 2024
1 parent e234134 commit 3374650
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 66 deletions.
4 changes: 2 additions & 2 deletions specs/fixtures/basic/pages/index.vue
Expand Up @@ -66,8 +66,8 @@ useHead(() => ({
go to product foo
</NuxtLink>
<NuxtLink id="link-history" :to="localePath({ name: 'history' })">go to history</NuxtLink>
<NuxtLink id="link-define-i18n-route-false" exact :to="localePath('/define-i18n-route-false')">
go to defineI18nRoute(false)
<NuxtLink id="link-define-i18n-route-false" exact :to="localePath('/define-i18n-route-false')"
>go to defineI18nRoute(false)
</NuxtLink>
<section>
<span id="issue-2020-existing">{{ localePath('/test-route?foo=bar') }}</span>
Expand Down
7 changes: 7 additions & 0 deletions specs/routing_strategies/prefix.spec.ts
Expand Up @@ -153,6 +153,13 @@ describe('strategy: prefix', async () => {
await restore()
})

test('should not transform `defineI18nRoute()` inside template', async () => {
const { page } = await renderPage('/', { locale: 'en' })
await waitForURL(page, '/en')

expect(await getText(page, '#link-define-i18n-route-false')).toEqual('go to defineI18nRoute(false)')
})

test("(#2132) should redirect on root url with `redirectOn: 'no prefix'`", async () => {
const restore = await startServerWithRuntimeConfig({
public: {
Expand Down
38 changes: 2 additions & 36 deletions src/transform/i18n-function-injection.ts
Expand Up @@ -9,10 +9,9 @@ import createDebug from 'debug'
import MagicString from 'magic-string'
import { walk } from 'estree-walker'
import { transform } from 'sucrase'
import { pathToFileURL } from 'node:url'
import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo'
import { parse as parseSFC } from '@vue/compiler-sfc'
import { isVue } from './utils'

import type { Node } from 'estree-walker'
import type { CallExpression, Pattern } from 'estree'
Expand Down Expand Up @@ -138,6 +137,7 @@ export const TransformI18nFunctionPlugin = createUnplugin((options: TransformI18
if (s.hasChanged()) {
debug('transformed: id -> ', id)
debug('transformed: code -> ', s.toString())

return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined
Expand All @@ -159,40 +159,6 @@ function extractScriptContent(html: string) {
return null
}

// from https://github.com/nuxt/nuxt/blob/a80d1a0d6349bf1003666fc79a513c0d7370c931/packages/nuxt/src/core/utils/plugins.ts#L4-L35
function isVue(id: string, opts: { type?: Array<'template' | 'script' | 'style'> } = {}) {
// Bare `.vue` file (in Vite)
const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
if (id.endsWith('.vue') && !search) {
return true
}

if (!search) {
return false
}

const query = parseQuery(search)

// Component async/lazy wrapper
if (query.nuxt_component) {
return false
}

// Macro
if (query.macro && (search === '?macro=true' || !opts.type || opts.type.includes('script'))) {
return true
}

// Non-Vue or Styles
const type = 'setup' in query ? 'script' : (query.type as 'script' | 'template' | 'style')
if (!('vue' in query) || (opts.type && !opts.type.includes(type))) {
return false
}

// Query `?vue&type=template` (in Webpack or external template)
return true
}

// from https://github.com/nuxt/nuxt/blob/a80d1a0d6349bf1003666fc79a513c0d7370c931/packages/vite/src/plugins/composable-keys.ts#L148-L184
/*
* track scopes with unique keys. for example
Expand Down
57 changes: 29 additions & 28 deletions src/transform/macros.ts
Expand Up @@ -7,11 +7,10 @@
*/

import createDebug from 'debug'
import { pathToFileURL } from 'node:url'
import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo'
import MagicString from 'magic-string'
import { VIRTUAL_PREFIX_HEX } from './utils'
import { createUnplugin } from 'unplugin'
import { parse as parseSFC } from '@vue/compiler-sfc'
import { VIRTUAL_PREFIX_HEX, isVue } from './utils'
import { NUXT_I18N_COMPOSABLE_DEFINE_ROUTE } from '../constants'

export interface TransformMacroPluginOptions {
Expand All @@ -29,46 +28,48 @@ const debug = createDebug('@nuxtjs/i18n:transform:macros')
export const TransformMacroPlugin = createUnplugin((options: TransformMacroPluginOptions) => {
return {
name: 'nuxtjs:i18n-macros-transform',
enforce: 'post',
enforce: 'pre',

transformInclude(id) {
if (!id || id.startsWith(VIRTUAL_PREFIX_HEX)) {
return false
}
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
return pathname.endsWith('.vue') || !!parseQuery(search).macro

return isVue(id, { type: ['script'] })
},

transform(code, id) {
debug('transform', id)

const s = new MagicString(code)
const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href))

function result() {
if (s.hasChanged()) {
debug('transformed: id -> ', id)
debug('transformed: code -> ', s.toString())
return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined
}
}
const parsed = parseSFC(code, { sourceMap: false })
// only transform <script>
const script = parsed.descriptor.scriptSetup ?? parsed.descriptor.script
if (!script) {
return
}

// tree-shake out any runtime references to the macro.
// we do this first as it applies to all files, not just those with the query
const match = code.match(new RegExp(`\\b${NUXT_I18N_COMPOSABLE_DEFINE_ROUTE}\\s*\\(\\s*`))
const s = new MagicString(code)

// match content inside <script>
const match = script.content.match(new RegExp(`\\b${NUXT_I18N_COMPOSABLE_DEFINE_ROUTE}\\s*\\(\\s*`))
if (match?.[0]) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
s.overwrite(match.index!, match.index! + match[0].length, `false && /*#__PURE__*/ ${match[0]}`)
}
// tree-shake out any runtime references to the macro.
const scriptString = new MagicString(script.content)
scriptString.overwrite(match.index!, match.index! + match[0].length, `false && /*#__PURE__*/ ${match[0]}`)

if (!parseQuery(search).macro) {
return result()
// using the locations from the parsed result we only replace the <script> contents
s.overwrite(script.loc.start.offset, script.loc.end.offset, scriptString.toString())
}

return result()
if (s.hasChanged()) {
debug('transformed: id -> ', id)
debug('transformed: code -> ', s.toString())

return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined
}
}
}
}
})
37 changes: 37 additions & 0 deletions src/transform/utils.ts
@@ -1,3 +1,6 @@
import { pathToFileURL } from 'node:url'
import { parseQuery, parseURL } from 'ufo'

import type { UnpluginContextMeta } from 'unplugin'

export const VIRTUAL_PREFIX = '\0' as const
Expand All @@ -15,3 +18,37 @@ export function getVirtualId(id: string, framework: UnpluginContextMeta['framewo
export function asVirtualId(id: string, framework: UnpluginContextMeta['framework'] = 'vite') {
return framework === 'vite' ? VIRTUAL_PREFIX + id : id
}

// from https://github.com/nuxt/nuxt/blob/a80d1a0d6349bf1003666fc79a513c0d7370c931/packages/nuxt/src/core/utils/plugins.ts#L4-L35
export function isVue(id: string, opts: { type?: Array<'template' | 'script' | 'style'> } = {}) {
// Bare `.vue` file (in Vite)
const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
if (id.endsWith('.vue') && !search) {
return true
}

if (!search) {
return false
}

const query = parseQuery(search)

// Component async/lazy wrapper
if (query.nuxt_component) {
return false
}

// Macro
if (query.macro && (search === '?macro=true' || !opts.type || opts.type.includes('script'))) {
return true
}

// Non-Vue or Styles
const type = 'setup' in query ? 'script' : (query.type as 'script' | 'template' | 'style')
if (!('vue' in query) || (opts.type && !opts.type.includes(type))) {
return false
}

// Query `?vue&type=template` (in Webpack or external template)
return true
}

0 comments on commit 3374650

Please sign in to comment.