UNPKG

vue-router

Version:

> - This is the repository for Vue Router 4 (for Vue 3) > - For Vue Router 3 (for Vue 2) see [vuejs/vue-router](https://github.com/vuejs/vue-router). > To see what versions are currently supported, please refer to the [Security Policy](./packages/router

1,368 lines (1,357 loc) 94.2 kB
/*! * vue-router v5.0.6 * (c) 2026 Eduardo San Martin Morote * @license MIT */ import { _ as formatMultilineUnion, b as ts, c as appendExtensionListToPattern, f as joinPath, g as warn, h as throttle, i as resolveOptions, l as asRoutePath, m as mergeRouteRecordOverride, o as ESCAPED_TRAILING_SLASH_RE, p as logTree, r as mergeAllExtensions, s as ImportsMap, v as pad, y as toStringLiteral } from "./options-DrVGtYpN.mjs"; import { createUnplugin } from "unplugin"; import { camelCase } from "scule"; import { promises } from "node:fs"; import path, { dirname, join, parse, relative, resolve } from "pathe"; import { MagicString, babelParse, checkInvalidScopeReference, generateTransform, getLang, isCallOf, parseSFC } from "@vue-macros/common"; import { glob } from "tinyglobby"; import { parse as parse$1 } from "@vue/compiler-sfc"; import JSON5 from "json5"; import { parse as parse$2 } from "yaml"; import { watch } from "chokidar"; import picomatch from "picomatch"; import { generate } from "@babel/generator"; import { walkAST } from "ast-walker-scope"; import { findStaticImports, parseStaticImport } from "mlly"; import { createFilter } from "unplugin-utils"; import MagicString$1 from "magic-string"; //#region src/unplugin/utils/encoding.ts /** * Encoding Rules (␣ = Space) * - Path: ␣ " < > # ? { } * - Query: ␣ " < > # & = * - Hash: ␣ " < > ` * * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2) * defines some extra characters to be encoded. Most browsers do not encode them * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to * also encode `!'()*`. Leaving un-encoded only ASCII alphanumeric(`a-zA-Z0-9`) * plus `-._~`. This extra safety should be applied to query by patching the * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\` * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\` * into a `/` if directly typed in. The _backtick_ (`````) should also be * encoded everywhere because some browsers like FF encode it when directly * written while others don't. Safari and IE don't encode ``"<>{}``` in hash. */ const HASH_RE = /#/g; const IM_RE = /\?/g; /** * NOTE: It's not clear to me if we should encode the + symbol in queries, it * seems to be less flexible than not doing so and I can't find out the legacy * systems requiring this for regular requests like text/html. In the standard, * the encoding of the plus character is only mentioned for * application/x-www-form-urlencoded * (https://url.spec.whatwg.org/#urlencoded-parsing) and most browsers seems lo * leave the plus character as is in queries. To be more flexible, we allow the * plus character on the query, but it can also be manually encoded by the user. * * Resources: * - https://url.spec.whatwg.org/#urlencoded-parsing * - https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20 */ const ENC_BRACKET_OPEN_RE = /%5B/g; const ENC_BRACKET_CLOSE_RE = /%5D/g; const ENC_PIPE_RE = /%7C/g; /** * Encode characters that need to be encoded on the path, search and hash * sections of the URL. * * @internal * @param text - string to encode * @returns encoded string */ function commonEncode(text) { return text == null ? "" : encodeURI("" + text).replace(ENC_PIPE_RE, "|").replace(ENC_BRACKET_OPEN_RE, "[").replace(ENC_BRACKET_CLOSE_RE, "]"); } /** * Encode characters that need to be encoded on the path section of the URL. * * @param text - string to encode * @returns encoded string */ function encodePath(text) { return commonEncode(text).replace(HASH_RE, "%23").replace(IM_RE, "%3F"); } //#endregion //#region src/unplugin/core/treeNodeValue.ts let TreeNodeType = /* @__PURE__ */ function(TreeNodeType) { TreeNodeType[TreeNodeType["static"] = 0] = "static"; TreeNodeType[TreeNodeType["group"] = 1] = "group"; TreeNodeType[TreeNodeType["param"] = 2] = "param"; return TreeNodeType; }({}); const CONVENTION_OVERRIDE_NAME = "@@convention"; const EDITS_OVERRIDE_NAME = "@@edits"; var _TreeNodeValueBase = class { /** * flag based on the type of the segment */ _type; parent; /** * segment as defined by the file structure e.g. keeps the `index` name, `(group-name)` */ rawSegment; /** * transformed version of the segment into a vue-router path. e.g. `'index'` becomes `''` and `[param]` becomes * `:param`, `prefix-[param]-end` becomes `prefix-:param-end`. */ pathSegment; /** * Array of sub segments. This is usually one single elements but can have more for paths like `prefix-[param]-end.vue` */ subSegments; /** * Overrides defined by each file. The map is necessary to handle named views. */ _overrides = /* @__PURE__ */ new Map(); /** * View name (Vue Router feature) mapped to their corresponding file. By default, the view name is `default` unless * specified with a `@` e.g. `index@aux.vue` will have a view name of `aux`. */ components = /* @__PURE__ */ new Map(); constructor(rawSegment, parent, pathSegment = rawSegment, subSegments = [pathSegment]) { this._type = 0; this.rawSegment = rawSegment; this.pathSegment = pathSegment; this.subSegments = subSegments; this.parent = parent; } /** * Path of the node. Can be absolute or not. If it has been overridden, it * will return the overridden path. */ get path() { return this.overrides.path ?? this.pathSegment; } /** * Aliases of the node if any. */ get alias() { return this.overrides.alias ?? []; } /** * Full path of the node including parent nodes. */ get fullPath() { const pathSegment = this.path; if (pathSegment.startsWith("/")) return pathSegment; return joinPath(this.parent?.fullPath ?? "", pathSegment); } /** * Gets all the query params for the node. This does not include params from parent nodes. */ get queryParams() { const paramsQuery = this.overrides.params?.query; if (!paramsQuery) return []; const queryParams = []; for (var paramName in paramsQuery) { var config = paramsQuery[paramName]; if (!config) continue; if (typeof config === "string") queryParams.push({ paramName, parser: config, format: "value" }); else queryParams.push({ paramName, parser: config.parser || null, format: config.format || "value", defaultValue: config.default, required: config.required }); } return queryParams; } /** * Gets all the params for the node including path and query params. This * does not include params from parent nodes. */ get params() { return [...this.isParam() ? this.pathParams : [], ...this.queryParams]; } toString() { let value = ""; if (!this.pathSegment) value += "<index>" + (this.rawSegment === "index" ? "" : " " + this.rawSegment); else value += this.pathSegment; if (this.alias.length) value += ` alias(${this.alias.join(", ")})`; return value; } isParam() { return !!(this._type & TreeNodeType.param); } isStatic() { return this._type === TreeNodeType.static; } isGroup() { return this._type === TreeNodeType.group; } get overrides() { return [...this._overrides.entries()].sort(([nameA], [nameB]) => nameA === nameB ? 0 : nameA === CONVENTION_OVERRIDE_NAME || nameA !== EDITS_OVERRIDE_NAME && (nameA < nameB || nameB === EDITS_OVERRIDE_NAME) ? -1 : 1).reduce((acc, [_path, routeBlock]) => { return mergeRouteRecordOverride(acc, routeBlock); }, {}); } setOverride(filePath, routeBlock) { this._overrides.set(filePath, routeBlock || {}); } /** * Remove all overrides for a given key. * * @param key - key to remove from the override, e.g. path, name, etc */ removeOverride(key) { for (const [_filePath, routeBlock] of this._overrides) delete routeBlock[key]; } /** * Add an override to the current node by merging with the existing values. * * @param filePath - The file path to add to the override * @param routeBlock - The route block to add to the override */ mergeOverride(filePath, routeBlock) { const existing = this._overrides.get(filePath) || {}; this._overrides.set(filePath, mergeRouteRecordOverride(existing, routeBlock)); } /** * Add an override to the current node using the special file path `@@edits` that makes this added at build time. * * @param routeBlock - The route block to add to the override */ addEditOverride(routeBlock) { return this.mergeOverride(EDITS_OVERRIDE_NAME, routeBlock); } /** * Set a specific value in the _edits_ override. * * @param key - key to set in the override, e.g. path, name, etc * @param value - value to set in the override */ setEditOverride(key, value) { if (!this._overrides.has(EDITS_OVERRIDE_NAME)) this._overrides.set(EDITS_OVERRIDE_NAME, {}); const existing = this._overrides.get(EDITS_OVERRIDE_NAME); existing[key] = value; } }; /** * - Static * - Static + Custom Param (subSegments) * - Static + Param (subSegments) * - Custom Param * - Param * - CatchAll */ /** * Static path like `/users`, `/users/list`, etc * @extends _TreeNodeValueBase */ var TreeNodeValueStatic = class extends _TreeNodeValueBase { _type = TreeNodeType.static; score = [300]; constructor(rawSegment, parent, pathSegment = rawSegment) { super(rawSegment, parent, pathSegment); } }; var TreeNodeValueGroup = class extends _TreeNodeValueBase { _type = TreeNodeType.group; groupName; score = [300]; constructor(rawSegment, parent, pathSegment, groupName) { super(rawSegment, parent, pathSegment); this.groupName = groupName; } }; /** * Checks if a TreePathParam or TreeQueryParam is optional. * * @internal */ function isTreeParamOptional(param) { if ("optional" in param) return param.optional; return param.defaultValue !== void 0 || !param.required; } /** * Checks if a TreePathParam or TreeQueryParam is repeatable (array). * * @internal */ function isTreeParamRepeatable(param) { if ("repeatable" in param) return param.repeatable; return param.format === "array"; } /** * Checks if a param is a TreePathParam. * * @internal */ function isTreePathParam(param) { return "modifier" in param; } /** * To escape regex characters in the path segment. * @internal */ const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g; /** * Escapes regex characters in a string to be used in a regex pattern. * @param str - The string to escape. * * @internal */ const escapeRegex = (str) => str.replace(REGEX_CHARS_RE, "\\$&"); var TreeNodeValueParam = class extends _TreeNodeValueBase { _type = TreeNodeType.param; constructor(rawSegment, parent, pathParams, pathSegment, subSegments) { super(rawSegment, parent, pathSegment, subSegments); this.pathParams = pathParams; } get score() { return this.subSegments.map((segment) => { if (typeof segment === "string") return 300; else return 80 - (segment.isSplat ? 500 : (segment.optional ? 10 : 0) + (segment.repeatable ? 20 : 0)); }); } /** * Generates the regex pattern for the path segment. */ get re() { let regexp = ""; for (var i = 0; i < this.subSegments.length; i++) { var segment = this.subSegments[i]; if (!segment) continue; if (typeof segment === "string") regexp += escapeRegex(segment); else if (segment.isSplat) regexp += "(.*)"; else { var re = segment.repeatable ? "(.+?)" : "([^/]+?)"; if (segment.optional) { var prevSegment = this.subSegments[i - 1]; if ((!prevSegment || typeof prevSegment === "string" && prevSegment.endsWith("/")) && this.subSegments.length > 1) { re = `(?:\\/${re})?`; regexp = regexp.slice(0, -2); } else re += "?"; } regexp += re; } } return regexp; } toString() { const params = this.params.length > 0 ? ` 𝑥(` + this.params.map((p) => ("format" in p ? "?" : "") + `${p.paramName}${"modifier" in p ? p.modifier : ""}` + (p.parser ? "=" + p.parser : "")).join(", ") + ")" : ""; return `${this.pathSegment}` + params; } }; /** * Resolves the options for the TreeNodeValue. * * @param options - options to resolve * @returns resolved options */ function resolveTreeNodeValueOptions(options) { return { format: "file", dotNesting: true, ...options }; } /** * Creates a new TreeNodeValue based on the segment. The result can be a static segment, group segment or a param segment. * * @param segment - path segment * @param parent - parent node * @param options - options */ function createTreeNodeValue(segment, parent, opts = {}) { if (!segment || segment === "index") return new TreeNodeValueStatic(segment, parent, ""); const options = resolveTreeNodeValueOptions(opts); const openingPar = segment.indexOf("("); if (options.format === "file" && openingPar >= 0) { let groupName; const closingPar = segment.lastIndexOf(")"); if (closingPar < 0 || closingPar < openingPar) { warn(`Segment "${segment}" is missing the closing ")". It will be treated as a static segment.`); return new TreeNodeValueStatic(segment, parent, segment); } groupName = segment.slice(openingPar + 1, closingPar); const before = segment.slice(0, openingPar); const after = segment.slice(closingPar + 1); if (!before && !after) return new TreeNodeValueGroup(segment, parent, "", groupName); } const [pathSegment, pathParams, subSegments] = options.format === "path" ? parseRawPathSegment(segment) : parseFileSegment(segment, options); if (pathParams.length) return new TreeNodeValueParam(segment, parent, pathParams, pathSegment, subSegments); return new TreeNodeValueStatic(segment, parent, pathSegment); } var ParseFileSegmentState = /* @__PURE__ */ function(ParseFileSegmentState) { ParseFileSegmentState[ParseFileSegmentState["static"] = 0] = "static"; ParseFileSegmentState[ParseFileSegmentState["paramOptional"] = 1] = "paramOptional"; ParseFileSegmentState[ParseFileSegmentState["param"] = 2] = "param"; ParseFileSegmentState[ParseFileSegmentState["paramParser"] = 3] = "paramParser"; ParseFileSegmentState[ParseFileSegmentState["modifier"] = 4] = "modifier"; ParseFileSegmentState[ParseFileSegmentState["charCode"] = 5] = "charCode"; return ParseFileSegmentState; }(ParseFileSegmentState || {}); const IS_VARIABLE_CHAR_RE = /[0-9a-zA-Z_]/; /** * Parses a segment into the route path segment and the extracted params. * * @param segment - segment to parse without the extension * @returns - the pathSegment and the params */ function parseFileSegment(segment, { dotNesting }) { let buffer = ""; let paramParserBuffer = ""; let state = ParseFileSegmentState.static; const params = []; let pathSegment = ""; const subSegments = []; let currentTreeRouteParam = createEmptyRouteParam(); let pos = 0; let c; function consumeBuffer() { if (state === ParseFileSegmentState.static) { const encodedBuffer = buffer.split("/").map((part) => encodePath(part)).join("/"); pathSegment += encodedBuffer; subSegments.push(encodedBuffer); } else if (state === ParseFileSegmentState.modifier) { currentTreeRouteParam.paramName = buffer; currentTreeRouteParam.parser = paramParserBuffer || null; currentTreeRouteParam.modifier = currentTreeRouteParam.optional ? currentTreeRouteParam.repeatable ? "*" : "?" : currentTreeRouteParam.repeatable ? "+" : ""; buffer = ""; paramParserBuffer = ""; pathSegment += `:${currentTreeRouteParam.paramName}${currentTreeRouteParam.isSplat ? "(.*)" : pos < segment.length - 1 && IS_VARIABLE_CHAR_RE.test(segment[pos + 1]) ? "()" : ""}${currentTreeRouteParam.modifier}`; params.push(currentTreeRouteParam); subSegments.push(currentTreeRouteParam); currentTreeRouteParam = createEmptyRouteParam(); } else if (state === ParseFileSegmentState.charCode) { if (buffer.length !== 2) throw new SyntaxError(`Invalid character code in segment "${segment}". Hex code must be exactly 2 digits, got "${buffer}"`); const hexCode = parseInt(buffer, 16); if (!Number.isInteger(hexCode) || hexCode < 0 || hexCode > 255) throw new SyntaxError(`Invalid hex code "${buffer}" in segment "${segment}"`); pathSegment += String.fromCharCode(hexCode); } buffer = ""; } for (pos = 0; pos < segment.length; pos++) { c = segment[pos]; if (state === ParseFileSegmentState.static) if (c === "[") { if (buffer) consumeBuffer(); state = ParseFileSegmentState.paramOptional; } else buffer += dotNesting && c === "." ? "/" : c; else if (state === ParseFileSegmentState.paramOptional) { if (c === "[") currentTreeRouteParam.optional = true; else if (c === ".") { currentTreeRouteParam.isSplat = true; pos += 2; } else buffer += c; state = ParseFileSegmentState.param; } else if (state === ParseFileSegmentState.param) if (c === "]") { if (currentTreeRouteParam.optional) pos++; state = ParseFileSegmentState.modifier; } else if (c === ".") { currentTreeRouteParam.isSplat = true; pos += 2; } else if (c === "=") { state = ParseFileSegmentState.paramParser; paramParserBuffer = ""; } else if (c === "+" && buffer === "x" && !currentTreeRouteParam.isSplat && !currentTreeRouteParam.optional) { buffer = ""; state = ParseFileSegmentState.charCode; } else buffer += c; else if (state === ParseFileSegmentState.modifier) { if (c === "+") currentTreeRouteParam.repeatable = true; else pos--; consumeBuffer(); state = ParseFileSegmentState.static; } else if (state === ParseFileSegmentState.paramParser) if (c === "]") { if (currentTreeRouteParam.optional) pos++; state = ParseFileSegmentState.modifier; } else paramParserBuffer += c; else if (state === ParseFileSegmentState.charCode) if (c === "]") { consumeBuffer(); state = ParseFileSegmentState.static; } else buffer += c; } if (state === ParseFileSegmentState.param || state === ParseFileSegmentState.paramOptional || state === ParseFileSegmentState.paramParser || state === ParseFileSegmentState.charCode) throw new SyntaxError(`Invalid segment: "${segment}"`); if (buffer) consumeBuffer(); return [ pathSegment, params, subSegments ]; } var ParseRawPathSegmentState = /* @__PURE__ */ function(ParseRawPathSegmentState) { ParseRawPathSegmentState[ParseRawPathSegmentState["static"] = 0] = "static"; ParseRawPathSegmentState[ParseRawPathSegmentState["param"] = 1] = "param"; ParseRawPathSegmentState[ParseRawPathSegmentState["regexp"] = 2] = "regexp"; ParseRawPathSegmentState[ParseRawPathSegmentState["modifier"] = 3] = "modifier"; return ParseRawPathSegmentState; }(ParseRawPathSegmentState || {}); const IS_MODIFIER_RE = /[+*?]/; /** * Parses a raw path segment like the `:id` in a route `/users/:id`. * * @param segment - segment to parse without the extension * @returns - the pathSegment and the params */ function parseRawPathSegment(segment) { let buffer = ""; let state = ParseRawPathSegmentState.static; const params = []; const subSegments = []; let currentTreeRouteParam = createEmptyRouteParam(); let pos = 0; let c; function consumeBuffer() { if (state === ParseRawPathSegmentState.static) subSegments.push(buffer); else if (state === ParseRawPathSegmentState.param || state === ParseRawPathSegmentState.regexp || state === ParseRawPathSegmentState.modifier) { if (!currentTreeRouteParam.paramName) { warn(`Invalid parameter in path "${segment}": parameter name cannot be empty. Using default name "pathMatch" for ':()'.`); currentTreeRouteParam.paramName = "pathMatch"; } subSegments.push(currentTreeRouteParam); params.push(currentTreeRouteParam); currentTreeRouteParam = createEmptyRouteParam(); } buffer = ""; } for (pos = 0; pos < segment.length; pos++) { c = segment[pos]; if (c === "\\") { pos++; buffer += segment[pos]; continue; } if (state === ParseRawPathSegmentState.static) if (c === ":") { consumeBuffer(); state = ParseRawPathSegmentState.param; } else buffer += c; else if (state === ParseRawPathSegmentState.param) if (c === "(") { currentTreeRouteParam.paramName = buffer; buffer = ""; state = ParseRawPathSegmentState.regexp; } else if (IS_MODIFIER_RE.test(c)) { currentTreeRouteParam.modifier = c; currentTreeRouteParam.optional = c === "?" || c === "*"; currentTreeRouteParam.repeatable = c === "+" || c === "*"; consumeBuffer(); state = ParseRawPathSegmentState.static; } else if (IS_VARIABLE_CHAR_RE.test(c)) { buffer += c; currentTreeRouteParam.paramName = buffer; } else { currentTreeRouteParam.paramName = buffer; consumeBuffer(); pos--; state = ParseRawPathSegmentState.static; } else if (state === ParseRawPathSegmentState.regexp) if (c === ")") { if (buffer === ".*") currentTreeRouteParam.isSplat = true; state = ParseRawPathSegmentState.modifier; } else buffer += c; else if (state === ParseRawPathSegmentState.modifier) { if (IS_MODIFIER_RE.test(c)) { currentTreeRouteParam.modifier = c; currentTreeRouteParam.optional = c === "?" || c === "*"; currentTreeRouteParam.repeatable = c === "+" || c === "*"; } else pos--; consumeBuffer(); state = ParseRawPathSegmentState.static; } } if (state === ParseRawPathSegmentState.regexp) throw new Error(`Invalid segment: "${segment}"`); if (buffer || state === ParseRawPathSegmentState.modifier) consumeBuffer(); return [ segment, params, subSegments ]; } /** * Helper function to create an empty route param used by the parser. * * @returns an empty route param */ function createEmptyRouteParam() { return { paramName: "", parser: null, modifier: "", optional: false, repeatable: false, isSplat: false }; } //#endregion //#region src/unplugin/core/tree.ts var TreeNode = class TreeNode { /** * value of the node */ value; /** * children of the node */ children = /* @__PURE__ */ new Map(); /** * Parent node. */ parent; /** * Plugin options taken into account by the tree. */ options; /** * Set of file paths that use `definePage()` with runtime properties (meta, props, etc.) that require a `?definePage` * import at build time. Tracked per-file to avoid race conditions when multiple files (e.g. named views) map to the * same node. */ _needsDefinePageImport = /* @__PURE__ */ new Set(); /** * Whether at least one component file uses `definePage()` with runtime properties (meta, props, etc.) that require a * `?definePage` import at build time. */ get needsDefinePageImport() { return this._needsDefinePageImport.size > 0; } /** * Mark whether a file needs a `?definePage` import. */ setDefinePageImport(filePath, needsImport) { if (needsImport) this._needsDefinePageImport.add(filePath); else this._needsDefinePageImport.delete(filePath); } /** * Check if a specific file needs a `?definePage` import. */ fileNeedsDefinePageImport(filePath) { return this._needsDefinePageImport.has(filePath); } /** * Creates a new tree node. * * @param options - TreeNodeOptions shared by all nodes * @param pathSegment - path segment of this node e.g. `users` or `:id` * @param parent */ constructor(options, pathSegment, parent) { this.options = options; this.parent = parent; this.value = createTreeNodeValue(pathSegment, parent?.value, options.treeNodeOptions || options.pathParser); } /** * Adds a path to the tree. `path` cannot start with a `/`. * * @param path - path segment to insert. **It shouldn't contain the file extension** * @param filePath - file path, must be a file (not a folder) */ insert(path, filePath) { const { tail, segment, viewName } = splitFilePath(path); if (segment === "_parent" && !tail) { this.value.setOverride(CONVENTION_OVERRIDE_NAME, { name: false }); this.value.components.set(viewName, filePath); return this; } if (!this.children.has(segment)) this.children.set(segment, new TreeNode(this.options, segment, this)); const child = this.children.get(segment); if (!tail) child.value.components.set(viewName, filePath); else return child.insert(tail, filePath); return child; } /** * Adds a path that has already been parsed to the tree. `path` cannot start with a `/`. This method is similar to * `insert` but the path argument should be already parsed. e.g. `users/:id` for a file named `users/[id].vue`. * * @param path - path segment to insert, already parsed (e.g. users/:id) * @param filePath - file path, defaults to path for convenience and testing */ insertParsedPath(path, filePath = path) { const node = new TreeNode({ ...this.options, treeNodeOptions: { ...this.options.pathParser, format: "path" } }, path, this); this.children.set(path, node); node.value.components.set("default", filePath); return node; } /** * Saves a custom route block for a specific file path. The file path is used * as a key. Some special file paths will have a lower or higher priority. * * @param filePath - file path where the custom block is located * @param routeBlock - custom block to set */ setCustomRouteBlock(filePath, routeBlock) { this.value.setOverride(filePath, routeBlock); } /** * Generator that yields all descendants without sorting. * Use with Array.from() for now, native .map() support in Node 22+. */ *getChildrenDeep() { for (const child of this.children.values()) { yield child; yield* child.getChildrenDeep(); } } /** * Comparator function for sorting TreeNodes. * * @internal */ static compare(a, b) { return a.path.localeCompare(b.path, "en") || a.value.rawSegment.localeCompare(b.value.rawSegment, "en"); } /** * Get the children of this node sorted by their path. */ getChildrenSorted() { return Array.from(this.children.values()).sort(TreeNode.compare); } /** * Calls {@link getChildrenDeep} and sorts the result by path in the end. */ getChildrenDeepSorted() { return Array.from(this.getChildrenDeep()).sort(TreeNode.compare); } /** * Delete and detach itself from the tree. */ delete() { if (!this.parent) throw new Error("Cannot delete the root node."); this.parent.children.delete(this.value.rawSegment); this.parent = void 0; } /** * Remove a route from the tree. The path shouldn't start with a `/` but it can be a nested one. e.g. `foo/bar`. * The `path` should be relative to the page folder. * * @param path - path segment of the file */ remove(path) { const { tail, segment, viewName } = splitFilePath(path); if (segment === "_parent" && !tail) { this.value.components.delete(viewName); return; } const child = this.children.get(segment); if (!child) throw new Error(`Cannot Delete "${path}". "${segment}" not found at "${this.path}".`); if (tail) { child.remove(tail); if (child.children.size === 0 && child.value.components.size === 0) this.children.delete(segment); } else { child.value.components.delete(viewName); if (child.children.size === 0 && child.value.components.size === 0) this.children.delete(segment); } } /** * Returns the route path of the node without parent paths. If the path was overridden, it returns the override. */ get path() { return this.value.overrides.path ?? (this.parent?.isRoot() ? "/" : "") + this.value.pathSegment; } /** * Returns the route path of the node including parent paths. */ get fullPath() { return this.value.fullPath; } /** * Returns the alias of the node */ get alias() { return this.value.alias; } /** * Object of components (filepaths) for this node. */ get components() { return Object.fromEntries(this.value.components.entries()); } /** * Does this node render any component? */ get hasComponents() { return this.value.components.size > 0; } /** * Returns the route name of the node. If the name was overridden, it returns the override. */ get name() { const overrideName = this.value.overrides.name; return overrideName === void 0 ? this.options.getRouteName(this) : overrideName; } /** * Returns the meta property as an object. */ get metaAsObject() { return { ...this.value.overrides.meta }; } /** * Returns the JSON string of the meta object of the node. If the meta was overridden, it returns the override. If * there is no override, it returns an empty string. */ get meta() { const overrideMeta = this.metaAsObject; return Object.keys(overrideMeta).length > 0 ? JSON.stringify(overrideMeta, null, 2) : ""; } /** * Array of route params for this node. It includes **all** the params from the parents as well. */ get params() { const params = [...this.value.params]; let node = this.parent; while (node) { params.unshift(...node.value.params); node = node.parent; } return params; } /** * Array of route params coming from the path. It includes all the params from the parents as well. */ get pathParams() { const params = this.value.isParam() ? [...this.value.pathParams] : []; let node = this.parent; while (node) { if (node.value.isParam()) params.unshift(...node.value.pathParams); node = node.parent; } return params; } /** * Array of query params extracted from definePage. Only returns query params from this specific node. */ get queryParams() { return this.value.queryParams; } /** * Generates a regexp based on this node and its parents. This regexp is used by the custom resolver */ get regexp() { let node = this; const nodeList = []; while (node && !node.isRoot()) { nodeList.unshift(node); node = node.parent; } let re = ""; for (var i = 0; i < nodeList.length; i++) { node = nodeList[i]; if (node.value.isParam()) { var nodeRe = node.value.re; if ((re || i < nodeList.length - 1) && node.value.subSegments.length === 1 && node.value.subSegments.at(0).optional) re += `(?:\\/${nodeRe.slice(0, -1)})?`; else re += (re ? "\\/" : "") + nodeRe; } else if (node.value.pathSegment) re += (re ? "\\/" : "") + escapeRegex(node.value.pathSegment); } return "/^" + (re.startsWith("(?:\\/") ? "" : "\\/") + re.replace(ESCAPED_TRAILING_SLASH_RE, "") + "$/i"; } /** * Score of the path used for sorting routes. */ get score() { const scores = []; let node = this; while (node && !node.isRoot()) { scores.unshift(node.value.score); node = node.parent; } return scores; } /** * Is this node a splat (catch-all) param */ get isSplat() { return this.value.isParam() && this.value.pathParams.some((p) => p.isSplat); } /** * Returns an array of matcher parts that is consumed by * MatcherPatternPathDynamic to render the path. */ get matcherPatternPathDynamicParts() { const parts = []; let node = this; while (node && !node.isRoot()) { if (!node.value.pathSegment) { node = node.parent; continue; } const subSegments = node.value.subSegments.map((segment) => typeof segment === "string" ? segment : segment.isSplat ? 0 : 1); if (subSegments.length > 1) parts.unshift(subSegments); else if (subSegments.length === 1) parts.unshift(subSegments[0]); node = node.parent; } return parts; } /** * Is this tree node matchable? A matchable node has at least one component * and a name. */ isMatchable() { return this.value.components.size > 0 && this.name !== false; } /** * Returns wether this tree node is the root node of the tree. * * @returns true if the node is the root node */ isRoot() { return !this.parent && this.value.fullPath === "/" && !this.value.components.size; } /** * Returns wether this tree node has a name. This allows to coerce the type * of TreeNode */ isNamed() { return !!this.name; } toString() { return `${this.isRoot() ? "·" : this.value}${this.value.components.size > 1 || this.value.components.size === 1 && !this.value.components.get("default") ? ` ⎈(${Array.from(this.value.components.keys()).join(", ")})` : ""}${this.needsDefinePageImport ? " ⚑ definePage()" : ""}`; } /** * Iterates over the tree in a breadth-first way. It first yields the direct * children of the node, then their children and so on. The order of the * children is not guaranteed. */ *[Symbol.iterator]() { for (const [_name, child] of this.children) yield child; for (const [_name, child] of this.children) yield* child[Symbol.iterator](); } }; /** * Creates a new prefix tree. This is meant to only be the root node. It has access to extra methods that only make * sense on the root node. */ var PrefixTree = class extends TreeNode { map = /* @__PURE__ */ new Map(); constructor(options) { super(options, ""); } insert(path, filePath) { const node = super.insert(path, filePath); this.map.set(filePath, node); return node; } /** * Returns the tree node of the given file path. * * @param filePath - file path of the tree node to get */ getChild(filePath) { return this.map.get(filePath); } /** * Removes the tree node of the given file path. * * @param filePath - file path of the tree node to remove */ removeChild(filePath) { if (this.map.has(filePath)) { const node = this.map.get(filePath); const components = node.value.components; for (const [viewName, componentPath] of components) if (componentPath === filePath) { components.delete(viewName); break; } node.setDefinePageImport(filePath, false); this.map.delete(filePath); if (node.children.size === 0 && node.value.components.size === 0) { node.delete(); for (const [key, mappedNode] of this.map) if (mappedNode === node) this.map.delete(key); } } } }; /** * Returns a list of tree nodes that create the same route, the last one in the * list is the one that takes precedence. This is used to warn about duplicated * routes. * * @param tree - prefix tree to scan */ function collectDuplicatedRouteNodes(tree) { const seen = /* @__PURE__ */ new Map(); const treeNodes = new Set(...tree); for (const [filePath, node] of tree.map) { const key = `${node.fullPath}::${node.toString()}`; let nodes = seen.get(key); if (!nodes) { nodes = []; seen.set(key, nodes); } nodes.push({ filePath, node }); } return Array.from(seen.values()).filter((nodes) => nodes.length > nodes[0].node.value.components.size).map((nodes) => nodes.toSorted(({ node: a }, { node: b }) => { if (treeNodes.has(a) && !treeNodes.has(b)) return -1; else if (!treeNodes.has(a) && treeNodes.has(b)) return 1; else return 0; })); } /** * Splits a path into by finding the first '/' and returns the tail and segment. If it has an extension, it removes it. * If it contains a named view, it returns the view name as well (otherwise it's default). * * @param filePath - filePath to split */ function splitFilePath(filePath) { const slashPos = filePath.indexOf("/"); let head = slashPos < 0 ? filePath : filePath.slice(0, slashPos); const tail = slashPos < 0 ? "" : filePath.slice(slashPos + 1); let segment = head; let viewName = "default"; const namedSeparatorPos = segment.indexOf("@"); if (namedSeparatorPos > 0) { viewName = segment.slice(namedSeparatorPos + 1); segment = segment.slice(0, namedSeparatorPos); } return { segment, tail, viewName }; } //#endregion //#region src/unplugin/codegen/generateParamParsers.ts const NATIVE_PARAM_PARSERS = ["int", "bool"]; const NATIVE_PARAM_PARSERS_TYPES = { int: "number", bool: "boolean" }; function warnMissingParamParsers(tree, paramParsers) { for (const node of tree.getChildrenDeepSorted()) for (const param of node.params) if (param.parser && !paramParsers.has(param.parser)) { if (!NATIVE_PARAM_PARSERS.includes(param.parser)) console.warn(`Parameter parser "${param.parser}" not found for route "${node.fullPath}".`); } } function collectMissingParamParsers(tree, paramParsers) { const missing = []; for (const node of tree.getChildrenDeepSorted()) for (const param of node.params) if (param.parser && !paramParsers.has(param.parser)) { if (!NATIVE_PARAM_PARSERS.includes(param.parser)) missing.push({ parser: param.parser, routePath: node.fullPath, filePaths: Array.from(node.value.components.values()) }); } return missing; } function generateParamParsersTypesDeclarations(paramParsers) { return Array.from(paramParsers.values()).map(({ typeName, relativePath }) => { return `type ${typeName} = _ExtractParamParserType<typeof import('${relativePath.startsWith(".") ? relativePath : "./" + relativePath}').parser>`; }).join("\n"); } function generateParamsTypes(params, parparsersMap) { return params.map((param) => { if (param.parser) { if (parparsersMap.has(param.parser)) return parparsersMap.get(param.parser).typeName; else if (param.parser in NATIVE_PARAM_PARSERS_TYPES) return NATIVE_PARAM_PARSERS_TYPES[param.parser]; } return null; }); } function generateParamParserOptions(param, importsMap, paramParsers) { if (!param.parser) return ""; if (paramParsers.has(param.parser)) { const { name } = paramParsers.get(param.parser); return `_normalized_PARAM_PARSER__${name}`; } else if (NATIVE_PARAM_PARSERS.includes(param.parser)) { const varName = `PARAM_PARSER_${param.parser.toUpperCase()}`; importsMap.add("vue-router/experimental", varName); return varName; } return ""; } function generateNormalizedParamParsersDeclarations(paramParsers, importsMap) { const declarations = []; for (const [, { name, absolutePath }] of paramParsers) { const rawVar = `PARAM_PARSER__${name}`; const normalizedVar = `_normalized_PARAM_PARSER__${name}`; importsMap.add("vue-router/experimental", "_normalizeParamParser"); importsMap.add(absolutePath, { name: "parser", as: rawVar }); declarations.push(`const ${normalizedVar} = _normalizeParamParser(${rawVar})`); } return declarations.join("\n"); } function generateCustomParamParsersList(paramParsers) { const parserNames = Array.from(paramParsers.keys()).sort(); if (parserNames.length === 0) return ["never"]; return parserNames.map(toStringLiteral); } function generatePathParamsOptions(params, importsMap, paramParsers) { const paramOptions = params.map((param) => { const optionList = []; const parser = generateParamParserOptions(param, importsMap, paramParsers); optionList.push(parser || `/* no parser */`); if (param.optional || param.repeatable) optionList.push(`/* repeatable: ` + (param.repeatable ? `*/ true` : `false */`)); if (param.optional) optionList.push(`/* optional: ` + (param.optional ? `*/ true` : `false */`)); return ` ${param.paramName}: [${optionList.join(", ")}], `.slice(1, -1); }); return paramOptions.length === 0 ? "{}" : `{ ${paramOptions.join("\n ")} }`; } //#endregion //#region src/unplugin/codegen/generateRouteParams.ts function generateRouteParams(node, isRaw) { const nodeParams = node.pathParams; return nodeParams.length > 0 ? `{ ${nodeParams.filter((param) => { if (!param.paramName) { console.warn(`Warning: A parameter without a name was found in the route "${node.fullPath}" in segment "${node.path}".\n‼️ This is a bug, please report it at https://github.com/vuejs/router`); return false; } return true; }).map((param) => `${param.paramName}${param.optional ? "?" : ""}: ` + (param.modifier === "+" ? `ParamValueOneOrMore<${isRaw}>` : param.modifier === "*" ? `ParamValueZeroOrMore<${isRaw}>` : param.modifier === "?" ? `ParamValueZeroOrOne<${isRaw}>` : `ParamValue<${isRaw}>`)).join(", ")} }` : "Record<never, never>"; } function EXPERIMENTAL_generateRouteParams(node, types, isRaw) { const nodeParams = node.params; return nodeParams.length > 0 ? `{ ${nodeParams.map((param, i) => { const isOptional = isTreeParamOptional(param); const isRepeatable = isTreeParamRepeatable(param); const type = types[i]; let extractedType; if (type?.startsWith("Param_")) extractedType = isRepeatable ? `Extract<${type}, unknown[]>` : `Exclude<${type}, unknown[] | null>`; else extractedType = `${type ?? "string"}${isRepeatable ? "[]" : ""}`; let isOptionalQueryParam = false; if (isTreePathParam(param)) { if (isOptional && !isRepeatable) extractedType += " | null"; } else if (!param.required) { isOptionalQueryParam = true; if (!isRaw && (param.defaultValue === void 0 || param.defaultValue === "undefined")) extractedType += " | undefined"; } return `${param.paramName}${isRaw && isOptionalQueryParam ? "?" : ""}: ${extractedType}`; }).join(", ")} }` : "Record<never, never>"; } //#endregion //#region src/unplugin/codegen/generateRouteMap.ts function generateRouteNamedMap(node, options, paramParsersMap) { if (node.isRoot()) return `export interface RouteNamedMap { ${node.getChildrenSorted().map((n) => generateRouteNamedMap(n, options, paramParsersMap)).join("")}}`; return (node.value.components.size && node.isNamed() ? pad(2, `${toStringLiteral(node.name)}: ${generateRouteRecordInfo(node, options, paramParsersMap)},\n`) : "") + (node.children.size > 0 ? node.getChildrenSorted().map((n) => generateRouteNamedMap(n, options, paramParsersMap)).join("\n") : ""); } function generateRouteRecordInfo(node, options, paramParsersMap) { let paramParsers = []; if (options.experimental.paramParsers) paramParsers = generateParamsTypes(node.params, paramParsersMap); const typeParams = [ toStringLiteral(node.name), toStringLiteral(node.fullPath), options.experimental.paramParsers ? EXPERIMENTAL_generateRouteParams(node, paramParsers, true) : generateRouteParams(node, true), options.experimental.paramParsers ? EXPERIMENTAL_generateRouteParams(node, paramParsers, false) : generateRouteParams(node, false) ]; const childRouteNames = node.children.size > 0 ? Array.from(node.getChildrenDeep()).reduce((acc, childRoute) => { if (childRoute.value.components.size && childRoute.isNamed()) acc.push(childRoute.name); return acc; }, []).sort() : []; typeParams.push(formatMultilineUnion(childRouteNames.map(toStringLiteral), 4)); return `RouteRecordInfo< ${typeParams.map((line) => pad(4, line)).join(",\n")} >`; } //#endregion //#region src/unplugin/codegen/generateRouteFileInfoMap.ts function generateRouteFileInfoMap(node, { root }) { if (!node.isRoot()) throw new Error("The provided node is not a root node"); const routesInfoList = node.getChildrenSorted().flatMap((child) => generateRouteFileInfoLines(child, root)); const routesInfo = /* @__PURE__ */ new Map(); for (const routeInfo of routesInfoList) { let info = routesInfo.get(routeInfo.key); if (!info) routesInfo.set(routeInfo.key, info = { routes: [], views: [] }); info.routes.push(...routeInfo.routeNames); info.views.push(...routeInfo.childrenNamedViews || []); } return `export interface _RouteFileInfoMap { ${Array.from(routesInfo.entries()).map(([file, { routes, views }]) => ` ${toStringLiteral(file)}: { routes: ${formatMultilineUnion(routes.sort().map(toStringLiteral), 6)} views: ${formatMultilineUnion(views.sort().map(toStringLiteral), 6)} }`).join("\n")} }`; } /** * Generate the route file info for a non-root node. */ function generateRouteFileInfoLines(node, rootDir) { const deepChildren = node.children.size > 0 ? node.getChildrenDeepSorted() : null; const deepChildrenNamedViews = deepChildren ? Array.from(new Set(deepChildren.flatMap((child) => Array.from(child.value.components.keys())))) : null; const routeNames = [node, ...deepChildren ?? []].reduce((acc, node) => { if (node.isNamed() && node.value.components.size > 0) acc.push(node.name); return acc; }, []); const currentRouteInfo = routeNames.length === 0 ? [] : Array.from(node.value.components.values()).map((file) => ({ key: relative(rootDir, file).replaceAll("\\", "/"), routeNames, childrenNamedViews: deepChildrenNamedViews })); const childrenRouteInfo = node.getChildrenSorted().flatMap((child) => generateRouteFileInfoLines(child, rootDir)); return currentRouteInfo.concat(childrenRouteInfo); } //#endregion //#region src/unplugin/core/moduleConstants.ts const MODULE_ROUTES_PATH = `vue-router/auto-routes`; const MODULE_RESOLVER_PATH = `vue-router/auto-resolver`; let time = Date.now(); /** * Last time the routes were loaded from MODULE_ROUTES_PATH */ const ROUTES_LAST_LOAD_TIME = { get value() { return time; }, update(when = Date.now()) { time = when; } }; const VIRTUAL_PREFIX = "\0"; const ROUTE_BLOCK_ID = asVirtualId("vue-router/auto/route-block"); function getVirtualId(id) { return id.startsWith(VIRTUAL_PREFIX) ? id.slice(1) : null; } const routeBlockQueryRE = /\?vue&type=route/; function asVirtualId(id) { return VIRTUAL_PREFIX + id; } const DEFINE_PAGE_QUERY_RE = /\?.*\bdefinePage&vue\b/; //#endregion //#region src/unplugin/codegen/generateRouteRecords.ts /** * Generate the route records for the given node. * * @param node - the node to generate the route record for * @param options - the options to use * @param importsMap - the imports map to fill and use * @param indent - the indent level * @returns the code of the routes as a string */ function generateRouteRecords(node, options, importsMap, indent = 0) { if (node.isRoot()) return `[ ${node.getChildrenSorted().map((child) => generateRouteRecords(child, options, importsMap, indent + 1)).join(",\n")} ]`; if (!node.isMatchable() && node.children.size === 0) return ""; const definePageDataList = []; if (node.needsDefinePageImport) { for (const [name, filePath] of node.value.components) { if (!node.fileNeedsDefinePageImport(filePath)) continue; const pageDataImport = `_definePage_${name}_${importsMap.size}`; definePageDataList.push(pageDataImport); const lang = getLang(filePath); importsMap.addDefault(`${filePath}?definePage&` + (lang === "vue" ? "vue&lang.tsx" : `lang.${lang}`), pageDataImport); } if (definePageDataList.length > 0) indent++; } const startIndent = pad(indent * 2); const indentStr = pad((indent + 1) * 2); const overrides = node.value.overrides; const routeRecord = `${startIndent}{ ${indentStr}path: ${toStringLiteral(node.path)}, ${indentStr}${node.value.components.size ? node.isNamed() ? `name: ${toStringLiteral(node.name)},` : `/* no name */` : `/* internal name: ${typeof node.name === "string" ? toStringLiteral(node.name) : node.name} */`} ${indentStr}${node.value.components.size ? generateRouteRecordComponent$1(node, indentStr, options.importMode, importsMap) : "/* no component */"} ${overrides.props != null ? indentStr + `props: ${overrides.props},\n` : ""}${overrides.alias != null ? indentStr + `alias: ${JSON.stringify(overrides.alias)},\n` : ""}${indentStr}${node.children.size > 0 ? `children: [ ${node.getChildrenSorted().map((child) => generateRouteRecords(child, options, importsMap, indent + 2)).join(",\n")} ${indentStr}],` : "/* no children */"}${formatMeta(node, indentStr)} ${startIndent}}`; if (definePageDataList.length > 0) { const mergeCallIndent = startIndent.slice(2); importsMap.add("vue-router/experimental", "_mergeRouteRecord"); return `${mergeCallIndent}_mergeRouteRecord( ${routeRecord}, ${definePageDataList.map((s) => startIndent + s).join(",\n")} ${mergeCallIndent})`; } return routeRecord; } function generateRouteRecordComponent$1(node, indentStr, importMode, importsMap) { const files = Array.from(node.value.components); return files.length === 1 && files[0][0] === "default" ? `component: ${generatePageImport(files[0][1], importMode, importsMap)},` : `components: { ${files.map(([key, path]) => `${indentStr + " "}${toStringLiteral(key)}: ${generatePageImport(path, importMode, importsMap)}`).join(",\n")} ${indentStr}},`; } /** * Generate the import (dynamic or static) for the given filepath. If the filepath is a static import, add it to the importsMap. * * @param filepath - the filepath to the file * @param importMode - the import mode to use * @param importsMap - the import list to fill * @returns */ function generatePageImport(filepath, importMode, importsMap) { if ((typeof importMode === "function" ? importMode(filepath) : importMode) === "async") return `() => import(${toStringLiteral(filepath)})`; const existingEntry = importsMap.getImportList(filepath).find((entry) => entry.name === "default"); if (existingEntry) return existingEntry.as; const importName = `_page_${importsMap.size}`; importsMap.addDefault(filepath, importName); return importName; } function formatMeta(node, indent) { const meta = node.meta; const formatted = meta && meta.split("\n").map((line) => indent + line).join("\n") + ","; return formatted ? "\n" + indent + "meta: " + formatted.trimStart() : ""; } //#endregion //#region src/unplugin/core/customBlock.ts function getRouteBlock(path, content, options) { const blockStr = parse$1(content, { pad: "space" }).descriptor?.customBlocks.find((b) => b.type === "route"); if (blockStr) return parseCustomBlock(blockStr, path, options); } function parseCustomBlock(block, filePath, options) { const lang = block.lang ?? options.routeBlockLang; if (lang === "json5") try { return JSON5.parse(block.content); } catch (err) { warn(`Invalid JSON5 format of <${block.type}> content in ${filePath}\n${err.message}`); } else if (lang === "json") try { return JSON.parse(block.content); } catch (err) { warn(`Invalid JSON format of <${block.type}> content in ${filePath}\n${err.message}`); } else if (lang === "yaml" || lang === "yml") try { return parse$2(block.content); } catch (err) { warn(`Invalid YAML format of <${block.type}> content in ${filePath}\n${err.message}`); } else warn(`Language "${lang}" for <${block.type}> is not supported. Supported languages are: json5, json, yaml, yml. Found in in ${filePath}.`); } //#endregion //#region src/unplugin/core/RoutesFolderWatcher.ts var RoutesFolderWatcher = class { src; path; extensions; filePatterns; exclude; watcher; constructor(folderOptions) { this.src = folderOptions.src; this.path = folderOptions.path; this.exclude = folderOptions.exclude; this.extensions = folderOptions.extensions; this.filePatterns = folderOptions.pattern; const isMatch = picomatch(this.filePatterns, { ignore: this.exclude }); this.watcher = watch(".", { cwd: this.src, ignoreInitial: true, ignorePermissionErrors: true, awaitWriteFinish: !!process.env.CI, ignored: (filePath, stats) => { if (!stats || stats.isDirectory()) return false; return !isMatch(path.relative(this.src, filePath)); } }); } on(event, handler) { this.watcher.on(event, (filePath) => { filePath = resolv