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 = `` + 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 = ` + + + ` + + 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 + } + } + } + " + `) + }) +})