@vaadin/hilla-file-router 24.8.0-alpha5 → 24.8.0-alpha7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/hilla-file-router",
3
- "version": "24.8.0-alpha5",
3
+ "version": "24.8.0-alpha7",
4
4
  "description": "Hilla file-based router",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -61,9 +61,11 @@
61
61
  "react-router": "7"
62
62
  },
63
63
  "dependencies": {
64
- "@vaadin/hilla-generator-utils": "24.8.0-alpha5",
65
- "@vaadin/hilla-react-auth": "24.8.0-alpha5",
66
- "@vaadin/hilla-react-signals": "24.8.0-alpha5",
64
+ "@ungap/with-resolvers": "0.1.0",
65
+ "@vaadin/hilla-generator-utils": "24.8.0-alpha7",
66
+ "@vaadin/hilla-react-auth": "24.8.0-alpha7",
67
+ "@vaadin/hilla-react-signals": "24.8.0-alpha7",
68
+ "tsc-template": "0.2.3",
67
69
  "typescript": "5.8.3"
68
70
  }
69
71
  }
@@ -46,15 +46,15 @@ export class RouterConfigurationBuilder {
46
46
  withFileRoutes(routes) {
47
47
  return this.update(routes, ({ original, overriding: added, children }) => {
48
48
  if (added) {
49
- const { module, path, flowLayout } = added;
49
+ const { module, component = module?.default, config = module?.config, path, flowLayout } = added;
50
50
  if (module && !isReactRouteModule(module)) {
51
51
  throw new Error(`The module for the "${path}" section doesn't have the React component exported by default or a ViewConfig object exported as "config"`);
52
52
  }
53
- const element = module?.default ? createElement(module.default) : undefined;
53
+ const element = component ? createElement(component) : undefined;
54
54
  const handle = {
55
- ...module?.config,
56
- title: module?.config?.title ?? convertComponentNameToTitle(module?.default),
57
- flowLayout: module?.config?.flowLayout ?? flowLayout
55
+ ...config,
56
+ title: config?.title ?? convertComponentNameToTitle(component),
57
+ flowLayout: config?.flowLayout ?? flowLayout
58
58
  };
59
59
  if (path === "" && !children) {
60
60
  return {
@@ -66,7 +66,7 @@ export class RouterConfigurationBuilder {
66
66
  }
67
67
  return {
68
68
  ...original,
69
- path: module?.config?.route ?? path,
69
+ path: config?.route ?? path,
70
70
  element,
71
71
  children,
72
72
  handle
@@ -1 +1 @@
1
- {"mappings":"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","names":["module: Module","route: T","route: RouteObject","flag: T","#modifiers","routes: RouteList","routes: readonly AgnosticRoute[]","component: ComponentType","config?: ViewConfig","fallbackRoutes: RouteList","layoutComponent: ComponentType","redirectPath?: string","routes: readonly T[] | undefined","callback: RouteTransformer<T | undefined>","final: Array<RouteObject | undefined>","options?: RouterBuildOptions","#withLayoutSkipping"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/runtime/RouterConfigurationBuilder.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/consistent-type-assertions */\nimport { protectRoute } from '@vaadin/hilla-react-auth';\nimport { type ComponentType, createElement } from 'react';\nimport { createBrowserRouter, type IndexRouteObject, type NonIndexRouteObject, type RouteObject } from 'react-router';\nimport { convertComponentNameToTitle } from '../shared/convertComponentNameToTitle.js';\nimport { transformTree } from '../shared/transformTree.js';\nimport type {\n AgnosticRoute,\n Module,\n RouteModule,\n RouterBuildOptions,\n RouterConfiguration,\n ViewConfig,\n} from '../types.js';\n\ninterface RouteBase {\n path?: string;\n children?: readonly this[];\n}\n\nfunction isReactRouteModule(module: Module): module is RouteModule<ComponentType> {\n return (\n ('default' in module && typeof module.default === 'function') ||\n ('config' in module && typeof module.config === 'object')\n );\n}\n\nexport type RouteList = readonly RouteObject[];\nexport type WritableRouteList = RouteObject[];\n\nexport type RouteTransformerOptions<T> = Readonly<{\n children?: RouteList;\n original?: RouteObject;\n overriding?: T;\n dupe: boolean;\n}>;\n\nexport type RouteTransformer<T> = (opts: RouteTransformerOptions<T>) => RouteObject | undefined;\n\ntype RoutesModifier = (routes: RouteList | undefined) => RouteList | undefined;\n\nfunction createRouteKey<T extends RouteBase>(route: T): string {\n return `${route.path ?? ''}-${route.children ? 'n' : 'i'}`;\n}\n\nenum RouteHandleFlags {\n FLOW_LAYOUT = 'flowLayout',\n IGNORE_FALLBACK = 'ignoreFallback',\n SKIP_LAYOUTS = 'skipLayouts',\n}\n\nfunction getRouteHandleFlag<T extends RouteHandleFlags>(route: RouteObject, flag: T): boolean | undefined {\n if (typeof route.handle === 'object' && flag in route.handle) {\n return (route.handle as Record<T, boolean>)[flag];\n }\n\n return undefined;\n}\n\n/**\n * A builder for creating a Vaadin-specific router for React with\n * authentication and server routes support.\n */\nexport class RouterConfigurationBuilder {\n readonly #modifiers: RoutesModifier[] = [];\n\n /**\n * Adds the given routes to the current list of routes. All the routes are\n * deeply merged to preserve the path uniqueness.\n *\n * @param routes - A list of routes to add to the current list.\n */\n withReactRoutes(routes: RouteList): this {\n return this.update(routes);\n }\n\n /**\n * Adds the given file routes to the current list of routes. All the routes\n * are transformed to React RouterObjects and deeply merged to preserve the\n * path uniqueness.\n *\n * @param routes - A list of routes to add to the current list.\n */\n withFileRoutes(routes: readonly AgnosticRoute[]): this {\n return this.update(routes, ({ original, overriding: added, children }) => {\n if (added) {\n const { module, path, flowLayout } = added;\n if (module && !isReactRouteModule(module)) {\n throw new Error(\n `The module for the \"${path}\" section doesn't have the React component exported by default or a ViewConfig object exported as \"config\"`,\n );\n }\n\n const element = module?.default ? createElement(module.default) : undefined;\n const handle = {\n ...module?.config,\n title: module?.config?.title ?? convertComponentNameToTitle(module?.default),\n flowLayout: module?.config?.flowLayout ?? flowLayout,\n };\n\n if (path === '' && !children) {\n return {\n ...original,\n element,\n handle,\n index: true,\n } as IndexRouteObject;\n }\n\n return {\n ...original,\n path: module?.config?.route ?? path,\n element,\n children,\n handle,\n } as NonIndexRouteObject;\n }\n\n return original;\n });\n }\n\n /**\n * Adds the given server route element to each branch of the current list of\n * routes.\n *\n * @param component - The React component to add to each branch of the\n * current list of routes.\n * @param config - An optional configuration that will be applied to\n * each fallback component.\n */\n withFallback(component: ComponentType, config?: ViewConfig): this {\n this.withLayout(component);\n\n // Fallback adds two routes, so that the index (empty path) has a fallback too\n const fallbackRoutes: RouteList = [\n { path: '*', element: createElement(component), handle: config },\n { index: true, element: createElement(component), handle: config },\n ];\n\n this.update(fallbackRoutes, ({ original, overriding: added, children, dupe }) => {\n if (original && !getRouteHandleFlag(original, RouteHandleFlags.IGNORE_FALLBACK) && !dupe) {\n if (!children) {\n return original;\n }\n\n const _fallback = [...fallbackRoutes];\n\n if (children.some(({ path }) => path === '*')) {\n _fallback.shift();\n }\n\n if (children.some(({ index: i, path }) => i ?? path?.includes('?'))) {\n _fallback.pop();\n }\n\n return {\n ...original,\n children: [...children, ..._fallback],\n } as RouteObject;\n }\n\n return added!;\n });\n\n return this;\n }\n\n /**\n * Adds the layoutComponent as the parent layout to views with the flowLayouts ViewConfiguration set.\n *\n * @param layoutComponent - layout component to use, usually Flow\n */\n withLayout(layoutComponent: ComponentType): this {\n this.#modifiers.push((originalRoutes) => {\n if (!originalRoutes) {\n return originalRoutes;\n }\n\n type Accumulator<T extends RouteList> = Readonly<{\n server: T;\n client: T;\n ambivalent: T;\n }>;\n\n const result = transformTree<RouteList, Accumulator<RouteList>>(originalRoutes, null, (routes, next) =>\n // Split a single routes list onto three separate lists:\n // - A list of server routes\n // - A list of client routes\n // - A list of routes which will be moved to either server or client\n // list. It depends on the parent route.\n routes.reduce<Accumulator<WritableRouteList>>(\n (lists, route) => {\n const { server, client, ambivalent } = next(route.children ?? []);\n\n const flag = getRouteHandleFlag(route, RouteHandleFlags.FLOW_LAYOUT);\n\n // If the route has `flowLayout` flag explicitly enabled, it goes to\n // the server list. The children are also affected by the flag\n // unless they have it explicitly disabled.\n if (flag === true) {\n lists.server.push({\n ...route,\n children: route.children ? [...server, ...ambivalent] : undefined,\n } as RouteObject);\n } else if (server.length > 0) {\n // Even if the route doesn't have the flag, it goes to the server\n // list if any of the children has the flag enabled.\n lists.server.push({\n ...route,\n children: route.children ? server : undefined,\n } as RouteObject);\n }\n\n // If the route has `flowLayout` flag explicitly disabled, it goes\n // to the client list. The route children are not affected by the\n // flag.\n if (flag === false || client.length > 0) {\n lists.client.push({\n ...route,\n children: route.children ? client : undefined,\n } as RouteObject);\n }\n\n // The route without the flag go to the `default` list. Then it will\n // be moved to either server or client list based on the parent\n // route.\n if (\n flag === undefined &&\n (lists.server.every(({ path }) => path !== route.path) || ambivalent.length > 0)\n ) {\n lists.ambivalent.push({\n ...route,\n children: route.children ? ambivalent : undefined,\n } as RouteObject);\n }\n\n return lists;\n },\n { server: [], client: [], ambivalent: [] },\n ),\n );\n\n return [\n ...(result.server.length\n ? [\n // The server routes are wrapped with the route that has a layout\n // element. It also has the `IGNORE_FALLBACK` flag to remove the\n // fallback route from reach.\n {\n element: createElement(layoutComponent),\n children: result.server as RouteObject[],\n handle: {\n [RouteHandleFlags.IGNORE_FALLBACK]: true,\n },\n },\n ]\n : []),\n // The client routes are preserved without wrapping.\n ...result.client,\n // The ambivalent routes are considered as client routes.\n ...result.ambivalent,\n ];\n });\n\n return this;\n }\n\n /**\n * Protects all the routes that require authentication. For more details see\n * {@link @vaadin/hilla-react-auth#protectRoutes} function.\n *\n * @param redirectPath - the path to redirect to if the route is protected\n * and the user is not authenticated.\n */\n protect(redirectPath?: string): this {\n this.update(undefined, ({ original: route, children }) => {\n const finalRoute = protectRoute(route!, redirectPath);\n finalRoute.children = children as RouteObject[] | undefined;\n return finalRoute;\n });\n\n return this;\n }\n\n /**\n * Deeply updates the current list of routes with the given routes merging\n * them in process.\n *\n * @param routes - A list of routes to merge with the current list.\n * @param callback - A callback to transform the routes during the merge.\n */\n update(routes: undefined, callback: RouteTransformer<undefined>): this;\n update<T extends RouteBase>(routes: readonly T[], callback?: RouteTransformer<T>): this;\n update<T extends RouteBase>(\n routes: readonly T[] | undefined,\n callback: RouteTransformer<T | undefined> = ({ original, overriding, children }) =>\n ({\n ...original,\n ...overriding,\n children,\n }) as RouteObject,\n ): this {\n this.#modifiers.push((existingRoutes) =>\n // Going through the existing and added list of routes.\n transformTree<readonly [RouteList | undefined, readonly T[] | undefined], RouteList | undefined>(\n [existingRoutes, routes],\n null,\n ([original, added], next) => {\n if (original && added) {\n // If we have both original and added routes, we have to merge them.\n const final: Array<RouteObject | undefined> = [];\n const pathKeys = new Set([...original.map(createRouteKey), ...added.map(createRouteKey)]);\n\n for (const pathKey of pathKeys) {\n // We can have multiple routes with the same path, so we have to\n // consider all of them.\n const originalRoutes = original.filter((r) => createRouteKey(r) === pathKey);\n // We can have only one route with the same path in the added list.\n const addedRoutes = added.filter((r) => createRouteKey(r) === pathKey);\n\n if (addedRoutes.length > 1) {\n throw new Error('Adding multiple routes with the same path is not allowed');\n }\n\n const addedRoute = addedRoutes[0] as T | undefined;\n\n if (originalRoutes.length > 0 && addedRoute) {\n // In case we have both original and added routes, we run\n // the callback for each original route in pair with the added\n // route. To make the difference, we flag all the routes except\n // the last one as `dupe`.\n //\n // Why the last one is not `dupe`? According to the\n // `react-router` logic, the last route is the fallback for all\n // routes with the same path. So, if we apply callback to it,\n // we implicitly apply it to all other routes with the same\n // path.\n //\n // In case this logic doesn't work, the user can apply the\n // callback without considering the `dupe` flag.\n for (let i = 0; i < originalRoutes.length; i++) {\n final.push(\n callback({\n original: originalRoutes[i],\n overriding: addedRoute,\n children: next([originalRoutes[i].children, addedRoute.children]),\n dupe: i < originalRoutes.length - 1,\n }) ?? originalRoutes[i],\n );\n }\n } else if (originalRoutes.length > 0) {\n // In case we don't have the added route with the path being\n // processed, we run the callback for each original route.\n for (let i = 0; i < originalRoutes.length; i++) {\n final.push(\n callback({\n original: originalRoutes[i],\n children: next([originalRoutes[i].children, undefined]),\n dupe: i < originalRoutes.length - 1,\n }) ?? originalRoutes[i],\n );\n }\n } else {\n // In case we don't have the original route with the path being\n // processed, we run the callback for only the added route.\n const result = callback({\n overriding: addedRoute,\n children: next([undefined, addedRoute!.children]),\n dupe: false,\n });\n\n if (result) {\n final.push(result);\n }\n }\n }\n\n return final.filter((r) => r != null);\n } else if (original) {\n // If we have only original routes, we run the callback for each\n // original route.\n return original\n .map((route) =>\n callback({\n original: route,\n children: next([route.children, undefined]),\n dupe: false,\n }),\n )\n .filter((r) => r != null);\n } else if (added) {\n // If we have only added routes, we run the callback for each added\n // route.\n return added\n .map((route) =>\n callback({\n overriding: route,\n children: next([undefined, route.children]),\n dupe: false,\n }),\n )\n .filter((r) => r != null);\n }\n\n return undefined;\n },\n ),\n );\n return this;\n }\n\n /**\n * Builds the router with the current list of routes.\n */\n build(options?: RouterBuildOptions): RouterConfiguration {\n this.#withLayoutSkipping();\n const routes = this.#modifiers.reduce<RouteList | undefined>((acc, mod) => mod(acc) ?? acc, undefined) ?? [];\n\n return {\n routes,\n router: createBrowserRouter([...routes], { basename: new URL(document.baseURI).pathname, ...options }),\n };\n }\n\n #withLayoutSkipping(): this {\n this.#modifiers.push((originalRoutes) => {\n if (!originalRoutes) {\n return originalRoutes;\n }\n\n type Accumulator<T extends RouteList> = Readonly<{\n skipped: T;\n regular: T;\n }>;\n\n const result = transformTree<RouteList, Accumulator<RouteList>>(originalRoutes, null, (routes, next) =>\n // Split a single routes list onto two separate lists.\n routes.reduce<Accumulator<WritableRouteList>>(\n (lists, route) => {\n // If the route has `skipLayout` flag, it goes to the `skipped` list.\n if (getRouteHandleFlag(route, RouteHandleFlags.SKIP_LAYOUTS)) {\n lists.skipped.push(route);\n return lists;\n }\n\n // If the route is leaf, it goes to the `regular` list.\n if (!route.children?.length) {\n lists.regular.push(route);\n return lists;\n }\n\n // As of children, we have to split them into two lists as well.\n const { skipped, regular } = next(route.children ?? []);\n\n // If we have `skipped` list of children, we have to remove the\n // `element` property of the router to prevent the layout from\n // rendering. Then, we add the current route to the `skipped` list.\n if (skipped.length > 0) {\n const { element, ...rest } = route;\n\n lists.skipped.push({\n ...rest,\n children: skipped,\n } as RouteObject);\n }\n\n // In case of `regular` children, we just add the current route to\n // the `regular` list if there are any children.\n if (regular.length > 0) {\n lists.regular.push({\n ...route,\n children: regular,\n } as RouteObject);\n }\n\n return lists;\n },\n { skipped: [], regular: [] },\n ),\n );\n\n // We don't need a fallback for the skipped routes, so we have to wrap\n // them with the route with the `IGNORE_FALLBACK` flag.\n return [\n ...(result.skipped.length\n ? [\n {\n children: result.skipped as RouteObject[],\n handle: {\n [RouteHandleFlags.IGNORE_FALLBACK]: true,\n },\n },\n ]\n : []),\n ...result.regular,\n ];\n });\n\n return this;\n }\n}\n"],"version":3}
1
+ {"mappings":"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","names":["module: Module","route: T","route: RouteObject","flag: T","#modifiers","routes: RouteList","routes: readonly AgnosticRoute[]","component: ComponentType","config?: ViewConfig","fallbackRoutes: RouteList","layoutComponent: ComponentType","redirectPath?: string","routes: readonly T[] | undefined","callback: RouteTransformer<T | undefined>","final: Array<RouteObject | undefined>","options?: RouterBuildOptions","#withLayoutSkipping"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/runtime/RouterConfigurationBuilder.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/consistent-type-assertions */\nimport { protectRoute } from '@vaadin/hilla-react-auth';\nimport { type ComponentType, createElement } from 'react';\nimport { createBrowserRouter, type IndexRouteObject, type NonIndexRouteObject, type RouteObject } from 'react-router';\nimport { convertComponentNameToTitle } from '../shared/convertComponentNameToTitle.js';\nimport { transformTree } from '../shared/transformTree.js';\nimport type {\n AgnosticRoute,\n Module,\n RouteModule,\n RouterBuildOptions,\n RouterConfiguration,\n ViewConfig,\n} from '../types.js';\n\ninterface RouteBase {\n path?: string;\n children?: readonly this[];\n}\n\nfunction isReactRouteModule(module: Module): module is RouteModule {\n return (\n ('default' in module && typeof module.default === 'function') ||\n ('config' in module && typeof module.config === 'object')\n );\n}\n\nexport type RouteList = readonly RouteObject[];\nexport type WritableRouteList = RouteObject[];\n\nexport type RouteTransformerOptions<T> = Readonly<{\n children?: RouteList;\n original?: RouteObject;\n overriding?: T;\n dupe: boolean;\n}>;\n\nexport type RouteTransformer<T> = (opts: RouteTransformerOptions<T>) => RouteObject | undefined;\n\ntype RoutesModifier = (routes: RouteList | undefined) => RouteList | undefined;\n\nfunction createRouteKey<T extends RouteBase>(route: T): string {\n return `${route.path ?? ''}-${route.children ? 'n' : 'i'}`;\n}\n\nenum RouteHandleFlags {\n FLOW_LAYOUT = 'flowLayout',\n IGNORE_FALLBACK = 'ignoreFallback',\n SKIP_LAYOUTS = 'skipLayouts',\n}\n\nfunction getRouteHandleFlag<T extends RouteHandleFlags>(route: RouteObject, flag: T): boolean | undefined {\n if (typeof route.handle === 'object' && flag in route.handle) {\n return (route.handle as Record<T, boolean>)[flag];\n }\n\n return undefined;\n}\n\n/**\n * A builder for creating a Vaadin-specific router for React with\n * authentication and server routes support.\n */\nexport class RouterConfigurationBuilder {\n readonly #modifiers: RoutesModifier[] = [];\n\n /**\n * Adds the given routes to the current list of routes. All the routes are\n * deeply merged to preserve the path uniqueness.\n *\n * @param routes - A list of routes to add to the current list.\n */\n withReactRoutes(routes: RouteList): this {\n return this.update(routes);\n }\n\n /**\n * Adds the given file routes to the current list of routes. All the routes\n * are transformed to React RouterObjects and deeply merged to preserve the\n * path uniqueness.\n *\n * @param routes - A list of routes to add to the current list.\n */\n withFileRoutes(routes: readonly AgnosticRoute[]): this {\n return this.update(routes, ({ original, overriding: added, children }) => {\n if (added) {\n const { module, component = module?.default, config = module?.config, path, flowLayout } = added;\n\n if (module && !isReactRouteModule(module)) {\n throw new Error(\n `The module for the \"${path}\" section doesn't have the React component exported by default or a ViewConfig object exported as \"config\"`,\n );\n }\n\n const element = component ? createElement(component) : undefined;\n const handle = {\n ...config,\n title: config?.title ?? convertComponentNameToTitle(component),\n flowLayout: config?.flowLayout ?? flowLayout,\n };\n\n if (path === '' && !children) {\n return {\n ...original,\n element,\n handle,\n index: true,\n } as IndexRouteObject;\n }\n\n return {\n ...original,\n path: config?.route ?? path,\n element,\n children,\n handle,\n } as NonIndexRouteObject;\n }\n\n return original;\n });\n }\n\n /**\n * Adds the given server route element to each branch of the current list of\n * routes.\n *\n * @param component - The React component to add to each branch of the\n * current list of routes.\n * @param config - An optional configuration that will be applied to\n * each fallback component.\n */\n withFallback(component: ComponentType, config?: ViewConfig): this {\n this.withLayout(component);\n\n // Fallback adds two routes, so that the index (empty path) has a fallback too\n const fallbackRoutes: RouteList = [\n { path: '*', element: createElement(component), handle: config },\n { index: true, element: createElement(component), handle: config },\n ];\n\n this.update(fallbackRoutes, ({ original, overriding: added, children, dupe }) => {\n if (original && !getRouteHandleFlag(original, RouteHandleFlags.IGNORE_FALLBACK) && !dupe) {\n if (!children) {\n return original;\n }\n\n const _fallback = [...fallbackRoutes];\n\n if (children.some(({ path }) => path === '*')) {\n _fallback.shift();\n }\n\n if (children.some(({ index: i, path }) => i ?? path?.includes('?'))) {\n _fallback.pop();\n }\n\n return {\n ...original,\n children: [...children, ..._fallback],\n } as RouteObject;\n }\n\n return added!;\n });\n\n return this;\n }\n\n /**\n * Adds the layoutComponent as the parent layout to views with the flowLayouts ViewConfiguration set.\n *\n * @param layoutComponent - layout component to use, usually Flow\n */\n withLayout(layoutComponent: ComponentType): this {\n this.#modifiers.push((originalRoutes) => {\n if (!originalRoutes) {\n return originalRoutes;\n }\n\n type Accumulator<T extends RouteList> = Readonly<{\n server: T;\n client: T;\n ambivalent: T;\n }>;\n\n const result = transformTree<RouteList, Accumulator<RouteList>>(originalRoutes, null, (routes, next) =>\n // Split a single routes list onto three separate lists:\n // - A list of server routes\n // - A list of client routes\n // - A list of routes which will be moved to either server or client\n // list. It depends on the parent route.\n routes.reduce<Accumulator<WritableRouteList>>(\n (lists, route) => {\n const { server, client, ambivalent } = next(route.children ?? []);\n\n const flag = getRouteHandleFlag(route, RouteHandleFlags.FLOW_LAYOUT);\n\n // If the route has `flowLayout` flag explicitly enabled, it goes to\n // the server list. The children are also affected by the flag\n // unless they have it explicitly disabled.\n if (flag === true) {\n lists.server.push({\n ...route,\n children: route.children ? [...server, ...ambivalent] : undefined,\n } as RouteObject);\n } else if (server.length > 0) {\n // Even if the route doesn't have the flag, it goes to the server\n // list if any of the children has the flag enabled.\n lists.server.push({\n ...route,\n children: route.children ? server : undefined,\n } as RouteObject);\n }\n\n // If the route has `flowLayout` flag explicitly disabled, it goes\n // to the client list. The route children are not affected by the\n // flag.\n if (flag === false || client.length > 0) {\n lists.client.push({\n ...route,\n children: route.children ? client : undefined,\n } as RouteObject);\n }\n\n // The route without the flag go to the `default` list. Then it will\n // be moved to either server or client list based on the parent\n // route.\n if (\n flag === undefined &&\n (lists.server.every(({ path }) => path !== route.path) || ambivalent.length > 0)\n ) {\n lists.ambivalent.push({\n ...route,\n children: route.children ? ambivalent : undefined,\n } as RouteObject);\n }\n\n return lists;\n },\n { server: [], client: [], ambivalent: [] },\n ),\n );\n\n return [\n ...(result.server.length\n ? [\n // The server routes are wrapped with the route that has a layout\n // element. It also has the `IGNORE_FALLBACK` flag to remove the\n // fallback route from reach.\n {\n element: createElement(layoutComponent),\n children: result.server as RouteObject[],\n handle: {\n [RouteHandleFlags.IGNORE_FALLBACK]: true,\n },\n },\n ]\n : []),\n // The client routes are preserved without wrapping.\n ...result.client,\n // The ambivalent routes are considered as client routes.\n ...result.ambivalent,\n ];\n });\n\n return this;\n }\n\n /**\n * Protects all the routes that require authentication. For more details see\n * {@link @vaadin/hilla-react-auth#protectRoutes} function.\n *\n * @param redirectPath - the path to redirect to if the route is protected\n * and the user is not authenticated.\n */\n protect(redirectPath?: string): this {\n this.update(undefined, ({ original: route, children }) => {\n const finalRoute = protectRoute(route!, redirectPath);\n finalRoute.children = children as RouteObject[] | undefined;\n return finalRoute;\n });\n\n return this;\n }\n\n /**\n * Deeply updates the current list of routes with the given routes merging\n * them in process.\n *\n * @param routes - A list of routes to merge with the current list.\n * @param callback - A callback to transform the routes during the merge.\n */\n update(routes: undefined, callback: RouteTransformer<undefined>): this;\n update<T extends RouteBase>(routes: readonly T[], callback?: RouteTransformer<T>): this;\n update<T extends RouteBase>(\n routes: readonly T[] | undefined,\n callback: RouteTransformer<T | undefined> = ({ original, overriding, children }) =>\n ({\n ...original,\n ...overriding,\n children,\n }) as RouteObject,\n ): this {\n this.#modifiers.push((existingRoutes) =>\n // Going through the existing and added list of routes.\n transformTree<readonly [RouteList | undefined, readonly T[] | undefined], RouteList | undefined>(\n [existingRoutes, routes],\n null,\n ([original, added], next) => {\n if (original && added) {\n // If we have both original and added routes, we have to merge them.\n const final: Array<RouteObject | undefined> = [];\n const pathKeys = new Set([...original.map(createRouteKey), ...added.map(createRouteKey)]);\n\n for (const pathKey of pathKeys) {\n // We can have multiple routes with the same path, so we have to\n // consider all of them.\n const originalRoutes = original.filter((r) => createRouteKey(r) === pathKey);\n // We can have only one route with the same path in the added list.\n const addedRoutes = added.filter((r) => createRouteKey(r) === pathKey);\n\n if (addedRoutes.length > 1) {\n throw new Error('Adding multiple routes with the same path is not allowed');\n }\n\n const addedRoute = addedRoutes[0] as T | undefined;\n\n if (originalRoutes.length > 0 && addedRoute) {\n // In case we have both original and added routes, we run\n // the callback for each original route in pair with the added\n // route. To make the difference, we flag all the routes except\n // the last one as `dupe`.\n //\n // Why the last one is not `dupe`? According to the\n // `react-router` logic, the last route is the fallback for all\n // routes with the same path. So, if we apply callback to it,\n // we implicitly apply it to all other routes with the same\n // path.\n //\n // In case this logic doesn't work, the user can apply the\n // callback without considering the `dupe` flag.\n for (let i = 0; i < originalRoutes.length; i++) {\n final.push(\n callback({\n original: originalRoutes[i],\n overriding: addedRoute,\n children: next([originalRoutes[i].children, addedRoute.children]),\n dupe: i < originalRoutes.length - 1,\n }) ?? originalRoutes[i],\n );\n }\n } else if (originalRoutes.length > 0) {\n // In case we don't have the added route with the path being\n // processed, we run the callback for each original route.\n for (let i = 0; i < originalRoutes.length; i++) {\n final.push(\n callback({\n original: originalRoutes[i],\n children: next([originalRoutes[i].children, undefined]),\n dupe: i < originalRoutes.length - 1,\n }) ?? originalRoutes[i],\n );\n }\n } else {\n // In case we don't have the original route with the path being\n // processed, we run the callback for only the added route.\n const result = callback({\n overriding: addedRoute,\n children: next([undefined, addedRoute!.children]),\n dupe: false,\n });\n\n if (result) {\n final.push(result);\n }\n }\n }\n\n return final.filter((r) => r != null);\n } else if (original) {\n // If we have only original routes, we run the callback for each\n // original route.\n return original\n .map((route) =>\n callback({\n original: route,\n children: next([route.children, undefined]),\n dupe: false,\n }),\n )\n .filter((r) => r != null);\n } else if (added) {\n // If we have only added routes, we run the callback for each added\n // route.\n return added\n .map((route) =>\n callback({\n overriding: route,\n children: next([undefined, route.children]),\n dupe: false,\n }),\n )\n .filter((r) => r != null);\n }\n\n return undefined;\n },\n ),\n );\n return this;\n }\n\n /**\n * Builds the router with the current list of routes.\n */\n build(options?: RouterBuildOptions): RouterConfiguration {\n this.#withLayoutSkipping();\n const routes = this.#modifiers.reduce<RouteList | undefined>((acc, mod) => mod(acc) ?? acc, undefined) ?? [];\n\n return {\n routes,\n router: createBrowserRouter([...routes], { basename: new URL(document.baseURI).pathname, ...options }),\n };\n }\n\n #withLayoutSkipping(): this {\n this.#modifiers.push((originalRoutes) => {\n if (!originalRoutes) {\n return originalRoutes;\n }\n\n type Accumulator<T extends RouteList> = Readonly<{\n skipped: T;\n regular: T;\n }>;\n\n const result = transformTree<RouteList, Accumulator<RouteList>>(originalRoutes, null, (routes, next) =>\n // Split a single routes list onto two separate lists.\n routes.reduce<Accumulator<WritableRouteList>>(\n (lists, route) => {\n // If the route has `skipLayout` flag, it goes to the `skipped` list.\n if (getRouteHandleFlag(route, RouteHandleFlags.SKIP_LAYOUTS)) {\n lists.skipped.push(route);\n return lists;\n }\n\n // If the route is leaf, it goes to the `regular` list.\n if (!route.children?.length) {\n lists.regular.push(route);\n return lists;\n }\n\n // As of children, we have to split them into two lists as well.\n const { skipped, regular } = next(route.children ?? []);\n\n // If we have `skipped` list of children, we have to remove the\n // `element` property of the router to prevent the layout from\n // rendering. Then, we add the current route to the `skipped` list.\n if (skipped.length > 0) {\n const { element, ...rest } = route;\n\n lists.skipped.push({\n ...rest,\n children: skipped,\n } as RouteObject);\n }\n\n // In case of `regular` children, we just add the current route to\n // the `regular` list if there are any children.\n if (regular.length > 0) {\n lists.regular.push({\n ...route,\n children: regular,\n } as RouteObject);\n }\n\n return lists;\n },\n { skipped: [], regular: [] },\n ),\n );\n\n // We don't need a fallback for the skipped routes, so we have to wrap\n // them with the route with the `IGNORE_FALLBACK` flag.\n return [\n ...(result.skipped.length\n ? [\n {\n children: result.skipped as RouteObject[],\n handle: {\n [RouteHandleFlags.IGNORE_FALLBACK]: true,\n },\n },\n ]\n : []),\n ...result.regular,\n ];\n });\n\n return this;\n }\n}\n"],"version":3}
@@ -9,4 +9,4 @@ export declare const viewsSignal: Signal<Readonly<Record<string, Readonly<ViewCo
9
9
  *
10
10
  * @returns A list of menu items.
11
11
  */
12
- export declare function createMenuItems(): readonly MenuItem[];
12
+ export declare function createMenuItems<T = unknown>(): ReadonlyArray<MenuItem<T>>;
@@ -19,7 +19,7 @@ export function createMenuItems() {
19
19
  vaadinObj.registrations ??= [];
20
20
  vaadinObj.registrations.push({
21
21
  is: feature ? `@vaadin/hilla-file-router/${feature}` : "@vaadin/hilla-file-router",
22
- version: "24.8.0-alpha5"
22
+ version: "24.8.0-alpha7"
23
23
  });
24
24
  })("createMenuItems", window.Vaadin);
25
25
  const collator = new Intl.Collator("en-US");
@@ -31,7 +31,8 @@ export function createMenuItems() {
31
31
  to: path,
32
32
  icon: config.menu?.icon,
33
33
  title: config.menu?.title ?? config.title,
34
- order: config.menu?.order
34
+ order: config.menu?.order,
35
+ detail: config.detail
35
36
  })).sort((menuA, menuB) => {
36
37
  const ordersDiff = (menuA.order ?? Number.MAX_VALUE) - (menuB.order ?? Number.MAX_VALUE);
37
38
  return ordersDiff !== 0 ? ordersDiff : collator.compare(menuA.to, menuB.to);
@@ -1 +1 @@
1
- {"mappings":"AACA,SAAsB,2CAA4C;AAIlE,OAAO,MAAMA,cAAkF,OAC5F,OAAwB,QAAQ,MAClC;AAED,SAAS,WAAWC,OAA4B;AAC9C,UAAS,MAAM,MAAM;AACtB;AAED,SAAS,uBAAuBC,MAAuB;AACrD,QAAO,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,QAAQ,WAAW,IAAI,CAAC;AAClE;;;;;;;;;AAUD,OAAO,SAAS,kBAAuC;AAGrD,EAAC,CAAC,SAAS,YAAa,OAAO,WAAW,CAAE,MAAM;AAClD,YAAU,kBAAkB,CAAE;AAC9B,YAAU,cAAc,KAAK;GAC3B,IAAI,WAAW,4BAA4B,QAAQ,IAAI;GACvD,SAAS;EACV,EAAC;CACH,GAAE,mBAAoB,OAAwB,OAAO;CACpD,MAAM,WAAW,IAAI,KAAK,SAAS;AACnC,MAAK,YAAY,OAAO;AACtB,SAAO,CAAE;CACV;CAED,MAAM,QAAQ,OAAO,QAAQ,YAAY,MAAM;AAE/C,QACE,MAEG,OAAO,CAAC,CAAC,MAAM,MAAM,MAAM,WAAW,MAAM,KAAK,uBAAuB,KAAK,CAAC,CAE9E,IAAI,CAAC,CAAC,MAAM,OAAO,MAAM;EACxB,IAAI;EACJ,MAAM,OAAO,MAAM;EACnB,OAAO,OAAO,MAAM,SAAS,OAAO;EACpC,OAAO,OAAO,MAAM;CACrB,GAAE,CAEF,KAAK,CAAC,OAAO,UAAU;EACtB,MAAM,cAAc,MAAM,SAAS,OAAO,cAAc,MAAM,SAAS,OAAO;AAC9E,SAAO,eAAe,IAAI,aAAa,SAAS,QAAQ,MAAM,IAAI,MAAM,GAAG;CAC5E,EAAC;AAEP;AAED,IAAI,OAAO,KAAK,KAAK;AACnB,QAAO,KAAK,IAAI,GAAG,mBAAmB,MAAM;AAC1C,QAAM,iBAAiB,CACpB,KAAK,OAAO,SAAS,KAAK,MAAM,CAAC,CACjC,KAAK,CAAC,SAAS;AACd,eAAY,QAAQ;EACrB,EAAC,CACD,MAAM,CAACC,MAAe;AACrB,WAAQ,MAAM,8BAA8B,EAAE;EAC/C,EAAC;CACL,EAAC;AACH","names":["viewsSignal: Signal<Readonly<Record<string, Readonly<ViewConfig>>> | undefined>","value: ViewConfig","path: string","e: unknown"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/runtime/createMenuItems.ts"],"sourcesContent":["/// <reference types=\"vite/client\" />\nimport { type Signal, signal } from '@vaadin/hilla-react-signals';\nimport type { VaadinWindow } from '../shared/internal.js';\nimport type { MenuItem, ViewConfig } from '../types.js';\n\nexport const viewsSignal: Signal<Readonly<Record<string, Readonly<ViewConfig>>> | undefined> = signal(\n (window as VaadinWindow).Vaadin?.views,\n);\n\nfunction isExcluded(value: ViewConfig): boolean {\n return !!value.menu?.exclude;\n}\n\nfunction hasVariablePathSegment(path: string): boolean {\n return path.split('/').some((segment) => segment.startsWith(':'));\n}\n\n/**\n * Creates menu items from the views provided by the server. The views are sorted according to the\n * {@link ViewConfig.menu.order}, filtered out if they are explicitly excluded via {@link ViewConfig.menu.exclude}.\n * Note that views with no order are put below views with an order. Ties are resolved based on the path string\n * comparison.\n *\n * @returns A list of menu items.\n */\nexport function createMenuItems(): readonly MenuItem[] {\n // @ts-expect-error: esbuild injection\n // eslint-disable-next-line @typescript-eslint/no-unsafe-call\n ((feature, vaadinObj = (window.Vaadin ??= {})) => {\n vaadinObj.registrations ??= [];\n vaadinObj.registrations.push({\n is: feature ? `@vaadin/hilla-file-router/${feature}` : '@vaadin/hilla-file-router',\n version: '24.8.0-alpha5',\n });\n})('createMenuItems', (window as VaadinWindow).Vaadin);\n const collator = new Intl.Collator('en-US');\n if (!viewsSignal.value) {\n return [];\n }\n\n const views = Object.entries(viewsSignal.value);\n\n return (\n views\n // Filter out the views that are explicitly excluded from the menu.\n .filter(([path, value]) => !isExcluded(value) && !hasVariablePathSegment(path))\n // Map the views to menu items.\n .map(([path, config]) => ({\n to: path,\n icon: config.menu?.icon,\n title: config.menu?.title ?? config.title,\n order: config.menu?.order,\n }))\n // Sort views according to the order specified in the view configuration.\n .sort((menuA, menuB) => {\n const ordersDiff = (menuA.order ?? Number.MAX_VALUE) - (menuB.order ?? Number.MAX_VALUE);\n return ordersDiff !== 0 ? ordersDiff : collator.compare(menuA.to, menuB.to);\n })\n );\n}\n\nif (import.meta.hot) {\n import.meta.hot.on('fs-route-update', () => {\n fetch('?v-r=routeinfo')\n .then(async (resp) => resp.json())\n .then((json) => {\n viewsSignal.value = json;\n })\n .catch((e: unknown) => {\n console.error('Failed to fetch route info', e);\n });\n });\n}\n"],"version":3}
1
+ {"mappings":"AACA,SAAsB,2CAA4C;AAIlE,OAAO,MAAMA,cAAkF,OAC5F,OAAwB,QAAQ,MAClC;AAED,SAAS,WAAWC,OAA4B;AAC9C,UAAS,MAAM,MAAM;AACtB;AAED,SAAS,uBAAuBC,MAAuB;AACrD,QAAO,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,QAAQ,WAAW,IAAI,CAAC;AAClE;;;;;;;;;AAUD,OAAO,SAAS,kBAA2D;AAGzE,EAAC,CAAC,SAAS,YAAa,OAAO,WAAW,CAAE,MAAM;AAClD,YAAU,kBAAkB,CAAE;AAC9B,YAAU,cAAc,KAAK;GAC3B,IAAI,WAAW,4BAA4B,QAAQ,IAAI;GACvD,SAAS;EACV,EAAC;CACH,GAAE,mBAAoB,OAAwB,OAAO;CACpD,MAAM,WAAW,IAAI,KAAK,SAAS;AACnC,MAAK,YAAY,OAAO;AACtB,SAAO,CAAE;CACV;CAED,MAAM,QAAQ,OAAO,QAAQ,YAAY,MAAM;AAE/C,QACE,MAEG,OAAO,CAAC,CAAC,MAAM,MAAM,MAAM,WAAW,MAAM,KAAK,uBAAuB,KAAK,CAAC,CAE9E,IAAI,CAAC,CAAC,MAAM,OAAO,MAAM;EACxB,IAAI;EACJ,MAAM,OAAO,MAAM;EACnB,OAAO,OAAO,MAAM,SAAS,OAAO;EACpC,OAAO,OAAO,MAAM;EACpB,QAAQ,OAAO;CAChB,GAAE,CAEF,KAAK,CAAC,OAAO,UAAU;EACtB,MAAM,cAAc,MAAM,SAAS,OAAO,cAAc,MAAM,SAAS,OAAO;AAC9E,SAAO,eAAe,IAAI,aAAa,SAAS,QAAQ,MAAM,IAAI,MAAM,GAAG;CAC5E,EAAC;AAEP;AAED,IAAI,OAAO,KAAK,KAAK;AACnB,QAAO,KAAK,IAAI,GAAG,mBAAmB,MAAM;AAC1C,QAAM,iBAAiB,CACpB,KAAK,OAAO,SAAS,KAAK,MAAM,CAAC,CACjC,KAAK,CAAC,SAAS;AACd,eAAY,QAAQ;EACrB,EAAC,CACD,MAAM,CAACC,MAAe;AACrB,WAAQ,MAAM,8BAA8B,EAAE;EAC/C,EAAC;CACL,EAAC;AACH","names":["viewsSignal: Signal<Readonly<Record<string, Readonly<ViewConfig>>> | undefined>","value: ViewConfig","path: string","e: unknown"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/runtime/createMenuItems.ts"],"sourcesContent":["/// <reference types=\"vite/client\" />\nimport { type Signal, signal } from '@vaadin/hilla-react-signals';\nimport type { VaadinWindow } from '../shared/internal.js';\nimport type { MenuItem, ViewConfig } from '../types.js';\n\nexport const viewsSignal: Signal<Readonly<Record<string, Readonly<ViewConfig>>> | undefined> = signal(\n (window as VaadinWindow).Vaadin?.views,\n);\n\nfunction isExcluded(value: ViewConfig): boolean {\n return !!value.menu?.exclude;\n}\n\nfunction hasVariablePathSegment(path: string): boolean {\n return path.split('/').some((segment) => segment.startsWith(':'));\n}\n\n/**\n * Creates menu items from the views provided by the server. The views are sorted according to the\n * {@link ViewConfig.menu.order}, filtered out if they are explicitly excluded via {@link ViewConfig.menu.exclude}.\n * Note that views with no order are put below views with an order. Ties are resolved based on the path string\n * comparison.\n *\n * @returns A list of menu items.\n */\nexport function createMenuItems<T = unknown>(): ReadonlyArray<MenuItem<T>> {\n // @ts-expect-error: esbuild injection\n // eslint-disable-next-line @typescript-eslint/no-unsafe-call\n ((feature, vaadinObj = (window.Vaadin ??= {})) => {\n vaadinObj.registrations ??= [];\n vaadinObj.registrations.push({\n is: feature ? `@vaadin/hilla-file-router/${feature}` : '@vaadin/hilla-file-router',\n version: '24.8.0-alpha7',\n });\n})('createMenuItems', (window as VaadinWindow).Vaadin);\n const collator = new Intl.Collator('en-US');\n if (!viewsSignal.value) {\n return [];\n }\n\n const views = Object.entries(viewsSignal.value);\n\n return (\n views\n // Filter out the views that are explicitly excluded from the menu.\n .filter(([path, value]) => !isExcluded(value) && !hasVariablePathSegment(path))\n // Map the views to menu items.\n .map(([path, config]) => ({\n to: path,\n icon: config.menu?.icon,\n title: config.menu?.title ?? config.title,\n order: config.menu?.order,\n detail: config.detail as T | undefined,\n }))\n // Sort views according to the order specified in the view configuration.\n .sort((menuA, menuB) => {\n const ordersDiff = (menuA.order ?? Number.MAX_VALUE) - (menuB.order ?? Number.MAX_VALUE);\n return ordersDiff !== 0 ? ordersDiff : collator.compare(menuA.to, menuB.to);\n })\n );\n}\n\nif (import.meta.hot) {\n import.meta.hot.on('fs-route-update', () => {\n fetch('?v-r=routeinfo')\n .then(async (resp) => resp.json())\n .then((json) => {\n viewsSignal.value = json;\n })\n .catch((e: unknown) => {\n console.error('Failed to fetch route info', e);\n });\n });\n}\n"],"version":3}
@@ -1,3 +1,4 @@
1
+ import type { ComponentType } from "react";
1
2
  import type { AgnosticRoute, Module, ViewConfig } from "../types.js";
2
3
  /**
3
4
  * Extends a router module's config with additional properties. The original
@@ -6,6 +7,8 @@ import type { AgnosticRoute, Module, ViewConfig } from "../types.js";
6
7
  * @param module - The module to extend.
7
8
  * @param config - The extension config.
8
9
  * @returns
10
+ *
11
+ * @deprecated Use object spread syntax instead.
9
12
  */
10
13
  export declare function extendModule(module: Module | null, config?: ViewConfig): Module;
11
14
  /**
@@ -18,4 +21,24 @@ export declare function extendModule(module: Module | null, config?: ViewConfig)
18
21
  * @returns A framework-agnostic route object.
19
22
  */
20
23
  export declare function createRoute(path: string, children?: readonly AgnosticRoute[]): AgnosticRoute;
24
+ /**
25
+ * Create a single framework-agnostic route object. Later, it can be transformed into a framework-specific route object,
26
+ * e.g., the one used by React Router.
27
+ *
28
+ * @param path - A route path segment.
29
+ * @param module - A module that exports a component and an optional config object.
30
+ * @param children - An array of child routes.
31
+ *
32
+ * @deprecated Use `createRoute(path, component, config, children)` instead.
33
+ */
21
34
  export declare function createRoute(path: string, module: Module, children?: readonly AgnosticRoute[]): AgnosticRoute;
35
+ /**
36
+ * Create a single framework-agnostic route object. Later, it can be transformed into a framework-specific route object,
37
+ * e.g., the one used by React Router.
38
+ *
39
+ * @param path - A route path segment.
40
+ * @param component - A React component.
41
+ * @param config - An optional config object.
42
+ * @param children - An array of child routes.
43
+ */
44
+ export declare function createRoute(path: string, component: ComponentType, config: ViewConfig | undefined, children?: readonly AgnosticRoute[]): AgnosticRoute;
@@ -5,6 +5,8 @@
5
5
  * @param module - The module to extend.
6
6
  * @param config - The extension config.
7
7
  * @returns
8
+ *
9
+ * @deprecated Use object spread syntax instead.
8
10
  */
9
11
  export function extendModule(module, config) {
10
12
  return {
@@ -15,16 +17,30 @@ export function extendModule(module, config) {
15
17
  }
16
18
  };
17
19
  }
18
- export function createRoute(path, moduleOrChildren, children) {
19
- let module;
20
- if (Array.isArray(moduleOrChildren)) {
21
- children = moduleOrChildren;
22
- } else {
23
- module = moduleOrChildren;
20
+ function isReactComponent(value) {
21
+ return !!value && (typeof value === "function" || typeof value === "object" && "$$typeof" in value && value.$$typeof === Symbol.for("react.lazy"));
22
+ }
23
+ export function createRoute(path, moduleOrChildrenOrComponent, childrenOrConfig, children) {
24
+ let isDeprecatedModuleUsed = false;
25
+ let component;
26
+ let config;
27
+ if (Array.isArray(moduleOrChildrenOrComponent)) {
28
+ children = moduleOrChildrenOrComponent;
29
+ } else if (isReactComponent(moduleOrChildrenOrComponent) || !moduleOrChildrenOrComponent && !!childrenOrConfig && typeof childrenOrConfig === "object") {
30
+ component = moduleOrChildrenOrComponent;
31
+ config = childrenOrConfig;
32
+ } else if (moduleOrChildrenOrComponent) {
33
+ ({default: component, config} = moduleOrChildrenOrComponent);
34
+ isDeprecatedModuleUsed = true;
24
35
  }
25
36
  return {
26
37
  path,
27
- module,
38
+ module: isDeprecatedModuleUsed ? {
39
+ default: component,
40
+ config
41
+ } : undefined,
42
+ component,
43
+ config,
28
44
  children
29
45
  };
30
46
  }
@@ -1 +1 @@
1
- {"mappings":";;;;;;;;AAUA,OAAO,SAAS,aAAaA,QAAuBC,QAA6B;AAC/E,QAAO;EACL,GAAG;EACH,QAAQ;GACN,GAAG;GACH,GAAI,QAAQ;EACb;CACF;AACF;AAaD,OAAO,SAAS,YACdC,MACAC,kBACAC,UACe;CACf,IAAIC;AACJ,KAAI,MAAM,QAAQ,iBAAiB,EAAE;AAEnC,aAAW;CACZ,OAAM;AACL,WAAS;CACV;AAED,QAAO;EACL;EACA;EACA;CACD;AACF","names":["module: Module | null","config?: ViewConfig","path: string","moduleOrChildren?: Module | readonly AgnosticRoute[]","children?: readonly AgnosticRoute[]","module: Module | undefined"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/runtime/createRoute.ts"],"sourcesContent":["import type { AgnosticRoute, Module, ViewConfig } from '../types.js';\n\n/**\n * Extends a router module's config with additional properties. The original\n * module config is preferred over the extension.\n *\n * @param module - The module to extend.\n * @param config - The extension config.\n * @returns\n */\nexport function extendModule(module: Module | null, config?: ViewConfig): Module {\n return {\n ...module,\n config: {\n ...config,\n ...(module?.config as ViewConfig),\n },\n };\n}\n\n/**\n * Create a single framework-agnostic route object. Later, it can be transformed into a framework-specific route object,\n * e.g., the one used by React Router.\n *\n * @param path - A route path segment.\n * @param children - An array of child routes.\n *\n * @returns A framework-agnostic route object.\n */\nexport function createRoute(path: string, children?: readonly AgnosticRoute[]): AgnosticRoute;\nexport function createRoute(path: string, module: Module, children?: readonly AgnosticRoute[]): AgnosticRoute;\nexport function createRoute(\n path: string,\n moduleOrChildren?: Module | readonly AgnosticRoute[],\n children?: readonly AgnosticRoute[],\n): AgnosticRoute {\n let module: Module | undefined;\n if (Array.isArray(moduleOrChildren)) {\n // eslint-disable-next-line no-param-reassign\n children = moduleOrChildren;\n } else {\n module = moduleOrChildren as Module | undefined;\n }\n\n return {\n path,\n module,\n children,\n };\n}\n"],"version":3}
1
+ {"mappings":";;;;;;;;;;AAaA,OAAO,SAAS,aAAaA,QAAuBC,QAA6B;AAC/E,QAAO;EACL,GAAG;EACH,QAAQ;GACN,GAAG;GACH,GAAI,QAAQ;EACb;CACF;AACF;AAED,SAAS,iBAAiBC,OAAwC;AAChE,UACI,iBACM,UAAU,qBACR,UAAU,YAAY,cAAc,SAAS,MAAM,aAAa,OAAO,IAAI,aAAa;AAErG;AAsCD,OAAO,SAAS,YACdC,MACAC,6BACAC,kBACAC,UACe;CACf,IAAI,yBAAyB;CAC7B,IAAIC;CACJ,IAAIC;AACJ,KAAI,MAAM,QAAQ,4BAA4B,EAAE;AAE9C,aAAW;CACZ,WACC,iBAAiB,4BAA4B,KAC3C,iCAAiC,2BAA2B,qBAAqB,UACnF;AACA,cAAY;AACZ,WAAS;CACV,WAAU,6BAA6B;AACtC,GAAC,CAAE,SAAS,WAAW,OAAQ,GAAG;AAClC,2BAAyB;CAC1B;AAED,QAAO;EACL;EACA,QAAQ,yBAAyB;GAAE,SAAS;GAAW;EAAQ,IAAG;EAClE;EACA;EACA;CACD;AACF","names":["module: Module | null","config?: ViewConfig","value: unknown","path: string","moduleOrChildrenOrComponent?: Module | ComponentType | readonly AgnosticRoute[]","childrenOrConfig?: readonly AgnosticRoute[] | ViewConfig","children?: readonly AgnosticRoute[]","component: ComponentType | undefined","config: ViewConfig | undefined"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/runtime/createRoute.ts"],"sourcesContent":["import type { ComponentType } from 'react';\nimport type { AgnosticRoute, Module, RouteModule, ViewConfig } from '../types.js';\n\n/**\n * Extends a router module's config with additional properties. The original\n * module config is preferred over the extension.\n *\n * @param module - The module to extend.\n * @param config - The extension config.\n * @returns\n *\n * @deprecated Use object spread syntax instead.\n */\nexport function extendModule(module: Module | null, config?: ViewConfig): Module {\n return {\n ...module,\n config: {\n ...config,\n ...(module?.config as ViewConfig),\n },\n };\n}\n\nfunction isReactComponent(value: unknown): value is ComponentType {\n return (\n !!value &&\n (typeof value === 'function' ||\n (typeof value === 'object' && '$$typeof' in value && value.$$typeof === Symbol.for('react.lazy')))\n );\n}\n\n/**\n * Create a single framework-agnostic route object. Later, it can be transformed into a framework-specific route object,\n * e.g., the one used by React Router.\n *\n * @param path - A route path segment.\n * @param children - An array of child routes.\n *\n * @returns A framework-agnostic route object.\n */\nexport function createRoute(path: string, children?: readonly AgnosticRoute[]): AgnosticRoute;\n/**\n * Create a single framework-agnostic route object. Later, it can be transformed into a framework-specific route object,\n * e.g., the one used by React Router.\n *\n * @param path - A route path segment.\n * @param module - A module that exports a component and an optional config object.\n * @param children - An array of child routes.\n *\n * @deprecated Use `createRoute(path, component, config, children)` instead.\n */\nexport function createRoute(path: string, module: Module, children?: readonly AgnosticRoute[]): AgnosticRoute;\n/**\n * Create a single framework-agnostic route object. Later, it can be transformed into a framework-specific route object,\n * e.g., the one used by React Router.\n *\n * @param path - A route path segment.\n * @param component - A React component.\n * @param config - An optional config object.\n * @param children - An array of child routes.\n */\nexport function createRoute(\n path: string,\n component: ComponentType,\n config: ViewConfig | undefined,\n children?: readonly AgnosticRoute[],\n): AgnosticRoute;\nexport function createRoute(\n path: string,\n moduleOrChildrenOrComponent?: Module | ComponentType | readonly AgnosticRoute[],\n childrenOrConfig?: readonly AgnosticRoute[] | ViewConfig,\n children?: readonly AgnosticRoute[],\n): AgnosticRoute {\n let isDeprecatedModuleUsed = false;\n let component: ComponentType | undefined;\n let config: ViewConfig | undefined;\n if (Array.isArray(moduleOrChildrenOrComponent)) {\n // eslint-disable-next-line no-param-reassign\n children = moduleOrChildrenOrComponent;\n } else if (\n isReactComponent(moduleOrChildrenOrComponent) ||\n (!moduleOrChildrenOrComponent && !!childrenOrConfig && typeof childrenOrConfig === 'object')\n ) {\n component = moduleOrChildrenOrComponent;\n config = childrenOrConfig as ViewConfig;\n } else if (moduleOrChildrenOrComponent) {\n ({ default: component, config } = moduleOrChildrenOrComponent as RouteModule);\n isDeprecatedModuleUsed = true;\n }\n\n return {\n path,\n module: isDeprecatedModuleUsed ? { default: component, config } : undefined,\n component,\n config,\n children,\n };\n}\n"],"version":3}
@@ -4,10 +4,10 @@ import type { RouteParamType } from "./routeParamType.js";
4
4
  * Internal type used for server communication and menu building. It extends the
5
5
  * view configuration with the route parameters.
6
6
  */
7
- export type ServerViewConfig = Readonly<{
7
+ export type ServerViewConfig<T = unknown> = Readonly<{
8
8
  children?: readonly ServerViewConfig[]
9
9
  params?: Readonly<Record<string, RouteParamType>>
10
- }> & ViewConfig;
10
+ }> & ViewConfig<T>;
11
11
  export type VaadinObject = Readonly<{
12
12
  views: Readonly<Record<string, ViewConfig>>
13
13
  }>;
package/types.d.ts CHANGED
@@ -1,5 +1,12 @@
1
+ import type { ComponentType } from "react";
1
2
  import type { createBrowserRouter, RouteObject } from "react-router";
2
- export type ViewConfig = Readonly<{
3
+ /**
4
+ * A configuration object for a view. This is used to define the view's
5
+ * metadata, such as the title, roles allowed, and other properties.
6
+ *
7
+ * @typeParam T - The type of the detail object.
8
+ */
9
+ export type ViewConfig<T = unknown> = Readonly<{
3
10
  /**
4
11
  * View title used in the main layout header, as <title> and as the default
5
12
  * for the menu entry. If not defined, the component name will be taken,
@@ -29,6 +36,13 @@ export type ViewConfig = Readonly<{
29
36
  * Set to true to make the view render without enclosing in any layouts.
30
37
  */
31
38
  skipLayouts?: boolean
39
+ /**
40
+ * Set false to indicate that the view should not be lazy loaded. `/` and
41
+ * `/login` are always loaded eagerly.
42
+ *
43
+ * @defaultValue `true`
44
+ */
45
+ lazy?: boolean
32
46
  menu?: Readonly<{
33
47
  /**
34
48
  * Title to use in the menu. Falls back the title property of the view
@@ -51,6 +65,13 @@ export type ViewConfig = Readonly<{
51
65
  */
52
66
  icon?: string
53
67
  }>
68
+ /**
69
+ * Used to add additional properties to the view. This object will be
70
+ * available when building the menu.
71
+ *
72
+ * @see {@link ./runtime/createMenuItems.ts#createMenuItems}
73
+ */
74
+ detail?: T
54
75
  }>;
55
76
  /**
56
77
  * An unknown JavaScript module.
@@ -59,8 +80,8 @@ export type Module = Readonly<Record<string, unknown>>;
59
80
  /**
60
81
  * A module that exports a component and an optional view configuration.
61
82
  */
62
- export type RouteModule<C = unknown> = Module & Readonly<{
63
- default: C
83
+ export type RouteModule<C extends ComponentType = ComponentType> = Module & Readonly<{
84
+ default?: C
64
85
  config?: ViewConfig
65
86
  }>;
66
87
  /**
@@ -68,17 +89,25 @@ export type RouteModule<C = unknown> = Module & Readonly<{
68
89
  */
69
90
  export type AgnosticRoute = Readonly<{
70
91
  path: string
71
- module?: Module
92
+ /**
93
+ * @deprecated Use `component` and `config` separately instead.
94
+ */
95
+ module?: RouteModule
96
+ component?: ComponentType
97
+ config?: ViewConfig
72
98
  children?: readonly AgnosticRoute[]
73
99
  flowLayout?: boolean
74
100
  }>;
75
101
  /**
76
102
  * A menu item used in for building the navigation menu.
103
+ *
104
+ * @typeParam T - The type of the detail object, same as in the view configuration.
77
105
  */
78
- export type MenuItem = Readonly<{
106
+ export type MenuItem<T = unknown> = Readonly<{
79
107
  to: string
80
108
  icon?: string
81
109
  title?: string
110
+ detail?: T
82
111
  }>;
83
112
  export type RouterConfiguration = Readonly<{
84
113
  routes: readonly RouteObject[]
@@ -1,9 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join, relative } from "node:path/posix";
3
3
  import { transformTree } from "../shared/transformTree.js";
4
- function strip(path) {
5
- return path.replace(/^\/*(.+)\/*$/u, "$1");
6
- }
4
+ import { strip } from "./utils.js";
7
5
  /**
8
6
  * Enables Flow layout flag on the matching routes based on the information from the layouts JSON file.
9
7
  *
@@ -1 +1 @@
1
- {"mappings":"AAAA,SAAS,kCAAmC;AAC5C,SAAS,MAAM,iCAAkC;AACjD,SAAS,iDAAkD;AAU3D,SAAS,MAAMA,MAAc;AAC3B,QAAO,KAAK,QAAQ,iBAAiB,KAAK;AAC3C;;;;;;;;AASD,eAAe,eAAe,aAC5BC,WACAC,aAC+B;AAC/B,KAAI;EACF,MAAM,iBAAiB,MAAM,SAAS,aAAa,QAAQ;EAC3D,MAAMC,mBAA0C,KAAK,MAAM,eAAe;EAC1E,MAAM,cAAc,iBAAiB,IAAI,CAAC,WAAW,MAAM,OAAO,KAAK,CAAC;AAExE,SAAO,cACL,WACA,EAAE,MAAM,GAAI,GACZ,CAAC,OAAO,MAAM,QACZ,MAAM,IAAI,CAAC,SAAS;GAClB,MAAM,cAAc,KAAK,IAAI,MAAM,MAAM,KAAK,KAAK,CAAC;GACpD,MAAM,WAAW,KAAK,WAAW,KAAK,KAAK,UAAU,EAAE,MAAM,YAAa,EAAC,GAAG;AAE9E,UAAO,YAAY,KAAK,CAAC,UAAU,SAAS,MAAM,YAAY,CAAC,WAAW,KAAK,CAAC,GAC5E;IAAE,GAAG;IAAM,YAAY;IAAM;GAAU,IACvC;IAAE,GAAG;IAAM;GAAU;EAC1B,EAAC,CACL;CACF,SAAQC,GAAY;AACnB,MAAI,aAAa,SAAS,UAAU,KAAK,EAAE,SAAS,UAAU;AAC5D,UAAO;EACR;AAED,QAAM;CACP;AACF","names":["path: string","routeMeta: readonly RouteMeta[]","layoutsFile: URL","availableLayouts: readonly LayoutMeta[]","e: unknown"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/applyLayouts.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path/posix';\nimport { transformTree } from '../shared/transformTree.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\n\n/**\n * The information about a particular server-side layout.\n */\nexport type LayoutMeta = Readonly<{\n path: string;\n}>;\n\nfunction strip(path: string) {\n return path.replace(/^\\/*(.+)\\/*$/u, '$1');\n}\n\n/**\n * Enables Flow layout flag on the matching routes based on the information from the layouts JSON file.\n *\n * @param routeMeta - The routes tree to process.\n * @param layoutsFile - The server layouts JSON file.\n * @returns Processed routes tree.\n */\nexport default async function applyLayouts(\n routeMeta: readonly RouteMeta[],\n layoutsFile: URL,\n): Promise<readonly RouteMeta[]> {\n try {\n const layoutContents = await readFile(layoutsFile, 'utf-8');\n const availableLayouts: readonly LayoutMeta[] = JSON.parse(layoutContents);\n const layoutPaths = availableLayouts.map((layout) => strip(layout.path));\n\n return transformTree<readonly RouteMeta[], readonly RouteMeta[], { path: string }>(\n routeMeta,\n { path: '' },\n (metas, next, ctx) =>\n metas.map((meta) => {\n const currentPath = join(ctx.path, strip(meta.path));\n const children = meta.children ? next(meta.children, { path: currentPath }) : undefined;\n\n return layoutPaths.some((path) => !relative(path, currentPath).startsWith('..'))\n ? { ...meta, flowLayout: true, children }\n : { ...meta, children };\n }),\n );\n } catch (e: unknown) {\n if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {\n return routeMeta;\n }\n\n throw e;\n }\n}\n"],"version":3}
1
+ {"mappings":"AAAA,SAAS,kCAAmC;AAC5C,SAAS,MAAM,iCAAkC;AACjD,SAAS,iDAAkD;AAE3D,SAAS,yBAA0B;;;;;;;;AAgBnC,eAAe,eAAe,aAC5BA,WACAC,aAC+B;AAC/B,KAAI;EACF,MAAM,iBAAiB,MAAM,SAAS,aAAa,QAAQ;EAC3D,MAAMC,mBAA0C,KAAK,MAAM,eAAe;EAC1E,MAAM,cAAc,iBAAiB,IAAI,CAAC,WAAW,MAAM,OAAO,KAAK,CAAC;AAExE,SAAO,cACL,WACA,EAAE,MAAM,GAAI,GACZ,CAAC,OAAO,MAAM,QACZ,MAAM,IAAI,CAAC,SAAS;GAClB,MAAM,cAAc,KAAK,IAAI,MAAM,MAAM,KAAK,KAAK,CAAC;GACpD,MAAM,WAAW,KAAK,WAAW,KAAK,KAAK,UAAU,EAAE,MAAM,YAAa,EAAC,GAAG;AAE9E,UAAO,YAAY,KAAK,CAAC,UAAU,SAAS,MAAM,YAAY,CAAC,WAAW,KAAK,CAAC,GAC5E;IAAE,GAAG;IAAM,YAAY;IAAM;GAAU,IACvC;IAAE,GAAG;IAAM;GAAU;EAC1B,EAAC,CACL;CACF,SAAQC,GAAY;AACnB,MAAI,aAAa,SAAS,UAAU,KAAK,EAAE,SAAS,UAAU;AAC5D,UAAO;EACR;AAED,QAAM;CACP;AACF","names":["routeMeta: readonly RouteMeta[]","layoutsFile: URL","availableLayouts: readonly LayoutMeta[]","e: unknown"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/applyLayouts.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path/posix';\nimport { transformTree } from '../shared/transformTree.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\nimport { strip } from './utils.js';\n\n/**\n * The information about a particular server-side layout.\n */\nexport type LayoutMeta = Readonly<{\n path: string;\n}>;\n\n/**\n * Enables Flow layout flag on the matching routes based on the information from the layouts JSON file.\n *\n * @param routeMeta - The routes tree to process.\n * @param layoutsFile - The server layouts JSON file.\n * @returns Processed routes tree.\n */\nexport default async function applyLayouts(\n routeMeta: readonly RouteMeta[],\n layoutsFile: URL,\n): Promise<readonly RouteMeta[]> {\n try {\n const layoutContents = await readFile(layoutsFile, 'utf-8');\n const availableLayouts: readonly LayoutMeta[] = JSON.parse(layoutContents);\n const layoutPaths = availableLayouts.map((layout) => strip(layout.path));\n\n return transformTree<readonly RouteMeta[], readonly RouteMeta[], { path: string }>(\n routeMeta,\n { path: '' },\n (metas, next, ctx) =>\n metas.map((meta) => {\n const currentPath = join(ctx.path, strip(meta.path));\n const children = meta.children ? next(meta.children, { path: currentPath }) : undefined;\n\n return layoutPaths.some((path) => !relative(path, currentPath).startsWith('..'))\n ? { ...meta, flowLayout: true, children }\n : { ...meta, children };\n }),\n );\n } catch (e: unknown) {\n if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {\n return routeMeta;\n }\n\n throw e;\n }\n}\n"],"version":3}
@@ -1,9 +1,11 @@
1
1
  import type { Logger } from "vite";
2
+ import type { ViewConfig } from "../types.js";
2
3
  export type RouteMeta = Readonly<{
3
4
  path: string
4
5
  file?: URL
5
6
  layout?: URL
6
7
  flowLayout?: boolean
8
+ config?: ViewConfig
7
9
  children?: readonly RouteMeta[]
8
10
  }>;
9
11
  /**
@@ -1 +1 @@
1
- {"mappings":"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","names":["url: URL | undefined","logger: Logger","dir: URL","children: RouteMeta[]","layout: URL | undefined"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/collectRoutesFromFS.ts"],"sourcesContent":["import { opendir, readFile } from 'node:fs/promises';\nimport { basename, extname, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Logger } from 'vite';\nimport { RouteParamType } from '../shared/routeParamType.js';\nimport { cleanUp, routeParamTypeMap } from './utils.js';\n\nexport type RouteMeta = Readonly<{\n path: string;\n file?: URL;\n layout?: URL;\n flowLayout?: boolean;\n children?: readonly RouteMeta[];\n}>;\n\n/**\n * Routes collector options.\n */\nexport type CollectRoutesOptions = Readonly<{\n /**\n * The list of extensions for files that will be collected as routes.\n */\n extensions: readonly string[];\n /**\n * The parent directory of the current directory. This is a\n * nested parameter used inside the function only.\n */\n parent?: URL;\n /**\n * The Vite logger instance.\n */\n logger: Logger;\n}>;\n\nasync function checkFile(url: URL | undefined, logger: Logger): Promise<URL | undefined> {\n if (url) {\n const contents = await readFile(url, 'utf-8');\n if (contents.trim() === '') {\n return undefined;\n } else if (!contents.includes('export default')) {\n logger.error(`The file \"${String(url)}\" should contain a default export of a component`);\n return undefined;\n }\n }\n\n return url;\n}\n\nconst collator = new Intl.Collator('en-US');\n\nconst warningFor = ['.ts', '.js'];\n\n/**\n * Collect route metadata from the file system and build a route tree.\n *\n * It accepts files that start with `@` as special files.\n * - `@layout` contains a component that wraps the child components.\n * - `@index` contains a component that will be used as the index page of the directory.\n *\n * It accepts files that start with `_` as private files. They will be ignored.\n *\n * @param dir - The directory to collect routes from.\n * @param options - The options object.\n *\n * @returns The route metadata array.\n */\nexport default async function collectRoutesFromFS(\n dir: URL,\n { extensions, logger, parent = dir }: CollectRoutesOptions,\n): Promise<readonly RouteMeta[]> {\n const path = relative(fileURLToPath(parent), fileURLToPath(dir));\n let children: RouteMeta[] = [];\n let layout: URL | undefined;\n\n for await (const d of await opendir(dir)) {\n if (d.name.startsWith('_')) {\n continue;\n }\n\n const extension = extname(d.name);\n const name = basename(d.name, extension);\n\n if (extension !== '' && !extensions.includes(extension)) {\n if (warningFor.includes(extension)) {\n logger.warn(\n `File System based router expects only JSX files in 'Frontend/views/' directory, such as '*.tsx' and '*.jsx'. The file '${d.name}' will be ignored by router, as it doesn't match this convention. Please consider storing it in another directory.`,\n );\n }\n continue;\n }\n\n if (children.some(({ path: p }) => p === name)) {\n throw new Error(`You cannot create a file and a directory with the same name (\"${name}\"). Use \"@index\" instead`);\n }\n\n if (d.isDirectory()) {\n const directoryRoutes = await collectRoutesFromFS(new URL(`${name}/`, dir), {\n extensions,\n logger,\n parent: dir,\n });\n if (directoryRoutes.length === 1 && directoryRoutes[0].layout) {\n const [layoutRoute] = directoryRoutes;\n children.push(layoutRoute);\n } else if (directoryRoutes.length > 0) {\n children.push({ path: name, children: directoryRoutes });\n }\n continue;\n }\n\n const file = new URL(d.name, dir);\n const url = await checkFile(file, logger);\n if (url === undefined) {\n continue;\n }\n const optionalParamType = routeParamTypeMap.get(RouteParamType.Optional)!;\n\n if (\n (name === '@index' && children.some(({ path: p }) => p.search(optionalParamType) >= 0)) ||\n (name.search(optionalParamType) >= 0 && children.some(({ path: p }) => p === ''))\n ) {\n throw new Error('You cannot create an `@index` file in a directory with optional parameters');\n } else if (name === '@layout') {\n layout = file;\n } else if (name === '@index') {\n children.push({\n path: '',\n file,\n });\n } else if (name.startsWith('@')) {\n throw new Error(\n 'Symbol \"@\" is reserved for special directories and files; only \"@layout\" and \"@index\" are allowed',\n );\n } else {\n children.push({\n path: name,\n file,\n });\n }\n }\n\n [children, layout] = await Promise.all([\n Promise.all(\n children.map(async (child) => ({\n ...child,\n file: child.file,\n layout: await checkFile(child.layout, logger),\n })),\n ),\n checkFile(layout, logger),\n ]);\n\n children = children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b)));\n\n // If a layout was found, wrap the other routes with the layout route.\n return layout ? [{ path, layout, children }] : children;\n}\n"],"version":3}
1
+ {"mappings":"AAAA,SAAS,SAAS,kCAAmC;AACrD,SAAS,UAAU,SAAS,2BAA4B;AACxD,SAAS,+BAAgC;AAEzC,SAAS,mDAAoD;AAE7D,SAAS,SAAS,qCAAsC;AA8BxD,eAAe,UAAUA,KAAsBC,QAA0C;AACvF,KAAI,KAAK;EACP,MAAM,WAAW,MAAM,SAAS,KAAK,QAAQ;AAC7C,MAAI,SAAS,MAAM,KAAK,IAAI;AAC1B,UAAO;EACR,YAAW,SAAS,SAAS,iBAAiB,EAAE;AAC/C,UAAO,OAAO,YAAY,OAAO,IAAI,CAAC,kDAAkD;AACxF,UAAO;EACR;CACF;AAED,QAAO;AACR;AAED,MAAM,WAAW,IAAI,KAAK,SAAS;AAEnC,MAAM,aAAa,CAAC,OAAO,KAAM;;;;;;;;;;;;;;;AAgBjC,eAAe,eAAe,oBAC5BC,KACA,EAAE,YAAY,QAAQ,SAAS,KAA2B,EAC3B;CAC/B,MAAM,OAAO,SAAS,cAAc,OAAO,EAAE,cAAc,IAAI,CAAC;CAChE,IAAIC,WAAwB,CAAE;CAC9B,IAAIC;AAEJ,YAAW,MAAM,KAAK,MAAM,QAAQ,IAAI,EAAE;AACxC,MAAI,EAAE,KAAK,WAAW,IAAI,EAAE;AAC1B;EACD;EAED,MAAM,YAAY,QAAQ,EAAE,KAAK;EACjC,MAAM,OAAO,SAAS,EAAE,MAAM,UAAU;AAExC,MAAI,cAAc,OAAO,WAAW,SAAS,UAAU,EAAE;AACvD,OAAI,WAAW,SAAS,UAAU,EAAE;AAClC,WAAO,MACJ,yHAAyH,EAAE,KAAK,oHAClI;GACF;AACD;EACD;AAED,MAAI,SAAS,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,MAAM,KAAK,EAAE;AAC9C,SAAM,IAAI,OAAO,gEAAgE,KAAK;EACvF;AAED,MAAI,EAAE,aAAa,EAAE;GACnB,MAAM,kBAAkB,MAAM,oBAAoB,IAAI,KAAK,EAAE,KAAK,IAAI,MAAM;IAC1E;IACA;IACA,QAAQ;GACT,EAAC;AACF,OAAI,gBAAgB,WAAW,KAAK,gBAAgB,GAAG,QAAQ;IAC7D,MAAM,CAAC,YAAY,GAAG;AACtB,aAAS,KAAK,YAAY;GAC3B,WAAU,gBAAgB,SAAS,GAAG;AACrC,aAAS,KAAK;KAAE,MAAM;KAAM,UAAU;IAAiB,EAAC;GACzD;AACD;EACD;EAED,MAAM,OAAO,IAAI,IAAI,EAAE,MAAM;EAC7B,MAAM,MAAM,MAAM,UAAU,MAAM,OAAO;AACzC,MAAI,QAAQ,WAAW;AACrB;EACD;EACD,MAAM,oBAAoB,kBAAkB,IAAI,eAAe,SAAS;AAExE,MACG,SAAS,YAAY,SAAS,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,OAAO,kBAAkB,IAAI,EAAE,IACrF,KAAK,OAAO,kBAAkB,IAAI,KAAK,SAAS,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,MAAM,GAAG,EAChF;AACA,SAAM,IAAI,MAAM;EACjB,WAAU,SAAS,WAAW;AAC7B,YAAS;EACV,WAAU,SAAS,UAAU;AAC5B,YAAS,KAAK;IACZ,MAAM;IACN;GACD,EAAC;EACH,WAAU,KAAK,WAAW,IAAI,EAAE;AAC/B,SAAM,IAAI,MACR;EAEH,OAAM;AACL,YAAS,KAAK;IACZ,MAAM;IACN;GACD,EAAC;EACH;CACF;AAED,EAAC,UAAU,OAAO,GAAG,MAAM,QAAQ,IAAI,CACrC,QAAQ,IACN,SAAS,IAAI,OAAO,WAAW;EAC7B,GAAG;EACH,MAAM,MAAM;EACZ,QAAQ,MAAM,UAAU,MAAM,QAAQ,OAAO;CAC9C,GAAE,CACJ,EACD,UAAU,QAAQ,OAAO,AAC1B,EAAC;AAEF,YAAW,SAAS,KAAK,CAAC,EAAE,MAAM,GAAG,EAAE,EAAE,MAAM,GAAG,KAAK,SAAS,QAAQ,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;AAGhG,QAAO,SAAS,CAAC;EAAE;EAAM;EAAQ;CAAU,CAAC,IAAG;AAChD","names":["url: URL | undefined","logger: Logger","dir: URL","children: RouteMeta[]","layout: URL | undefined"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/collectRoutesFromFS.ts"],"sourcesContent":["import { opendir, readFile } from 'node:fs/promises';\nimport { basename, extname, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Logger } from 'vite';\nimport { RouteParamType } from '../shared/routeParamType.js';\nimport type { ViewConfig } from '../types.js';\nimport { cleanUp, routeParamTypeMap } from './utils.js';\n\nexport type RouteMeta = Readonly<{\n path: string;\n file?: URL;\n layout?: URL;\n flowLayout?: boolean;\n config?: ViewConfig;\n children?: readonly RouteMeta[];\n}>;\n\n/**\n * Routes collector options.\n */\nexport type CollectRoutesOptions = Readonly<{\n /**\n * The list of extensions for files that will be collected as routes.\n */\n extensions: readonly string[];\n /**\n * The parent directory of the current directory. This is a\n * nested parameter used inside the function only.\n */\n parent?: URL;\n /**\n * The Vite logger instance.\n */\n logger: Logger;\n}>;\n\nasync function checkFile(url: URL | undefined, logger: Logger): Promise<URL | undefined> {\n if (url) {\n const contents = await readFile(url, 'utf-8');\n if (contents.trim() === '') {\n return undefined;\n } else if (!contents.includes('export default')) {\n logger.error(`The file \"${String(url)}\" should contain a default export of a component`);\n return undefined;\n }\n }\n\n return url;\n}\n\nconst collator = new Intl.Collator('en-US');\n\nconst warningFor = ['.ts', '.js'];\n\n/**\n * Collect route metadata from the file system and build a route tree.\n *\n * It accepts files that start with `@` as special files.\n * - `@layout` contains a component that wraps the child components.\n * - `@index` contains a component that will be used as the index page of the directory.\n *\n * It accepts files that start with `_` as private files. They will be ignored.\n *\n * @param dir - The directory to collect routes from.\n * @param options - The options object.\n *\n * @returns The route metadata array.\n */\nexport default async function collectRoutesFromFS(\n dir: URL,\n { extensions, logger, parent = dir }: CollectRoutesOptions,\n): Promise<readonly RouteMeta[]> {\n const path = relative(fileURLToPath(parent), fileURLToPath(dir));\n let children: RouteMeta[] = [];\n let layout: URL | undefined;\n\n for await (const d of await opendir(dir)) {\n if (d.name.startsWith('_')) {\n continue;\n }\n\n const extension = extname(d.name);\n const name = basename(d.name, extension);\n\n if (extension !== '' && !extensions.includes(extension)) {\n if (warningFor.includes(extension)) {\n logger.warn(\n `File System based router expects only JSX files in 'Frontend/views/' directory, such as '*.tsx' and '*.jsx'. The file '${d.name}' will be ignored by router, as it doesn't match this convention. Please consider storing it in another directory.`,\n );\n }\n continue;\n }\n\n if (children.some(({ path: p }) => p === name)) {\n throw new Error(`You cannot create a file and a directory with the same name (\"${name}\"). Use \"@index\" instead`);\n }\n\n if (d.isDirectory()) {\n const directoryRoutes = await collectRoutesFromFS(new URL(`${name}/`, dir), {\n extensions,\n logger,\n parent: dir,\n });\n if (directoryRoutes.length === 1 && directoryRoutes[0].layout) {\n const [layoutRoute] = directoryRoutes;\n children.push(layoutRoute);\n } else if (directoryRoutes.length > 0) {\n children.push({ path: name, children: directoryRoutes });\n }\n continue;\n }\n\n const file = new URL(d.name, dir);\n const url = await checkFile(file, logger);\n if (url === undefined) {\n continue;\n }\n const optionalParamType = routeParamTypeMap.get(RouteParamType.Optional)!;\n\n if (\n (name === '@index' && children.some(({ path: p }) => p.search(optionalParamType) >= 0)) ||\n (name.search(optionalParamType) >= 0 && children.some(({ path: p }) => p === ''))\n ) {\n throw new Error('You cannot create an `@index` file in a directory with optional parameters');\n } else if (name === '@layout') {\n layout = file;\n } else if (name === '@index') {\n children.push({\n path: '',\n file,\n });\n } else if (name.startsWith('@')) {\n throw new Error(\n 'Symbol \"@\" is reserved for special directories and files; only \"@layout\" and \"@index\" are allowed',\n );\n } else {\n children.push({\n path: name,\n file,\n });\n }\n }\n\n [children, layout] = await Promise.all([\n Promise.all(\n children.map(async (child) => ({\n ...child,\n file: child.file,\n layout: await checkFile(child.layout, logger),\n })),\n ),\n checkFile(layout, logger),\n ]);\n\n children = children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b)));\n\n // If a layout was found, wrap the other routes with the layout route.\n return layout ? [{ path, layout, children }] : children;\n}\n"],"version":3}
@@ -1,3 +1,4 @@
1
+ import type { ServerViewConfig } from "../shared/internal.js";
1
2
  import type { RouteMeta } from "./collectRoutesFromFS.js";
2
3
  import type { RuntimeFileUrls } from "./generateRuntimeFiles.js";
3
- export default function createRoutesFromMeta(views: readonly RouteMeta[], urls: RuntimeFileUrls): string;
4
+ export default function createRoutesFromMeta(views: readonly RouteMeta[], viewConfigs: readonly ServerViewConfig[], urls: RuntimeFileUrls): string;
@@ -1,22 +1,30 @@
1
- import { template, transform as transformer } from "@vaadin/hilla-generator-utils/ast.js";
2
1
  import createSourceFile from "@vaadin/hilla-generator-utils/createSourceFile.js";
3
2
  import DependencyManager from "@vaadin/hilla-generator-utils/dependencies/DependencyManager.js";
4
3
  import PathManager from "@vaadin/hilla-generator-utils/dependencies/PathManager.js";
5
- import ts from "typescript";
4
+ import ast from "tsc-template";
5
+ import { createPrinter, factory, NewLineKind } from "typescript";
6
6
  import { transformTree } from "../shared/transformTree.js";
7
- import { convertFSRouteSegmentToURLPatternFormat } from "./utils.js";
8
- const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
7
+ import { convertFSRouteSegmentToURLPatternFormat, strip } from "./utils.js";
8
+ const printer = createPrinter({ newLine: NewLineKind.LineFeed });
9
9
  const fileExtensions = [
10
10
  ".ts",
11
11
  ".tsx",
12
12
  ".js",
13
13
  ".jsx"
14
14
  ];
15
+ const HILLA_FILE_ROUTER = "@vaadin/hilla-file-router";
16
+ const HILLA_FILE_ROUTER_RUNTIME = `${HILLA_FILE_ROUTER}/runtime.js`;
17
+ const HILLA_FILE_ROUTER_TYPES = `${HILLA_FILE_ROUTER}/types.js`;
18
+ function isLazyModule(mod) {
19
+ return !!mod && "path" in mod;
20
+ }
15
21
  class RouteFromMetaProcessor {
16
22
  #manager;
17
23
  #views;
18
- constructor(views, { code: codeFile }) {
24
+ #configs;
25
+ constructor(views, viewConfigs, { code: codeFile }) {
19
26
  this.#views = views;
27
+ this.#configs = viewConfigs;
20
28
  const codeDir = new URL("./", codeFile);
21
29
  this.#manager = new DependencyManager(new PathManager({
22
30
  extension: ".js",
@@ -25,40 +33,54 @@ class RouteFromMetaProcessor {
25
33
  }
26
34
  /**
27
35
  * Loads all the files from the received metadata and creates a framework-agnostic route tree.
28
- *
29
- * @param views - The abstract route tree.
30
- * @param generatedDir - The directory where the generated view file will be stored.
31
36
  */
32
37
  process() {
33
- const { paths, imports: { named, namespace } } = this.#manager;
38
+ const { paths, imports: { namespace, named } } = this.#manager;
34
39
  const errors = [];
35
- const routes = transformTree(this.#views, null, (metas, next) => {
40
+ const routes = transformTree([this.#views, this.#configs], null, ([metas, configs], next) => {
36
41
  errors.push(...metas.map((route) => route.path).filter((item, index, arr) => arr.indexOf(item) !== index).map((dup) => `console.error("Two views share the same path: ${dup}");`));
37
- return metas.map(({ file, layout, path, children, flowLayout }) => {
42
+ return metas.map(({ file, layout, path, children, flowLayout }, index) => {
43
+ const config = configs[index];
38
44
  let _children;
39
45
  if (children) {
40
- _children = next(children);
46
+ _children = next([children, config.children]);
41
47
  }
42
- let mod;
48
+ let module;
49
+ let relativePath;
50
+ let fileName;
43
51
  if (file) {
44
52
  const fileExt = fileExtensions.find((ext) => file.pathname.endsWith(ext));
45
- mod = namespace.add(paths.createRelativePath(file, fileExt), `Page`);
53
+ relativePath = paths.createRelativePath(file, fileExt);
54
+ fileName = "Page";
46
55
  } else if (layout) {
47
56
  const fileExt = fileExtensions.find((ext) => layout.pathname.endsWith(ext));
48
- mod = namespace.add(paths.createRelativePath(layout, fileExt), `Layout`);
57
+ relativePath = paths.createRelativePath(layout, fileExt);
58
+ fileName = "Layout";
59
+ }
60
+ if (relativePath && fileName) {
61
+ if (this.#isLazy(path, config)) {
62
+ module = {
63
+ path: relativePath,
64
+ config,
65
+ lazyId: named.getIdentifier("react", "lazy") ?? named.add("react", "lazy")
66
+ };
67
+ } else {
68
+ const mod = namespace.add(relativePath, fileName);
69
+ const reactModuleType = named.getIdentifier(HILLA_FILE_ROUTER_TYPES, "RouteModule") ?? named.add(HILLA_FILE_ROUTER_TYPES, "RouteModule", true);
70
+ module = {
71
+ componentId: ast`${mod}.default`.node,
72
+ configId: ast`(${mod} as ${reactModuleType}).config`.node,
73
+ flowLayout
74
+ };
75
+ }
49
76
  }
50
- const moduleExtension = flowLayout ? { flowLayout } : undefined;
51
- return this.#createRouteData(convertFSRouteSegmentToURLPatternFormat(path), mod, moduleExtension, _children);
77
+ return this.#createRouteData(convertFSRouteSegmentToURLPatternFormat(path), module, _children);
52
78
  });
53
79
  });
54
- const agnosticRouteId = named.getIdentifier("@vaadin/hilla-file-router/types.js", "AgnosticRoute") ?? named.add("@vaadin/hilla-file-router/types.js", "AgnosticRoute", true);
55
- let routeDeclaration = template(`${errors.join("\n")}
56
-
57
- const routes: readonly AGNOSTIC_ROUTE[] = ROUTE;
58
-
59
- export default routes;
60
- `, [transformer((node) => ts.isIdentifier(node) && node.text === "ROUTE" ? ts.factory.createArrayLiteralExpression(routes, true) : node), transformer((node) => ts.isIdentifier(node) && node.text === "AGNOSTIC_ROUTE" ? agnosticRouteId : node)]);
61
- routeDeclaration = [...this.#manager.imports.toCode(), ...routeDeclaration];
80
+ const agnosticRouteId = named.getIdentifier(HILLA_FILE_ROUTER_TYPES, "AgnosticRoute") ?? named.add(HILLA_FILE_ROUTER_TYPES, "AgnosticRoute", true);
81
+ const routeDeclaration = [...this.#manager.imports.toCode(), ...ast`${errors.join("\n")}
82
+ const routes: readonly ${agnosticRouteId}[] = ${factory.createArrayLiteralExpression(routes, true)};
83
+ export default routes;`.source.statements];
62
84
  const file = createSourceFile(routeDeclaration, "file-routes.ts");
63
85
  return printer.printFile(file);
64
86
  }
@@ -67,29 +89,34 @@ export default routes;
67
89
  * create a route tree.
68
90
  *
69
91
  * @param path - The path of the route.
70
- * @param mod - The name of the route module imported as a namespace.
92
+ * @param module - The module to build route from.
71
93
  * @param children - The list of child route call expressions.
72
94
  */
73
- #createRouteData(path, mod, extension, children) {
95
+ #createRouteData(path, module, children) {
74
96
  const { named } = this.#manager.imports;
75
- let extendModuleId;
76
- let modNode = "";
77
- if (extension) {
78
- extendModuleId = named.getIdentifier("@vaadin/hilla-file-router/runtime.js", "extendModule") ?? named.add("@vaadin/hilla-file-router/runtime.js", "extendModule");
79
- modNode = `, EXTEND_MODULE(MOD, ${JSON.stringify(extension)})`;
80
- } else if (mod) {
81
- modNode = `, MOD`;
97
+ const createRouteId = named.getIdentifier(HILLA_FILE_ROUTER_RUNTIME, "createRoute") ?? named.add(HILLA_FILE_ROUTER_RUNTIME, "createRoute");
98
+ let component;
99
+ let config;
100
+ if (isLazyModule(module)) {
101
+ const { children: _c, params: _p,...viewConfig } = module.config;
102
+ component = ast`${module.lazyId}(() => import('${module.path}')`.node;
103
+ config = Object.keys(viewConfig).length > 0 ? JSON.stringify(viewConfig) : "";
104
+ } else if (module) {
105
+ component = module.componentId;
106
+ config = module.flowLayout ? ast`const a = %{ { ...${module.configId}, flowLayout: ${module.flowLayout.toString()} } }%`.node : module.configId;
107
+ }
108
+ const _children = children && children.length > 0 ? factory.createArrayLiteralExpression(children, true) : "";
109
+ return ast`${createRouteId}("${path}", ${component ?? ""}, ${config ?? ""}, ${_children})`.node;
110
+ }
111
+ #isLazy(path, config) {
112
+ const strippedPath = strip(path);
113
+ if (this.#configs.includes(config) && (strippedPath === "" || strippedPath === "login")) {
114
+ return false;
82
115
  }
83
- const createRouteId = named.getIdentifier("@vaadin/hilla-file-router/runtime.js", "createRoute") ?? named.add("@vaadin/hilla-file-router/runtime.js", "createRoute");
84
- return template(`const route = CREATE_ROUTE("${path}", ${modNode}${children ? `, CHILDREN` : ""})`, ([statement]) => statement.declarationList.declarations[0].initializer, [
85
- transformer((node) => ts.isIdentifier(node) && node.text === "CHILDREN" ? ts.factory.createArrayLiteralExpression(children, true) : node),
86
- transformer((node) => ts.isIdentifier(node) && node.text === "MOD" ? mod ?? ts.factory.createNull() : node),
87
- transformer((node) => ts.isIdentifier(node) && node.text === "EXTEND_MODULE" ? extendModuleId : node),
88
- transformer((node) => ts.isIdentifier(node) && node.text === "CREATE_ROUTE" ? createRouteId : node)
89
- ]);
116
+ return config.lazy ?? true;
90
117
  }
91
118
  }
92
- export default function createRoutesFromMeta(views, urls) {
93
- return new RouteFromMetaProcessor(views, urls).process();
119
+ export default function createRoutesFromMeta(views, viewConfigs, urls) {
120
+ return new RouteFromMetaProcessor(views, viewConfigs, urls).process();
94
121
  }
95
122
  //# sourceMappingURL=./createRoutesFromMeta.js.map
@@ -1 +1 @@
1
- {"mappings":"AAAA,SAAS,UAAU,aAAa,yDAA0D;AAC1F,OAAO,yEAA0E;AACjF,OAAO,wFAAyF;AAChG,OAAO,4EAA6E;AACpF,OAAO,oBAAuF;AAE9F,SAAS,iDAAkD;AAG3D,SAAS,2DAA4D;AAErE,MAAM,UAAU,GAAG,cAAc,EAAE,SAAS,GAAG,YAAY,SAAU,EAAC;AAEtE,MAAM,iBAAiB;CAAC;CAAO;CAAQ;CAAO;AAAO;AAErD,MAAM,uBAAuB;CAC3B,AAASA;CACT,AAASC;CAET,YAAYC,OAA6B,EAAE,MAAM,UAA2B,EAAE;AAC5E,OAAKD,SAAS;EAEd,MAAM,UAAU,IAAI,IAAI,MAAM;AAC9B,OAAKD,WAAW,IAAI,kBAAkB,IAAI,YAAY;GAAE,WAAW;GAAO,YAAY;EAAS;CAChG;;;;;;;CAQD,UAAkB;EAChB,MAAM,EACJ,OACA,SAAS,EAAE,OAAO,WAAW,EAC9B,GAAG,KAAKA;EACT,MAAMG,SAAmB,CAAE;EAE3B,MAAM,SAAS,cAA+D,KAAKF,QAAQ,MAAM,CAAC,OAAO,SAAS;AAChH,UAAO,KACL,GAAG,MACA,IAAI,CAAC,UAAU,MAAM,KAAK,CAC1B,OAAO,CAAC,MAAM,OAAO,QAAQ,IAAI,QAAQ,KAAK,KAAK,MAAM,CACzD,IAAI,CAAC,SAAS,gDAAgD,IAAI,KAAK,CAC3E;AAED,UAAO,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,MAAM,UAAU,YAAY,KAAK;IACjE,IAAIG;AAEJ,QAAI,UAAU;AACZ,iBAAY,KAAK,SAAS;IAC3B;IAED,IAAIC;AACJ,QAAI,MAAM;KACR,MAAM,UAAU,eAAe,KAAK,CAAC,QAAQ,KAAK,SAAS,SAAS,IAAI,CAAC;AACzE,WAAM,UAAU,IAAI,MAAM,mBAAmB,MAAM,QAAQ,GAAG,MAAM;IACrE,WAAU,QAAQ;KACjB,MAAM,UAAU,eAAe,KAAK,CAAC,QAAQ,OAAO,SAAS,SAAS,IAAI,CAAC;AAC3E,WAAM,UAAU,IAAI,MAAM,mBAAmB,QAAQ,QAAQ,GAAG,QAAQ;IACzE;IAED,MAAM,kBAAkB,aAAa,EAAE,WAAY,IAAG;AAEtD,WAAO,KAAKC,iBAAiB,wCAAwC,KAAK,EAAE,KAAK,iBAAiB,UAAU;GAC7G,EAAC;EACH,EAAC;EAEF,MAAM,kBACJ,MAAM,cAAc,sCAAsC,gBAAgB,IAC1E,MAAM,IAAI,sCAAsC,iBAAiB,KAAK;EAExE,IAAI,mBAAmB,UACpB,EAAE,OAAO,KAAK,KAAK,CAAC;;;;;GAMrB,CACE,YAAY,CAAC,SACX,GAAG,aAAa,KAAK,IAAI,KAAK,SAAS,UAAU,GAAG,QAAQ,6BAA6B,QAAQ,KAAK,GAAG,KAC1G,EACD,YAAY,CAAC,SAAU,GAAG,aAAa,KAAK,IAAI,KAAK,SAAS,mBAAmB,kBAAkB,KAAM,AAC1G,EACF;AAED,qBAAmB,CAAC,GAAG,KAAKN,SAAS,QAAQ,QAAQ,EAAE,GAAG,gBAAiB;EAE3E,MAAM,OAAO,iBAAiB,kBAAkB,iBAAiB;AACjE,SAAO,QAAQ,UAAU,KAAK;CAC/B;;;;;;;;;CAUD,iBACEO,MACAF,KACAG,WACAC,UACgB;EAChB,MAAM,EAAE,OAAO,GAAG,KAAKT,SAAS;EAEhC,IAAIU;EACJ,IAAI,UAAU;AAEd,MAAI,WAAW;AACb,oBACE,MAAM,cAAc,wCAAwC,eAAe,IAC3E,MAAM,IAAI,wCAAwC,eAAe;AACnE,cAAW,uBAAuB,KAAK,UAAU,UAAU,CAAC;EAC7D,WAAU,KAAK;AACd,cAAW;EACZ;EAED,MAAM,gBACJ,MAAM,cAAc,wCAAwC,cAAc,IAC1E,MAAM,IAAI,wCAAwC,cAAc;AAElE,SAAO,UACJ,8BAA8B,KAAK,KAAK,QAAQ,EAAE,YAAY,cAAc,GAAG,IAChF,CAAC,CAAC,UAAU,KAAM,UAAgC,gBAAgB,aAAa,GAAG,aAClF;GACE,YAAY,CAAC,SACX,GAAG,aAAa,KAAK,IAAI,KAAK,SAAS,aACnC,GAAG,QAAQ,6BAA6B,UAAU,KAAK,GACvD,KACL;GACD,YAAY,CAAC,SAAU,GAAG,aAAa,KAAK,IAAI,KAAK,SAAS,QAAS,OAAO,GAAG,QAAQ,YAAY,GAAI,KAAM;GAC/G,YAAY,CAAC,SAAU,GAAG,aAAa,KAAK,IAAI,KAAK,SAAS,kBAAkB,iBAAiB,KAAM;GACvG,YAAY,CAAC,SAAU,GAAG,aAAa,KAAK,IAAI,KAAK,SAAS,iBAAiB,gBAAgB,KAAM;EACtG,EACF;CACF;AACF;AAED,eAAe,SAAS,qBAAqBR,OAA6BS,MAA+B;AACvG,QAAO,IAAI,uBAAuB,OAAO,MAAM,SAAS;AACzD","names":["#manager","#views","views: readonly RouteMeta[]","errors: string[]","_children: readonly CallExpression[] | undefined","mod: Identifier | undefined","#createRouteData","path: string","extension?: Readonly<Record<string, unknown>>","children?: readonly CallExpression[]","extendModuleId: Identifier | undefined","urls: RuntimeFileUrls"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/createRoutesFromMeta.ts"],"sourcesContent":["import { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js';\nimport createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';\nimport DependencyManager from '@vaadin/hilla-generator-utils/dependencies/DependencyManager.js';\nimport PathManager from '@vaadin/hilla-generator-utils/dependencies/PathManager.js';\nimport ts, { type CallExpression, type Identifier, type VariableStatement } from 'typescript';\n\nimport { transformTree } from '../shared/transformTree.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\nimport type { RuntimeFileUrls } from './generateRuntimeFiles.js';\nimport { convertFSRouteSegmentToURLPatternFormat } from './utils.js';\n\nconst printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });\n\nconst fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];\n\nclass RouteFromMetaProcessor {\n readonly #manager: DependencyManager;\n readonly #views: readonly RouteMeta[];\n\n constructor(views: readonly RouteMeta[], { code: codeFile }: RuntimeFileUrls) {\n this.#views = views;\n\n const codeDir = new URL('./', codeFile);\n this.#manager = new DependencyManager(new PathManager({ extension: '.js', relativeTo: codeDir }));\n }\n\n /**\n * Loads all the files from the received metadata and creates a framework-agnostic route tree.\n *\n * @param views - The abstract route tree.\n * @param generatedDir - The directory where the generated view file will be stored.\n */\n process(): string {\n const {\n paths,\n imports: { named, namespace },\n } = this.#manager;\n const errors: string[] = [];\n\n const routes = transformTree<readonly RouteMeta[], readonly CallExpression[]>(this.#views, null, (metas, next) => {\n errors.push(\n ...metas\n .map((route) => route.path)\n .filter((item, index, arr) => arr.indexOf(item) !== index)\n .map((dup) => `console.error(\"Two views share the same path: ${dup}\");`),\n );\n\n return metas.map(({ file, layout, path, children, flowLayout }) => {\n let _children: readonly CallExpression[] | undefined;\n\n if (children) {\n _children = next(children);\n }\n\n let mod: Identifier | undefined;\n if (file) {\n const fileExt = fileExtensions.find((ext) => file.pathname.endsWith(ext));\n mod = namespace.add(paths.createRelativePath(file, fileExt), `Page`);\n } else if (layout) {\n const fileExt = fileExtensions.find((ext) => layout.pathname.endsWith(ext));\n mod = namespace.add(paths.createRelativePath(layout, fileExt), `Layout`);\n }\n\n const moduleExtension = flowLayout ? { flowLayout } : undefined;\n\n return this.#createRouteData(convertFSRouteSegmentToURLPatternFormat(path), mod, moduleExtension, _children);\n });\n });\n\n const agnosticRouteId =\n named.getIdentifier('@vaadin/hilla-file-router/types.js', 'AgnosticRoute') ??\n named.add('@vaadin/hilla-file-router/types.js', 'AgnosticRoute', true);\n\n let routeDeclaration = template(\n `${errors.join('\\n')}\n\nconst routes: readonly AGNOSTIC_ROUTE[] = ROUTE;\n\nexport default routes;\n`,\n [\n transformer((node) =>\n ts.isIdentifier(node) && node.text === 'ROUTE' ? ts.factory.createArrayLiteralExpression(routes, true) : node,\n ),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'AGNOSTIC_ROUTE' ? agnosticRouteId : node)),\n ],\n );\n\n routeDeclaration = [...this.#manager.imports.toCode(), ...routeDeclaration];\n\n const file = createSourceFile(routeDeclaration, 'file-routes.ts');\n return printer.printFile(file);\n }\n\n /**\n * Create an abstract route creation function call. The nested function calls\n * create a route tree.\n *\n * @param path - The path of the route.\n * @param mod - The name of the route module imported as a namespace.\n * @param children - The list of child route call expressions.\n */\n #createRouteData(\n path: string,\n mod: Identifier | undefined,\n extension?: Readonly<Record<string, unknown>>,\n children?: readonly CallExpression[],\n ): CallExpression {\n const { named } = this.#manager.imports;\n\n let extendModuleId: Identifier | undefined;\n let modNode = '';\n\n if (extension) {\n extendModuleId =\n named.getIdentifier('@vaadin/hilla-file-router/runtime.js', 'extendModule') ??\n named.add('@vaadin/hilla-file-router/runtime.js', 'extendModule');\n modNode = `, EXTEND_MODULE(MOD, ${JSON.stringify(extension)})`;\n } else if (mod) {\n modNode = `, MOD`;\n }\n\n const createRouteId =\n named.getIdentifier('@vaadin/hilla-file-router/runtime.js', 'createRoute') ??\n named.add('@vaadin/hilla-file-router/runtime.js', 'createRoute');\n\n return template(\n `const route = CREATE_ROUTE(\"${path}\", ${modNode}${children ? `, CHILDREN` : ''})`,\n ([statement]) => (statement as VariableStatement).declarationList.declarations[0].initializer as CallExpression,\n [\n transformer((node) =>\n ts.isIdentifier(node) && node.text === 'CHILDREN'\n ? ts.factory.createArrayLiteralExpression(children, true)\n : node,\n ),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'MOD' ? (mod ?? ts.factory.createNull()) : node)),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'EXTEND_MODULE' ? extendModuleId : node)),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'CREATE_ROUTE' ? createRouteId : node)),\n ],\n );\n }\n}\n\nexport default function createRoutesFromMeta(views: readonly RouteMeta[], urls: RuntimeFileUrls): string {\n return new RouteFromMetaProcessor(views, urls).process();\n}\n"],"version":3}
1
+ {"mappings":"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","names":["mod: LazyModule | RegularModule | undefined","#manager","#views","#configs","views: readonly RouteMeta[]","viewConfigs: readonly ServerViewConfig[]","errors: string[]","_children: readonly CallExpression[] | undefined","module: LazyModule | RegularModule | undefined","relativePath: string | undefined","fileName: string | undefined","#isLazy","#createRouteData","path: string","children: readonly CallExpression[] | undefined","component: PropertyAccessExpression | CallExpression | undefined","config: PropertyAccessExpression | ObjectLiteralExpression | string | undefined","config: ServerViewConfig","urls: RuntimeFileUrls"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/createRoutesFromMeta.ts"],"sourcesContent":["import createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';\nimport DependencyManager from '@vaadin/hilla-generator-utils/dependencies/DependencyManager.js';\nimport PathManager from '@vaadin/hilla-generator-utils/dependencies/PathManager.js';\nimport ast from 'tsc-template';\nimport {\n type CallExpression,\n createPrinter,\n factory,\n type Identifier,\n NewLineKind,\n type ObjectLiteralExpression,\n type PropertyAccessExpression,\n} from 'typescript';\nimport type { ServerViewConfig } from '../shared/internal.js';\nimport { transformTree } from '../shared/transformTree.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\nimport type { RuntimeFileUrls } from './generateRuntimeFiles.js';\nimport { convertFSRouteSegmentToURLPatternFormat, strip } from './utils.js';\n\nconst printer = createPrinter({ newLine: NewLineKind.LineFeed });\n\nconst fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];\n\nconst HILLA_FILE_ROUTER = '@vaadin/hilla-file-router';\nconst HILLA_FILE_ROUTER_RUNTIME = `${HILLA_FILE_ROUTER}/runtime.js`;\nconst HILLA_FILE_ROUTER_TYPES = `${HILLA_FILE_ROUTER}/types.js`;\n\ntype LazyModule = Readonly<{\n path: string;\n config: ServerViewConfig;\n lazyId: Identifier;\n}>;\n\ntype RegularModule = Readonly<{\n componentId: PropertyAccessExpression;\n configId: PropertyAccessExpression;\n flowLayout?: boolean;\n}>;\n\nfunction isLazyModule(mod: LazyModule | RegularModule | undefined): mod is LazyModule {\n return !!mod && 'path' in mod;\n}\n\nclass RouteFromMetaProcessor {\n readonly #manager: DependencyManager;\n readonly #views: readonly RouteMeta[];\n readonly #configs: readonly ServerViewConfig[];\n\n constructor(\n views: readonly RouteMeta[],\n viewConfigs: readonly ServerViewConfig[],\n { code: codeFile }: RuntimeFileUrls,\n ) {\n this.#views = views;\n this.#configs = viewConfigs;\n\n const codeDir = new URL('./', codeFile);\n this.#manager = new DependencyManager(new PathManager({ extension: '.js', relativeTo: codeDir }));\n }\n\n /**\n * Loads all the files from the received metadata and creates a framework-agnostic route tree.\n */\n process(): string {\n const {\n paths,\n imports: { namespace, named },\n } = this.#manager;\n const errors: string[] = [];\n\n const routes = transformTree<\n [metas: readonly RouteMeta[], configs: readonly ServerViewConfig[]],\n readonly CallExpression[]\n >([this.#views, this.#configs], null, ([metas, configs], next) => {\n errors.push(\n ...metas\n .map((route) => route.path)\n .filter((item, index, arr) => arr.indexOf(item) !== index)\n .map((dup) => `console.error(\"Two views share the same path: ${dup}\");`),\n );\n\n return metas.map(({ file, layout, path, children, flowLayout }, index) => {\n const config = configs[index];\n let _children: readonly CallExpression[] | undefined;\n\n if (children) {\n // Assuming that if we have `children` in the route, we also have\n // `children` in the config.\n _children = next([children, config.children!]);\n }\n\n let module: LazyModule | RegularModule | undefined;\n\n let relativePath: string | undefined;\n let fileName: string | undefined;\n\n if (file) {\n const fileExt = fileExtensions.find((ext) => file.pathname.endsWith(ext));\n relativePath = paths.createRelativePath(file, fileExt);\n fileName = 'Page';\n } else if (layout) {\n const fileExt = fileExtensions.find((ext) => layout.pathname.endsWith(ext));\n relativePath = paths.createRelativePath(layout, fileExt);\n fileName = 'Layout';\n }\n\n if (relativePath && fileName) {\n if (this.#isLazy(path, config)) {\n module = {\n path: relativePath,\n config,\n lazyId: named.getIdentifier('react', 'lazy') ?? named.add('react', 'lazy'),\n };\n } else {\n const mod = namespace.add(relativePath, fileName);\n const reactModuleType =\n named.getIdentifier(HILLA_FILE_ROUTER_TYPES, 'RouteModule') ??\n named.add(HILLA_FILE_ROUTER_TYPES, 'RouteModule', true);\n\n module = {\n componentId: ast`${mod}.default`.node as PropertyAccessExpression,\n configId: ast`(${mod} as ${reactModuleType}).config`.node as PropertyAccessExpression,\n flowLayout,\n };\n }\n }\n\n return this.#createRouteData(convertFSRouteSegmentToURLPatternFormat(path), module, _children);\n });\n });\n\n const agnosticRouteId =\n named.getIdentifier(HILLA_FILE_ROUTER_TYPES, 'AgnosticRoute') ??\n named.add(HILLA_FILE_ROUTER_TYPES, 'AgnosticRoute', true);\n const routeDeclaration = [\n ...this.#manager.imports.toCode(),\n ...ast`${errors.join('\\n')}\nconst routes: readonly ${agnosticRouteId}[] = ${factory.createArrayLiteralExpression(routes, true)};\nexport default routes;`.source.statements,\n ];\n\n const file = createSourceFile(routeDeclaration, 'file-routes.ts');\n return printer.printFile(file);\n }\n\n /**\n * Create an abstract route creation function call. The nested function calls\n * create a route tree.\n *\n * @param path - The path of the route.\n * @param module - The module to build route from.\n * @param children - The list of child route call expressions.\n */\n #createRouteData(\n path: string,\n module: LazyModule | RegularModule | undefined,\n children: readonly CallExpression[] | undefined,\n ): CallExpression {\n const { named } = this.#manager.imports;\n\n const createRouteId =\n named.getIdentifier(HILLA_FILE_ROUTER_RUNTIME, 'createRoute') ??\n named.add(HILLA_FILE_ROUTER_RUNTIME, 'createRoute');\n\n let component: PropertyAccessExpression | CallExpression | undefined;\n let config: PropertyAccessExpression | ObjectLiteralExpression | string | undefined;\n\n if (isLazyModule(module)) {\n const { children: _c, params: _p, ...viewConfig } = module.config;\n\n component = ast`${module.lazyId}(() => import('${module.path}')`.node as CallExpression;\n config = Object.keys(viewConfig).length > 0 ? JSON.stringify(viewConfig) : '';\n } else if (module) {\n component = module.componentId;\n config = module.flowLayout\n ? (ast`const a = %{ { ...${module.configId}, flowLayout: ${module.flowLayout.toString()} } }%`\n .node as ObjectLiteralExpression)\n : module.configId;\n }\n\n const _children = children && children.length > 0 ? factory.createArrayLiteralExpression(children, true) : '';\n\n // ```ts\n // createRoute(\"grandparent\", {...grandparentConfig, flowLayout: true}, [\n // createRoute(\"parent\", ParentPage, parentConfig, [\n // createRoute(\"child\", lazy(() => import('../ChildPage.js')), { title: 'Child Page' }),\n // ]),\n // ]),\n // ```\n return ast`${createRouteId}(\"${path}\", ${component ?? ''}, ${config ?? ''}, ${_children})`.node as CallExpression;\n }\n\n #isLazy(path: string, config: ServerViewConfig): boolean {\n const strippedPath = strip(path);\n\n if (this.#configs.includes(config) && (strippedPath === '' || strippedPath === 'login')) {\n return false;\n }\n\n return config.lazy ?? true;\n }\n}\n\nexport default function createRoutesFromMeta(\n views: readonly RouteMeta[],\n viewConfigs: readonly ServerViewConfig[],\n urls: RuntimeFileUrls,\n): string {\n return new RouteFromMetaProcessor(views, viewConfigs, urls).process();\n}\n"],"version":3}
@@ -1,8 +1,10 @@
1
+ import type { ServerViewConfig } from "../shared/internal.js";
1
2
  import type { RouteMeta } from "./collectRoutesFromFS.js";
2
3
  /**
3
4
  * Creates a map of all leaf routes to their configuration. This file is used by the server to provide server-side
4
5
  * routes along with managing the client-side routes.
5
6
  *
6
7
  * @param views - The route metadata tree.
8
+ * @returns A view configuration tree.
7
9
  */
8
- export default function createViewConfigJson(views: readonly RouteMeta[]): Promise<string>;
10
+ export default function createViewConfigJson(views: readonly RouteMeta[]): Promise<readonly ServerViewConfig[]>;
@@ -20,9 +20,10 @@ function* walkAST(node) {
20
20
  * routes along with managing the client-side routes.
21
21
  *
22
22
  * @param views - The route metadata tree.
23
+ * @returns A view configuration tree.
23
24
  */
24
25
  export default async function createViewConfigJson(views) {
25
- const res = await transformTree(views, null, async (routes, next) => await Promise.all(routes.map(async ({ path, file, layout, children, flowLayout }) => {
26
+ return await transformTree(views, null, async (routes, next) => await Promise.all(routes.map(async ({ path, file, layout, children, flowLayout }) => {
26
27
  const newChildren = children ? await next(children) : undefined;
27
28
  if (!file && !layout) {
28
29
  return {
@@ -72,6 +73,5 @@ export default async function createViewConfigJson(views) {
72
73
  children: newChildren ?? (layout ? [] : undefined)
73
74
  };
74
75
  })));
75
- return JSON.stringify(res, undefined, 2);
76
76
  }
77
77
  //# sourceMappingURL=./createViewConfigJson.js.map
@@ -1 +1 @@
1
- {"mappings":"AAAA,SAAS,kCAAmC;AAC5C,SAAS,uBAAwB;AACjC,OAAO,oBAAoC;AAC3C,SAAS,6EAA8E;AAEvF,SAAS,iDAAkD;AAG3D,SAAS,yCAAyC,oDAAqD;;;;;;AAOvG,UAAU,QAAQA,MAA6B;AAC7C,OAAM;AAEN,MAAK,MAAM,SAAS,KAAK,aAAa,EAAE;AACtC,SAAO,QAAQ,MAAM;CACtB;AACF;;;;;;;AAQD,eAAe,eAAe,qBAAqBC,OAA8C;CAC/F,MAAM,MAAM,MAAM,cAChB,OACA,MACA,OAAO,QAAQ,SACb,MAAM,QAAQ,IACZ,OAAO,IAAI,OAAO,EAAE,MAAM,MAAM,QAAQ,UAAU,YAAY,KAAK;EACjE,MAAM,cAAc,WAAW,MAAM,KAAK,SAAS,GAAG;AAEtD,OAAK,SAAS,QAAQ;AACpB,UAAO;IACL,OAAO,wCAAwC,KAAK;IACpD,QAAQ,iCAAiC,KAAK;IAC9C,UAAU;GACX;EACF;EAED,MAAM,aAAa,GAAG,iBACpB,QACA,MAAM,SAAS,QAAQ,QAAS,OAAO,EACvC,GAAG,aAAa,QAChB,KACD;EACD,IAAIC;EACJ,IAAI,uBAAuB;EAC3B,IAAIC;AAEJ,OAAK,MAAM,QAAQ,QAAQ,WAAW,EAAE;AACtC,OAAI,GAAG,sBAAsB,KAAK,IAAI,GAAG,aAAa,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS,UAAU;AAC/F,QAAI,KAAK,eAAe,GAAG,0BAA0B,KAAK,YAAY,EAAE;KACtE,MAAM,OAAO,KAAK,YAAY,QAAQ,WAAW;KACjD,MAAM,SAAS,IAAI,QAAQ,GAAG,KAAK;AACnC,cAAS,OAAO,kBAAkB;AAClC,SAAI,OAAO,eAAe,WAAW;MACnC,MAAM,OAAO,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC;AAE/C,WAAK,aAAa,cAAc;AAChC,eAAS;KACV;IACF;GACF,WAAU,KAAK,QAAQ,WAAW,CAAC,WAAW,iBAAiB,EAAE;AAChE,2BAAuB;GACxB,WAAU,wBAAwB,GAAG,aAAa,KAAK,EAAE;AACxD,oBAAgB,KAAK;AACrB,2BAAuB;GACxB;EACF;AAED,aAAW,EAAE,YAAY,cAAc,MAAO;EAE9C,IAAIC;AAEJ,MAAI,OAAO,OAAO;AAChB,IAAC,CAAE,MAAO,GAAG;EACd,OAAM;AACL,QAAK,eAAe;AAClB,UAAM,IAAI,OACP,YAAY,OAAO,QAAQ,OAAQ,CAAC;GAExC;AAED,WAAQ,4BAA4B,cAAc;EACnD;AAED,SAAO;GACL,OAAO,wCAAwC,KAAK;GACpD,GAAG;GACH,QAAQ,iCAAiC,OAAO,SAAS,KAAK;GAC9D;GACA,UAAU,gBAAgB,SAAS,CAAE,IAAG;EACzC;CACF,EAAC,CACH,CACJ;AAED,QAAO,KAAK,UAAU,KAAK,WAAW,EAAE;AACzC","names":["node: Node","views: readonly RouteMeta[]","config: ViewConfig | undefined","componentName: string | undefined","title: string"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/createViewConfigJson.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { Script } from 'node:vm';\nimport ts, { type Node } from 'typescript';\nimport { convertComponentNameToTitle } from '../shared/convertComponentNameToTitle.js';\nimport type { ServerViewConfig } from '../shared/internal.js';\nimport { transformTree } from '../shared/transformTree.js';\nimport type { ViewConfig } from '../types.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\nimport { convertFSRouteSegmentToURLPatternFormat, extractParameterFromRouteSegment } from './utils.js';\n\n/**\n * Walks the TypeScript AST using the deep-first search algorithm.\n *\n * @param node - The node to walk.\n */\nfunction* walkAST(node: Node): Generator<Node> {\n yield node;\n\n for (const child of node.getChildren()) {\n yield* walkAST(child);\n }\n}\n\n/**\n * Creates a map of all leaf routes to their configuration. This file is used by the server to provide server-side\n * routes along with managing the client-side routes.\n *\n * @param views - The route metadata tree.\n */\nexport default async function createViewConfigJson(views: readonly RouteMeta[]): Promise<string> {\n const res = await transformTree<readonly RouteMeta[], Promise<readonly ServerViewConfig[]>>(\n views,\n null,\n async (routes, next) =>\n await Promise.all(\n routes.map(async ({ path, file, layout, children, flowLayout }) => {\n const newChildren = children ? await next(children) : undefined;\n\n if (!file && !layout) {\n return {\n route: convertFSRouteSegmentToURLPatternFormat(path),\n params: extractParameterFromRouteSegment(path),\n children: newChildren,\n } satisfies ServerViewConfig;\n }\n\n const sourceFile = ts.createSourceFile(\n 'f.ts',\n await readFile(file ?? layout!, 'utf8'),\n ts.ScriptTarget.ESNext,\n true,\n );\n let config: ViewConfig | undefined;\n let waitingForIdentifier = false;\n let componentName: string | undefined;\n\n for (const node of walkAST(sourceFile)) {\n if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'config') {\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\n const code = node.initializer.getText(sourceFile);\n const script = new Script(`(${code})`);\n config = script.runInThisContext() as ViewConfig;\n if (config.flowLayout === undefined) {\n const copy = JSON.parse(JSON.stringify(config));\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n copy.flowLayout = flowLayout ?? false;\n config = copy;\n }\n }\n } else if (node.getText(sourceFile).startsWith('export default')) {\n waitingForIdentifier = true;\n } else if (waitingForIdentifier && ts.isIdentifier(node)) {\n componentName = node.text;\n waitingForIdentifier = false;\n }\n }\n\n config ??= { flowLayout: flowLayout ?? false };\n\n let title: string;\n\n if (config.title) {\n ({ title } = config);\n } else {\n if (!componentName) {\n throw new Error(\n `The file \"${String(file ?? layout!)}\" must contain a default export of a component whose name will be used as title by default`,\n );\n }\n\n title = convertComponentNameToTitle(componentName);\n }\n\n return {\n route: convertFSRouteSegmentToURLPatternFormat(path),\n ...config,\n params: extractParameterFromRouteSegment(config.route ?? path),\n title,\n children: newChildren ?? (layout ? [] : undefined),\n } satisfies ServerViewConfig;\n }),\n ),\n );\n\n return JSON.stringify(res, undefined, 2);\n}\n"],"version":3}
1
+ {"mappings":"AAAA,SAAS,kCAAmC;AAC5C,SAAS,uBAAwB;AACjC,OAAO,oBAAoC;AAC3C,SAAS,6EAA8E;AAEvF,SAAS,iDAAkD;AAG3D,SAAS,yCAAyC,oDAAqD;;;;;;AAOvG,UAAU,QAAQA,MAA6B;AAC7C,OAAM;AAEN,MAAK,MAAM,SAAS,KAAK,aAAa,EAAE;AACtC,SAAO,QAAQ,MAAM;CACtB;AACF;;;;;;;;AASD,eAAe,eAAe,qBAAqBC,OAAmE;AACpH,QAAO,MAAM,cACX,OACA,MACA,OAAO,QAAQ,SACb,MAAM,QAAQ,IACZ,OAAO,IAAI,OAAO,EAAE,MAAM,MAAM,QAAQ,UAAU,YAAY,KAAK;EACjE,MAAM,cAAc,WAAW,MAAM,KAAK,SAAS,GAAG;AAEtD,OAAK,SAAS,QAAQ;AACpB,UAAO;IACL,OAAO,wCAAwC,KAAK;IACpD,QAAQ,iCAAiC,KAAK;IAC9C,UAAU;GACX;EACF;EAED,MAAM,aAAa,GAAG,iBACpB,QACA,MAAM,SAAS,QAAQ,QAAS,OAAO,EACvC,GAAG,aAAa,QAChB,KACD;EACD,IAAIC;EACJ,IAAI,uBAAuB;EAC3B,IAAIC;AAEJ,OAAK,MAAM,QAAQ,QAAQ,WAAW,EAAE;AACtC,OAAI,GAAG,sBAAsB,KAAK,IAAI,GAAG,aAAa,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS,UAAU;AAC/F,QAAI,KAAK,eAAe,GAAG,0BAA0B,KAAK,YAAY,EAAE;KACtE,MAAM,OAAO,KAAK,YAAY,QAAQ,WAAW;KACjD,MAAM,SAAS,IAAI,QAAQ,GAAG,KAAK;AACnC,cAAS,OAAO,kBAAkB;AAClC,SAAI,OAAO,eAAe,WAAW;MACnC,MAAM,OAAO,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC;AAE/C,WAAK,aAAa,cAAc;AAChC,eAAS;KACV;IACF;GACF,WAAU,KAAK,QAAQ,WAAW,CAAC,WAAW,iBAAiB,EAAE;AAChE,2BAAuB;GACxB,WAAU,wBAAwB,GAAG,aAAa,KAAK,EAAE;AACxD,oBAAgB,KAAK;AACrB,2BAAuB;GACxB;EACF;AAED,aAAW,EAAE,YAAY,cAAc,MAAO;EAE9C,IAAIC;AAEJ,MAAI,OAAO,OAAO;AAChB,IAAC,CAAE,MAAO,GAAG;EACd,OAAM;AACL,QAAK,eAAe;AAClB,UAAM,IAAI,OACP,YAAY,OAAO,QAAQ,OAAQ,CAAC;GAExC;AAED,WAAQ,4BAA4B,cAAc;EACnD;AAED,SAAO;GACL,OAAO,wCAAwC,KAAK;GACpD,GAAG;GACH,QAAQ,iCAAiC,OAAO,SAAS,KAAK;GAC9D;GACA,UAAU,gBAAgB,SAAS,CAAE,IAAG;EACzC;CACF,EAAC,CACH,CACJ;AACF","names":["node: Node","views: readonly RouteMeta[]","config: ViewConfig | undefined","componentName: string | undefined","title: string"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/createViewConfigJson.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { Script } from 'node:vm';\nimport ts, { type Node } from 'typescript';\nimport { convertComponentNameToTitle } from '../shared/convertComponentNameToTitle.js';\nimport type { ServerViewConfig } from '../shared/internal.js';\nimport { transformTree } from '../shared/transformTree.js';\nimport type { ViewConfig } from '../types.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\nimport { convertFSRouteSegmentToURLPatternFormat, extractParameterFromRouteSegment } from './utils.js';\n\n/**\n * Walks the TypeScript AST using the deep-first search algorithm.\n *\n * @param node - The node to walk.\n */\nfunction* walkAST(node: Node): Generator<Node> {\n yield node;\n\n for (const child of node.getChildren()) {\n yield* walkAST(child);\n }\n}\n\n/**\n * Creates a map of all leaf routes to their configuration. This file is used by the server to provide server-side\n * routes along with managing the client-side routes.\n *\n * @param views - The route metadata tree.\n * @returns A view configuration tree.\n */\nexport default async function createViewConfigJson(views: readonly RouteMeta[]): Promise<readonly ServerViewConfig[]> {\n return await transformTree<readonly RouteMeta[], Promise<readonly ServerViewConfig[]>>(\n views,\n null,\n async (routes, next) =>\n await Promise.all(\n routes.map(async ({ path, file, layout, children, flowLayout }) => {\n const newChildren = children ? await next(children) : undefined;\n\n if (!file && !layout) {\n return {\n route: convertFSRouteSegmentToURLPatternFormat(path),\n params: extractParameterFromRouteSegment(path),\n children: newChildren,\n } satisfies ServerViewConfig;\n }\n\n const sourceFile = ts.createSourceFile(\n 'f.ts',\n await readFile(file ?? layout!, 'utf8'),\n ts.ScriptTarget.ESNext,\n true,\n );\n let config: ViewConfig | undefined;\n let waitingForIdentifier = false;\n let componentName: string | undefined;\n\n for (const node of walkAST(sourceFile)) {\n if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'config') {\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\n const code = node.initializer.getText(sourceFile);\n const script = new Script(`(${code})`);\n config = script.runInThisContext() as ViewConfig;\n if (config.flowLayout === undefined) {\n const copy = JSON.parse(JSON.stringify(config));\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n copy.flowLayout = flowLayout ?? false;\n config = copy;\n }\n }\n } else if (node.getText(sourceFile).startsWith('export default')) {\n waitingForIdentifier = true;\n } else if (waitingForIdentifier && ts.isIdentifier(node)) {\n componentName = node.text;\n waitingForIdentifier = false;\n }\n }\n\n config ??= { flowLayout: flowLayout ?? false };\n\n let title: string;\n\n if (config.title) {\n ({ title } = config);\n } else {\n if (!componentName) {\n throw new Error(\n `The file \"${String(file ?? layout!)}\" must contain a default export of a component whose name will be used as title by default`,\n );\n }\n\n title = convertComponentNameToTitle(componentName);\n }\n\n return {\n route: convertFSRouteSegmentToURLPatternFormat(path),\n ...config,\n params: extractParameterFromRouteSegment(config.route ?? path),\n title,\n children: newChildren ?? (layout ? [] : undefined),\n } satisfies ServerViewConfig;\n }),\n ),\n );\n}\n"],"version":3}
@@ -59,9 +59,9 @@ export async function generateRuntimeFiles(viewsDir, urls, extensions, logger, d
59
59
  logger.info("Collected file-based routes");
60
60
  }
61
61
  routeMeta = await applyLayouts(routeMeta, urls.layouts);
62
- const runtimeRoutesCode = createRoutesFromMeta(routeMeta, urls);
63
62
  const viewConfigJson = await createViewConfigJson(routeMeta);
64
- const jsonWritten = await generateRuntimeFile(urls.json, viewConfigJson);
63
+ const runtimeRoutesCode = createRoutesFromMeta(routeMeta, viewConfigJson, urls);
64
+ const jsonWritten = await generateRuntimeFile(urls.json, JSON.stringify(viewConfigJson, null, 2));
65
65
  if (debug) {
66
66
  logger.info(`Frontend route list is generated: ${String(urls.json)}`);
67
67
  }
@@ -1 +1 @@
1
- {"mappings":"AAAA,SAAS,OAAO,UAAU,mCAAoC;AAE9D,OAAO,qCAAsC;AAC7C,OAAO,mDAAwE;AAC/E,OAAO,qDAAsD;AAC7D,OAAO,qDAAsD;;;;;;;;;;;AA+B7D,eAAe,oBAAoBA,KAAUC,MAAcC,YAAwC;AACjG,OAAM,MAAM,IAAI,IAAI,MAAM,MAAM,EAAE,WAAW,KAAM,EAAC;CACpD,IAAI,cAAc,cAAc;AAChC,MAAK,YAAY;EACf,IAAIC;AACJ,MAAI;AACF,cAAW,MAAM,SAAS,KAAK,QAAQ;EACxC,SAAQC,GAAY;AACnB,SAAM,KAAK,eAAe,MAAM,YAAY,UAAU,KAAK,EAAE,SAAS,WAAW;AAC/E,UAAM;GACP;EACF;AACD,gBAAc,aAAa;CAC5B;AACD,KAAI,aAAa;AACf,QAAM,UAAU,KAAK,MAAM,QAAQ;CACpC;AAED,QAAO;AACR;;;;;;;;;;AAYD,OAAO,eAAe,qBACpBC,UACAC,MACAC,YACAC,QACAC,OACkB;CAClB,IAAIC;AACJ,KAAI;AACF,cAAY,MAAM,oBAAoB,UAAU;GAAE;GAAY;EAAQ,EAAC;CACxE,SAAQN,GAAY;AACnB,MAAI,aAAa,SAAS,UAAU,KAAK,EAAE,SAAS,UAAU;AAC5D,eAAY,CAAE;EACf,OAAM;AACL,SAAM;EACP;CACF;AAED,KAAI,OAAO;AACT,SAAO,KAAK,8BAA8B;CAC3C;AACD,aAAY,MAAM,aAAa,WAAW,KAAK,QAAQ;CACvD,MAAM,oBAAoB,qBAAqB,WAAW,KAAK;CAC/D,MAAM,iBAAiB,MAAM,qBAAqB,UAAU;CAE5D,MAAM,cAAc,MAAM,oBAAoB,KAAK,MAAM,eAAe;AACxE,KAAI,OAAO;AACT,SAAO,MAAM,oCAAoC,OAAO,KAAK,KAAK,CAAC,EAAE;CACtE;CAID,MAAM,cAAc,MAAM,oBAAoB,KAAK,MAAM,mBAAmB,YAAY;AACxF,KAAI,OAAO;AACT,SAAO,MAAM,kCAAkC,OAAO,KAAK,KAAK,CAAC,EAAE;CACpE;AACD,QAAO;AACR","names":["url: URL","data: string","forceWrite?: boolean","contents: string | undefined","e: unknown","viewsDir: URL","urls: RuntimeFileUrls","extensions: readonly string[]","logger: Logger","debug: boolean","routeMeta: readonly RouteMeta[]"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport type { Logger } from 'vite';\nimport applyLayouts from './applyLayouts.js';\nimport collectRoutesFromFS, { type RouteMeta } from './collectRoutesFromFS.js';\nimport createRoutesFromMeta from './createRoutesFromMeta.js';\nimport createViewConfigJson from './createViewConfigJson.js';\n\n/**\n * The URLs of the files to generate.\n */\nexport type RuntimeFileUrls = Readonly<{\n /**\n * The URL of the JSON file with the leaf routes and their metadata. This file\n * will be processed by the server to provide the final route configuration.\n */\n json: URL;\n /**\n * The URL of the module with the routes tree in a framework-agnostic format.\n */\n code: URL;\n /**\n * The URL of the JSON file containing server layout path information.\n */\n layouts: URL;\n}>;\n\n/**\n * Generates a file conditionally. If the file already exists and its content is the same as the\n * given data, the file will not be overwritten. It is useful to avoid unnecessary server\n * reboot during development.\n *\n * @param url - The URL of the file to generate.\n * @param data - The data to write to the file.\n * @param forceWrite - true to force writing the file even if there are no changes\n * @returns true if the file was written, false otherwise.\n */\nasync function generateRuntimeFile(url: URL, data: string, forceWrite?: boolean): Promise<boolean> {\n await mkdir(new URL('./', url), { recursive: true });\n let shouldWrite = forceWrite ?? false;\n if (!forceWrite) {\n let contents: string | undefined;\n try {\n contents = await readFile(url, 'utf-8');\n } catch (e: unknown) {\n if (!(e != null && typeof e === 'object' && 'code' in e && e.code === 'ENOENT')) {\n throw e;\n }\n }\n shouldWrite = contents !== data;\n }\n if (shouldWrite) {\n await writeFile(url, data, 'utf-8');\n }\n\n return shouldWrite;\n}\n\n/**\n * Collects all file-based routes from the given directory, and based on them generates two files\n * described by {@link RuntimeFileUrls} type.\n * @param viewsDir - The directory that contains file-based routes (views).\n * @param urls - The URLs of the files to generate.\n * @param extensions - The list of extensions that will be collected as routes.\n * @param logger - The Vite logger instance.\n * @param debug - true to debug\n */\n// eslint-disable-next-line @typescript-eslint/max-params\nexport async function generateRuntimeFiles(\n viewsDir: URL,\n urls: RuntimeFileUrls,\n extensions: readonly string[],\n logger: Logger,\n debug: boolean,\n): Promise<boolean> {\n let routeMeta: readonly RouteMeta[];\n try {\n routeMeta = await collectRoutesFromFS(viewsDir, { extensions, logger });\n } catch (e: unknown) {\n if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {\n routeMeta = [];\n } else {\n throw e;\n }\n }\n\n if (debug) {\n logger.info('Collected file-based routes');\n }\n routeMeta = await applyLayouts(routeMeta, urls.layouts);\n const runtimeRoutesCode = createRoutesFromMeta(routeMeta, urls);\n const viewConfigJson = await createViewConfigJson(routeMeta);\n\n const jsonWritten = await generateRuntimeFile(urls.json, viewConfigJson);\n if (debug) {\n logger.info(`Frontend route list is generated: ${String(urls.json)}`);\n }\n // If the metadata has changed, we need to write the TS file also to make Vite HMR updates work properly.\n // Even though the contents of the TS file does not change, the contents of the files imported in the TS\n // files changes and the routes must be re-applied to the React router\n const codeWritten = await generateRuntimeFile(urls.code, runtimeRoutesCode, jsonWritten);\n if (debug) {\n logger.info(`File Route module is generated: ${String(urls.code)}`);\n }\n return codeWritten;\n}\n"],"version":3}
1
+ {"mappings":"AAAA,SAAS,OAAO,UAAU,mCAAoC;AAE9D,OAAO,qCAAsC;AAC7C,OAAO,mDAAwE;AAC/E,OAAO,qDAAsD;AAC7D,OAAO,qDAAsD;;;;;;;;;;;AA+B7D,eAAe,oBAAoBA,KAAUC,MAAcC,YAAwC;AACjG,OAAM,MAAM,IAAI,IAAI,MAAM,MAAM,EAAE,WAAW,KAAM,EAAC;CACpD,IAAI,cAAc,cAAc;AAChC,MAAK,YAAY;EACf,IAAIC;AACJ,MAAI;AACF,cAAW,MAAM,SAAS,KAAK,QAAQ;EACxC,SAAQC,GAAY;AACnB,SAAM,KAAK,eAAe,MAAM,YAAY,UAAU,KAAK,EAAE,SAAS,WAAW;AAC/E,UAAM;GACP;EACF;AACD,gBAAc,aAAa;CAC5B;AACD,KAAI,aAAa;AACf,QAAM,UAAU,KAAK,MAAM,QAAQ;CACpC;AAED,QAAO;AACR;;;;;;;;;;AAYD,OAAO,eAAe,qBACpBC,UACAC,MACAC,YACAC,QACAC,OACkB;CAClB,IAAIC;AACJ,KAAI;AACF,cAAY,MAAM,oBAAoB,UAAU;GAAE;GAAY;EAAQ,EAAC;CACxE,SAAQN,GAAY;AACnB,MAAI,aAAa,SAAS,UAAU,KAAK,EAAE,SAAS,UAAU;AAC5D,eAAY,CAAE;EACf,OAAM;AACL,SAAM;EACP;CACF;AAED,KAAI,OAAO;AACT,SAAO,KAAK,8BAA8B;CAC3C;AACD,aAAY,MAAM,aAAa,WAAW,KAAK,QAAQ;CACvD,MAAM,iBAAiB,MAAM,qBAAqB,UAAU;CAC5D,MAAM,oBAAoB,qBAAqB,WAAW,gBAAgB,KAAK;CAE/E,MAAM,cAAc,MAAM,oBAAoB,KAAK,MAAM,KAAK,UAAU,gBAAgB,MAAM,EAAE,CAAC;AACjG,KAAI,OAAO;AACT,SAAO,MAAM,oCAAoC,OAAO,KAAK,KAAK,CAAC,EAAE;CACtE;CAID,MAAM,cAAc,MAAM,oBAAoB,KAAK,MAAM,mBAAmB,YAAY;AACxF,KAAI,OAAO;AACT,SAAO,MAAM,kCAAkC,OAAO,KAAK,KAAK,CAAC,EAAE;CACpE;AACD,QAAO;AACR","names":["url: URL","data: string","forceWrite?: boolean","contents: string | undefined","e: unknown","viewsDir: URL","urls: RuntimeFileUrls","extensions: readonly string[]","logger: Logger","debug: boolean","routeMeta: readonly RouteMeta[]"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport type { Logger } from 'vite';\nimport applyLayouts from './applyLayouts.js';\nimport collectRoutesFromFS, { type RouteMeta } from './collectRoutesFromFS.js';\nimport createRoutesFromMeta from './createRoutesFromMeta.js';\nimport createViewConfigJson from './createViewConfigJson.js';\n\n/**\n * The URLs of the files to generate.\n */\nexport type RuntimeFileUrls = Readonly<{\n /**\n * The URL of the JSON file with the leaf routes and their metadata. This file\n * will be processed by the server to provide the final route configuration.\n */\n json: URL;\n /**\n * The URL of the module with the routes tree in a framework-agnostic format.\n */\n code: URL;\n /**\n * The URL of the JSON file containing server layout path information.\n */\n layouts: URL;\n}>;\n\n/**\n * Generates a file conditionally. If the file already exists and its content is the same as the\n * given data, the file will not be overwritten. It is useful to avoid unnecessary server\n * reboot during development.\n *\n * @param url - The URL of the file to generate.\n * @param data - The data to write to the file.\n * @param forceWrite - true to force writing the file even if there are no changes\n * @returns true if the file was written, false otherwise.\n */\nasync function generateRuntimeFile(url: URL, data: string, forceWrite?: boolean): Promise<boolean> {\n await mkdir(new URL('./', url), { recursive: true });\n let shouldWrite = forceWrite ?? false;\n if (!forceWrite) {\n let contents: string | undefined;\n try {\n contents = await readFile(url, 'utf-8');\n } catch (e: unknown) {\n if (!(e != null && typeof e === 'object' && 'code' in e && e.code === 'ENOENT')) {\n throw e;\n }\n }\n shouldWrite = contents !== data;\n }\n if (shouldWrite) {\n await writeFile(url, data, 'utf-8');\n }\n\n return shouldWrite;\n}\n\n/**\n * Collects all file-based routes from the given directory, and based on them generates two files\n * described by {@link RuntimeFileUrls} type.\n * @param viewsDir - The directory that contains file-based routes (views).\n * @param urls - The URLs of the files to generate.\n * @param extensions - The list of extensions that will be collected as routes.\n * @param logger - The Vite logger instance.\n * @param debug - true to debug\n */\n// eslint-disable-next-line @typescript-eslint/max-params\nexport async function generateRuntimeFiles(\n viewsDir: URL,\n urls: RuntimeFileUrls,\n extensions: readonly string[],\n logger: Logger,\n debug: boolean,\n): Promise<boolean> {\n let routeMeta: readonly RouteMeta[];\n try {\n routeMeta = await collectRoutesFromFS(viewsDir, { extensions, logger });\n } catch (e: unknown) {\n if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {\n routeMeta = [];\n } else {\n throw e;\n }\n }\n\n if (debug) {\n logger.info('Collected file-based routes');\n }\n routeMeta = await applyLayouts(routeMeta, urls.layouts);\n const viewConfigJson = await createViewConfigJson(routeMeta);\n const runtimeRoutesCode = createRoutesFromMeta(routeMeta, viewConfigJson, urls);\n\n const jsonWritten = await generateRuntimeFile(urls.json, JSON.stringify(viewConfigJson, null, 2));\n if (debug) {\n logger.info(`Frontend route list is generated: ${String(urls.json)}`);\n }\n // If the metadata has changed, we need to write the TS file also to make Vite HMR updates work properly.\n // Even though the contents of the TS file does not change, the contents of the files imported in the TS\n // files changes and the routes must be re-applied to the React router\n const codeWritten = await generateRuntimeFile(urls.code, runtimeRoutesCode, jsonWritten);\n if (debug) {\n logger.info(`File Route module is generated: ${String(urls.code)}`);\n }\n return codeWritten;\n}\n"],"version":3}
@@ -26,3 +26,4 @@ export declare function extractParameterFromRouteSegment(segment: string): Reado
26
26
  * order to sort the routes alphabetically.
27
27
  */
28
28
  export declare function cleanUp(path: string): string;
29
+ export declare function strip(path: string): string;
@@ -61,4 +61,7 @@ export function cleanUp(path) {
61
61
  }
62
62
  return res;
63
63
  }
64
+ export function strip(path) {
65
+ return path.replace(/^\/*(.+)\/*$/u, "$1");
66
+ }
64
67
  //# sourceMappingURL=./utils.js.map
@@ -1 +1 @@
1
- {"mappings":"AAAA,SAAS,mDAAoD;AAE7D,OAAO,MAAMA,oBAAyD,IAAI,IAAI;CAC5E,CAAC,eAAe,UAAU,kBAAmB;CAC7C,CAAC,eAAe,UAAU,gBAAiB;CAC3C,CAAC,eAAe,UAAU,YAAa;AACxC;AAGD,SAAS,YAAYC,MAA8B;AAEjD,SAAQ,MAAR;EACE,KAAK,eAAe,SAClB,QAAO;EACT,KAAK,eAAe,SAClB,QAAO;EACT,KAAK,eAAe,SAClB,QAAO;CACV;AACF;;;;;;;;;;;;;;AAeD,OAAO,SAAS,wCAAwCC,SAAyB;CAC/E,IAAI,MAAM;AAEV,mBAAkB,QAAQ,CAAC,SAAS,SAAS;AAC3C,QAAM,IAAI,WAAW,SAAS,YAAY,KAAK,CAAC;CACjD,EAAC;AAEF,QAAO;AACR;;;;;;;AAQD,OAAO,SAAS,iCAAiCA,SAA2D;CAC1G,IAAI,WAAW;CACf,MAAMC,SAAyC,CAAE;AAEjD,MAAK,MAAM,CAAC,MAAM,QAAQ,IAAI,mBAAmB;EAC/C,MAAM,WAAW,IAAI,OAAO,QAAQ,QAAQ,QAAQ;AACpD,aAAW,SAAS,WAAW,UAAU,CAAC,UAAU;GAClD,MAAM,MAAM,MAAM,WAAW,SAAS,YAAY,KAAK,CAAC;AACxD,UAAO,OAAO;AACd,UAAO;EACR,EAAC;CACH;AAED,QAAO;AACR;;;;;AAMD,OAAO,SAAS,QAAQC,MAAsB;CAC5C,IAAI,MAAM;AAEV,MAAK,MAAM,WAAW,kBAAkB,QAAQ,EAAE;AAChD,QAAM,IAAI,WAAW,SAAS,KAAK;CACpC;AAED,QAAO;AACR","names":["routeParamTypeMap: ReadonlyMap<RouteParamType, RegExp>","type: RouteParamType","segment: string","params: Record<string, RouteParamType>","path: string"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/utils.ts"],"sourcesContent":["import { RouteParamType } from '../shared/routeParamType.js';\n\nexport const routeParamTypeMap: ReadonlyMap<RouteParamType, RegExp> = new Map([\n [RouteParamType.Wildcard, /\\{\\.\\.\\.(.+)\\}/gu], // e.g. \"{...wildcard}\"\n [RouteParamType.Optional, /\\{\\{(.+)\\}\\}/gu], // e.g. \"{{param}}\"\n [RouteParamType.Required, /\\{(.+)\\}/gu], // e.g. \"{param}\"\n]);\n\n// eslint-disable-next-line @typescript-eslint/consistent-return\nfunction getReplacer(type: RouteParamType): string {\n // eslint-disable-next-line default-case\n switch (type) {\n case RouteParamType.Wildcard:\n return '*';\n case RouteParamType.Optional:\n return ':$1?';\n case RouteParamType.Required:\n return ':$1';\n }\n}\n\n/**\n * Converts a file system pattern to an URL pattern string.\n *\n * @param segment - a string representing a file system pattern:\n * - `{param}` - for a required single parameter;\n * - `{{param}}` - for an optional single parameter;\n * - `{...wildcard}` - for multiple parameters, including none.\n *\n * @returns a string representing a URL pattern, respectively:\n * - `:param`;\n * - `:param?`;\n * - `*`.\n */\nexport function convertFSRouteSegmentToURLPatternFormat(segment: string): string {\n let res = segment;\n\n routeParamTypeMap.forEach((pattern, type) => {\n res = res.replaceAll(pattern, getReplacer(type));\n });\n\n return res;\n}\n\n/**\n * Extracts the parameter name and its type from the route segment.\n *\n * @param segment - A part of the FS route URL.\n * @returns A map of parameter names and their types.\n */\nexport function extractParameterFromRouteSegment(segment: string): Readonly<Record<string, RouteParamType>> {\n let _segment = segment;\n const params: Record<string, RouteParamType> = {};\n\n for (const [type, pattern] of routeParamTypeMap) {\n const _pattern = new RegExp(pattern.source, pattern.flags);\n _segment = _segment.replaceAll(_pattern, (match) => {\n const key = match.replaceAll(pattern, getReplacer(type));\n params[key] = type;\n return '';\n });\n }\n\n return params;\n}\n\n/**\n * A small helper function that clears route path of the control characters in\n * order to sort the routes alphabetically.\n */\nexport function cleanUp(path: string): string {\n let res = path;\n\n for (const pattern of routeParamTypeMap.values()) {\n res = res.replaceAll(pattern, '$1');\n }\n\n return res;\n}\n"],"version":3}
1
+ {"mappings":"AAAA,SAAS,mDAAoD;AAE7D,OAAO,MAAMA,oBAAyD,IAAI,IAAI;CAC5E,CAAC,eAAe,UAAU,kBAAmB;CAC7C,CAAC,eAAe,UAAU,gBAAiB;CAC3C,CAAC,eAAe,UAAU,YAAa;AACxC;AAGD,SAAS,YAAYC,MAA8B;AAEjD,SAAQ,MAAR;EACE,KAAK,eAAe,SAClB,QAAO;EACT,KAAK,eAAe,SAClB,QAAO;EACT,KAAK,eAAe,SAClB,QAAO;CACV;AACF;;;;;;;;;;;;;;AAeD,OAAO,SAAS,wCAAwCC,SAAyB;CAC/E,IAAI,MAAM;AAEV,mBAAkB,QAAQ,CAAC,SAAS,SAAS;AAC3C,QAAM,IAAI,WAAW,SAAS,YAAY,KAAK,CAAC;CACjD,EAAC;AAEF,QAAO;AACR;;;;;;;AAQD,OAAO,SAAS,iCAAiCA,SAA2D;CAC1G,IAAI,WAAW;CACf,MAAMC,SAAyC,CAAE;AAEjD,MAAK,MAAM,CAAC,MAAM,QAAQ,IAAI,mBAAmB;EAC/C,MAAM,WAAW,IAAI,OAAO,QAAQ,QAAQ,QAAQ;AACpD,aAAW,SAAS,WAAW,UAAU,CAAC,UAAU;GAClD,MAAM,MAAM,MAAM,WAAW,SAAS,YAAY,KAAK,CAAC;AACxD,UAAO,OAAO;AACd,UAAO;EACR,EAAC;CACH;AAED,QAAO;AACR;;;;;AAMD,OAAO,SAAS,QAAQC,MAAsB;CAC5C,IAAI,MAAM;AAEV,MAAK,MAAM,WAAW,kBAAkB,QAAQ,EAAE;AAChD,QAAM,IAAI,WAAW,SAAS,KAAK;CACpC;AAED,QAAO;AACR;AAED,OAAO,SAAS,MAAMA,MAAsB;AAC1C,QAAO,KAAK,QAAQ,iBAAiB,KAAK;AAC3C","names":["routeParamTypeMap: ReadonlyMap<RouteParamType, RegExp>","type: RouteParamType","segment: string","params: Record<string, RouteParamType>","path: string"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/file-router/src/vite-plugin/utils.ts"],"sourcesContent":["import { RouteParamType } from '../shared/routeParamType.js';\n\nexport const routeParamTypeMap: ReadonlyMap<RouteParamType, RegExp> = new Map([\n [RouteParamType.Wildcard, /\\{\\.\\.\\.(.+)\\}/gu], // e.g. \"{...wildcard}\"\n [RouteParamType.Optional, /\\{\\{(.+)\\}\\}/gu], // e.g. \"{{param}}\"\n [RouteParamType.Required, /\\{(.+)\\}/gu], // e.g. \"{param}\"\n]);\n\n// eslint-disable-next-line @typescript-eslint/consistent-return\nfunction getReplacer(type: RouteParamType): string {\n // eslint-disable-next-line default-case\n switch (type) {\n case RouteParamType.Wildcard:\n return '*';\n case RouteParamType.Optional:\n return ':$1?';\n case RouteParamType.Required:\n return ':$1';\n }\n}\n\n/**\n * Converts a file system pattern to an URL pattern string.\n *\n * @param segment - a string representing a file system pattern:\n * - `{param}` - for a required single parameter;\n * - `{{param}}` - for an optional single parameter;\n * - `{...wildcard}` - for multiple parameters, including none.\n *\n * @returns a string representing a URL pattern, respectively:\n * - `:param`;\n * - `:param?`;\n * - `*`.\n */\nexport function convertFSRouteSegmentToURLPatternFormat(segment: string): string {\n let res = segment;\n\n routeParamTypeMap.forEach((pattern, type) => {\n res = res.replaceAll(pattern, getReplacer(type));\n });\n\n return res;\n}\n\n/**\n * Extracts the parameter name and its type from the route segment.\n *\n * @param segment - A part of the FS route URL.\n * @returns A map of parameter names and their types.\n */\nexport function extractParameterFromRouteSegment(segment: string): Readonly<Record<string, RouteParamType>> {\n let _segment = segment;\n const params: Record<string, RouteParamType> = {};\n\n for (const [type, pattern] of routeParamTypeMap) {\n const _pattern = new RegExp(pattern.source, pattern.flags);\n _segment = _segment.replaceAll(_pattern, (match) => {\n const key = match.replaceAll(pattern, getReplacer(type));\n params[key] = type;\n return '';\n });\n }\n\n return params;\n}\n\n/**\n * A small helper function that clears route path of the control characters in\n * order to sort the routes alphabetically.\n */\nexport function cleanUp(path: string): string {\n let res = path;\n\n for (const pattern of routeParamTypeMap.values()) {\n res = res.replaceAll(pattern, '$1');\n }\n\n return res;\n}\n\nexport function strip(path: string): string {\n return path.replace(/^\\/*(.+)\\/*$/u, '$1');\n}\n"],"version":3}