From faa5178d329b10940598ba83cb71ba3de536a3aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Bedn=C3=A1r?=
<33372892+jakubednar@users.noreply.github.com>
Date: Fri, 24 May 2024 21:27:16 +0200
Subject: [PATCH] feat(nuxt): handle nuxt route injection for `this.$route`
(#27313)
---
.../nuxt/src/pages/plugins/route-injection.ts | 33 +++++++--
packages/nuxt/test/route-injection.test.ts | 73 +++++++++++++++++++
2 files changed, 99 insertions(+), 7 deletions(-)
create mode 100644 packages/nuxt/test/route-injection.test.ts
diff --git a/packages/nuxt/src/pages/plugins/route-injection.ts b/packages/nuxt/src/pages/plugins/route-injection.ts
index 257f104e19..41d235b2da 100644
--- a/packages/nuxt/src/pages/plugins/route-injection.ts
+++ b/packages/nuxt/src/pages/plugins/route-injection.ts
@@ -1,10 +1,13 @@
import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import type { Nuxt } from '@nuxt/schema'
+import { stripLiteral } from 'strip-literal'
import { isVue } from '../../core/utils'
-const INJECTION_RE = /\b_ctx\.\$route\b/g
-const INJECTION_SINGLE_RE = /\b_ctx\.\$route\b/
+const INJECTION_RE_TEMPLATE = /\b_ctx\.\$route\b/g
+const INJECTION_RE_SCRIPT = /\bthis\.\$route\b/g
+
+const INJECTION_SINGLE_RE = /\bthis\.\$route\b|\b_ctx\.\$route\b/
export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
return {
@@ -14,14 +17,30 @@ export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
return isVue(id, { type: ['template', 'script'] })
},
transform (code) {
- if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol')) { return }
+ if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol') || code.includes('this._.provides[__nuxt_route_symbol')) { return }
let replaced = false
const s = new MagicString(code)
- s.replace(INJECTION_RE, () => {
- replaced = true
- return '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)'
- })
+ const strippedCode = stripLiteral(code)
+
+ // Local helper function for regex-based replacements using `strippedCode`
+ const replaceMatches = (regExp: RegExp, replacement: string) => {
+ for (const match of strippedCode.matchAll(regExp)) {
+ const start = match.index!
+ const end = start + match[0].length
+ s.overwrite(start, end, replacement)
+ if (!replaced) {
+ replaced = true
+ }
+ }
+ }
+
+ // handles `$route` in template
+ replaceMatches(INJECTION_RE_TEMPLATE, '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)')
+
+ // handles `this.$route` in script
+ replaceMatches(INJECTION_RE_SCRIPT, '(this._.provides[__nuxt_route_symbol] || this.$route)')
+
if (replaced) {
s.prepend('import { PageRouteSymbol as __nuxt_route_symbol } from \'#app/components/injections\';\n')
}
diff --git a/packages/nuxt/test/route-injection.test.ts b/packages/nuxt/test/route-injection.test.ts
new file mode 100644
index 0000000000..d2b7b6f69a
--- /dev/null
+++ b/packages/nuxt/test/route-injection.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from 'vitest'
+import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc'
+import type { Plugin } from 'vite'
+import type { Nuxt } from '@nuxt/schema'
+
+import { RouteInjectionPlugin } from '../src/pages/plugins/route-injection'
+
+describe('route-injection:transform', () => {
+ const injectionPlugin = RouteInjectionPlugin({ options: { sourcemap: { client: false, server: false } } } as Nuxt).raw({}, { framework: 'rollup' }) as Plugin
+
+ const transform = async (source: string) => {
+ const result = await (injectionPlugin.transform! as Function).call({ error: null, warn: null } as any, source, 'test.vue')
+ const code: string = typeof result === 'string' ? result : result?.code
+ let depth = 0
+ return code.split('\n').map((l) => {
+ l = l.trim()
+ if (l.match(/^[}\]]/)) { depth-- }
+ const res = ''.padStart(depth * 2, ' ') + l
+ if (l.match(/[{[]$/)) { depth++ }
+ return res
+ }).join('\n')
+ }
+
+ it('should correctly inject route in template', async () => {
+ const sfc = `{{ $route.path }}`
+ const res = compileTemplate({
+ filename: 'test.vue',
+ id: 'test.vue',
+ source: sfc,
+ })
+ const transformResult = await transform(res.code)
+ expect(transformResult).toMatchInlineSnapshot(`
+ "import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';
+ import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+ export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock("template", null, [
+ _createTextVNode(_toDisplayString((_ctx._.provides[__nuxt_route_symbol] || _ctx.$route).path), 1 /* TEXT */)
+ ]))
+ }"
+ `)
+ })
+
+ it('should correctly inject route in options api', async () => {
+ const sfc = `
+ {{ thing }}
+
+ `
+
+ const res = compileScript(parse(sfc).descriptor, { id: 'test.vue' })
+ const transformResult = await transform(res.content)
+ expect(transformResult).toMatchInlineSnapshot(`
+ "import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';
+
+ export default {
+ computed: {
+ thing () {
+ return (this._.provides[__nuxt_route_symbol] || this.$route).path
+ }
+ }
+ }
+ "
+ `)
+ })
+})