220 lines
7.8 KiB
JavaScript
220 lines
7.8 KiB
JavaScript
import { computed, defineComponent, h, isReadonly, reactive } from "vue";
|
|
import { isEqual, joinURL, parseQuery, stringifyParsedURL, stringifyQuery, withoutBase } from "ufo";
|
|
import { createError } from "h3";
|
|
import { defineNuxtPlugin, useRuntimeConfig } from "../nuxt.js";
|
|
import { getRouteRules } from "../composables/manifest.js";
|
|
import { clearError, showError } from "../composables/error.js";
|
|
import { navigateTo } from "../composables/router.js";
|
|
import { globalMiddleware } from "#build/middleware";
|
|
import { appManifest as isAppManifestEnabled } from "#build/nuxt.config.mjs";
|
|
function getRouteFromPath(fullPath) {
|
|
const route = fullPath && typeof fullPath === "object" ? fullPath : {};
|
|
if (typeof fullPath === "object") {
|
|
fullPath = stringifyParsedURL({
|
|
pathname: fullPath.path || "",
|
|
search: stringifyQuery(fullPath.query || {}),
|
|
hash: fullPath.hash || ""
|
|
});
|
|
}
|
|
const url = new URL(fullPath.toString(), import.meta.client ? window.location.href : "http://localhost");
|
|
return {
|
|
path: url.pathname,
|
|
fullPath,
|
|
query: parseQuery(url.search),
|
|
hash: url.hash,
|
|
// stub properties for compat with vue-router
|
|
params: route.params || {},
|
|
name: void 0,
|
|
matched: route.matched || [],
|
|
redirectedFrom: void 0,
|
|
meta: route.meta || {},
|
|
href: fullPath
|
|
};
|
|
}
|
|
export default defineNuxtPlugin({
|
|
name: "nuxt:router",
|
|
enforce: "pre",
|
|
setup(nuxtApp) {
|
|
const initialURL = import.meta.client ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash : nuxtApp.ssrContext.url;
|
|
const routes = [];
|
|
const hooks = {
|
|
"navigate:before": [],
|
|
"resolve:before": [],
|
|
"navigate:after": [],
|
|
"error": []
|
|
};
|
|
const registerHook = (hook, guard) => {
|
|
hooks[hook].push(guard);
|
|
return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1);
|
|
};
|
|
const baseURL = useRuntimeConfig().app.baseURL;
|
|
const route = reactive(getRouteFromPath(initialURL));
|
|
async function handleNavigation(url, replace) {
|
|
try {
|
|
const to = getRouteFromPath(url);
|
|
for (const middleware of hooks["navigate:before"]) {
|
|
const result = await middleware(to, route);
|
|
if (result === false || result instanceof Error) {
|
|
return;
|
|
}
|
|
if (typeof result === "string" && result.length) {
|
|
return handleNavigation(result, true);
|
|
}
|
|
}
|
|
for (const handler of hooks["resolve:before"]) {
|
|
await handler(to, route);
|
|
}
|
|
Object.assign(route, to);
|
|
if (import.meta.client) {
|
|
window.history[replace ? "replaceState" : "pushState"]({}, "", joinURL(baseURL, to.fullPath));
|
|
if (!nuxtApp.isHydrating) {
|
|
await nuxtApp.runWithContext(clearError);
|
|
}
|
|
}
|
|
for (const middleware of hooks["navigate:after"]) {
|
|
await middleware(to, route);
|
|
}
|
|
} catch (err) {
|
|
if (import.meta.dev && !hooks.error.length) {
|
|
console.warn("No error handlers registered to handle middleware errors. You can register an error handler with `router.onError()`", err);
|
|
}
|
|
for (const handler of hooks.error) {
|
|
await handler(err);
|
|
}
|
|
}
|
|
}
|
|
const currentRoute = computed(() => route);
|
|
const router = {
|
|
currentRoute,
|
|
isReady: () => Promise.resolve(),
|
|
// These options provide a similar API to vue-router but have no effect
|
|
options: {},
|
|
install: () => Promise.resolve(),
|
|
// Navigation
|
|
push: (url) => handleNavigation(url, false),
|
|
replace: (url) => handleNavigation(url, true),
|
|
back: () => window.history.go(-1),
|
|
go: (delta) => window.history.go(delta),
|
|
forward: () => window.history.go(1),
|
|
// Guards
|
|
beforeResolve: (guard) => registerHook("resolve:before", guard),
|
|
beforeEach: (guard) => registerHook("navigate:before", guard),
|
|
afterEach: (guard) => registerHook("navigate:after", guard),
|
|
onError: (handler) => registerHook("error", handler),
|
|
// Routes
|
|
resolve: getRouteFromPath,
|
|
addRoute: (parentName, route2) => {
|
|
routes.push(route2);
|
|
},
|
|
getRoutes: () => routes,
|
|
hasRoute: (name) => routes.some((route2) => route2.name === name),
|
|
removeRoute: (name) => {
|
|
const index = routes.findIndex((route2) => route2.name === name);
|
|
if (index !== -1) {
|
|
routes.splice(index, 1);
|
|
}
|
|
}
|
|
};
|
|
nuxtApp.vueApp.component("RouterLink", defineComponent({
|
|
functional: true,
|
|
props: {
|
|
to: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
custom: Boolean,
|
|
replace: Boolean,
|
|
// Not implemented
|
|
activeClass: String,
|
|
exactActiveClass: String,
|
|
ariaCurrentValue: String
|
|
},
|
|
setup: (props, { slots }) => {
|
|
const navigate = () => handleNavigation(props.to, props.replace);
|
|
return () => {
|
|
const route2 = router.resolve(props.to);
|
|
return props.custom ? slots.default?.({ href: props.to, navigate, route: route2 }) : h("a", { href: props.to, onClick: (e) => {
|
|
e.preventDefault();
|
|
return navigate();
|
|
} }, slots);
|
|
};
|
|
}
|
|
}));
|
|
if (import.meta.client) {
|
|
window.addEventListener("popstate", (event) => {
|
|
const location = event.target.location;
|
|
router.replace(location.href.replace(location.origin, ""));
|
|
});
|
|
}
|
|
nuxtApp._route = route;
|
|
nuxtApp._middleware ||= {
|
|
global: [],
|
|
named: {}
|
|
};
|
|
const initialLayout = nuxtApp.payload.state._layout;
|
|
nuxtApp.hooks.hookOnce("app:created", async () => {
|
|
router.beforeEach(async (to, from) => {
|
|
to.meta = reactive(to.meta || {});
|
|
if (nuxtApp.isHydrating && initialLayout && !isReadonly(to.meta.layout)) {
|
|
to.meta.layout = initialLayout;
|
|
}
|
|
nuxtApp._processingMiddleware = true;
|
|
if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
|
|
const middlewareEntries = /* @__PURE__ */ new Set([...globalMiddleware, ...nuxtApp._middleware.global]);
|
|
if (isAppManifestEnabled) {
|
|
const routeRules = await nuxtApp.runWithContext(() => getRouteRules({ path: to.path }));
|
|
if (routeRules.appMiddleware) {
|
|
for (const key in routeRules.appMiddleware) {
|
|
const guard = nuxtApp._middleware.named[key];
|
|
if (!guard) {
|
|
return;
|
|
}
|
|
if (routeRules.appMiddleware[key]) {
|
|
middlewareEntries.add(guard);
|
|
} else {
|
|
middlewareEntries.delete(guard);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const middleware of middlewareEntries) {
|
|
const result = await nuxtApp.runWithContext(() => middleware(to, from));
|
|
if (import.meta.server) {
|
|
if (result === false || result instanceof Error) {
|
|
const error = result || createError({
|
|
statusCode: 404,
|
|
statusMessage: `Page Not Found: ${initialURL}`,
|
|
data: {
|
|
path: initialURL
|
|
}
|
|
});
|
|
delete nuxtApp._processingMiddleware;
|
|
return nuxtApp.runWithContext(() => showError(error));
|
|
}
|
|
}
|
|
if (result === true) {
|
|
continue;
|
|
}
|
|
if (result || result === false) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
router.afterEach(() => {
|
|
delete nuxtApp._processingMiddleware;
|
|
});
|
|
await router.replace(initialURL);
|
|
if (!isEqual(route.fullPath, initialURL)) {
|
|
await nuxtApp.runWithContext(() => navigateTo(route.fullPath));
|
|
}
|
|
});
|
|
return {
|
|
provide: {
|
|
route,
|
|
router
|
|
}
|
|
};
|
|
}
|
|
});
|