UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

214 lines (182 loc) 6.46 kB
import type { One } from '../vite/types' /** Match `[page]` -> `page` or `[...page]` -> `page` with deep flag */ const dynamicNameRe = /^\[([^[\]]+?)\]$/ export interface DynamicNameMatch { name: string deep: boolean } /** Match `[page]` -> `{ name: 'page', deep: false }` or `[...page]` -> `{ name: 'page', deep: true }` */ export function matchDynamicName(name: string): DynamicNameMatch | undefined { const paramName = name.match(dynamicNameRe)?.[1] if (paramName == null) { return undefined } else if (paramName.startsWith('...')) { return { name: paramName.slice(3), deep: true } } else { return { name: paramName, deep: false } } } /** * Match `[...page]` -> `page` * @deprecated Use matchDynamicName instead which returns {name, deep} */ export function matchDeepDynamicRouteName(name: string): string | undefined { return name.match(/^\[\.\.\.([^/]+?)\]$/)?.[1] } /** Test `/` -> `page` */ export function testNotFound(name: string): boolean { return name.endsWith('+not-found') } /** Match `(page)` -> `page` */ export function matchGroupName(name: string): string | undefined { return name.match(/^(?:[^\\(\\)])*?\(([^\\/]+)\).*?$/)?.[1] } /** Match the first array group name `(a,b,c)/(d,c)` -> `'a,b,c'` */ export function matchArrayGroupName(name: string) { return name.match(/(?:[^\\(\\)])*?\(?([^\\/()]+,[^\\/()]+)\)?.*?$/)?.[1] } export function getNameFromFilePath(name: string): string { return removeSupportedExtensions(removeFileSystemDots(name)) } export function getContextKey(name: string): string { // The root path is `` (empty string) so always prepend `/` to ensure // there is some value. const normal = '/' + getNameFromFilePath(name) if (!normal.endsWith('_layout')) { return normal } return normal.replace(/\/?_layout$/, '') } /** Remove `.js`, `.ts`, `.jsx`, `.tsx` */ export function removeSupportedExtensions(name: string): string { return name.replace(/(\+(api|spa|ssg|ssr))?\.[jt]sx?$/g, '') } // Remove any amount of `./` and `../` from the start of the string export function removeFileSystemDots(filePath: string): string { return filePath.replace(/^(?:\.\.?\/)+/g, '') } export function stripGroupSegmentsFromPath(path: string): string { return path .split('/') .reduce((acc, v) => { if (matchGroupName(v) == null) { acc.push(v) } return acc }, [] as string[]) .join('/') } export function stripInvisibleSegmentsFromPath(path: string): string { return stripGroupSegmentsFromPath(path).replace(/\/?index$/, '') } /** * Match: * - _layout files, +html, +not-found, string+api, etc * - Routes can still use `+`, but it cannot be in the last segment. * - .d.ts files (type definition files) */ export function isTypedRoute(name: string) { return ( !name.startsWith('+') && !name.endsWith('.d.ts') && name.match(/(_layout|[^/]*?\+[^/]*?)\.[tj]sx?$/) === null ) } // ============================================ // Directory Render Modes // ============================================ /** Match directory render mode suffixes: dashboard+ssr, blog+ssg, etc. */ const directoryRenderModeRe = /^(.+)\+(api|ssg|ssr|spa)$/ export interface DirectoryRenderModeMatch { /** Directory name without the render mode suffix */ name: string /** The render mode for this directory */ renderMode: One.RouteRenderMode | 'api' } /** * Match directory render mode suffixes * * Examples: * - "dashboard+ssr" -> { name: "dashboard", renderMode: "ssr" } * - "blog+ssg" -> { name: "blog", renderMode: "ssg" } * - "admin+spa" -> { name: "admin", renderMode: "spa" } */ export function matchDirectoryRenderMode( name: string ): DirectoryRenderModeMatch | undefined { const match = name.match(directoryRenderModeRe) if (!match) return undefined return { name: match[1], renderMode: match[2] as One.RouteRenderMode | 'api', } } // ============================================ // Parallel Routes & Intercepting Routes // ============================================ /** Match @slot directories: @modal, @sidebar, etc. */ const slotPrefixRe = /^@([a-zA-Z][a-zA-Z0-9_-]*)$/ /** Match @modal -> 'modal', @sidebar -> 'sidebar' */ export function matchSlotName(name: string): string | undefined { return name.match(slotPrefixRe)?.[1] } /** Check if a directory name is a slot directory */ export function isSlotDirectory(name: string): boolean { return slotPrefixRe.test(name) } export interface InterceptMatch { /** Number of levels up (0 = same level, 1 = parent, Infinity = root) */ levels: number /** The actual route path after stripping intercept prefix */ targetPath: string /** Original segment like "(.)photos" or "(..)photos" */ originalSegment: string } /** * Match intercept prefixes: (.), (..), (...), (..)(..) etc. * * Examples: * - "(.)photos" -> { levels: 0, targetPath: "photos" } * - "(..)photos" -> { levels: 1, targetPath: "photos" } * - "(...)photos" -> { levels: Infinity, targetPath: "photos" } * - "(..)(..)photos" -> { levels: 2, targetPath: "photos" } */ export function matchInterceptPrefix(segment: string): InterceptMatch | undefined { // Match one or more intercept prefixes followed by the target path const match = segment.match(/^((?:\(\.{1,3}\))+)(.+)$/) if (!match) return undefined const [, prefixes, targetPath] = match // (...) means from root (Infinity levels) if (prefixes.includes('(...)')) { return { levels: Infinity, targetPath, originalSegment: segment } } // Count (..) for levels up, (.) means same level (0) const doubleDotMatches = prefixes.match(/\(\.{2}\)/g) || [] const levels = doubleDotMatches.length return { levels, targetPath, originalSegment: segment } } /** * Strip intercept prefixes from a path segment * "(.)photos" -> "photos" * "(..)settings" -> "settings" */ export function stripInterceptPrefix(segment: string): string { const match = matchInterceptPrefix(segment) return match ? match.targetPath : segment } /** * Check if a segment has an intercept prefix */ export function hasInterceptPrefix(segment: string): boolean { return /^\(\.{1,3}\)/.test(segment) } /** * Strip slot prefix from path for URL generation * Removes @slot segments from path */ export function stripSlotSegmentsFromPath(path: string): string { return path .split('/') .filter((segment) => !isSlotDirectory(segment)) .join('/') }