nuxt
Version:
239 lines (238 loc) • 9.29 kB
JavaScript
import { traceAsync } from "../internal/tracing.js";
import { defineNuxtPlugin, useRuntimeConfig } from "../nuxt.js";
import { isAbsoluteHref, isRootedPath, sanitizeAnchorHref } from "../utils.js";
import { navigationDiagnostics } from "../diagnostics/navigation.js";
import { navigateTo } from "../composables/router.js";
import { clearError, showError } from "../composables/error.js";
import { getRouteRules } from "../composables/manifest.js";
import { computed, defineComponent, h, isReadonly, reactive } from "vue";
import { tracingChannelNuxt } from "#build/nuxt.config.mjs";
import { isEqual, joinURL, parseQuery, stringifyParsedURL, stringifyQuery, withoutBase } from "ufo";
import { createError } from "@nuxt/nitro-server/h3";
import { globalMiddleware } from "#build/middleware";
//#region src/app/plugins/router.ts
const LITERAL_PATH_RE = /^\/[\w\-./~!$&'()*+,;=:@]*$/;
/** Split a rooted path as the `URL` parser would, or `undefined` if it needs the parser. */
function splitLiteralPath(fullPath) {
const hashIndex = fullPath.indexOf("#");
const withoutHash = hashIndex === -1 ? fullPath : fullPath.slice(0, hashIndex);
const searchIndex = withoutHash.indexOf("?");
const path = searchIndex === -1 ? withoutHash : withoutHash.slice(0, searchIndex);
if (path.includes("/.") || !LITERAL_PATH_RE.test(path)) return;
return {
path,
search: searchIndex === -1 ? "" : withoutHash.slice(searchIndex),
hash: hashIndex === -1 || hashIndex === fullPath.length - 1 ? "" : fullPath.slice(hashIndex)
};
}
function getRouteFromPath(fullPath, baseURL = "/") {
const route = fullPath && typeof fullPath === "object" ? fullPath : {};
if (typeof fullPath === "object") fullPath = stringifyParsedURL({
pathname: fullPath.path || "",
search: stringifyQuery(fullPath.query || {}),
hash: fullPath.hash || ""
});
const isRooted = isRootedPath(fullPath);
let parts = isRooted ? splitLiteralPath(fullPath) : void 0;
if (!parts) {
const url = new URL(fullPath, import.meta.client ? window.location.href : "http://localhost");
parts = {
path: url.pathname,
search: url.search,
hash: url.hash
};
}
return {
path: parts.path,
fullPath,
query: parts.search ? parseQuery(parts.search) : {},
hash: parts.hash,
params: route.params || {},
name: void 0,
matched: route.matched || [],
redirectedFrom: void 0,
meta: route.meta || {},
href: isRooted ? baseURL === "/" ? fullPath : joinURL(baseURL, fullPath) : sanitizeAnchorHref(fullPath)
};
}
const plugin = /* @__PURE__ */ defineNuxtPlugin({
name: "nuxt:router",
enforce: "pre",
setup(nuxtApp) {
const initialURL = import.meta.client ? withoutBase(window.location.pathname, (/* @__PURE__ */ 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 () => {
const index = hooks[hook].indexOf(guard);
if (index !== -1) hooks[hook].splice(index, 1);
};
};
const baseURL = (/* @__PURE__ */ useRuntimeConfig()).app.baseURL;
const route = reactive(getRouteFromPath(initialURL, baseURL));
let navigationCounter = 0;
async function handleNavigation(url, replace) {
const navigationId = ++navigationCounter;
try {
const to = getRouteFromPath(url, baseURL);
for (const middleware of hooks["navigate:before"]) {
const result = await middleware(to, route);
if (navigationId !== navigationCounter) return;
if (result === false || result instanceof Error) return;
if (typeof result === "string" && result.length) return await handleNavigation(result, true);
}
for (const handler of hooks["resolve:before"]) {
await handler(to, route);
if (navigationId !== navigationCounter) return;
}
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) navigationDiagnostics.NUXT_E2009({ cause: err });
for (const handler of hooks.error) await handler(err);
} finally {
if (navigationId === navigationCounter) {
delete nuxtApp._processingMiddleware;
if (import.meta.server) delete nuxtApp._middlewareTo;
}
}
}
const router = {
currentRoute: computed(() => route),
isReady: () => Promise.resolve(),
options: {},
install: () => Promise.resolve(),
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),
beforeResolve: (guard) => registerHook("resolve:before", guard),
beforeEach: (guard) => registerHook("navigate:before", guard),
afterEach: (guard) => registerHook("navigate:after", guard),
onError: (handler) => registerHook("error", handler),
resolve: (url) => getRouteFromPath(url, baseURL),
addRoute: (parentName, route) => {
routes.push(route);
},
getRoutes: () => routes,
hasRoute: (name) => routes.some((route) => route.name === name),
removeRoute: (name) => {
const index = routes.findIndex((route) => route.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,
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: String
},
setup: (props, { slots }) => {
const navigate = () => handleNavigation(props.to, props.replace);
const resolved = computed(() => router.resolve(props.to));
const isExternal = computed(() => isAbsoluteHref(props.to));
return () => {
const href = resolved.value.href;
if (props.custom) return slots.default?.({
href,
navigate,
route: resolved.value
});
if (isExternal.value) return h("a", { href }, slots);
return h("a", {
href,
onClick: (e) => {
e.preventDefault();
return navigate();
}
}, slots);
};
}
}));
if (import.meta.client) window.addEventListener("popstate", (event) => {
const location = event.target.location;
router.replace(withoutBase(location.pathname, baseURL) + location.search + location.hash);
});
nuxtApp._route = route;
nuxtApp._middleware ||= {
global: [],
named: {}
};
const initialLayout = nuxtApp.payload.state._layout;
const initialLayoutProps = nuxtApp.payload.state._layoutProps;
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;
to.meta.layoutProps = initialLayoutProps;
}
nuxtApp._processingMiddleware = true;
if (import.meta.server) nuxtApp._middlewareTo = to;
if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
const middlewareEntries = /* @__PURE__ */ new Set([...globalMiddleware, ...nuxtApp._middleware.global]);
const routeRules = getRouteRules({ path: to.path });
if (routeRules.appMiddleware) for (const key in routeRules.appMiddleware) {
const guard = nuxtApp._middleware.named[key];
if (!guard) continue;
if (routeRules.appMiddleware[key]) middlewareEntries.add(guard);
else middlewareEntries.delete(guard);
}
for (const middleware of middlewareEntries) {
if (import.meta.dev) nuxtApp._processingMiddleware = middleware._path || true;
const run = () => nuxtApp.runWithContext(() => middleware(to, from));
const result = await (import.meta.server && tracingChannelNuxt ? traceAsync("nuxt.middleware", { middleware: {
name: middleware._name || middleware.name || void 0,
path: middleware._path,
global: globalMiddleware.includes(middleware) || nuxtApp._middleware.global.includes(middleware)
} }, run) : run());
if (import.meta.server) {
if (result === false || result instanceof Error) {
const error = result || createError({
status: 404,
statusText: `Page Not Found: ${initialURL}`,
data: { path: initialURL }
});
delete nuxtApp._processingMiddleware;
delete nuxtApp._middlewareTo;
return nuxtApp.runWithContext(() => showError(error));
}
}
if (result === true) continue;
if (result || result === false) return result;
}
}
});
router.afterEach(() => {
delete nuxtApp._processingMiddleware;
if (import.meta.server) delete nuxtApp._middlewareTo;
});
await router.replace(initialURL);
if (!isEqual(route.fullPath, initialURL)) await nuxtApp.runWithContext(() => navigateTo(route.fullPath));
});
return { provide: {
route,
router
} };
}
});
//#endregion
export { plugin as default };