@wippy-fe/router 0.0.64 → 0.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -256,7 +256,7 @@ declare const RouterLink: vue.DefineComponent<vue.ExtractPropTypes<{
|
|
|
256
256
|
* /<lit1>/<lit2>/:part(.*)* nested prefix mount
|
|
257
257
|
*
|
|
258
258
|
* Literal segments must match `[a-z0-9-]+` (lowercase alphanumerics + hyphens).
|
|
259
|
-
* The param name `:part` (not `:pathMatch`) matches
|
|
259
|
+
* The param name `:part` (not `:pathMatch`) matches the Wippy Web Host's existing
|
|
260
260
|
* `/c/:id/:part*` convention, so `route.params.part` is a uniform shape
|
|
261
261
|
* regardless of whether a page was reached via a mount route or via `/c/<id>`.
|
|
262
262
|
*/
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/classify.ts","../src/create-app-router.ts","../src/host-router-link.ts","../src/router-link.ts","../src/validate-mount-routes.ts"],"sourcesContent":["/**\n * Re-export of the link classifier from @wippy-fe/proxy for one-import\n * convenience in Vue subapps. Canonical home for the classifier is\n * @wippy-fe/proxy — vanilla JS subapps should import directly from there.\n */\nexport { classifyLink, setLocalRouter } from '@wippy-fe/proxy'\nexport type {\n ClassifierContext,\n LinkClassification,\n LinkKind,\n} from '@wippy-fe/proxy'\n","import type { Router, RouteRecordRaw } from 'vue-router'\nimport { host as defaultHost, on as defaultOn, setLocalRouter } from '@wippy-fe/proxy'\nimport { createMemoryHistory, createRouter } from 'vue-router'\n\nexport interface CreateAppRouterOptions {\n /**\n * Initial route path to prime history with. Usually `config.context?.route`\n * or the subapp's current location from its mount context.\n */\n initialPath?: string\n /**\n * Override the host API (testing / dev harness). Defaults to the `host`\n * export from `@wippy-fe/proxy`.\n */\n host?: typeof defaultHost\n /**\n * Override the event subscription API. Set to `null` to disable the\n * `@history` subscription entirely. Defaults to `on` from `@wippy-fe/proxy`.\n */\n on?: typeof defaultOn | null\n}\n\n/**\n * Canonical memory-router factory for Wippy subapps.\n *\n * Every subapp currently duplicates the same boilerplate:\n * - create a memory history\n * - prime it with an initial path from context\n * - call `host.onRouteChanged` in `afterEach`\n * - listen for `@history` events from the host and mirror them to the router\n *\n * This factory encapsulates that boilerplate so subapps become one-liners:\n *\n * ```ts\n * import { createAppRouter } from '@wippy-fe/router'\n * import { routes } from './routes'\n *\n * const router = createAppRouter(routes, {\n * initialPath: appConfig.context?.route,\n * })\n * app.use(router)\n * ```\n *\n * Memory history is hardcoded — srcdoc iframes do not support `createWebHistory`\n * (confirmed dead end — `window.location` is non-configurable in srcdoc, and\n * framework routers read `location.pathname` directly). Only memory-based\n * routing works inside child iframes.\n */\nexport function createAppRouter(\n routes: RouteRecordRaw[],\n options: CreateAppRouterOptions = {},\n): Router {\n const host = options.host ?? defaultHost\n const on = options.on === undefined ? defaultOn : options.on\n\n const history = createMemoryHistory()\n if (options.initialPath)\n history.replace(options.initialPath)\n\n const router = createRouter({ history, routes })\n\n // Register this router with the link classifier so it can check own\n // routes before falling back to mount route / system route matching.\n setLocalRouter((path: string) => router.resolve(path))\n\n // Track the last navId applied from a parent-initiated update.\n // When the router navigates in response to a parent @history event,\n // suppress the resulting afterEach from sending CmdRouteChanged back up\n // (which would create an echo loop).\n let lastAppliedNavId: number | undefined\n\n router.afterEach((to) => {\n // If this navigation was triggered by a parent @history update,\n // pass the navId so the host can identify it as an echo and suppress.\n const navId = lastAppliedNavId\n lastAppliedNavId = undefined\n host.onRouteChanged(to.fullPath, navId)\n })\n\n if (on) {\n on('@history', ({ path, navId }: { path?: string, navId?: number }) => {\n if (!path)\n return\n if (navId !== undefined)\n lastAppliedNavId = navId\n const normalized = path.startsWith('/') ? path : `/${path}`\n if (router.currentRoute.value.fullPath !== normalized)\n router.push(normalized)\n })\n }\n\n return router\n}\n","import type { PropType } from 'vue'\nimport { host } from '@wippy-fe/proxy'\nimport { defineComponent, h } from 'vue'\n\n/**\n * Explicit host-level navigation link. Use when you want to navigate\n * to a known host page regardless of the current nesting level.\n *\n * Unlike `AutoRouterLink` (which classifies links automatically),\n * `HostRouterLink` ALWAYS sends navigation to the host via postMessage.\n *\n * Use cases:\n * - Inside a nav-owner child linking to a different host page\n * - Anywhere you explicitly want host-level navigation\n *\n * Renders a real `<a>` for accessibility / right-click / SEO.\n * Left-click calls `host.navigate()`. Modifier keys (ctrl, meta, shift)\n * fall through to the browser default (open in new tab).\n */\nexport const HostRouterLink = defineComponent({\n name: 'WippyHostRouterLink',\n props: {\n to: {\n type: String as PropType<string>,\n required: true,\n },\n },\n setup(props, { slots }) {\n return () => h(\n 'a',\n {\n href: props.to,\n onClick: (e: MouseEvent) => {\n if (e.defaultPrevented || e.button !== 0)\n return\n if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)\n return\n e.preventDefault()\n host.navigate(props.to)\n },\n },\n slots.default?.(),\n )\n },\n})\n","import type { PropType } from 'vue'\nimport type { RouteLocationRaw } from 'vue-router'\nimport { host } from '@wippy-fe/proxy'\nimport { defineComponent, h } from 'vue'\nimport { useRouter, RouterLink as VueRouterLink } from 'vue-router'\n\n/**\n * Drop-in replacement for Vue Router's `<RouterLink>` that classifies the\n * target href and dispatches intelligently:\n *\n * - `host-nav` → preventDefault + `host.navigate(href)` via postMessage\n * - `child-nav` → delegate to the real `<RouterLink>` (normal in-app nav)\n * - `external` → render an `<a>` with `target=\"_blank\" rel=\"noopener\"`\n * - `ignore` → render an `<a>` with a click-preventing handler\n *\n * Subapps migrate by changing a single import:\n *\n * ```ts\n * // before\n * import { RouterLink } from 'vue-router'\n *\n * // after\n * import { RouterLink } from '@wippy-fe/router'\n * ```\n *\n * Props are a compatible subset of vue-router's RouterLink. Slot API\n * mirrors the default (non-custom) slot of the real RouterLink.\n */\nexport const AutoRouterLink = defineComponent({\n name: 'WippyAutoRouterLink',\n props: {\n to: {\n type: [String, Object] as PropType<RouteLocationRaw>,\n required: true,\n },\n replace: {\n type: Boolean,\n default: false,\n },\n activeClass: {\n type: String,\n default: undefined,\n },\n exactActiveClass: {\n type: String,\n default: undefined,\n },\n ariaCurrentValue: {\n type: String as PropType<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>,\n default: 'page',\n },\n /**\n * For `external` kind: sets the anchor's target attribute.\n * Defaults to `_blank` with safe `rel`. Pass empty string for same-tab.\n */\n externalTarget: {\n type: String,\n default: '_blank',\n },\n },\n setup(props, { slots }) {\n const router = useRouter()\n\n return () => {\n const resolved = router.resolve(props.to)\n const classification = host.classifyLink(resolved.href)\n\n if (classification.kind === 'host-nav') {\n return h(\n 'a',\n {\n 'href': resolved.href,\n 'onClick': (event: MouseEvent) => {\n if (event.defaultPrevented)\n return\n if (event.button !== 0)\n return\n if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)\n return\n event.preventDefault()\n host.navigate(classification.normalizedPath ?? classification.href)\n },\n 'aria-current': props.ariaCurrentValue,\n },\n slots.default?.(),\n )\n }\n\n if (classification.kind === 'external') {\n const isBlank = props.externalTarget === '_blank'\n return h(\n 'a',\n {\n href: resolved.href,\n target: props.externalTarget || undefined,\n rel: isBlank ? 'noopener noreferrer' : undefined,\n },\n slots.default?.(),\n )\n }\n\n if (classification.kind === 'ignore') {\n return h(\n 'a',\n {\n href: resolved.href || '#',\n onClick: (event: MouseEvent) => event.preventDefault(),\n },\n slots.default?.(),\n )\n }\n\n // child-nav → delegate to real vue-router RouterLink\n return h(\n VueRouterLink,\n {\n to: props.to,\n replace: props.replace,\n activeClass: props.activeClass,\n exactActiveClass: props.exactActiveClass,\n ariaCurrentValue: props.ariaCurrentValue,\n },\n slots.default\n ? { default: (scope: unknown) => slots.default?.(scope) }\n : undefined,\n )\n }\n },\n})\n\n/** @deprecated Use AutoRouterLink instead */\nexport const RouterLink = AutoRouterLink\n","/**\n * Client-side validator for mountRoute patterns.\n *\n * This mirrors the backend (wippy-framework/src/views/page_registry.lua)\n * validator logic. The backend is the authoritative source — the frontend\n * re-validates as a defensive safety net for two scenarios:\n *\n * 1. A subapp receives `mountRoutes` from `ChildAppConfig` and wants to\n * sanity-check them before registering or relying on them (e.g. during\n * nested-navigation mount, if a subapp acts as a mini-host).\n * 2. The host's `syncMountRoutes` calls this to detect conflicts before\n * mutating the live Vue Router (fail-fast with a clear error rather\n * than leaving the router in a hybrid state).\n *\n * The validator is intentionally strict. v1 accepts ONLY the canonical form:\n * /:part(.*)* root mount\n * /<literal>/:part(.*)* prefix mount (one or more literal segments)\n * /<lit1>/<lit2>/:part(.*)* nested prefix mount\n *\n * Literal segments must match `[a-z0-9-]+` (lowercase alphanumerics + hyphens).\n * The param name `:part` (not `:pathMatch`) matches gen-2-chat's existing\n * `/c/:id/:part*` convention, so `route.params.part` is a uniform shape\n * regardless of whether a page was reached via a mount route or via `/c/<id>`.\n */\n\nexport const MOUNT_ROUTE_V1_REGEX = /^\\/([a-z0-9-]+(\\/[a-z0-9-]+)*\\/)?:part\\(\\.\\*\\)\\*$/\n\nexport type MountRouteIssueKind = 'syntax' | 'duplicate'\n\nexport interface MountRouteIssue {\n kind: MountRouteIssueKind\n mountRoute: string\n pageId: string\n conflictingPageId?: string\n}\n\nexport class MountRouteValidationError extends Error {\n constructor(public readonly issues: MountRouteIssue[]) {\n super(formatMessage(issues))\n this.name = 'MountRouteValidationError'\n }\n}\n\nfunction formatMessage(issues: MountRouteIssue[]): string {\n const lines: string[] = ['[wippy-fe/router] mountRoute validation failed:', '']\n for (const issue of issues) {\n if (issue.kind === 'syntax') {\n lines.push(\n ` • Page \"${issue.pageId}\" has invalid mountRoute \"${issue.mountRoute}\".`,\n ` v1 only supports \"/<literal>/:part(.*)*\" and \"/:part(.*)*\".`,\n )\n }\n else {\n lines.push(\n ` • Pages \"${issue.pageId}\" and \"${issue.conflictingPageId}\" both claim`,\n ` mountRoute \"${issue.mountRoute}\". Mount routes must be unique.`,\n )\n }\n lines.push('')\n }\n return lines.join('\\n')\n}\n\n/**\n * Validate an array of [mountRoute, pageId] tuples.\n * Returns every issue found (does not stop at first). Empty result = clean.\n *\n * Accepts tuples (not a plain object) so duplicate mount routes surface as\n * separate entries rather than being silently collapsed by object-key dedup.\n */\nexport function validateMountRoutes(\n entries: Array<[mountRoute: string, pageId: string]>,\n): MountRouteIssue[] {\n const issues: MountRouteIssue[] = []\n const seen = new Map<string, string>()\n\n for (const [mountRoute, pageId] of entries) {\n if (typeof mountRoute !== 'string' || !MOUNT_ROUTE_V1_REGEX.test(mountRoute)) {\n issues.push({ kind: 'syntax', mountRoute: String(mountRoute), pageId })\n continue\n }\n const previousOwner = seen.get(mountRoute)\n if (previousOwner && previousOwner !== pageId) {\n issues.push({\n kind: 'duplicate',\n mountRoute,\n pageId,\n conflictingPageId: previousOwner,\n })\n continue\n }\n seen.set(mountRoute, pageId)\n }\n\n return issues\n}\n\n/**\n * Assert that a map of mountRoutes is valid. Throws MountRouteValidationError\n * with a detailed message on any issue. Convenience for subapps that want to\n * fail-fast rather than iterate the issues themselves.\n */\nexport function assertValidMountRoutes(\n mountRoutes: Record<string, string>,\n): void {\n const entries: Array<[string, string]> = Object.entries(mountRoutes)\n const issues = validateMountRoutes(entries)\n if (issues.length > 0)\n throw new MountRouteValidationError(issues)\n}\n"],"mappings":"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","names":["setLocalRouter","host","host","defineComponent","h"]}
|
|
1
|
+
{"version":3,"sources":["../src/classify.ts","../src/create-app-router.ts","../src/host-router-link.ts","../src/router-link.ts","../src/validate-mount-routes.ts"],"sourcesContent":["/**\n * Re-export of the link classifier from @wippy-fe/proxy for one-import\n * convenience in Vue subapps. Canonical home for the classifier is\n * @wippy-fe/proxy — vanilla JS subapps should import directly from there.\n */\nexport { classifyLink, setLocalRouter } from '@wippy-fe/proxy'\nexport type {\n ClassifierContext,\n LinkClassification,\n LinkKind,\n} from '@wippy-fe/proxy'\n","import type { Router, RouteRecordRaw } from 'vue-router'\nimport { host as defaultHost, on as defaultOn, setLocalRouter } from '@wippy-fe/proxy'\nimport { createMemoryHistory, createRouter } from 'vue-router'\n\nexport interface CreateAppRouterOptions {\n /**\n * Initial route path to prime history with. Usually `config.context?.route`\n * or the subapp's current location from its mount context.\n */\n initialPath?: string\n /**\n * Override the host API (testing / dev harness). Defaults to the `host`\n * export from `@wippy-fe/proxy`.\n */\n host?: typeof defaultHost\n /**\n * Override the event subscription API. Set to `null` to disable the\n * `@history` subscription entirely. Defaults to `on` from `@wippy-fe/proxy`.\n */\n on?: typeof defaultOn | null\n}\n\n/**\n * Canonical memory-router factory for Wippy subapps.\n *\n * Every subapp currently duplicates the same boilerplate:\n * - create a memory history\n * - prime it with an initial path from context\n * - call `host.onRouteChanged` in `afterEach`\n * - listen for `@history` events from the host and mirror them to the router\n *\n * This factory encapsulates that boilerplate so subapps become one-liners:\n *\n * ```ts\n * import { createAppRouter } from '@wippy-fe/router'\n * import { routes } from './routes'\n *\n * const router = createAppRouter(routes, {\n * initialPath: appConfig.context?.route,\n * })\n * app.use(router)\n * ```\n *\n * Memory history is hardcoded — srcdoc iframes do not support `createWebHistory`\n * (confirmed dead end — `window.location` is non-configurable in srcdoc, and\n * framework routers read `location.pathname` directly). Only memory-based\n * routing works inside child iframes.\n */\nexport function createAppRouter(\n routes: RouteRecordRaw[],\n options: CreateAppRouterOptions = {},\n): Router {\n const host = options.host ?? defaultHost\n const on = options.on === undefined ? defaultOn : options.on\n\n const history = createMemoryHistory()\n if (options.initialPath)\n history.replace(options.initialPath)\n\n const router = createRouter({ history, routes })\n\n // Register this router with the link classifier so it can check own\n // routes before falling back to mount route / system route matching.\n setLocalRouter((path: string) => router.resolve(path))\n\n // Track the last navId applied from a parent-initiated update.\n // When the router navigates in response to a parent @history event,\n // suppress the resulting afterEach from sending CmdRouteChanged back up\n // (which would create an echo loop).\n let lastAppliedNavId: number | undefined\n\n router.afterEach((to) => {\n // If this navigation was triggered by a parent @history update,\n // pass the navId so the host can identify it as an echo and suppress.\n const navId = lastAppliedNavId\n lastAppliedNavId = undefined\n host.onRouteChanged(to.fullPath, navId)\n })\n\n if (on) {\n on('@history', ({ path, navId }: { path?: string, navId?: number }) => {\n if (!path)\n return\n if (navId !== undefined)\n lastAppliedNavId = navId\n const normalized = path.startsWith('/') ? path : `/${path}`\n if (router.currentRoute.value.fullPath !== normalized)\n router.push(normalized)\n })\n }\n\n return router\n}\n","import type { PropType } from 'vue'\nimport { host } from '@wippy-fe/proxy'\nimport { defineComponent, h } from 'vue'\n\n/**\n * Explicit host-level navigation link. Use when you want to navigate\n * to a known host page regardless of the current nesting level.\n *\n * Unlike `AutoRouterLink` (which classifies links automatically),\n * `HostRouterLink` ALWAYS sends navigation to the host via postMessage.\n *\n * Use cases:\n * - Inside a nav-owner child linking to a different host page\n * - Anywhere you explicitly want host-level navigation\n *\n * Renders a real `<a>` for accessibility / right-click / SEO.\n * Left-click calls `host.navigate()`. Modifier keys (ctrl, meta, shift)\n * fall through to the browser default (open in new tab).\n */\nexport const HostRouterLink = defineComponent({\n name: 'WippyHostRouterLink',\n props: {\n to: {\n type: String as PropType<string>,\n required: true,\n },\n },\n setup(props, { slots }) {\n return () => h(\n 'a',\n {\n href: props.to,\n onClick: (e: MouseEvent) => {\n if (e.defaultPrevented || e.button !== 0)\n return\n if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)\n return\n e.preventDefault()\n host.navigate(props.to)\n },\n },\n slots.default?.(),\n )\n },\n})\n","import type { PropType } from 'vue'\nimport type { RouteLocationRaw } from 'vue-router'\nimport { host } from '@wippy-fe/proxy'\nimport { defineComponent, h } from 'vue'\nimport { useRouter, RouterLink as VueRouterLink } from 'vue-router'\n\n/**\n * Drop-in replacement for Vue Router's `<RouterLink>` that classifies the\n * target href and dispatches intelligently:\n *\n * - `host-nav` → preventDefault + `host.navigate(href)` via postMessage\n * - `child-nav` → delegate to the real `<RouterLink>` (normal in-app nav)\n * - `external` → render an `<a>` with `target=\"_blank\" rel=\"noopener\"`\n * - `ignore` → render an `<a>` with a click-preventing handler\n *\n * Subapps migrate by changing a single import:\n *\n * ```ts\n * // before\n * import { RouterLink } from 'vue-router'\n *\n * // after\n * import { RouterLink } from '@wippy-fe/router'\n * ```\n *\n * Props are a compatible subset of vue-router's RouterLink. Slot API\n * mirrors the default (non-custom) slot of the real RouterLink.\n */\nexport const AutoRouterLink = defineComponent({\n name: 'WippyAutoRouterLink',\n props: {\n to: {\n type: [String, Object] as PropType<RouteLocationRaw>,\n required: true,\n },\n replace: {\n type: Boolean,\n default: false,\n },\n activeClass: {\n type: String,\n default: undefined,\n },\n exactActiveClass: {\n type: String,\n default: undefined,\n },\n ariaCurrentValue: {\n type: String as PropType<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>,\n default: 'page',\n },\n /**\n * For `external` kind: sets the anchor's target attribute.\n * Defaults to `_blank` with safe `rel`. Pass empty string for same-tab.\n */\n externalTarget: {\n type: String,\n default: '_blank',\n },\n },\n setup(props, { slots }) {\n const router = useRouter()\n\n return () => {\n const resolved = router.resolve(props.to)\n const classification = host.classifyLink(resolved.href)\n\n if (classification.kind === 'host-nav') {\n return h(\n 'a',\n {\n 'href': resolved.href,\n 'onClick': (event: MouseEvent) => {\n if (event.defaultPrevented)\n return\n if (event.button !== 0)\n return\n if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)\n return\n event.preventDefault()\n host.navigate(classification.normalizedPath ?? classification.href)\n },\n 'aria-current': props.ariaCurrentValue,\n },\n slots.default?.(),\n )\n }\n\n if (classification.kind === 'external') {\n const isBlank = props.externalTarget === '_blank'\n return h(\n 'a',\n {\n href: resolved.href,\n target: props.externalTarget || undefined,\n rel: isBlank ? 'noopener noreferrer' : undefined,\n },\n slots.default?.(),\n )\n }\n\n if (classification.kind === 'ignore') {\n return h(\n 'a',\n {\n href: resolved.href || '#',\n onClick: (event: MouseEvent) => event.preventDefault(),\n },\n slots.default?.(),\n )\n }\n\n // child-nav → delegate to real vue-router RouterLink\n return h(\n VueRouterLink,\n {\n to: props.to,\n replace: props.replace,\n activeClass: props.activeClass,\n exactActiveClass: props.exactActiveClass,\n ariaCurrentValue: props.ariaCurrentValue,\n },\n slots.default\n ? { default: (scope: unknown) => slots.default?.(scope) }\n : undefined,\n )\n }\n },\n})\n\n/** @deprecated Use AutoRouterLink instead */\nexport const RouterLink = AutoRouterLink\n","/**\n * Client-side validator for mountRoute patterns.\n *\n * This mirrors the backend (wippy-framework/src/views/page_registry.lua)\n * validator logic. The backend is the authoritative source — the frontend\n * re-validates as a defensive safety net for two scenarios:\n *\n * 1. A subapp receives `mountRoutes` from `ChildAppConfig` and wants to\n * sanity-check them before registering or relying on them (e.g. during\n * nested-navigation mount, if a subapp acts as a mini-host).\n * 2. The host's `syncMountRoutes` calls this to detect conflicts before\n * mutating the live Vue Router (fail-fast with a clear error rather\n * than leaving the router in a hybrid state).\n *\n * The validator is intentionally strict. v1 accepts ONLY the canonical form:\n * /:part(.*)* root mount\n * /<literal>/:part(.*)* prefix mount (one or more literal segments)\n * /<lit1>/<lit2>/:part(.*)* nested prefix mount\n *\n * Literal segments must match `[a-z0-9-]+` (lowercase alphanumerics + hyphens).\n * The param name `:part` (not `:pathMatch`) matches the Wippy Web Host's existing\n * `/c/:id/:part*` convention, so `route.params.part` is a uniform shape\n * regardless of whether a page was reached via a mount route or via `/c/<id>`.\n */\n\nexport const MOUNT_ROUTE_V1_REGEX = /^\\/([a-z0-9-]+(\\/[a-z0-9-]+)*\\/)?:part\\(\\.\\*\\)\\*$/\n\nexport type MountRouteIssueKind = 'syntax' | 'duplicate'\n\nexport interface MountRouteIssue {\n kind: MountRouteIssueKind\n mountRoute: string\n pageId: string\n conflictingPageId?: string\n}\n\nexport class MountRouteValidationError extends Error {\n constructor(public readonly issues: MountRouteIssue[]) {\n super(formatMessage(issues))\n this.name = 'MountRouteValidationError'\n }\n}\n\nfunction formatMessage(issues: MountRouteIssue[]): string {\n const lines: string[] = ['[wippy-fe/router] mountRoute validation failed:', '']\n for (const issue of issues) {\n if (issue.kind === 'syntax') {\n lines.push(\n ` • Page \"${issue.pageId}\" has invalid mountRoute \"${issue.mountRoute}\".`,\n ` v1 only supports \"/<literal>/:part(.*)*\" and \"/:part(.*)*\".`,\n )\n }\n else {\n lines.push(\n ` • Pages \"${issue.pageId}\" and \"${issue.conflictingPageId}\" both claim`,\n ` mountRoute \"${issue.mountRoute}\". Mount routes must be unique.`,\n )\n }\n lines.push('')\n }\n return lines.join('\\n')\n}\n\n/**\n * Validate an array of [mountRoute, pageId] tuples.\n * Returns every issue found (does not stop at first). Empty result = clean.\n *\n * Accepts tuples (not a plain object) so duplicate mount routes surface as\n * separate entries rather than being silently collapsed by object-key dedup.\n */\nexport function validateMountRoutes(\n entries: Array<[mountRoute: string, pageId: string]>,\n): MountRouteIssue[] {\n const issues: MountRouteIssue[] = []\n const seen = new Map<string, string>()\n\n for (const [mountRoute, pageId] of entries) {\n if (typeof mountRoute !== 'string' || !MOUNT_ROUTE_V1_REGEX.test(mountRoute)) {\n issues.push({ kind: 'syntax', mountRoute: String(mountRoute), pageId })\n continue\n }\n const previousOwner = seen.get(mountRoute)\n if (previousOwner && previousOwner !== pageId) {\n issues.push({\n kind: 'duplicate',\n mountRoute,\n pageId,\n conflictingPageId: previousOwner,\n })\n continue\n }\n seen.set(mountRoute, pageId)\n }\n\n return issues\n}\n\n/**\n * Assert that a map of mountRoutes is valid. Throws MountRouteValidationError\n * with a detailed message on any issue. Convenience for subapps that want to\n * fail-fast rather than iterate the issues themselves.\n */\nexport function assertValidMountRoutes(\n mountRoutes: Record<string, string>,\n): void {\n const entries: Array<[string, string]> = Object.entries(mountRoutes)\n const issues = validateMountRoutes(entries)\n if (issues.length > 0)\n throw new MountRouteValidationError(issues)\n}\n"],"mappings":"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","names":["setLocalRouter","host","host","defineComponent","h"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wippy-fe/router",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"description": "Drop-in Vue Router helpers for Wippy subapps - RouterLink with host-nav detection, canonical memory-router factory, and link classifier re-export.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"vue": "^3.5.0",
|
|
24
24
|
"vue-router": "^4.5.0 || ^5.0.0",
|
|
25
|
-
"@wippy-fe/proxy": "^0.0.
|
|
25
|
+
"@wippy-fe/proxy": "^0.0.65"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsup",
|