elearning/Frontend-Learner/node_modules/unplugin-vue-router/dist/src-CZrWKhKa.mjs
2026-01-13 10:48:02 +07:00

2374 lines
No EOL
84 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { 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 } from "./options-CAGVi1wo.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/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/core/treeNodeValue.ts
let TreeNodeType = /* @__PURE__ */ function(TreeNodeType$1) {
TreeNodeType$1[TreeNodeType$1["static"] = 0] = "static";
TreeNodeType$1[TreeNodeType$1["group"] = 1] = "group";
TreeNodeType$1[TreeNodeType$1["param"] = 2] = "param";
return TreeNodeType$1;
}({});
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;
}
/**
* 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
});
}
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() {
if (!this.pathSegment) return "<index>" + (this.rawSegment === "index" ? "" : " " + this.rawSegment);
return this.pathSegment;
}
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 !== 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;
}
/**
* 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$1) {
ParseFileSegmentState$1[ParseFileSegmentState$1["static"] = 0] = "static";
ParseFileSegmentState$1[ParseFileSegmentState$1["paramOptional"] = 1] = "paramOptional";
ParseFileSegmentState$1[ParseFileSegmentState$1["param"] = 2] = "param";
ParseFileSegmentState$1[ParseFileSegmentState$1["paramParser"] = 3] = "paramParser";
ParseFileSegmentState$1[ParseFileSegmentState$1["modifier"] = 4] = "modifier";
ParseFileSegmentState$1[ParseFileSegmentState$1["charCode"] = 5] = "charCode";
return ParseFileSegmentState$1;
}(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$1) {
ParseRawPathSegmentState$1[ParseRawPathSegmentState$1["static"] = 0] = "static";
ParseRawPathSegmentState$1[ParseRawPathSegmentState$1["param"] = 1] = "param";
ParseRawPathSegmentState$1[ParseRawPathSegmentState$1["regexp"] = 2] = "regexp";
ParseRawPathSegmentState$1[ParseRawPathSegmentState$1["modifier"] = 3] = "modifier";
return ParseRawPathSegmentState$1;
}(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/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;
/**
* Should this page import the page info
*/
hasDefinePage = false;
/**
* 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$1, filePath) {
const { tail, segment, viewName } = splitFilePath(path$1);
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$1, filePath = path$1) {
const node = new TreeNode({
...this.options,
treeNodeOptions: {
...this.options.pathParser,
format: "path"
}
}, path$1, this);
this.children.set(path$1, 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");
}
/**
* 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$1) {
const { tail, segment, viewName } = splitFilePath(path$1);
const child = this.children.get(segment);
if (!child) throw new Error(`Cannot Delete "${path$1}". "${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;
}
/**
* 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 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()) {
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.hasDefinePage ? " ⚑ definePage()" : ""}`;
}
};
/**
* 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$1, filePath) {
const node = super.insert(path$1, 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)) {
this.map.get(filePath).delete();
this.map.delete(filePath);
}
}
};
/**
* 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/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 generateParamParsersTypesDeclarations(paramParsers) {
return Array.from(paramParsers.values()).map(({ typeName, relativePath }) => `type ${typeName} = ReturnType<NonNullable<typeof import('./${relativePath}').parser['get']>>`).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, absolutePath } = paramParsers.get(param.parser);
const varName = `PARAM_PARSER__${name}`;
importsMap.add(absolutePath, {
name: "parser",
as: varName
});
return varName;
} 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 generateParamParserCustomType(paramParsers) {
const parserNames = Array.from(paramParsers.keys()).sort();
if (parserNames.length === 0) return "never";
if (parserNames.length === 1) return `'${parserNames[0]}'`;
return parserNames.map((name) => ` | '${name}'`).join("\n");
}
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/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/posva/unplugin-vue-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" : "Exclude"}<${type}, unknown[]>`;
else extractedType = `${type ?? "string"}${isRepeatable ? "[]" : ""}`;
extractedType += isTreePathParam(param) && isOptional && !isRepeatable ? " | null" : "";
return `${param.paramName}${isRaw && isOptional ? "?" : ""}: ${extractedType}`;
}).join(", ")} }` : "Record<never, never>";
}
//#endregion
//#region src/utils/index.ts
const ts = String.raw;
/**
* Pads a single-line string with spaces.
*
* @internal
*
* @param spaces The number of spaces to pad with.
* @param str The string to pad, none if omitted.
* @returns The padded string.
*/
function pad(spaces, str = "") {
return " ".repeat(spaces) + str;
}
/**
* Formats an array of union items as a multiline union type.
*
* @internal
*
* @param items The items to format.
* @param spaces The number of spaces to indent each line.
* @returns The formatted multiline union type.
*/
function formatMultilineUnion(items, spaces) {
return (items.length ? items : ["never"]).map((s) => `| ${s}`).join(`\n${pad(spaces)}`);
}
/**
* Converts a string value to a TS string literal type.
*
* @internal
*
* @param str the string to convert to a string type
* @returns The string wrapped in single quotes.
* @example
* stringToStringType('hello') // returns "'hello'"
*/
function stringToStringType(str) {
return `'${str}'`;
}
//#endregion
//#region src/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, `${stringToStringType(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 = [
stringToStringType(node.name),
stringToStringType(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(stringToStringType), 4));
return `RouteRecordInfo<
${typeParams.map((line) => pad(4, line)).join(",\n")}
>`;
}
//#endregion
//#region src/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 }]) => `
'${file}': {
routes:
${formatMultilineUnion(routes.sort().map(stringToStringType), 6)}
views:
${formatMultilineUnion(views.sort().map(stringToStringType), 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$1) => {
if (node$1.isNamed() && node$1.value.components.size > 0) acc.push(node$1.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/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/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")}
]`;
const definePageDataList = [];
if (node.hasDefinePage) {
for (const [name, filePath] of node.value.components) {
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: '${node.path}',
${indentStr}${node.value.components.size ? node.isNamed() ? `name: ${stringToStringType(node.name)},` : `/* no name */` : `/* internal name: ${typeof node.name === "string" ? stringToStringType(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("unplugin-vue-router/runtime", "_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$1]) => `${indentStr + " "}'${key}': ${generatePageImport(path$1, 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('${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/core/customBlock.ts
function getRouteBlock(path$1, content, options) {
const blockStr = parse$1(content, { pad: "space" }).descriptor?.customBlocks.find((b) => b.type === "route");
if (blockStr) return parseCustomBlock(blockStr, path$1, 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/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 = resolve(this.src, filePath);
handler({
filePath,
routePath: asRoutePath({
src: this.src,
path: this.path,
extensions: this.extensions
}, filePath)
});
});
return this;
}
close() {
return this.watcher.close();
}
};
function resolveFolderOptions(globalOptions, folderOptions) {
const extensions = overrideOption(globalOptions.extensions, folderOptions.extensions);
const filePatterns = overrideOption(globalOptions.filePatterns, folderOptions.filePatterns);
return {
src: path.resolve(globalOptions.root, folderOptions.src),
pattern: appendExtensionListToPattern(filePatterns, extensions),
path: folderOptions.path || "",
extensions,
filePatterns,
exclude: overrideOption(globalOptions.exclude, folderOptions.exclude).map((p) => p.startsWith("**") ? p : resolve(p))
};
}
function overrideOption(existing, newValue) {
const asArray = typeof existing === "string" ? [existing] : existing;
if (typeof newValue === "function") return newValue(asArray);
if (typeof newValue !== "undefined") return typeof newValue === "string" ? [newValue] : newValue;
return asArray;
}
//#endregion
//#region src/codegen/generateDTS.ts
/**
* Removes empty lines and indent by two spaces to match the rest of the file.
*/
function normalizeLines(code) {
return code.split("\n").filter((line) => line.length !== 0).map((line) => pad(2, line)).join("\n");
}
function generateDTS({ routesModule, routeNamedMap, routeFileInfoMap, paramsTypesDeclaration, customParamsType }) {
return ts`
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection ES6UnusedImports
// Generated by unplugin-vue-router. !! DO NOT MODIFY THIS FILE !!
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
${paramsTypesDeclaration ? `
// Custom route params parsers
${paramsTypesDeclaration}
`.trimStart() : ""}declare module 'vue-router/auto-resolver' {
export type ParamParserCustom = ${customParamsType}
}
declare module '${routesModule}' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
${normalizeLines(routeNamedMap)}
/**
* Route file to route info map by unplugin-vue-router.
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* Each key is a file path relative to the project root with 2 properties:
* - routes: union of route names of the possible routes when in this page (passed to useRoute<...>())
* - views: names of nested views (can be passed to <RouterView name="...">)
*
* @internal
*/
${normalizeLines(routeFileInfoMap)}
/**
* Get a union of possible route names in a certain route component file.
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* @internal
*/
export type _RouteNamesForFilePath<FilePath extends string> =
_RouteFileInfoMap extends Record<FilePath, infer Info>
? Info['routes']
: keyof RouteNamedMap
}
`.trimStart();
}
//#endregion
//#region src/core/definePage.ts
const MACRO_DEFINE_PAGE = "definePage";
const MACRO_DEFINE_PAGE_QUERY = /[?&]definePage\b/;
/**
* Generate the ast from a code string and an id. Works with SFC and non-SFC files.
*/
function getCodeAst(code, id) {
let offset = 0;
let ast;
const lang = getLang(id.split(MACRO_DEFINE_PAGE_QUERY)[0]);
if (lang === "vue") {
const sfc = parseSFC(code, id);
if (sfc.scriptSetup) {
ast = sfc.getSetupAst();
offset = sfc.scriptSetup.loc.start.offset;
} else if (sfc.script) {
ast = sfc.getScriptAst();
offset = sfc.script.loc.start.offset;
}
} else if (/[jt]sx?$/.test(lang)) ast = babelParse(code, lang);
const definePageNodes = (ast?.body || []).map((node) => {
const definePageCallNode = node.type === "ExpressionStatement" ? node.expression : node;
return isCallOf(definePageCallNode, MACRO_DEFINE_PAGE) ? definePageCallNode : null;
}).filter((node) => !!node);
return {
ast,
offset,
definePageNodes
};
}
function definePageTransform({ code, id }) {
const isExtractingDefinePage = MACRO_DEFINE_PAGE_QUERY.test(id);
if (!code.includes(MACRO_DEFINE_PAGE)) return isExtractingDefinePage ? "export default {}" : void 0;
let ast;
let offset;
let definePageNodes;
try {
const result = getCodeAst(code, id);
ast = result.ast;
offset = result.offset;
definePageNodes = result.definePageNodes;
} catch (error) {
warn(`[${id}]: Failed to process definePage: ${error instanceof Error ? error.message : "Unknown error"}`);
return isExtractingDefinePage ? "export default {}" : void 0;
}
if (!ast) return;
if (!definePageNodes.length) return isExtractingDefinePage ? "export default {}" : null;
else if (definePageNodes.length > 1) throw new SyntaxError(`duplicate definePage() call`);
const definePageNode = definePageNodes[0];
if (isExtractingDefinePage) {
const s = new MagicString(code);
const routeRecord = definePageNode.arguments[0];
if (!routeRecord) throw new SyntaxError(`[${id}]: definePage() expects an object expression as its only argument`);
const scriptBindings = ast.body ? getIdentifiers(ast.body) : [];
try {
checkInvalidScopeReference(routeRecord, MACRO_DEFINE_PAGE, scriptBindings);
} catch (error) {
warn(`[${id}]: ${error instanceof Error ? error.message : "Invalid scope reference in definePage"}`);
return "export default {}";
}
s.remove(offset + routeRecord.end, code.length);
s.remove(0, offset + routeRecord.start);
s.prepend(`export default `);
const staticImports = findStaticImports(code);
const usedIds = /* @__PURE__ */ new Set();
const localIds = /* @__PURE__ */ new Set();
walkAST(routeRecord, {
enter(node) {
if (this.parent?.type === "ObjectProperty" && this.parent.key === node && !this.parent.computed && node.type === "Identifier") this.skip();
else if (this.parent?.type === "MemberExpression" && this.parent.property === node && !this.parent.computed && node.type === "Identifier") this.skip();
else if (node.type === "TSTypeAnnotation") this.skip();
else if (node.type === "Identifier" && !localIds.has(node.name)) usedIds.add(node.name);
else if ("scopeIds" in node && node.scopeIds instanceof Set) for (const id$1 of node.scopeIds) localIds.add(id$1);
},
leave(node) {
if ("scopeIds" in node && node.scopeIds instanceof Set) for (const id$1 of node.scopeIds) localIds.delete(id$1);
}
});
for (const imp of staticImports) {
const importCode = generateFilteredImportStatement(parseStaticImport(imp), usedIds);
if (importCode) s.prepend(importCode + "\n");
}
return generateTransform(s, id);
} else {
const s = new MagicString(code);
s.remove(offset + definePageNode.start, offset + definePageNode.end);
return generateTransform(s, id);
}
}
/**
* Extracts name, path, and params from definePage(). Those do not require
* extracting the whole definePage object as a different import
*/
function extractDefinePageInfo(sfcCode, id) {
if (!sfcCode.includes(MACRO_DEFINE_PAGE)) return;
let ast;
let definePageNodes;
try {
const result = getCodeAst(sfcCode, id);
ast = result.ast;
definePageNodes = result.definePageNodes;
} catch (error) {
warn(`[${id}]: Failed to extract definePage info: ${error instanceof Error ? error.message : "Unknown error"}`);
return;
}
if (!ast) return;
if (!definePageNodes.length) return;
else if (definePageNodes.length > 1) throw new SyntaxError(`duplicate definePage() call`);
const routeRecord = definePageNodes[0].arguments[0];
if (!routeRecord) throw new SyntaxError(`[${id}]: definePage() expects an object expression as its only argument`);
if (routeRecord.type !== "ObjectExpression") throw new SyntaxError(`[${id}]: definePage() expects an object expression as its only argument`);
const routeInfo = {};
for (const prop of routeRecord.properties) if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
if (prop.key.name === "name") if (prop.value.type !== "StringLiteral" && (prop.value.type !== "BooleanLiteral" || prop.value.value !== false)) warn(`route name must be a string literal or false. Found in "${id}".`);
else routeInfo.name = prop.value.value;
else if (prop.key.name === "path") if (prop.value.type !== "StringLiteral") warn(`route path must be a string literal. Found in "${id}".`);
else routeInfo.path = prop.value.value;
else if (prop.key.name === "params") {
if (prop.value.type === "ObjectExpression") routeInfo.params = extractParamsInfo(prop.value, id);
}
}
return routeInfo;
}
function extractParamsInfo(paramsObj, id) {
const params = {};
for (const prop of paramsObj.properties) if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
if (prop.key.name === "query" && prop.value.type === "ObjectExpression") params.query = extractQueryParams(prop.value, id);
else if (prop.key.name === "path" && prop.value.type === "ObjectExpression") params.path = extractPathParams(prop.value, id);
}
return params;
}
function extractQueryParams(queryObj, _id) {
const queryParams = {};
for (const prop of queryObj.properties) if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
const paramName = prop.key.name;
if (prop.value.type === "StringLiteral") queryParams[paramName] = { parser: prop.value.value };
else if (prop.value.type === "ObjectExpression") {
const paramInfo = {};
for (const paramProp of prop.value.properties) if (paramProp.type === "ObjectProperty" && paramProp.key.type === "Identifier") {
if (paramProp.key.name === "parser" && paramProp.value.type === "StringLiteral") paramInfo.parser = paramProp.value.value;
else if (paramProp.key.name === "format" && paramProp.value.type === "StringLiteral") paramInfo.format = paramProp.value.value;
else if (paramProp.key.name === "default") if (typeof paramProp.value.extra?.raw === "string") paramInfo.default = paramProp.value.extra.raw;
else if (paramProp.value.type === "NumericLiteral") paramInfo.default = String(paramProp.value.value);
else if (paramProp.value.type === "StringLiteral") paramInfo.default = JSON.stringify(paramProp.value.value);
else if (paramProp.value.type === "BooleanLiteral") paramInfo.default = String(paramProp.value.value);
else if (paramProp.value.type === "NullLiteral") paramInfo.default = "null";
else if (paramProp.value.type === "UnaryExpression" && (paramProp.value.operator === "-" || paramProp.value.operator === "+" || paramProp.value.operator === "!" || paramProp.value.operator === "~") && paramProp.value.argument.type === "NumericLiteral") paramInfo.default = `${paramProp.value.operator}${paramProp.value.argument.value}`;
else if (paramProp.value.type === "ArrowFunctionExpression") paramInfo.default = generate(paramProp.value).code;
else warn(`Unrecognized default value in definePage() for query param "${paramName}". Typeof value: "${paramProp.value.type}". This is a bug or a missing type of value, open an issue on https://github.com/posva/unplugin-vue-router and provide the definePage() code.`);
}
queryParams[paramName] = paramInfo;
}
}
return queryParams;
}
function extractPathParams(pathObj, _id) {
const pathParams = {};
for (const prop of pathObj.properties) if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.value.type === "StringLiteral") pathParams[prop.key.name] = prop.value.value;
return pathParams;
}
const getIdentifiers = (stmts) => {
let ids = [];
walkAST({
type: "Program",
body: stmts,
directives: [],
sourceType: "module"
}, {
enter(node) {
if (node.type === "BlockStatement") this.skip();
},
leave(node) {
if (node.type !== "Program") return;
ids = Object.keys(this.scope);
}
});
return ids;
};
/**
* Generate a filtere import statement based on a set of identifiers that should be kept.
*
* @param parsedImports - parsed imports with mlly
* @param usedIds - set of used identifiers
* @returns `null` if no import statement should be generated, otherwise the import statement as a string without a newline
*/
function generateFilteredImportStatement(parsedImports, usedIds) {
if (!parsedImports || usedIds.size < 1) return null;
const { namedImports, defaultImport, namespacedImport } = parsedImports;
if (namespacedImport && usedIds.has(namespacedImport)) return `import * as ${namespacedImport} from '${parsedImports.specifier}'`;
let importListCode = "";
if (defaultImport && usedIds.has(defaultImport)) importListCode += defaultImport;
let namedImportListCode = "";
for (const importName in namedImports) if (usedIds.has(importName)) {
namedImportListCode += namedImportListCode ? `, ` : "";
namedImportListCode += importName === namedImports[importName] ? importName : `${importName} as ${namedImports[importName]}`;
}
importListCode += importListCode && namedImportListCode ? ", " : "";
importListCode += namedImportListCode ? `{${namedImportListCode}}` : "";
if (!importListCode) return null;
return `import ${importListCode} from '${parsedImports.specifier}'`;
}
//#endregion
//#region src/core/extendRoutes.ts
/**
* A route node that can be modified by the user. The tree can be iterated to be traversed.
* @example
* ```js
* [...node] // creates an array of all the children
* for (const child of node) {
* // do something with the child node
* }
* ```
*
* @experimental
*/
var EditableTreeNode = class EditableTreeNode {
node;
constructor(node) {
this.node = node;
}
/**
* Remove and detach the current route node from the tree. Subsequently, its children will be removed as well.
*/
delete() {
return this.node.delete();
}
/**
* Inserts a new route as a child of this route. This route cannot use `definePage()`. If it was meant to be included,
* add it to the `routesFolder` option.
*
* @param path - path segment to insert. Note this is relative to the current route. **It shouldn't start with `/`**. If it does, it will be added to the root of the tree.
* @param filePath - file path
* @returns the new editable route node
*/
insert(path$1, filePath) {
let addBackLeadingSlash = false;
if (path$1.startsWith("/")) {
path$1 = path$1.slice(1);
addBackLeadingSlash = !this.node.isRoot();
}
const node = this.node.insertParsedPath(path$1, filePath);
const editable = new EditableTreeNode(node);
if (addBackLeadingSlash) editable.path = "/" + node.path;
return editable;
}
/**
* Get an editable version of the parent node if it exists.
*/
get parent() {
return this.node.parent && new EditableTreeNode(this.node.parent);
}
/**
* Return a Map of the files associated to the current route. The key of the map represents the name of the view (Vue
* Router feature) while the value is the **resolved** file path.
* By default, the name of the view is `default`.
*/
get components() {
return this.node.value.components;
}
/**
* Alias for `route.components.get('default')`.
*/
get component() {
return this.node.value.components.get("default");
}
/**
* Name of the route. Note that **all routes are named** but when the final `routes` array is generated, routes
* without a `component` will not include their `name` property to avoid accidentally navigating to them and display
* nothing.
* @see {@link isPassThrough}
*/
get name() {
return this.node.name;
}
/**
* Override the name of the route.
*/
set name(name) {
this.node.value.addEditOverride({ name });
}
/**
* Whether the route is a pass-through route. A pass-through route is a route that does not have a component and is
* used to group other routes under the same prefix `path` and/or `meta` properties.
*/
get isPassThrough() {
return this.node.value.components.size === 0;
}
/**
* Meta property of the route as an object. Note this property is readonly and will be serialized as JSON. It won't contain the meta properties defined with `definePage()` as it could contain expressions **but it does contain the meta properties defined with `<route>` blocks**.
*/
get meta() {
return this.node.metaAsObject;
}
/**
* Override the meta property of the route. This will discard any other meta property defined with `<route>` blocks or
* through other means. If you want to keep the existing meta properties, use `addToMeta`.
* @see {@link addToMeta}
*/
set meta(meta) {
this.node.value.removeOverride("meta");
this.node.value.setEditOverride("meta", meta);
}
/**
* Add meta properties to the route keeping the existing ones. The passed object will be deeply merged with the
* existing meta object if any. Note that the meta property is later on serialized as JSON so you can't pass functions
* or any other non-serializable value.
*/
addToMeta(meta) {
this.node.value.addEditOverride({ meta });
}
/**
* Path of the route without parent paths.
*/
get path() {
return this.node.path;
}
/**
* Override the path of the route. You must ensure `params` match with the existing path.
*/
set path(path$1) {
if ((!this.node.parent || this.node.parent.isRoot()) && !path$1.startsWith("/")) path$1 = "/" + path$1;
this.node.value.addEditOverride({ path: path$1 });
}
/**
* Alias of the route.
*/
get alias() {
return this.node.value.overrides.alias;
}
/**
* Add an alias to the route.
*
* @param alias - Alias to add to the route
*/
addAlias(alias) {
this.node.value.addEditOverride({ alias });
}
/**
* Array of the route params and all of its parent's params. Note that
* changing the params will not update the path, you need to update both.
*/
get params() {
return this.node.pathParams;
}
/**
* Path of the route including parent paths.
*/
get fullPath() {
return this.node.fullPath;
}
/**
* Computes an array of EditableTreeNode from the current node. Differently from iterating over the tree, this method
* **only returns direct children**.
*/
get children() {
return [...this.node.children.values()].map((node) => new EditableTreeNode(node));
}
/**
* DFS traversal of the tree.
* @example
* ```ts
* for (const node of tree) {
* // ...
* }
* ```
*/
*traverseDFS() {
if (!this.node.isRoot()) yield this;
for (const [_name, child] of this.node.children) yield* new EditableTreeNode(child).traverseDFS();
}
*[Symbol.iterator]() {
yield* this.traverseBFS();
}
/**
* BFS traversal of the tree as a generator.
*
* @example
* ```ts
* for (const node of tree) {
* // ...
* }
* ```
*/
*traverseBFS() {
for (const [_name, child] of this.node.children) yield new EditableTreeNode(child);
for (const [_name, child] of this.node.children) yield* new EditableTreeNode(child).traverseBFS();
}
};
//#endregion
//#region src/codegen/generateRouteResolver.ts
/**
* Compare two score arrays for sorting routes by priority.
* Higher scores should come first (more specific routes).
*/
function compareRouteScore(a, b) {
const maxLength = Math.max(a.length, b.length);
for (let i = 0; i < maxLength; i++) {
const aSegment = a[i] || [];
const bSegment = b[i] || [];
const aMinScore = aSegment.length > 0 ? Math.min(...aSegment) : 0;
const bMinScore = bSegment.length > 0 ? Math.min(...bSegment) : 0;
if (aMinScore !== bMinScore) return bMinScore - aMinScore;
const aAvgScore = aSegment.length > 0 ? aSegment.reduce((sum, s) => sum + s, 0) / aSegment.length : 0;
const bAvgScore = bSegment.length > 0 ? bSegment.reduce((sum, s) => sum + s, 0) / bSegment.length : 0;
if (aAvgScore !== bAvgScore) return bAvgScore - aAvgScore;
if (aSegment.length !== bSegment.length) return aSegment.length - bSegment.length;
}
return a.length - b.length;
}
const ROUTE_RECORD_VAR_PREFIX = "__route_";
function generateRouteResolver(tree, options, importsMap, paramParsersMap) {
const state = {
id: 0,
matchableRecords: []
};
const records = tree.getChildrenSorted().map((node) => generateRouteRecord({
node,
parentVar: null,
parentNode: null,
state,
options,
importsMap,
paramParsersMap
}));
importsMap.add("vue-router/experimental", "createFixedResolver");
importsMap.add("vue-router/experimental", "MatcherPatternPathStatic");
importsMap.add("vue-router/experimental", "MatcherPatternPathDynamic");
importsMap.add("vue-router/experimental", "normalizeRouteRecord");
return ts`
${records.join("\n\n")}
export const resolver = createFixedResolver([
${state.matchableRecords.sort((a, b) => compareRouteScore(a.score, b.score)).map(({ varName, path: path$1 }) => ` ${varName}, ${" ".repeat(String(state.id).length - varName.length + 8)}// ${path$1}`).join("\n")}
])
`;
}
/**
* Generates the route record in the format expected by the static resolver.
*/
function generateRouteRecord({ node, parentVar, parentNode, state, options, importsMap, paramParsersMap }) {
const isMatchable = node.isMatchable();
const shouldSkipNode = !isMatchable && !node.meta && !node.hasComponents;
let varName = null;
let recordDeclaration = "";
const definePageDataList = [];
if (node.hasDefinePage) for (const [name, filePath] of node.value.components) {
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 (!shouldSkipNode) {
varName = `${ROUTE_RECORD_VAR_PREFIX}${state.id++}`;
let recordName;
const recordComponents = generateRouteRecordComponent(node, " ", options.importMode, importsMap);
if (isMatchable) {
state.matchableRecords.push({
path: node.fullPath,
varName,
score: node.score
});
recordName = `name: '${node.name}',`;
} else recordName = node.name ? `/* (internal) name: '${node.name}' */` : `/* (removed) name: false */`;
const queryProperty = generateRouteRecordQuery({
node,
importsMap,
paramParsersMap
});
const routeRecordObject = `{
${recordName}
${generateRouteRecordPath({
node,
importsMap,
paramParsersMap,
parentVar,
parentNode
})}${queryProperty ? `\n ${queryProperty}` : ""}${formatMeta(node, " ")}
${recordComponents}${parentVar ? `\n parent: ${parentVar},` : ""}
}`;
recordDeclaration = definePageDataList.length > 0 ? `
const ${varName} = normalizeRouteRecord(
${generateRouteRecordMerge(routeRecordObject, definePageDataList, importsMap)}
)
` : `
const ${varName} = normalizeRouteRecord(${routeRecordObject})
`.trim().split("\n").filter((l) => l.trimStart().length > 0).join("\n");
}
const children = node.getChildrenSorted().map((child) => generateRouteRecord({
node: child,
parentVar: shouldSkipNode ? parentVar : varName,
parentNode: shouldSkipNode ? parentNode : node,
state,
options,
importsMap,
paramParsersMap
}));
return recordDeclaration + (children.length ? (recordDeclaration ? "\n" : "") + children.join("\n") : "");
}
function generateRouteRecordComponent(node, indentStr, importMode, importsMap) {
if (!node.hasComponents) return "";
return `components: {
${Array.from(node.value.components).map(([key, path$1]) => `${indentStr + " "}'${key}': ${generatePageImport(path$1, importMode, importsMap)}`).join(",\n")}
${indentStr}},`;
}
/**
* Generates the `path` property of a route record for the static resolver.
*/
function generateRouteRecordPath({ node, importsMap, paramParsersMap, parentVar, parentNode }) {
if (!node.isMatchable() && node.name) return "";
if (parentVar && parentNode && node.regexp === parentNode.regexp) return `path: ${parentVar}.path,`;
const params = node.pathParams;
if (params.length > 0) return `path: new MatcherPatternPathDynamic(
${node.regexp},
${generatePathParamsOptions(params, importsMap, paramParsersMap)},
${JSON.stringify(node.matcherPatternPathDynamicParts)},
${node.isSplat ? "null," : "/* trailingSlash */"}
),`;
else return `path: new MatcherPatternPathStatic('${node.fullPath}'),`;
}
/**
* Generates the `query` property of a route record for the static resolver.
*/
function generateRouteRecordQuery({ node, importsMap, paramParsersMap }) {
const queryParams = node.queryParams;
if (queryParams.length === 0) return "";
importsMap.add("vue-router/experimental", "MatcherPatternQueryParam");
return `query: [
${queryParams.map((param) => {
const parserOptions = generateParamParserOptions(param, importsMap, paramParsersMap);
const args = [
`'${param.paramName}'`,
`'${param.paramName}'`,
`'${param.format}'`
];
if (parserOptions || param.defaultValue !== void 0) args.push(parserOptions || "{}");
if (param.defaultValue !== void 0) args.push(param.defaultValue);
return ` new MatcherPatternQueryParam(${args.join(", ")})`;
}).join(",\n")}
],`;
}
/**
* Generates a merge call for route records with definePage data in the experimental resolver format.
*/
function generateRouteRecordMerge(routeRecordObject, definePageDataList, importsMap) {
if (definePageDataList.length === 0) return routeRecordObject;
importsMap.add("vue-router/experimental", "_mergeRouteRecord");
return `_mergeRouteRecord(
${routeRecordObject.split("\n").map((line) => {
return line && ` ${line}`;
}).join("\n")},
${definePageDataList.map((name) => ` ${name}`).join(",\n")}
)`;
}
//#endregion
//#region src/core/context.ts
function createRoutesContext(options) {
const { dts: preferDTS, root, routesFolder } = options;
const dts = preferDTS === false ? false : preferDTS === true ? resolve(root, "typed-router.d.ts") : resolve(root, preferDTS);
const routeTree = new PrefixTree(options);
const editableRoutes = new EditableTreeNode(routeTree);
const logger = new Proxy(console, { get(target, prop) {
const res = Reflect.get(target, prop);
if (typeof res === "function") return options.logs ? res : () => {};
return res;
} });
const watchers = [];
const paramParsersMap = /* @__PURE__ */ new Map();
async function scanPages(startWatchers = true) {
if (options.extensions.length < 1) throw new Error("\"extensions\" cannot be empty. Please specify at least one extension.");
if (watchers.length > 0) return;
const PARAM_PARSER_GLOB = "*.{ts,js}";
const isParamParserMatch = picomatch(PARAM_PARSER_GLOB);
await Promise.all([...routesFolder.map((folder) => resolveFolderOptions(options, folder)).map((folder) => {
if (startWatchers) watchers.push(setupWatcher(new RoutesFolderWatcher(folder)));
const ignorePattern = folder.exclude.map((f) => f.startsWith("**") ? f : relative(folder.src, f));
return glob(folder.pattern, {
cwd: folder.src,
ignore: ignorePattern,
expandDirectories: false
}).then((files) => Promise.all(files.map((file) => resolve(folder.src, file)).map((file) => addPage({
routePath: asRoutePath(folder, file),
filePath: file
}))));
}), ...options.experimental.paramParsers?.dir.map((folder) => {
if (startWatchers) watchers.push(setupParamParserWatcher(watch(".", {
cwd: folder,
ignoreInitial: true,
ignorePermissionErrors: true,
ignored: (filePath, stats) => {
if (!stats || stats.isDirectory()) return false;
return !isParamParserMatch(relative(folder, filePath));
}
}), folder));
return glob(PARAM_PARSER_GLOB, {
cwd: folder,
onlyFiles: true,
expandDirectories: false
}).then((paramParserFiles) => {
for (const file of paramParserFiles) {
const fileName = parse(file).name;
const name = camelCase(fileName);
const absolutePath = resolve(folder, file);
paramParsersMap.set(fileName, {
name,
typeName: `Param_${name}`,
absolutePath,
relativePath: relative(options.root, absolutePath)
});
}
logger.log("Parsed param parsers", [...paramParsersMap].map((p) => p[0]));
});
}) || []]);
for (const route of editableRoutes) await options.extendRoute?.(route);
await _writeConfigFiles();
}
async function writeRouteInfoToNode(node, filePath) {
const content = await promises.readFile(filePath, "utf8");
node.hasDefinePage ||= content.includes("definePage");
const definedPageInfo = extractDefinePageInfo(content, filePath);
const routeBlock = getRouteBlock(filePath, content, options);
node.setCustomRouteBlock(filePath, {
...routeBlock,
...definedPageInfo
});
server?.invalidatePage(filePath);
}
async function addPage({ filePath, routePath }, triggerExtendRoute = false) {
logger.log(`added "${routePath}" for "${filePath}"`);
const node = routeTree.insert(routePath, filePath);
await writeRouteInfoToNode(node, filePath);
if (triggerExtendRoute) await options.extendRoute?.(new EditableTreeNode(node));
server?.updateRoutes();
}
async function updatePage({ filePath, routePath }) {
logger.log(`updated "${routePath}" for "${filePath}"`);
const node = routeTree.getChild(filePath);
if (!node) {
logger.warn(`Cannot update "${filePath}": Not found.`);
return;
}
await writeRouteInfoToNode(node, filePath);
await options.extendRoute?.(new EditableTreeNode(node));
server?.updateRoutes();
}
function removePage({ filePath, routePath }) {
logger.log(`remove "${routePath}" for "${filePath}"`);
routeTree.removeChild(filePath);
server?.updateRoutes();
}
function setupParamParserWatcher(watcher, cwd) {
logger.log(`🤖 Scanning param parsers in ${cwd}`);
return watcher.on("add", (file) => {
const fileName = parse(file).name;
const name = camelCase(fileName);
const absolutePath = resolve(cwd, file);
paramParsersMap.set(fileName, {
name,
typeName: `Param_${name}`,
absolutePath,
relativePath: "./" + relative(options.root, absolutePath)
});
writeConfigFiles();
}).on("unlink", (file) => {
paramParsersMap.delete(parse(file).name);
writeConfigFiles();
});
}
function setupWatcher(watcher) {
logger.log(`🤖 Scanning files in ${watcher.src}`);
return watcher.on("change", async (ctx) => {
await updatePage(ctx);
writeConfigFiles();
}).on("add", async (ctx) => {
await addPage(ctx, true);
writeConfigFiles();
}).on("unlink", (ctx) => {
removePage(ctx);
writeConfigFiles();
});
}
function generateResolver() {
const importsMap = new ImportsMap();
const resolverCode = generateRouteResolver(routeTree, options, importsMap, paramParsersMap);
let imports = importsMap.toString();
if (imports) imports += "\n";
const hmr = ts`
export function handleHotUpdate(_router, _hotUpdateCallback) {
if (import.meta.hot) {
import.meta.hot.data.router = _router
import.meta.hot.data.router_hotUpdateCallback = _hotUpdateCallback
}
}
if (import.meta.hot) {
import.meta.hot.accept((mod) => {
const router = import.meta.hot.data.router
if (!router) {
import.meta.hot.invalidate('[unplugin-vue-router:HMR] Cannot replace the resolver because there is no active router. Reloading.')
return
}
router._hmrReplaceResolver(mod.resolver)
// call the hotUpdateCallback for custom updates
import.meta.hot.data.router_hotUpdateCallback?.(mod.resolver)
const route = router.currentRoute.value
router.replace({
path: route.path,
query: route.query,
hash: route.hash,
force: true
})
})
}`;
return `${imports}${resolverCode}\n${hmr}`;
}
function generateRoutes() {
const importsMap = new ImportsMap();
const routeList = `export const routes = ${generateRouteRecords(routeTree, options, importsMap)}\n`;
const hmr = ts`
export function handleHotUpdate(_router, _hotUpdateCallback) {
if (import.meta.hot) {
import.meta.hot.data.router = _router
import.meta.hot.data.router_hotUpdateCallback = _hotUpdateCallback
}
}
if (import.meta.hot) {
import.meta.hot.accept((mod) => {
const router = import.meta.hot.data.router
if (!router) {
import.meta.hot.invalidate('[unplugin-vue-router:HMR] Cannot replace the routes because there is no active router. Reloading.')
return
}
router.clearRoutes()
for (const route of mod.routes) {
router.addRoute(route)
}
// call the hotUpdateCallback for custom updates
import.meta.hot.data.router_hotUpdateCallback?.(mod.routes)
const route = router.currentRoute.value
router.replace({
...route,
// NOTE: we should be able to just do ...route but the router
// currently skips resolving and can give errors with renamed routes
// so we explicitly set remove matched and name
name: undefined,
matched: undefined,
force: true
})
})
}
`;
let imports = importsMap.toString();
if (imports) imports += "\n";
return `${imports}${routeList}${hmr}\n`;
}
function generateDTS$1() {
if (options.experimental.paramParsers?.dir.length) warnMissingParamParsers(routeTree, paramParsersMap);
return generateDTS({
routesModule: MODULE_ROUTES_PATH,
routeNamedMap: generateRouteNamedMap(routeTree, options, paramParsersMap),
routeFileInfoMap: generateRouteFileInfoMap(routeTree, { root }),
paramsTypesDeclaration: generateParamParsersTypesDeclarations(paramParsersMap),
customParamsType: generateParamParserCustomType(paramParsersMap)
});
}
let lastDTS;
async function _writeConfigFiles() {
logger.time("writeConfigFiles");
if (options.beforeWriteFiles) {
await options.beforeWriteFiles(editableRoutes);
logger.timeLog("writeConfigFiles", "beforeWriteFiles()");
}
logTree(routeTree, logger.log);
if (dts) {
const content = generateDTS$1();
if (lastDTS !== content) {
await promises.mkdir(dirname(dts), { recursive: true });
await promises.writeFile(dts, content, "utf-8");
logger.timeLog("writeConfigFiles", "wrote dts file");
lastDTS = content;
server?.updateRoutes();
}
}
logger.timeEnd("writeConfigFiles");
}
const writeConfigFiles = throttle(_writeConfigFiles, 500, 100);
function stopWatcher() {
if (watchers.length) {
logger.log("🛑 stopping watcher");
watchers.forEach((watcher) => watcher.close());
}
}
let server;
function setServerContext(_server) {
server = _server;
}
return {
scanPages,
writeConfigFiles,
setServerContext,
stopWatcher,
generateRoutes,
generateResolver,
definePageTransform(code, id) {
return definePageTransform({
code,
id
});
}
};
}
//#endregion
//#region src/core/vite/index.ts
function createViteContext(server) {
function invalidate(path$1) {
const foundModule = server.moduleGraph.getModuleById(path$1);
if (foundModule) return server.reloadModule(foundModule);
return !!foundModule;
}
function invalidatePage(filepath) {
const pageModules = server.moduleGraph.getModulesByFile(filepath);
if (pageModules) return Promise.all([...pageModules].map((mod) => server.reloadModule(mod))).then(() => {});
return false;
}
function reload() {
server.ws.send({
type: "full-reload",
path: "*"
});
}
/**
* Triggers HMR for the vue-router/auto-routes module.
*/
async function updateRoutes() {
const autoRoutesMod = server.moduleGraph.getModuleById(asVirtualId(MODULE_ROUTES_PATH));
const autoResolvedMod = server.moduleGraph.getModuleById(asVirtualId(MODULE_RESOLVER_PATH));
await Promise.all([autoRoutesMod && server.reloadModule(autoRoutesMod), autoResolvedMod && server.reloadModule(autoResolvedMod)]);
}
return {
invalidate,
invalidatePage,
updateRoutes,
reload
};
}
//#endregion
//#region src/data-loaders/auto-exports.ts
function extractLoadersToExport(code, filterPaths, root) {
return findStaticImports(code).flatMap((i) => {
const parsed = parseStaticImport(i);
if (!filterPaths(resolve(root, parsed.specifier.startsWith("/") ? parsed.specifier.slice(1) : parsed.specifier))) return [];
return [parsed.defaultImport, ...Object.values(parsed.namedImports || {})].filter((v) => !!v && !v.startsWith("_"));
});
}
const PLUGIN_NAME = "unplugin-vue-router:data-loaders-auto-export";
/**
* Vite Plugin to automatically export loaders from page components.
*
* @param options Options
* @experimental - This API is experimental and can be changed in the future. It's used internally by `experimental.autoExportsDataLoaders`
*/
function AutoExportLoaders({ transformFilter, loadersPathsGlobs, root = process.cwd() }) {
const filterPaths = createFilter(loadersPathsGlobs);
return {
name: PLUGIN_NAME,
transform: {
order: "post",
filter: { id: transformFilter },
handler(code) {
const loadersToExports = extractLoadersToExport(code, filterPaths, root);
if (loadersToExports.length <= 0) return;
const s = new MagicString$1(code);
s.append(`\nexport const __loaders = [\n${loadersToExports.join(",\n")}\n];\n`);
return {
code: s.toString(),
map: s.generateMap()
};
}
}
};
}
function createAutoExportPlugin(options) {
return {
name: PLUGIN_NAME,
vite: AutoExportLoaders(options)
};
}
//#endregion
//#region src/index.ts
var src_default = createUnplugin((opt = {}, _meta) => {
const options = resolveOptions(opt);
const ctx = createRoutesContext(options);
function getVirtualId$1(id) {
if (options._inspect) return id;
return getVirtualId(id);
}
function asVirtualId$1(id) {
if (options._inspect) return id;
return asVirtualId(id);
}
const pageFilePattern = appendExtensionListToPattern(options.filePatterns, mergeAllExtensions(options));
const IDS_TO_INCLUDE = options.routesFolder.flatMap((routeOption) => pageFilePattern.map((pattern) => join(routeOption.src, pattern)));
const plugins = [{
name: "unplugin-vue-router",
enforce: "pre",
resolveId: {
filter: { id: { include: [
/* @__PURE__ */ new RegExp(`^${MODULE_ROUTES_PATH}$`),
/* @__PURE__ */ new RegExp(`^${MODULE_RESOLVER_PATH}$`),
routeBlockQueryRE
] } },
handler(id) {
if (id === MODULE_ROUTES_PATH || id === MODULE_RESOLVER_PATH) return asVirtualId$1(id);
return ROUTE_BLOCK_ID;
}
},
async buildStart() {
await ctx.scanPages(options.watch);
},
buildEnd() {
ctx.stopWatcher();
},
transform: {
filter: { id: {
include: [...IDS_TO_INCLUDE, DEFINE_PAGE_QUERY_RE],
exclude: options.exclude
} },
handler(code, id) {
return ctx.definePageTransform(code, id);
}
},
load: {
filter: { id: { include: [
/* @__PURE__ */ new RegExp(`^${ROUTE_BLOCK_ID}$`),
/* @__PURE__ */ new RegExp(`^${VIRTUAL_PREFIX}${MODULE_ROUTES_PATH}$`),
/* @__PURE__ */ new RegExp(`^${VIRTUAL_PREFIX}${MODULE_RESOLVER_PATH}$`)
] } },
handler(id) {
if (id === ROUTE_BLOCK_ID) return {
code: `export default {}`,
map: null
};
const resolvedId = getVirtualId$1(id);
if (resolvedId === MODULE_ROUTES_PATH) {
ROUTES_LAST_LOAD_TIME.update();
return ctx.generateRoutes();
}
if (resolvedId === MODULE_RESOLVER_PATH) {
ROUTES_LAST_LOAD_TIME.update();
return ctx.generateResolver();
}
}
},
vite: { configureServer(server) {
ctx.setServerContext(createViteContext(server));
} }
}];
if (options.experimental.autoExportsDataLoaders) plugins.push(createAutoExportPlugin({
transformFilter: {
include: IDS_TO_INCLUDE,
exclude: options.exclude
},
loadersPathsGlobs: options.experimental.autoExportsDataLoaders,
root: options.root
}));
return plugins;
});
/**
* Adds useful auto imports to the AutoImport config:
* @example
* ```js
* import { VueRouterAutoImports } from 'unplugin-vue-router'
*
* AutoImport({
* imports: [VueRouterAutoImports],
* }),
* ```
*/
const VueRouterAutoImports = {
"vue-router": [
"useRoute",
"useRouter",
"onBeforeRouteUpdate",
"onBeforeRouteLeave"
],
"unplugin-vue-router/runtime": []
};
//#endregion
export { EditableTreeNode as a, createRoutesContext as i, src_default as n, createTreeNodeValue as o, AutoExportLoaders as r, VueRouterAutoImports as t };