Skip to content

Commit 191681e

Browse files
committed
feat(warn): catch non exposed loaders through NavigationResult
Close #759
1 parent 13a50b6 commit 191681e

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

src/data-loaders/defineLoader.spec.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,5 +253,97 @@ describe(
253253
expect(nestedPending.value).toEqual(false)
254254
})
255255
})
256+
257+
describe('warns', () => {
258+
// this might happen if the user forgets to export a loader
259+
// https://github.com/posva/unplugin-vue-router/issues/759
260+
it('warns if a NavigationResult is committed', async () => {
261+
const router = getRouter()
262+
const l1 = mockedLoader({ key: 'my-key' })
263+
router.addRoute({
264+
name: '_test',
265+
path: '/fetch',
266+
component: defineComponent({
267+
setup() {
268+
const { data } = l1.loader()
269+
return { data }
270+
},
271+
template: `<p>{{ !!data }}</p>`,
272+
}),
273+
meta: {
274+
// oops! no loader exported
275+
loaders: [],
276+
},
277+
})
278+
const wrapper = mount(RouterViewMock, {
279+
global: {
280+
plugins: [
281+
[
282+
DataLoaderPlugin,
283+
{
284+
router,
285+
},
286+
],
287+
router,
288+
],
289+
},
290+
})
291+
292+
l1.spy.mockResolvedValueOnce(new NavigationResult('/?redirected=true'))
293+
294+
await router.push('/fetch')
295+
await vi.advanceTimersByTimeAsync(1)
296+
expect(router.currentRoute.value.fullPath).toBe('/fetch')
297+
// no data
298+
expect(wrapper.text()).toBe('false')
299+
300+
expect('NavigationResult').toHaveBeenWarned()
301+
expect('key: "my-key"').toHaveBeenWarned()
302+
})
303+
304+
it('warns if a NavigationResult is throw on a non exposed loader', async () => {
305+
const router = getRouter()
306+
const l1 = mockedLoader({ key: 'my-key' })
307+
router.addRoute({
308+
name: '_test',
309+
path: '/fetch',
310+
component: defineComponent({
311+
setup() {
312+
const { data } = l1.loader()
313+
return { data }
314+
},
315+
template: `<p>{{ !!data }}</p>`,
316+
}),
317+
meta: {
318+
// oops! no loader exported
319+
loaders: [],
320+
},
321+
})
322+
const wrapper = mount(RouterViewMock, {
323+
global: {
324+
plugins: [
325+
[
326+
DataLoaderPlugin,
327+
{
328+
router,
329+
},
330+
],
331+
router,
332+
],
333+
},
334+
})
335+
336+
l1.spy.mockRejectedValue(new NavigationResult('/?redirected=true'))
337+
338+
await router.push('/fetch')
339+
await vi.advanceTimersByTimeAsync(1)
340+
expect(router.currentRoute.value.fullPath).toBe('/fetch')
341+
// no data
342+
expect(wrapper.text()).toBe('false')
343+
344+
expect('NavigationResult').toHaveBeenWarned()
345+
expect('key: "my-key"').toHaveBeenWarned()
346+
})
347+
})
256348
}
257349
)

src/data-loaders/defineLoader.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getCurrentContext,
2424
setCurrentContext,
2525
IS_SSR_KEY,
26+
LOADER_SET_KEY,
2627
} from 'unplugin-vue-router/data-loaders'
2728

2829
import { shallowRef } from 'vue'
@@ -31,6 +32,7 @@ import {
3132
toLazyValue,
3233
} from './createDataLoader'
3334
import type { ErrorDefault } from './types-config'
35+
import { warn } from '../core/utils'
3436

3537
/**
3638
* Creates a data loader composable that can be exported by pages to attach the data loading to a route. In this version `data` is always defined.
@@ -215,6 +217,10 @@ export function defineBasicLoader<Data>(
215217
// let the navigation guard collect the result
216218
if (d instanceof NavigationResult) {
217219
to.meta[NAVIGATION_RESULTS_KEY]!.push(d)
220+
// help users find non-exposed loaders during development
221+
if (process.env.NODE_ENV !== 'production') {
222+
warnNonExposedLoader({ to, options, useDataLoader })
223+
}
218224
} else {
219225
entry.staged = d
220226
entry.stagedError = null
@@ -229,6 +235,12 @@ export function defineBasicLoader<Data>(
229235
// e
230236
// )
231237
if (entry.pendingLoad === currentLoad) {
238+
// help users find non-exposed loaders during development
239+
if (process.env.NODE_ENV !== 'production') {
240+
if (e instanceof NavigationResult) {
241+
warnNonExposedLoader({ to, options, useDataLoader })
242+
}
243+
}
232244
// in this case, commit will never be called so we should just drop the error
233245
// console.log(`🚨 error in "${options.key}"`, e)
234246
entry.stagedError = e
@@ -412,6 +424,32 @@ export function defineBasicLoader<Data>(
412424
return useDataLoader
413425
}
414426

427+
/**
428+
* Dev only warning for loaders that return/throw NavigationResult but are not exposed
429+
*
430+
* @param to - [TODO:description]
431+
* @param options - [TODO:description]
432+
* @param useDataLoader - [TODO:description]
433+
*/
434+
function warnNonExposedLoader({
435+
to,
436+
options,
437+
useDataLoader,
438+
}: {
439+
to: RouteLocationNormalizedLoaded
440+
options: DefineDataLoaderOptions_LaxData
441+
useDataLoader: UseDataLoader
442+
}) {
443+
const loaders = to.meta[LOADER_SET_KEY]
444+
console.log(options.key)
445+
if (loaders && !loaders.has(useDataLoader)) {
446+
warn(
447+
'A loader returned a NavigationResult but is not registered on the route. Did you forget to "export" it from the page component?' +
448+
(options.key ? ` (loader key: "${options.key}")` : '')
449+
)
450+
}
451+
}
452+
415453
export interface DefineDataLoaderOptions_LaxData
416454
extends DefineDataLoaderOptionsBase_LaxData {
417455
/**

0 commit comments

Comments
 (0)