Repository: vuejs/router (TypeScript)

Issue:
`beforeRouteEnter` does not have an injection context if component is not yet loaded
### Reproduction

https://jsfiddle.net/rv3p5ajn/13/

<details><summary>Code, if JSFiddle is down</summary>

```html
<div id="app">
  <pre>url: {{ $route.fullPath }}</pre>
  <ul>
    <li><router-link to="/page1">Page 1</router-link></li>
    <li><router-link to="/page2">Page 2</router-link></li>
  </ul>
  <router-view></router-view>
</div>

<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
      "vue-router": "https://unpkg.com/vue-router@4/dist/vue-router.esm-browser.js",
      "@vue/devtools-api": "https://unpkg.com/@vue/devtools-api@6/lib/esm/index.js"
    }
  }
</script>

<script type="module">
import { createApp, hasInjectionContext } from "vue";
import { createRouter, createWebHistory } from "vue-router";

const Page1 = {
  template: `<div>page 1</div>`,
  beforeRouteEnter: () => console.log(hasInjectionContext()),
};
const Page2 = {
  template: `<div>page 2</div>`,
  beforeRouteEnter: () => console.log(hasInjectionContext()),
};

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: "/page1", component: async () => Page1 },
    { path: "/page2", component: async () => Page2 },
  ],
});

const app = createApp({});
app.use(router);

window.vm = app.mount("#app");
</script>
```

</details>

### Steps to reproduce the bug

1. Open JSFiddle console
2. Click "Page 1"
3. Click "Page 2"
4. Click "Page 1" again
5. Click "Page 2" again

### Expected behavior

After all clicks value of `hasInjectionContext()` should be `true`.

### Actual behavior

On first and second clicks `hasInjectionContext()` is `false`. On third and fourth, when promises are already resolved, `hasInjectionContext()` is `true`.

### Additional information

According to the [docs](https://router.vuejs.org/guide/advanced/navigation-guards.html#Global-injections-within-guards), all guards should have an injection context.

All other guards do have an injection context, **but only before the first yield**.

I think the problem lies somewhere within `runWithContext`, because it behaves weirdly with promises:
```javascript
const { createApp, hasInjectionContext } = Vue;
const app = createApp({});

app.runWithContext(async () => {
  console.log(hasInjectionContext());
  await new Promise((r) => setTimeout(r));
  console.log(hasInjectionContext());
});
```
Output is `true`, then `false` -- context is reset after first yield.
Maybe the bug is in `vuejs/core`?

EDIT: Turns out that the weird behaviour of `runWithContext` with async functions is known and kind of documented in Nuxt's docs: https://nuxt.com/docs/api/composables/use-nuxt-app#runwithcontext


Discussion:
It seems that because of lazy loaded routes, the actual guard is not executed with the context anymore. Once the component is lazy loaded it's no longer loaded again, that's why it only happens with `beforeRouteEnter`.
I created https://github.com/vuejs/router/pull/2053 but it still needs tests. Feel free to add them. As a workaround you can force a redirect if the injectioncontext is not there:

```ts
{
    template: `<div>page 1</div>`,
    beforeRouteEnter: (to) => {
		if (!hasInjectionContext()) {
    	return to.fullPath
    }
    console.log(hasInjectionContext())
    },
  }
```



Patch:
diff --git a/packages/router/src/navigationGuards.ts b/packages/router/src/navigationGuards.ts
index 148a51f27..27eac6cfc 100644
--- a/packages/router/src/navigationGuards.ts
+++ b/packages/router/src/navigationGuards.ts
@@ -117,14 +117,16 @@ export function guardToPromiseFn(
   to: RouteLocationNormalized,
   from: RouteLocationNormalizedLoaded,
   record: RouteRecordNormalized,
-  name: string
+  name: string,
+  runWithContext: <T>(fn: () => T) => T
 ): () => Promise<void>
 export function guardToPromiseFn(
   guard: NavigationGuard,
   to: RouteLocationNormalized,
   from: RouteLocationNormalizedLoaded,
   record?: RouteRecordNormalized,
-  name?: string
+  name?: string,
+  runWithContext: <T>(fn: () => T) => T = fn => fn()
 ): () => Promise<void> {
   // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
   const enterCallbackArray =
@@ -173,11 +175,13 @@ export function guardToPromiseFn(
       }
 
       // wrapping with Promise.resolve allows it to work with both async and sync guards
-      const guardReturn = guard.call(
-        record && record.instances[name!],
-        to,
-        from,
-        __DEV__ ? canOnlyBeCalledOnce(next, to, from) : next
+      const guardReturn = runWithContext(() =>
+        guard.call(
+          record && record.instances[name!],
+          to,
+          from,
+          __DEV__ ? canOnlyBeCalledOnce(next, to, from) : next
+        )
       )
       let guardCall = Promise.resolve(guardReturn)
 
@@ -231,7 +235,8 @@ export function extractComponentsGuards(
   matched: RouteRecordNormalized[],
   guardType: GuardType,
   to: RouteLocationNormalized,
-  from: RouteLocationNormalizedLoaded
+  from: RouteLocationNormalizedLoaded,
+  runWithContext: <T>(fn: () => T) => T = fn => fn()
 ) {
   const guards: Array<() => Promise<void>> = []
 
@@ -292,7 +297,10 @@ export function extractComponentsGuards(
         const options: ComponentOptions =
           (rawComponent as any).__vccOpts || rawComponent
         const guard = options[guardType]
-        guard && guards.push(guardToPromiseFn(guard, to, from, record, name))
+        guard &&
+          guards.push(
+            guardToPromiseFn(guard, to, from, record, name, runWithContext)
+          )
       } else {
         // start requesting the chunk already
         let componentPromise: Promise<
@@ -324,7 +332,10 @@ export function extractComponentsGuards(
             const options: ComponentOptions =
               (resolvedComponent as any).__vccOpts || resolvedComponent
             const guard = options[guardType]
-            return guard && guardToPromiseFn(guard, to, from, record, name)()
+            return (
+              guard &&
+              guardToPromiseFn(guard, to, from, record, name, runWithContext)()
+            )
           })
         )
       }
diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts
index f5cc271ee..b1ec39dfd 100644
--- a/packages/router/src/router.ts
+++ b/packages/router/src/router.ts
@@ -879,7 +879,8 @@ export function createRouter(options: RouterOptions): Router {
             enteringRecords,
             'beforeRouteEnter',
             to,
-            from
+            from,
+            runWithContext
           )
           guards.push(canceledNavigationCheck)
 


Test patch:
diff --git a/packages/router/__tests__/guards/navigatioGuardsInjections.spec.ts b/packages/router/__tests__/guards/navigatioGuardsInjections.spec.ts
index a660b1c12..fe20236be 100644
--- a/packages/router/__tests__/guards/navigatioGuardsInjections.spec.ts
+++ b/packages/router/__tests__/guards/navigatioGuardsInjections.spec.ts
@@ -48,4 +48,92 @@ describe('inject() within navigation guards', () => {
       await router.isReady()
     })
   }
+
+  describe('in-component guards', () => {
+    it('beforeRouteEnter', async () => {
+      expect.assertions(1)
+      const router = createRouter({
+        routes: [
+          {
+            path: '/',
+            component: {
+              template: `<div>Page</div>`,
+              beforeRouteEnter() {
+                expect(inject('test')).toBe('hello')
+              },
+            },
+          },
+        ],
+      })
+      factory(router)
+      await router.isReady()
+      await router.push('/')
+    })
+
+    it('beforeRouteEnter + lazy load', async () => {
+      expect.assertions(1)
+      const router = createRouter({
+        routes: [
+          {
+            path: '/',
+            component: () =>
+              new Promise(r =>
+                r({
+                  template: `<div>Page</div>`,
+                  beforeRouteEnter() {
+                    expect(inject('test')).toBe('hello')
+                  },
+                })
+              ),
+          },
+        ],
+      })
+      factory(router)
+      await router.isReady()
+      await router.push('/')
+    })
+
+    it('beforeRouteUpdate', async () => {
+      expect.assertions(1)
+      const router = createRouter({
+        routes: [
+          {
+            path: '/',
+            component: {
+              template: `<div>Page</div>`,
+              beforeRouteUpdate() {
+                expect(inject('test')).toBe('hello')
+              },
+            },
+          },
+        ],
+      })
+      factory(router)
+      await router.isReady()
+      await router.push('/')
+      await router.push('/#other')
+    })
+
+    it('beforeRouteLeave', async () => {
+      expect.assertions(1)
+      const router = createRouter({
+        routes: [
+          { path: '/', component: PageComponent },
+          {
+            path: '/foo',
+            component: {
+              template: `<div>Page</div>`,
+              beforeRouteLeave() {
+                expect(inject('test')).toBe('hello')
+              },
+            },
+          },
+        ],
+      })
+      factory(router)
+      await router.isReady()
+      await router.push('/foo')
+      await router.push('/')
+    })
+  })
 })