@vaadin/hilla-file-router 24.4.0-alpha14 → 24.4.0-alpha16

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.4.0-alpha14",
3
+ "version": "24.4.0-alpha16",
4
4
  "description": "Hilla file-based router",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -80,7 +80,7 @@
80
80
  "type-fest": "^4.9.0"
81
81
  },
82
82
  "dependencies": {
83
- "@vaadin/hilla-generator-utils": "24.4.0-alpha14",
83
+ "@vaadin/hilla-generator-utils": "24.4.0-alpha16",
84
84
  "react": "^18.2.0",
85
85
  "rollup": "^4.12.0",
86
86
  "typescript": "5.3.2"
@@ -8,7 +8,7 @@ import type { MenuItem } from '../types.js';
8
8
  */
9
9
  export declare function createMenuItems(vaadinObject?: Readonly<{
10
10
  server?: Readonly<{
11
- views: Readonly<Record<string, import("../shared/internal.js").ServerViewConfig>>;
11
+ views: Readonly<Record<string, import("../shared/internal.js").ServerViewMapItem>>;
12
12
  }> | undefined;
13
13
  }> | undefined): readonly MenuItem[];
14
14
  //# sourceMappingURL=createMenuItems.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { AgnosticRoute } from '../types.js';
1
+ import type { AgnosticRoute, Module } from '../types.js';
2
2
  /**
3
3
  * Create a single framework-agnostic route object. Later, it can be transformed into a framework-specific route object,
4
4
  * e.g., the one used by React Router.
@@ -8,6 +8,6 @@ import type { AgnosticRoute } from '../types.js';
8
8
  *
9
9
  * @returns A framework-agnostic route object.
10
10
  */
11
- export declare function createRoute<C = unknown>(path: string, children?: ReadonlyArray<AgnosticRoute<C>>): AgnosticRoute<C>;
12
- export declare function createRoute<C = unknown>(path: string, module: Readonly<Record<string, unknown>>, children?: ReadonlyArray<AgnosticRoute<C>>): AgnosticRoute<C>;
11
+ export declare function createRoute(path: string, children?: readonly AgnosticRoute[]): AgnosticRoute;
12
+ export declare function createRoute(path: string, module: Module, children?: readonly AgnosticRoute[]): AgnosticRoute;
13
13
  //# sourceMappingURL=createRoute.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createRoute.d.ts","sourceRoot":"","sources":["../src/runtime/createRoute.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,aAAa,CAAC;AAE9D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;AACrH,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,EACrC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,QAAQ,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GACzC,aAAa,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"createRoute.d.ts","sourceRoot":"","sources":["../src/runtime/createRoute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,CAAC;AAC9F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,CAAC"}
@@ -6,7 +6,7 @@ function createRoute(path, moduleOrChildren, children) {
6
6
  module = moduleOrChildren;
7
7
  }
8
8
  return {
9
- path: module?.config?.route ?? path,
9
+ path,
10
10
  module,
11
11
  children
12
12
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/runtime/createRoute.ts"],
4
- "sourcesContent": ["/* eslint-disable no-param-reassign */\nimport type { AgnosticRoute, RouteModule } from '../types.js';\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<C = unknown>(path: string, children?: ReadonlyArray<AgnosticRoute<C>>): AgnosticRoute<C>;\nexport function createRoute<C = unknown>(\n path: string,\n module: Readonly<Record<string, unknown>>,\n children?: ReadonlyArray<AgnosticRoute<C>>,\n): AgnosticRoute<C>;\nexport function createRoute<C = unknown>(\n path: string,\n moduleOrChildren?: Readonly<Record<string, unknown>> | ReadonlyArray<AgnosticRoute<C>>,\n children?: ReadonlyArray<AgnosticRoute<C>>,\n): AgnosticRoute<C> {\n let module: RouteModule<C> | undefined;\n if (Array.isArray(moduleOrChildren)) {\n children = moduleOrChildren;\n } else {\n module = moduleOrChildren as RouteModule<C> | undefined;\n }\n\n return {\n path: module?.config?.route ?? path,\n module,\n children,\n };\n}\n"],
5
- "mappings": "AAkBO,SAAS,YACd,MACA,kBACA,UACkB;AAClB,MAAI;AACJ,MAAI,MAAM,QAAQ,gBAAgB,GAAG;AACnC,eAAW;AAAA,EACb,OAAO;AACL,aAAS;AAAA,EACX;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,QAAQ,SAAS;AAAA,IAC/B;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { AgnosticRoute, Module } from '../types.js';\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"],
5
+ "mappings": "AAaO,SAAS,YACd,MACA,kBACA,UACe;AACf,MAAI;AACJ,MAAI,MAAM,QAAQ,gBAAgB,GAAG;AAEnC,eAAW;AAAA,EACb,OAAO;AACL,aAAS;AAAA,EACX;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,4 @@
1
- import { type ComponentType } from 'react';
2
- import type { RouteObject } from 'react-router-dom';
1
+ import type { RouteObject as ReactRouteObject } from 'react-router-dom';
3
2
  import type { AgnosticRoute } from '../types.js';
4
3
  /**
5
4
  * Transforms framework-agnostic route tree into a format that can be used by React Router.
@@ -8,5 +7,5 @@ import type { AgnosticRoute } from '../types.js';
8
7
  *
9
8
  * @returns The React Router tree.
10
9
  */
11
- export declare function toReactRouter(routes: AgnosticRoute<ComponentType>): RouteObject;
10
+ export declare function toReactRouter(routes: AgnosticRoute): ReactRouteObject;
12
11
  //# sourceMappingURL=toReactRouter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"toReactRouter.d.ts","sourceRoot":"","sources":["../src/runtime/toReactRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,OAAO,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,WAAW,CAkB/E"}
1
+ {"version":3,"file":"toReactRouter.d.ts","sourceRoot":"","sources":["../src/runtime/toReactRouter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,IAAI,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGxE,OAAO,KAAK,EAAE,aAAa,EAAuB,MAAM,aAAa,CAAC;AAMtE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,gBAAgB,CAsBrE"}
@@ -1,14 +1,20 @@
1
1
  import { createElement } from "react";
2
2
  import { convertComponentNameToTitle } from "../shared/convertComponentNameToTitle.js";
3
- import { transformRoute } from "./utils.js";
3
+ import { transformTreeSync } from "../shared/transformTree.js";
4
+ function isReactRouteModule(module) {
5
+ return module ? "default" in module && typeof module.default === "function" : true;
6
+ }
4
7
  function toReactRouter(routes) {
5
- return transformRoute(
8
+ return transformTreeSync(
6
9
  routes,
7
10
  (route) => route.children?.values(),
8
11
  ({ path, module }, children) => {
12
+ if (!isReactRouteModule(module)) {
13
+ throw new Error(`The module for the "${path}" section doesn't have the React component exported by default`);
14
+ }
9
15
  const title = module?.config?.title ?? convertComponentNameToTitle(module?.default);
10
16
  return {
11
- path,
17
+ path: module?.config?.route ?? path,
12
18
  element: module?.default ? createElement(module.default) : void 0,
13
19
  children: children.length > 0 ? children : void 0,
14
20
  handle: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/runtime/toReactRouter.ts"],
4
- "sourcesContent": ["import { type ComponentType, createElement } from 'react';\nimport type { RouteObject } from 'react-router-dom';\nimport { convertComponentNameToTitle } from '../shared/convertComponentNameToTitle.js';\nimport type { AgnosticRoute } from '../types.js';\nimport { transformRoute } from './utils.js';\n\n/**\n * Transforms framework-agnostic route tree into a format that can be used by React Router.\n *\n * @param routes - Generated routes\n *\n * @returns The React Router tree.\n */\nexport function toReactRouter(routes: AgnosticRoute<ComponentType>): RouteObject {\n return transformRoute(\n routes,\n (route) => route.children?.values(),\n ({ path, module }, children) => {\n const title = module?.config?.title ?? convertComponentNameToTitle(module?.default);\n\n return {\n path,\n element: module?.default ? createElement(module.default) : undefined,\n children: children.length > 0 ? (children as RouteObject[]) : undefined,\n handle: {\n ...module?.config,\n title,\n },\n } satisfies RouteObject;\n },\n );\n}\n"],
5
- "mappings": "AAAA,SAA6B,qBAAqB;AAElD,SAAS,mCAAmC;AAE5C,SAAS,sBAAsB;AASxB,SAAS,cAAc,QAAmD;AAC/E,SAAO;AAAA,IACL;AAAA,IACA,CAAC,UAAU,MAAM,UAAU,OAAO;AAAA,IAClC,CAAC,EAAE,MAAM,OAAO,GAAG,aAAa;AAC9B,YAAM,QAAQ,QAAQ,QAAQ,SAAS,4BAA4B,QAAQ,OAAO;AAElF,aAAO;AAAA,QACL;AAAA,QACA,SAAS,QAAQ,UAAU,cAAc,OAAO,OAAO,IAAI;AAAA,QAC3D,UAAU,SAAS,SAAS,IAAK,WAA6B;AAAA,QAC9D,QAAQ;AAAA,UACN,GAAG,QAAQ;AAAA,UACX;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { type ComponentType, createElement } from 'react';\nimport type { RouteObject as ReactRouteObject } from 'react-router-dom';\nimport { convertComponentNameToTitle } from '../shared/convertComponentNameToTitle.js';\nimport { transformTreeSync } from '../shared/transformTree.js';\nimport type { AgnosticRoute, Module, RouteModule } from '../types.js';\n\nfunction isReactRouteModule(module?: Module): module is RouteModule<ComponentType> | undefined {\n return module ? 'default' in module && typeof module.default === 'function' : true;\n}\n\n/**\n * Transforms framework-agnostic route tree into a format that can be used by React Router.\n *\n * @param routes - Generated routes\n *\n * @returns The React Router tree.\n */\nexport function toReactRouter(routes: AgnosticRoute): ReactRouteObject {\n return transformTreeSync(\n routes,\n (route) => route.children?.values(),\n ({ path, module }, children) => {\n if (!isReactRouteModule(module)) {\n throw new Error(`The module for the \"${path}\" section doesn't have the React component exported by default`);\n }\n\n const title = module?.config?.title ?? convertComponentNameToTitle(module?.default);\n\n return {\n path: module?.config?.route ?? path,\n element: module?.default ? createElement(module.default) : undefined,\n children: children.length > 0 ? (children as ReactRouteObject[]) : undefined,\n handle: {\n ...module?.config,\n title,\n },\n } satisfies ReactRouteObject;\n },\n );\n}\n"],
5
+ "mappings": "AAAA,SAA6B,qBAAqB;AAElD,SAAS,mCAAmC;AAC5C,SAAS,yBAAyB;AAGlC,SAAS,mBAAmB,QAAmE;AAC7F,SAAO,SAAS,aAAa,UAAU,OAAO,OAAO,YAAY,aAAa;AAChF;AASO,SAAS,cAAc,QAAyC;AACrE,SAAO;AAAA,IACL;AAAA,IACA,CAAC,UAAU,MAAM,UAAU,OAAO;AAAA,IAClC,CAAC,EAAE,MAAM,OAAO,GAAG,aAAa;AAC9B,UAAI,CAAC,mBAAmB,MAAM,GAAG;AAC/B,cAAM,IAAI,MAAM,uBAAuB,IAAI,gEAAgE;AAAA,MAC7G;AAEA,YAAM,QAAQ,QAAQ,QAAQ,SAAS,4BAA4B,QAAQ,OAAO;AAElF,aAAO;AAAA,QACL,MAAM,QAAQ,QAAQ,SAAS;AAAA,QAC/B,SAAS,QAAQ,UAAU,cAAc,OAAO,OAAO,IAAI;AAAA,QAC3D,UAAU,SAAS,SAAS,IAAK,WAAkC;AAAA,QACnE,QAAQ;AAAA,UACN,GAAG,QAAQ;AAAA,UACX;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -5,10 +5,19 @@ import type { RouteParamType } from './routeParamType.js';
5
5
  * Internal type used for server communication and menu building. It extends the
6
6
  * view configuration with the route parameters.
7
7
  */
8
- export type ServerViewConfig = Readonly<{ params?: Readonly<Record<string, RouteParamType>> }> & ViewConfig;
8
+ export type ServerViewConfig = Readonly<{
9
+ children: readonly ServerViewConfig[];
10
+ params?: Readonly<Record<string, RouteParamType>>;
11
+ }> &
12
+ ViewConfig;
13
+
14
+ export type ServerViewMapItem = Readonly<{
15
+ params?: Readonly<Record<string, RouteParamType>>;
16
+ }> &
17
+ ViewConfig;
9
18
 
10
19
  export type VaadinServer = Readonly<{
11
- views: Readonly<Record<string, ServerViewConfig>>;
20
+ views: Readonly<Record<string, ServerViewMapItem>>;
12
21
  }>;
13
22
 
14
23
  export type VaadinObject = Readonly<{
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Transforms the whole route tree into a new format.
3
+ *
4
+ * @param node - The route to transform.
5
+ * @param getChildren - A function that returns the children of the route.
6
+ * @param transformer - A function that transforms the route and its children.
7
+ *
8
+ * @returns The transformed route.
9
+ */
10
+ export declare function transformTreeSync<T, U>(node: T, getChildren: (node: T) => IterableIterator<T> | null | undefined, transformer: (node: T, children: readonly U[]) => U): U;
11
+ export declare function transformTree<T, U>(node: T, getChildren: (node: T) => IterableIterator<T> | null | undefined, transformer: (node: T, children: readonly U[]) => Promise<U>): Promise<U>;
12
+ //# sourceMappingURL=transformTree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformTree.d.ts","sourceRoot":"","sources":["../src/shared/transformTree.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,CAAC,EACpC,IAAI,EAAE,CAAC,EACP,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAChE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,GAClD,CAAC,CAOH;AAED,wBAAsB,aAAa,CAAC,CAAC,EAAE,CAAC,EACtC,IAAI,EAAE,CAAC,EACP,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAChE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3D,OAAO,CAAC,CAAC,CAAC,CASZ"}
@@ -0,0 +1,19 @@
1
+ function transformTreeSync(node, getChildren, transformer) {
2
+ const children = getChildren(node);
3
+ return transformer(
4
+ node,
5
+ children ? Array.from(children, (child) => transformTreeSync(child, getChildren, transformer)) : []
6
+ );
7
+ }
8
+ async function transformTree(node, getChildren, transformer) {
9
+ const children = getChildren(node);
10
+ return transformer(
11
+ node,
12
+ children ? await Promise.all(Array.from(children, async (child) => transformTree(child, getChildren, transformer))) : []
13
+ );
14
+ }
15
+ export {
16
+ transformTree,
17
+ transformTreeSync
18
+ };
19
+ //# sourceMappingURL=transformTree.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/shared/transformTree.ts"],
4
+ "sourcesContent": ["/**\n * Transforms the whole route tree into a new format.\n *\n * @param node - The route to transform.\n * @param getChildren - A function that returns the children of the route.\n * @param transformer - A function that transforms the route and its children.\n *\n * @returns The transformed route.\n */\nexport function transformTreeSync<T, U>(\n node: T,\n getChildren: (node: T) => IterableIterator<T> | null | undefined,\n transformer: (node: T, children: readonly U[]) => U,\n): U {\n const children = getChildren(node);\n\n return transformer(\n node,\n children ? Array.from(children, (child) => transformTreeSync(child, getChildren, transformer)) : [],\n );\n}\n\nexport async function transformTree<T, U>(\n node: T,\n getChildren: (node: T) => IterableIterator<T> | null | undefined,\n transformer: (node: T, children: readonly U[]) => Promise<U>,\n): Promise<U> {\n const children = getChildren(node);\n\n return transformer(\n node,\n children\n ? await Promise.all(Array.from(children, async (child) => transformTree(child, getChildren, transformer)))\n : [],\n );\n}\n"],
5
+ "mappings": "AASO,SAAS,kBACd,MACA,aACA,aACG;AACH,QAAM,WAAW,YAAY,IAAI;AAEjC,SAAO;AAAA,IACL;AAAA,IACA,WAAW,MAAM,KAAK,UAAU,CAAC,UAAU,kBAAkB,OAAO,aAAa,WAAW,CAAC,IAAI,CAAC;AAAA,EACpG;AACF;AAEA,eAAsB,cACpB,MACA,aACA,aACY;AACZ,QAAM,WAAW,YAAY,IAAI;AAEjC,SAAO;AAAA,IACL;AAAA,IACA,WACI,MAAM,QAAQ,IAAI,MAAM,KAAK,UAAU,OAAO,UAAU,cAAc,OAAO,aAAa,WAAW,CAAC,CAAC,IACvG,CAAC;AAAA,EACP;AACF;",
6
+ "names": []
7
+ }
package/types.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import type { RouteParamType } from './shared/routeParamType.js';
2
-
3
1
  export type ViewConfig = Readonly<{
4
2
  /**
5
3
  * View title used in the main layout header, as <title> and as the default
@@ -13,6 +11,11 @@ export type ViewConfig = Readonly<{
13
11
  */
14
12
  rolesAllowed?: readonly string[];
15
13
 
14
+ /**
15
+ * Set to true to require the user to be logged in to access the view.
16
+ */
17
+ loginRequired?: boolean;
18
+
16
19
  /**
17
20
  * Allows overriding the route path configuration. Uses the same syntax as
18
21
  * the path property with React Router. This can be used to define a route
@@ -45,20 +48,26 @@ export type ViewConfig = Readonly<{
45
48
  }>;
46
49
  }>;
47
50
 
51
+ /**
52
+ * An unknown JavaScript module.
53
+ */
54
+ export type Module = Readonly<Record<string, unknown>>;
55
+
48
56
  /**
49
57
  * A module that exports a component and an optional view configuration.
50
58
  */
51
- export type RouteModule<C = unknown> = Readonly<{
52
- default: C;
53
- config?: ViewConfig;
54
- }>;
59
+ export type RouteModule<C = unknown> = Module &
60
+ Readonly<{
61
+ default: C;
62
+ config?: ViewConfig;
63
+ }>;
55
64
 
56
65
  /**
57
66
  * A framework-agnostic object generated from the file-based route.
58
67
  */
59
- export type AgnosticRoute<C = unknown> = Readonly<{
68
+ export type AgnosticRoute = Readonly<{
60
69
  path: string;
61
- module?: RouteModule<C>;
70
+ module?: Module;
62
71
  children?: ReadonlyArray<AgnosticRoute<T>>;
63
72
  }>;
64
73
 
@@ -1 +1 @@
1
- {"version":3,"file":"collectRoutesFromFS.d.ts","sourceRoot":"","sources":["../src/vite-plugin/collectRoutesFromFS.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAiBH;;;;;;;;;;;;;GAaG;AACH,wBAA8B,mBAAmB,CAC/C,GAAG,EAAE,GAAG,EACR,EAAE,UAAU,EAAE,MAAM,EAAE,MAAY,EAAE,EAAE,oBAAoB,GACzD,OAAO,CAAC,SAAS,CAAC,CAuDpB"}
1
+ {"version":3,"file":"collectRoutesFromFS.d.ts","sourceRoot":"","sources":["../src/vite-plugin/collectRoutesFromFS.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAmBH;;;;;;;;;;;;;GAaG;AACH,wBAA8B,mBAAmB,CAC/C,GAAG,EAAE,GAAG,EACR,EAAE,UAAU,EAAE,MAAM,EAAE,MAAY,EAAE,EAAE,oBAAoB,GACzD,OAAO,CAAC,SAAS,CAAC,CAyDpB"}
@@ -14,12 +14,13 @@ async function checkFile(url, logger) {
14
14
  return url;
15
15
  }
16
16
  const collator = new Intl.Collator("en-US");
17
+ const phantomDirPattern = /\/?\$phantom/giu;
17
18
  async function collectRoutesFromFS(dir, { extensions, logger, parent = dir }) {
18
- const path = relative(fileURLToPath(parent), fileURLToPath(dir));
19
+ const path = relative(fileURLToPath(parent), fileURLToPath(dir)).replaceAll(phantomDirPattern, "");
19
20
  let children = [];
20
21
  let layout;
21
22
  for await (const d of await opendir(dir)) {
22
- if (d.isDirectory()) {
23
+ if (d.isDirectory() && !d.name.startsWith("_")) {
23
24
  children.push(await collectRoutesFromFS(new URL(`${d.name}/`, dir), { extensions, logger, parent: dir }));
24
25
  } else if (d.isFile() && extensions.includes(extname(d.name))) {
25
26
  const file = new URL(d.name, dir);
@@ -34,7 +35,9 @@ async function collectRoutesFromFS(dir, { extensions, logger, parent = dir }) {
34
35
  children: []
35
36
  });
36
37
  } else {
37
- throw new Error('Symbol "$" is reserved for special files; only "$layout" and "$index" are allowed');
38
+ throw new Error(
39
+ 'Symbol "$" is reserved for special directories and files; only "$layout", "$phantom" and "$index" are allowed'
40
+ );
38
41
  }
39
42
  } else if (!name.startsWith("_")) {
40
43
  children.push({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/vite-plugin/collectRoutesFromFS.ts"],
4
- "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 { cleanUp } from './utils.js';\n\nexport type RouteMeta = Readonly<{\n path: string;\n file?: URL;\n layout?: URL;\n children: 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 === '') {\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 }\n }\n\n return url;\n}\n\nconst collator = new Intl.Collator('en-US');\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 tree.\n */\nexport default async function collectRoutesFromFS(\n dir: URL,\n { extensions, logger, parent = dir }: CollectRoutesOptions,\n): Promise<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.isDirectory()) {\n children.push(await collectRoutesFromFS(new URL(`${d.name}/`, dir), { extensions, logger, parent: dir }));\n } else if (d.isFile() && extensions.includes(extname(d.name))) {\n const file = new URL(d.name, dir);\n const name = basename(d.name, extname(d.name));\n\n if (name.startsWith('$')) {\n if (name === '$layout') {\n layout = file;\n } else if (name === '$index') {\n children.push({\n path: '',\n file,\n children: [],\n });\n } else {\n throw new Error('Symbol \"$\" is reserved for special files; only \"$layout\" and \"$index\" are allowed');\n }\n } else if (!name.startsWith('_')) {\n children.push({\n path: name,\n file,\n children: [],\n });\n }\n }\n }\n\n [children, layout] = await Promise.all([\n Promise.all(\n children.map(async (child) => {\n let { file: f, layout: l } = child;\n [f, l] = await Promise.all([checkFile(f, logger), checkFile(l, logger)]);\n\n return {\n ...child,\n file: f,\n layout: l,\n };\n }),\n ),\n checkFile(layout, logger),\n ]);\n\n return {\n path,\n layout,\n children: children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b))),\n };\n}\n"],
5
- "mappings": "AAAA,SAAS,SAAS,gBAAgB;AAClC,SAAS,UAAU,SAAS,gBAAgB;AAC5C,SAAS,qBAAqB;AAE9B,SAAS,eAAe;AA4BxB,eAAe,UAAU,KAAsB,QAA0C;AACvF,MAAI,KAAK;AACP,UAAM,WAAW,MAAM,SAAS,KAAK,OAAO;AAC5C,QAAI,aAAa,IAAI;AACnB,aAAO;AAAA,IACT,WAAW,CAAC,SAAS,SAAS,gBAAgB,GAAG;AAC/C,aAAO,MAAM,aAAa,OAAO,GAAG,CAAC,kDAAkD;AAAA,IACzF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,MAAM,WAAW,IAAI,KAAK,SAAS,OAAO;AAgB1C,eAAO,oBACL,KACA,EAAE,YAAY,QAAQ,SAAS,IAAI,GACf;AACpB,QAAM,OAAO,SAAS,cAAc,MAAM,GAAG,cAAc,GAAG,CAAC;AAC/D,MAAI,WAAwB,CAAC;AAC7B,MAAI;AAEJ,mBAAiB,KAAK,MAAM,QAAQ,GAAG,GAAG;AACxC,QAAI,EAAE,YAAY,GAAG;AACnB,eAAS,KAAK,MAAM,oBAAoB,IAAI,IAAI,GAAG,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,YAAY,QAAQ,QAAQ,IAAI,CAAC,CAAC;AAAA,IAC1G,WAAW,EAAE,OAAO,KAAK,WAAW,SAAS,QAAQ,EAAE,IAAI,CAAC,GAAG;AAC7D,YAAM,OAAO,IAAI,IAAI,EAAE,MAAM,GAAG;AAChC,YAAM,OAAO,SAAS,EAAE,MAAM,QAAQ,EAAE,IAAI,CAAC;AAE7C,UAAI,KAAK,WAAW,GAAG,GAAG;AACxB,YAAI,SAAS,WAAW;AACtB,mBAAS;AAAA,QACX,WAAW,SAAS,UAAU;AAC5B,mBAAS,KAAK;AAAA,YACZ,MAAM;AAAA,YACN;AAAA,YACA,UAAU,CAAC;AAAA,UACb,CAAC;AAAA,QACH,OAAO;AACL,gBAAM,IAAI,MAAM,mFAAmF;AAAA,QACrG;AAAA,MACF,WAAW,CAAC,KAAK,WAAW,GAAG,GAAG;AAChC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN;AAAA,UACA,UAAU,CAAC;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,GAAC,UAAU,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,IACrC,QAAQ;AAAA,MACN,SAAS,IAAI,OAAO,UAAU;AAC5B,YAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,IAAI;AAC7B,SAAC,GAAG,CAAC,IAAI,MAAM,QAAQ,IAAI,CAAC,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC;AAEvE,eAAO;AAAA,UACL,GAAG;AAAA,UACH,MAAM;AAAA,UACN,QAAQ;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,UAAU,QAAQ,MAAM;AAAA,EAC1B,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,SAAS,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,QAAQ,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;AAAA,EAChG;AACF;",
4
+ "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 { cleanUp } from './utils.js';\n\nexport type RouteMeta = Readonly<{\n path: string;\n file?: URL;\n layout?: URL;\n children: 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 === '') {\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 }\n }\n\n return url;\n}\n\nconst collator = new Intl.Collator('en-US');\n\nconst phantomDirPattern = /\\/?\\$phantom/giu;\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 tree.\n */\nexport default async function collectRoutesFromFS(\n dir: URL,\n { extensions, logger, parent = dir }: CollectRoutesOptions,\n): Promise<RouteMeta> {\n const path = relative(fileURLToPath(parent), fileURLToPath(dir)).replaceAll(phantomDirPattern, '');\n let children: RouteMeta[] = [];\n let layout: URL | undefined;\n\n for await (const d of await opendir(dir)) {\n if (d.isDirectory() && !d.name.startsWith('_')) {\n children.push(await collectRoutesFromFS(new URL(`${d.name}/`, dir), { extensions, logger, parent: dir }));\n } else if (d.isFile() && extensions.includes(extname(d.name))) {\n const file = new URL(d.name, dir);\n const name = basename(d.name, extname(d.name));\n\n if (name.startsWith('$')) {\n if (name === '$layout') {\n layout = file;\n } else if (name === '$index') {\n children.push({\n path: '',\n file,\n children: [],\n });\n } else {\n throw new Error(\n 'Symbol \"$\" is reserved for special directories and files; only \"$layout\", \"$phantom\" and \"$index\" are allowed',\n );\n }\n } else if (!name.startsWith('_')) {\n children.push({\n path: name,\n file,\n children: [],\n });\n }\n }\n }\n\n [children, layout] = await Promise.all([\n Promise.all(\n children.map(async (child) => {\n let { file: f, layout: l } = child;\n [f, l] = await Promise.all([checkFile(f, logger), checkFile(l, logger)]);\n\n return {\n ...child,\n file: f,\n layout: l,\n };\n }),\n ),\n checkFile(layout, logger),\n ]);\n\n return {\n path,\n layout,\n children: children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b))),\n };\n}\n"],
5
+ "mappings": "AAAA,SAAS,SAAS,gBAAgB;AAClC,SAAS,UAAU,SAAS,gBAAgB;AAC5C,SAAS,qBAAqB;AAE9B,SAAS,eAAe;AA4BxB,eAAe,UAAU,KAAsB,QAA0C;AACvF,MAAI,KAAK;AACP,UAAM,WAAW,MAAM,SAAS,KAAK,OAAO;AAC5C,QAAI,aAAa,IAAI;AACnB,aAAO;AAAA,IACT,WAAW,CAAC,SAAS,SAAS,gBAAgB,GAAG;AAC/C,aAAO,MAAM,aAAa,OAAO,GAAG,CAAC,kDAAkD;AAAA,IACzF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,MAAM,WAAW,IAAI,KAAK,SAAS,OAAO;AAE1C,MAAM,oBAAoB;AAgB1B,eAAO,oBACL,KACA,EAAE,YAAY,QAAQ,SAAS,IAAI,GACf;AACpB,QAAM,OAAO,SAAS,cAAc,MAAM,GAAG,cAAc,GAAG,CAAC,EAAE,WAAW,mBAAmB,EAAE;AACjG,MAAI,WAAwB,CAAC;AAC7B,MAAI;AAEJ,mBAAiB,KAAK,MAAM,QAAQ,GAAG,GAAG;AACxC,QAAI,EAAE,YAAY,KAAK,CAAC,EAAE,KAAK,WAAW,GAAG,GAAG;AAC9C,eAAS,KAAK,MAAM,oBAAoB,IAAI,IAAI,GAAG,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,YAAY,QAAQ,QAAQ,IAAI,CAAC,CAAC;AAAA,IAC1G,WAAW,EAAE,OAAO,KAAK,WAAW,SAAS,QAAQ,EAAE,IAAI,CAAC,GAAG;AAC7D,YAAM,OAAO,IAAI,IAAI,EAAE,MAAM,GAAG;AAChC,YAAM,OAAO,SAAS,EAAE,MAAM,QAAQ,EAAE,IAAI,CAAC;AAE7C,UAAI,KAAK,WAAW,GAAG,GAAG;AACxB,YAAI,SAAS,WAAW;AACtB,mBAAS;AAAA,QACX,WAAW,SAAS,UAAU;AAC5B,mBAAS,KAAK;AAAA,YACZ,MAAM;AAAA,YACN;AAAA,YACA,UAAU,CAAC;AAAA,UACb,CAAC;AAAA,QACH,OAAO;AACL,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAAA,MACF,WAAW,CAAC,KAAK,WAAW,GAAG,GAAG;AAChC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN;AAAA,UACA,UAAU,CAAC;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,GAAC,UAAU,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,IACrC,QAAQ;AAAA,MACN,SAAS,IAAI,OAAO,UAAU;AAC5B,YAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,IAAI;AAC7B,SAAC,GAAG,CAAC,IAAI,MAAM,QAAQ,IAAI,CAAC,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC;AAEvE,eAAO;AAAA,UACL,GAAG;AAAA,UACH,MAAM;AAAA,UACN,QAAQ;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,UAAU,QAAQ,MAAM;AAAA,EAC1B,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,SAAS,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,QAAQ,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;AAAA,EAChG;AACF;",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createRoutesFromMeta.d.ts","sourceRoot":"","sources":["../src/vite-plugin/createRoutesFromMeta.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAmDjE;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,eAAe,GAAG,MAAM,CAgD1G"}
1
+ {"version":3,"file":"createRoutesFromMeta.d.ts","sourceRoot":"","sources":["../src/vite-plugin/createRoutesFromMeta.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAmDjE;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,eAAe,GAAG,MAAM,CAgD1G"}
@@ -4,7 +4,7 @@ import { template, transform as transformer } from "@vaadin/hilla-generator-util
4
4
  import createSourceFile from "@vaadin/hilla-generator-utils/createSourceFile.js";
5
5
  import ts, {
6
6
  } from "typescript";
7
- import { transformRoute } from "../runtime/utils.js";
7
+ import { transformTreeSync } from "../shared/transformTree.js";
8
8
  import { convertFSRouteSegmentToURLPatternFormat } from "./utils.js";
9
9
  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
10
10
  function relativize(url, generatedDir) {
@@ -39,7 +39,7 @@ function createRoutesFromMeta(views, { code: codeFile }) {
39
39
  )
40
40
  ];
41
41
  let id = 0;
42
- const routes = transformRoute(
42
+ const routes = transformTreeSync(
43
43
  views,
44
44
  (view) => view.children.values(),
45
45
  ({ file: file2, layout, path }, children) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/vite-plugin/createRoutesFromMeta.ts"],
4
- "sourcesContent": ["import { sep, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js';\nimport createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';\nimport ts, {\n type CallExpression,\n type ImportDeclaration,\n type StringLiteral,\n type VariableStatement,\n} from 'typescript';\nimport { transformRoute } from '../runtime/utils.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\n/**\n * Convert a file URL to a relative path from the generated directory.\n *\n * @param url - The file URL to convert.\n * @param generatedDir - The directory where the generated view file will be stored.\n */\nfunction relativize(url: URL, generatedDir: URL): string {\n const result = relative(fileURLToPath(generatedDir), fileURLToPath(url)).replaceAll(sep, '/');\n\n if (!result.startsWith('.')) {\n return `./${result}`;\n }\n\n return result;\n}\n\n/**\n * Create an import declaration for a `views` module.\n *\n * @param mod - The name of the route module to import.\n * @param file - The file path of the module.\n */\nfunction createImport(mod: string, file: string): ImportDeclaration {\n const path = `${file.substring(0, file.lastIndexOf('.'))}.js`;\n return template(`import * as ${mod} from '${path}';\\n`, ([statement]) => statement as ts.ImportDeclaration);\n}\n\n/**\n * Create an abstract route creation function call. The nested function calls 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 */\nfunction createRouteData(path: string, mod: string | undefined, children: readonly CallExpression[]): CallExpression {\n return template(\n `const route = createRoute(\"${path}\"${mod ? `, ${mod}` : ''}${children.length > 0 ? `, CHILDREN` : ''})`,\n ([statement]) => (statement as VariableStatement).declarationList.declarations[0].initializer as CallExpression,\n [\n transformer((node) =>\n ts.isIdentifier(node) && node.text === 'CHILDREN' ? ts.factory.createArrayLiteralExpression(children) : node,\n ),\n ],\n );\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 */\nexport default function createRoutesFromMeta(views: RouteMeta, { code: codeFile }: RuntimeFileUrls): string {\n const codeDir = new URL('./', codeFile);\n const imports: ImportDeclaration[] = [\n template(\n 'import { createRoute } from \"@vaadin/hilla-file-router/runtime.js\";',\n ([statement]) => statement as ts.ImportDeclaration,\n ),\n ];\n let id = 0;\n\n const routes = transformRoute<RouteMeta, CallExpression>(\n views,\n (view) => view.children.values(),\n ({ file, layout, path }, children) => {\n const currentId = id;\n id += 1;\n\n let mod: string | undefined;\n if (file) {\n mod = `Page${currentId}`;\n imports.push(createImport(mod, relativize(file, codeDir)));\n } else if (layout) {\n mod = `Layout${currentId}`;\n imports.push(createImport(mod, relativize(layout, codeDir)));\n }\n\n return createRouteData(convertFSRouteSegmentToURLPatternFormat(path), mod, children);\n },\n );\n\n const routeDeclaration = template(\n `import a from 'IMPORTS';\n\nconst routes = ROUTE;\n\nexport default routes;\n`,\n [\n transformer((node) =>\n ts.isImportDeclaration(node) && (node.moduleSpecifier as StringLiteral).text === 'IMPORTS' ? imports : node,\n ),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'ROUTE' ? routes : node)),\n ],\n );\n\n const file = createSourceFile(routeDeclaration, 'views.ts');\n\n return printer.printFile(file);\n}\n"],
5
- "mappings": "AAAA,SAAS,KAAK,gBAAgB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,UAAU,aAAa,mBAAmB;AACnD,OAAO,sBAAsB;AAC7B,OAAO;AAAA,OAKA;AACP,SAAS,sBAAsB;AAG/B,SAAS,+CAA+C;AAExD,MAAM,UAAU,GAAG,cAAc,EAAE,SAAS,GAAG,YAAY,SAAS,CAAC;AAQrE,SAAS,WAAW,KAAU,cAA2B;AACvD,QAAM,SAAS,SAAS,cAAc,YAAY,GAAG,cAAc,GAAG,CAAC,EAAE,WAAW,KAAK,GAAG;AAE5F,MAAI,CAAC,OAAO,WAAW,GAAG,GAAG;AAC3B,WAAO,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO;AACT;AAQA,SAAS,aAAa,KAAa,MAAiC;AAClE,QAAM,OAAO,GAAG,KAAK,UAAU,GAAG,KAAK,YAAY,GAAG,CAAC,CAAC;AACxD,SAAO,SAAS,eAAe,GAAG,UAAU,IAAI;AAAA,GAAQ,CAAC,CAAC,SAAS,MAAM,SAAiC;AAC5G;AASA,SAAS,gBAAgB,MAAc,KAAyB,UAAqD;AACnH,SAAO;AAAA,IACL,8BAA8B,IAAI,IAAI,MAAM,KAAK,GAAG,KAAK,EAAE,GAAG,SAAS,SAAS,IAAI,eAAe,EAAE;AAAA,IACrG,CAAC,CAAC,SAAS,MAAO,UAAgC,gBAAgB,aAAa,CAAC,EAAE;AAAA,IAClF;AAAA,MACE;AAAA,QAAY,CAAC,SACX,GAAG,aAAa,IAAI,KAAK,KAAK,SAAS,aAAa,GAAG,QAAQ,6BAA6B,QAAQ,IAAI;AAAA,MAC1G;AAAA,IACF;AAAA,EACF;AACF;AAQe,SAAR,qBAAsC,OAAkB,EAAE,MAAM,SAAS,GAA4B;AAC1G,QAAM,UAAU,IAAI,IAAI,MAAM,QAAQ;AACtC,QAAM,UAA+B;AAAA,IACnC;AAAA,MACE;AAAA,MACA,CAAC,CAAC,SAAS,MAAM;AAAA,IACnB;AAAA,EACF;AACA,MAAI,KAAK;AAET,QAAM,SAAS;AAAA,IACb;AAAA,IACA,CAAC,SAAS,KAAK,SAAS,OAAO;AAAA,IAC/B,CAAC,EAAE,MAAAA,OAAM,QAAQ,KAAK,GAAG,aAAa;AACpC,YAAM,YAAY;AAClB,YAAM;AAEN,UAAI;AACJ,UAAIA,OAAM;AACR,cAAM,OAAO,SAAS;AACtB,gBAAQ,KAAK,aAAa,KAAK,WAAWA,OAAM,OAAO,CAAC,CAAC;AAAA,MAC3D,WAAW,QAAQ;AACjB,cAAM,SAAS,SAAS;AACxB,gBAAQ,KAAK,aAAa,KAAK,WAAW,QAAQ,OAAO,CAAC,CAAC;AAAA,MAC7D;AAEA,aAAO,gBAAgB,wCAAwC,IAAI,GAAG,KAAK,QAAQ;AAAA,IACrF;AAAA,EACF;AAEA,QAAM,mBAAmB;AAAA,IACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA;AAAA,MACE;AAAA,QAAY,CAAC,SACX,GAAG,oBAAoB,IAAI,KAAM,KAAK,gBAAkC,SAAS,YAAY,UAAU;AAAA,MACzG;AAAA,MACA,YAAY,CAAC,SAAU,GAAG,aAAa,IAAI,KAAK,KAAK,SAAS,UAAU,SAAS,IAAK;AAAA,IACxF;AAAA,EACF;AAEA,QAAM,OAAO,iBAAiB,kBAAkB,UAAU;AAE1D,SAAO,QAAQ,UAAU,IAAI;AAC/B;",
4
+ "sourcesContent": ["import { sep, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js';\nimport createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';\nimport ts, {\n type CallExpression,\n type ImportDeclaration,\n type StringLiteral,\n type VariableStatement,\n} from 'typescript';\n\nimport { transformTreeSync } 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\n/**\n * Convert a file URL to a relative path from the generated directory.\n *\n * @param url - The file URL to convert.\n * @param generatedDir - The directory where the generated view file will be stored.\n */\nfunction relativize(url: URL, generatedDir: URL): string {\n const result = relative(fileURLToPath(generatedDir), fileURLToPath(url)).replaceAll(sep, '/');\n\n if (!result.startsWith('.')) {\n return `./${result}`;\n }\n\n return result;\n}\n\n/**\n * Create an import declaration for a `views` module.\n *\n * @param mod - The name of the route module to import.\n * @param file - The file path of the module.\n */\nfunction createImport(mod: string, file: string): ImportDeclaration {\n const path = `${file.substring(0, file.lastIndexOf('.'))}.js`;\n return template(`import * as ${mod} from '${path}';\\n`, ([statement]) => statement as ts.ImportDeclaration);\n}\n\n/**\n * Create an abstract route creation function call. The nested function calls 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 */\nfunction createRouteData(path: string, mod: string | undefined, children: readonly CallExpression[]): CallExpression {\n return template(\n `const route = createRoute(\"${path}\"${mod ? `, ${mod}` : ''}${children.length > 0 ? `, CHILDREN` : ''})`,\n ([statement]) => (statement as VariableStatement).declarationList.declarations[0].initializer as CallExpression,\n [\n transformer((node) =>\n ts.isIdentifier(node) && node.text === 'CHILDREN' ? ts.factory.createArrayLiteralExpression(children) : node,\n ),\n ],\n );\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 */\nexport default function createRoutesFromMeta(views: RouteMeta, { code: codeFile }: RuntimeFileUrls): string {\n const codeDir = new URL('./', codeFile);\n const imports: ImportDeclaration[] = [\n template(\n 'import { createRoute } from \"@vaadin/hilla-file-router/runtime.js\";',\n ([statement]) => statement as ts.ImportDeclaration,\n ),\n ];\n let id = 0;\n\n const routes = transformTreeSync<RouteMeta, CallExpression>(\n views,\n (view) => view.children.values(),\n ({ file, layout, path }, children) => {\n const currentId = id;\n id += 1;\n\n let mod: string | undefined;\n if (file) {\n mod = `Page${currentId}`;\n imports.push(createImport(mod, relativize(file, codeDir)));\n } else if (layout) {\n mod = `Layout${currentId}`;\n imports.push(createImport(mod, relativize(layout, codeDir)));\n }\n\n return createRouteData(convertFSRouteSegmentToURLPatternFormat(path), mod, children);\n },\n );\n\n const routeDeclaration = template(\n `import a from 'IMPORTS';\n\nconst routes = ROUTE;\n\nexport default routes;\n`,\n [\n transformer((node) =>\n ts.isImportDeclaration(node) && (node.moduleSpecifier as StringLiteral).text === 'IMPORTS' ? imports : node,\n ),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'ROUTE' ? routes : node)),\n ],\n );\n\n const file = createSourceFile(routeDeclaration, 'views.ts');\n\n return printer.printFile(file);\n}\n"],
5
+ "mappings": "AAAA,SAAS,KAAK,gBAAgB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,UAAU,aAAa,mBAAmB;AACnD,OAAO,sBAAsB;AAC7B,OAAO;AAAA,OAKA;AAEP,SAAS,yBAAyB;AAGlC,SAAS,+CAA+C;AAExD,MAAM,UAAU,GAAG,cAAc,EAAE,SAAS,GAAG,YAAY,SAAS,CAAC;AAQrE,SAAS,WAAW,KAAU,cAA2B;AACvD,QAAM,SAAS,SAAS,cAAc,YAAY,GAAG,cAAc,GAAG,CAAC,EAAE,WAAW,KAAK,GAAG;AAE5F,MAAI,CAAC,OAAO,WAAW,GAAG,GAAG;AAC3B,WAAO,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO;AACT;AAQA,SAAS,aAAa,KAAa,MAAiC;AAClE,QAAM,OAAO,GAAG,KAAK,UAAU,GAAG,KAAK,YAAY,GAAG,CAAC,CAAC;AACxD,SAAO,SAAS,eAAe,GAAG,UAAU,IAAI;AAAA,GAAQ,CAAC,CAAC,SAAS,MAAM,SAAiC;AAC5G;AASA,SAAS,gBAAgB,MAAc,KAAyB,UAAqD;AACnH,SAAO;AAAA,IACL,8BAA8B,IAAI,IAAI,MAAM,KAAK,GAAG,KAAK,EAAE,GAAG,SAAS,SAAS,IAAI,eAAe,EAAE;AAAA,IACrG,CAAC,CAAC,SAAS,MAAO,UAAgC,gBAAgB,aAAa,CAAC,EAAE;AAAA,IAClF;AAAA,MACE;AAAA,QAAY,CAAC,SACX,GAAG,aAAa,IAAI,KAAK,KAAK,SAAS,aAAa,GAAG,QAAQ,6BAA6B,QAAQ,IAAI;AAAA,MAC1G;AAAA,IACF;AAAA,EACF;AACF;AAQe,SAAR,qBAAsC,OAAkB,EAAE,MAAM,SAAS,GAA4B;AAC1G,QAAM,UAAU,IAAI,IAAI,MAAM,QAAQ;AACtC,QAAM,UAA+B;AAAA,IACnC;AAAA,MACE;AAAA,MACA,CAAC,CAAC,SAAS,MAAM;AAAA,IACnB;AAAA,EACF;AACA,MAAI,KAAK;AAET,QAAM,SAAS;AAAA,IACb;AAAA,IACA,CAAC,SAAS,KAAK,SAAS,OAAO;AAAA,IAC/B,CAAC,EAAE,MAAAA,OAAM,QAAQ,KAAK,GAAG,aAAa;AACpC,YAAM,YAAY;AAClB,YAAM;AAEN,UAAI;AACJ,UAAIA,OAAM;AACR,cAAM,OAAO,SAAS;AACtB,gBAAQ,KAAK,aAAa,KAAK,WAAWA,OAAM,OAAO,CAAC,CAAC;AAAA,MAC3D,WAAW,QAAQ;AACjB,cAAM,SAAS,SAAS;AACxB,gBAAQ,KAAK,aAAa,KAAK,WAAW,QAAQ,OAAO,CAAC,CAAC;AAAA,MAC7D;AAEA,aAAO,gBAAgB,wCAAwC,IAAI,GAAG,KAAK,QAAQ;AAAA,IACrF;AAAA,EACF;AAEA,QAAM,mBAAmB;AAAA,IACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA;AAAA,MACE;AAAA,QAAY,CAAC,SACX,GAAG,oBAAoB,IAAI,KAAM,KAAK,gBAAkC,SAAS,YAAY,UAAU;AAAA,MACzG;AAAA,MACA,YAAY,CAAC,SAAU,GAAG,aAAa,IAAI,KAAK,KAAK,SAAS,UAAU,SAAS,IAAK;AAAA,IACxF;AAAA,EACF;AAEA,QAAM,OAAO,iBAAiB,kBAAkB,UAAU;AAE1D,SAAO,QAAQ,UAAU,IAAI;AAC/B;",
6
6
  "names": ["file"]
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createViewConfigJson.d.ts","sourceRoot":"","sources":["../src/vite-plugin/createViewConfigJson.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAgB1D;;;;;GAKG;AACH,wBAA8B,oBAAoB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DpF"}
1
+ {"version":3,"file":"createViewConfigJson.d.ts","sourceRoot":"","sources":["../src/vite-plugin/createViewConfigJson.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAgB1D;;;;;GAKG;AACH,wBAA8B,oBAAoB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAiDpF"}
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { Script } from "node:vm";
3
3
  import ts, {} from "typescript";
4
4
  import { convertComponentNameToTitle } from "../shared/convertComponentNameToTitle.js";
5
- import traverse from "../shared/traverse.js";
5
+ import { transformTree } from "../shared/transformTree.js";
6
6
  import { convertFSRouteSegmentToURLPatternFormat, extractParameterFromRouteSegment } from "./utils.js";
7
7
  function* walkAST(node) {
8
8
  yield node;
@@ -11,58 +11,50 @@ function* walkAST(node) {
11
11
  }
12
12
  }
13
13
  async function createViewConfigJson(views) {
14
- const res = await Promise.all(
15
- Array.from(traverse(views), async (branch) => {
16
- const configs = await Promise.all(
17
- branch.map(async ({ path, file, layout }) => {
18
- if (!file && !layout) {
19
- return [
20
- convertFSRouteSegmentToURLPatternFormat(path),
21
- { params: extractParameterFromRouteSegment(path) }
22
- ];
23
- }
24
- const sourceFile = ts.createSourceFile(
25
- "f.ts",
26
- await readFile(file ?? layout, "utf8"),
27
- ts.ScriptTarget.ESNext,
28
- true
29
- );
30
- let config;
31
- let waitingForIdentifier = false;
32
- let componentName;
33
- for (const node of walkAST(sourceFile)) {
34
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "config") {
35
- if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
36
- const code = node.initializer.getText(sourceFile);
37
- const script = new Script(`(${code})`);
38
- config = script.runInThisContext();
39
- }
40
- } else if (node.getText(sourceFile).startsWith("export default")) {
41
- waitingForIdentifier = true;
42
- } else if (waitingForIdentifier && ts.isIdentifier(node)) {
43
- componentName = node.text;
44
- break;
45
- }
46
- }
47
- const _path = config?.route ?? path;
48
- const pattern = convertFSRouteSegmentToURLPatternFormat(_path);
49
- return [
50
- pattern,
51
- {
52
- ...config,
53
- params: extractParameterFromRouteSegment(_path),
54
- title: config?.title ?? convertComponentNameToTitle(componentName)
55
- }
56
- ];
57
- })
14
+ const res = await transformTree(
15
+ views,
16
+ (route) => route.children.values(),
17
+ async ({ path, file, layout }, children) => {
18
+ if (!file && !layout) {
19
+ return {
20
+ route: path,
21
+ params: extractParameterFromRouteSegment(path),
22
+ children
23
+ };
24
+ }
25
+ const sourceFile = ts.createSourceFile(
26
+ "f.ts",
27
+ await readFile(file ?? layout, "utf8"),
28
+ ts.ScriptTarget.ESNext,
29
+ true
58
30
  );
59
- const key = configs.map(([path]) => path).join("/");
60
- const params = configs.reduce((acc, [, { params: p }]) => Object.assign(acc, p), {});
61
- const [, value] = configs[configs.length - 1];
62
- return [key, { ...value, params: Object.keys(params).length > 0 ? params : void 0 }];
63
- })
31
+ let config;
32
+ let waitingForIdentifier = false;
33
+ let componentName;
34
+ for (const node of walkAST(sourceFile)) {
35
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "config") {
36
+ if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
37
+ const code = node.initializer.getText(sourceFile);
38
+ const script = new Script(`(${code})`);
39
+ config = script.runInThisContext();
40
+ }
41
+ } else if (node.getText(sourceFile).startsWith("export default")) {
42
+ waitingForIdentifier = true;
43
+ } else if (waitingForIdentifier && ts.isIdentifier(node)) {
44
+ componentName = node.text;
45
+ break;
46
+ }
47
+ }
48
+ return {
49
+ route: convertFSRouteSegmentToURLPatternFormat(path),
50
+ ...config,
51
+ params: extractParameterFromRouteSegment(config?.route ?? path),
52
+ title: config?.title ?? convertComponentNameToTitle(componentName),
53
+ children
54
+ };
55
+ }
64
56
  );
65
- return JSON.stringify(Object.fromEntries(res));
57
+ return JSON.stringify(res);
66
58
  }
67
59
  export {
68
60
  createViewConfigJson as default
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/vite-plugin/createViewConfigJson.ts"],
4
- "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 traverse from '../shared/traverse.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: RouteMeta): Promise<string> {\n const res = await Promise.all(\n Array.from(traverse(views), async (branch) => {\n const configs = await Promise.all(\n branch.map(async ({ path, file, layout }): Promise<[string, ServerViewConfig]> => {\n if (!file && !layout) {\n return [\n convertFSRouteSegmentToURLPatternFormat(path),\n { params: extractParameterFromRouteSegment(path) },\n ] as const;\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 }\n } else if (node.getText(sourceFile).startsWith('export default')) {\n waitingForIdentifier = true;\n } else if (waitingForIdentifier && ts.isIdentifier(node)) {\n componentName = node.text;\n break;\n }\n }\n const _path = config?.route ?? path;\n const pattern = convertFSRouteSegmentToURLPatternFormat(_path);\n\n return [\n pattern,\n {\n ...config,\n params: extractParameterFromRouteSegment(_path),\n title: config?.title ?? convertComponentNameToTitle(componentName),\n },\n ] as const;\n }),\n );\n\n const key = configs.map(([path]) => path).join('/');\n const params = configs.reduce((acc, [, { params: p }]) => Object.assign(acc, p), {});\n const [, value] = configs[configs.length - 1];\n\n return [key, { ...value, params: Object.keys(params).length > 0 ? params : undefined }] as const;\n }),\n );\n\n return JSON.stringify(Object.fromEntries(res));\n}\n"],
5
- "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,OAAO,YAAuB;AAC9B,SAAS,mCAAmC;AAE5C,OAAO,cAAc;AAGrB,SAAS,yCAAyC,wCAAwC;AAO1F,UAAU,QAAQ,MAA6B;AAC7C,QAAM;AAEN,aAAW,SAAS,KAAK,YAAY,GAAG;AACtC,WAAO,QAAQ,KAAK;AAAA,EACtB;AACF;AAQA,eAAO,qBAA4C,OAAmC;AACpF,QAAM,MAAM,MAAM,QAAQ;AAAA,IACxB,MAAM,KAAK,SAAS,KAAK,GAAG,OAAO,WAAW;AAC5C,YAAM,UAAU,MAAM,QAAQ;AAAA,QAC5B,OAAO,IAAI,OAAO,EAAE,MAAM,MAAM,OAAO,MAA2C;AAChF,cAAI,CAAC,QAAQ,CAAC,QAAQ;AACpB,mBAAO;AAAA,cACL,wCAAwC,IAAI;AAAA,cAC5C,EAAE,QAAQ,iCAAiC,IAAI,EAAE;AAAA,YACnD;AAAA,UACF;AAEA,gBAAM,aAAa,GAAG;AAAA,YACpB;AAAA,YACA,MAAM,SAAS,QAAQ,QAAS,MAAM;AAAA,YACtC,GAAG,aAAa;AAAA,YAChB;AAAA,UACF;AACA,cAAI;AACJ,cAAI,uBAAuB;AAC3B,cAAI;AAEJ,qBAAW,QAAQ,QAAQ,UAAU,GAAG;AACtC,gBAAI,GAAG,sBAAsB,IAAI,KAAK,GAAG,aAAa,KAAK,IAAI,KAAK,KAAK,KAAK,SAAS,UAAU;AAC/F,kBAAI,KAAK,eAAe,GAAG,0BAA0B,KAAK,WAAW,GAAG;AACtE,sBAAM,OAAO,KAAK,YAAY,QAAQ,UAAU;AAChD,sBAAM,SAAS,IAAI,OAAO,IAAI,IAAI,GAAG;AACrC,yBAAS,OAAO,iBAAiB;AAAA,cACnC;AAAA,YACF,WAAW,KAAK,QAAQ,UAAU,EAAE,WAAW,gBAAgB,GAAG;AAChE,qCAAuB;AAAA,YACzB,WAAW,wBAAwB,GAAG,aAAa,IAAI,GAAG;AACxD,8BAAgB,KAAK;AACrB;AAAA,YACF;AAAA,UACF;AACA,gBAAM,QAAQ,QAAQ,SAAS;AAC/B,gBAAM,UAAU,wCAAwC,KAAK;AAE7D,iBAAO;AAAA,YACL;AAAA,YACA;AAAA,cACE,GAAG;AAAA,cACH,QAAQ,iCAAiC,KAAK;AAAA,cAC9C,OAAO,QAAQ,SAAS,4BAA4B,aAAa;AAAA,YACnE;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH;AAEA,YAAM,MAAM,QAAQ,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI,EAAE,KAAK,GAAG;AAClD,YAAM,SAAS,QAAQ,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,MAAM,OAAO,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;AACnF,YAAM,CAAC,EAAE,KAAK,IAAI,QAAQ,QAAQ,SAAS,CAAC;AAE5C,aAAO,CAAC,KAAK,EAAE,GAAG,OAAO,QAAQ,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS,OAAU,CAAC;AAAA,IACxF,CAAC;AAAA,EACH;AAEA,SAAO,KAAK,UAAU,OAAO,YAAY,GAAG,CAAC;AAC/C;",
4
+ "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: RouteMeta): Promise<string> {\n const res = await transformTree<RouteMeta, ServerViewConfig>(\n views,\n (route) => route.children.values(),\n async ({ path, file, layout }, children) => {\n if (!file && !layout) {\n return {\n route: path,\n params: extractParameterFromRouteSegment(path),\n children,\n } as const;\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 }\n } else if (node.getText(sourceFile).startsWith('export default')) {\n waitingForIdentifier = true;\n } else if (waitingForIdentifier && ts.isIdentifier(node)) {\n componentName = node.text;\n break;\n }\n }\n\n return {\n route: convertFSRouteSegmentToURLPatternFormat(path),\n ...config,\n params: extractParameterFromRouteSegment(config?.route ?? path),\n title: config?.title ?? convertComponentNameToTitle(componentName),\n children,\n } as const;\n },\n );\n\n return JSON.stringify(res);\n}\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,OAAO,YAAuB;AAC9B,SAAS,mCAAmC;AAE5C,SAAS,qBAAqB;AAG9B,SAAS,yCAAyC,wCAAwC;AAO1F,UAAU,QAAQ,MAA6B;AAC7C,QAAM;AAEN,aAAW,SAAS,KAAK,YAAY,GAAG;AACtC,WAAO,QAAQ,KAAK;AAAA,EACtB;AACF;AAQA,eAAO,qBAA4C,OAAmC;AACpF,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,UAAU,MAAM,SAAS,OAAO;AAAA,IACjC,OAAO,EAAE,MAAM,MAAM,OAAO,GAAG,aAAa;AAC1C,UAAI,CAAC,QAAQ,CAAC,QAAQ;AACpB,eAAO;AAAA,UACL,OAAO;AAAA,UACP,QAAQ,iCAAiC,IAAI;AAAA,UAC7C;AAAA,QACF;AAAA,MACF;AAEA,YAAM,aAAa,GAAG;AAAA,QACpB;AAAA,QACA,MAAM,SAAS,QAAQ,QAAS,MAAM;AAAA,QACtC,GAAG,aAAa;AAAA,QAChB;AAAA,MACF;AACA,UAAI;AACJ,UAAI,uBAAuB;AAC3B,UAAI;AAEJ,iBAAW,QAAQ,QAAQ,UAAU,GAAG;AACtC,YAAI,GAAG,sBAAsB,IAAI,KAAK,GAAG,aAAa,KAAK,IAAI,KAAK,KAAK,KAAK,SAAS,UAAU;AAC/F,cAAI,KAAK,eAAe,GAAG,0BAA0B,KAAK,WAAW,GAAG;AACtE,kBAAM,OAAO,KAAK,YAAY,QAAQ,UAAU;AAChD,kBAAM,SAAS,IAAI,OAAO,IAAI,IAAI,GAAG;AACrC,qBAAS,OAAO,iBAAiB;AAAA,UACnC;AAAA,QACF,WAAW,KAAK,QAAQ,UAAU,EAAE,WAAW,gBAAgB,GAAG;AAChE,iCAAuB;AAAA,QACzB,WAAW,wBAAwB,GAAG,aAAa,IAAI,GAAG;AACxD,0BAAgB,KAAK;AACrB;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,QACL,OAAO,wCAAwC,IAAI;AAAA,QACnD,GAAG;AAAA,QACH,QAAQ,iCAAiC,QAAQ,SAAS,IAAI;AAAA,QAC9D,OAAO,QAAQ,SAAS,4BAA4B,aAAa;AAAA,QACjE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,KAAK,UAAU,GAAG;AAC3B;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { RouteParamType } from '../shared/routeParamType.js';
2
2
  /**
3
- * Converts a file system pattern to a URL pattern string.
3
+ * Converts a file system pattern to an URL pattern string.
4
4
  *
5
5
  * @param segment - a string representing a file system pattern:
6
6
  * - `{param}` - for a required single parameter;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/vite-plugin/utils.ts"],
4
- "sourcesContent": ["import { RouteParamType } from '../shared/routeParamType.js';\n\nconst routeParamTypeMap: ReadonlyMap<RouteParamType, RegExp> = new Map([\n [RouteParamType.Wildcard, /\\{\\.{3}(.+)\\}/gu],\n [RouteParamType.Optional, /\\{{2}(.+)\\}{2}/gu],\n [RouteParamType.Required, /\\{(.+)\\}/gu],\n]);\n\n// eslint-disable-next-line 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 a 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"],
4
+ "sourcesContent": ["import { RouteParamType } from '../shared/routeParamType.js';\n\nconst routeParamTypeMap: ReadonlyMap<RouteParamType, RegExp> = new Map([\n [RouteParamType.Wildcard, /\\{\\.{3}(.+)\\}/gu],\n [RouteParamType.Optional, /\\{{2}(.+)\\}{2}/gu],\n [RouteParamType.Required, /\\{(.+)\\}/gu],\n]);\n\n// eslint-disable-next-line 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"],
5
5
  "mappings": "AAAA,SAAS,sBAAsB;AAE/B,MAAM,oBAAyD,oBAAI,IAAI;AAAA,EACrE,CAAC,eAAe,UAAU,iBAAiB;AAAA,EAC3C,CAAC,eAAe,UAAU,kBAAkB;AAAA,EAC5C,CAAC,eAAe,UAAU,YAAY;AACxC,CAAC;AAGD,SAAS,YAAY,MAA8B;AAEjD,UAAQ,MAAM;AAAA,IACZ,KAAK,eAAe;AAClB,aAAO;AAAA,IACT,KAAK,eAAe;AAClB,aAAO;AAAA,IACT,KAAK,eAAe;AAClB,aAAO;AAAA,EACX;AACF;AAeO,SAAS,wCAAwC,SAAyB;AAC/E,MAAI,MAAM;AAEV,oBAAkB,QAAQ,CAAC,SAAS,SAAS;AAC3C,UAAM,IAAI,WAAW,SAAS,YAAY,IAAI,CAAC;AAAA,EACjD,CAAC;AAED,SAAO;AACT;AAQO,SAAS,iCAAiC,SAA2D;AAC1G,MAAI,WAAW;AACf,QAAM,SAAyC,CAAC;AAEhD,aAAW,CAAC,MAAM,OAAO,KAAK,mBAAmB;AAC/C,UAAM,WAAW,IAAI,OAAO,QAAQ,QAAQ,QAAQ,KAAK;AACzD,eAAW,SAAS,WAAW,UAAU,CAAC,UAAU;AAClD,YAAM,MAAM,MAAM,WAAW,SAAS,YAAY,IAAI,CAAC;AACvD,aAAO,GAAG,IAAI;AACd,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAMO,SAAS,QAAQ,MAAsB;AAC5C,MAAI,MAAM;AAEV,aAAW,WAAW,kBAAkB,OAAO,GAAG;AAChD,UAAM,IAAI,WAAW,SAAS,IAAI;AAAA,EACpC;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,11 +0,0 @@
1
- /**
2
- * Transforms the whole route tree into a new format.
3
- *
4
- * @param route - The route to transform.
5
- * @param getChildren - A function that returns the children of the route.
6
- * @param transformer - A function that transforms the route and its children.
7
- *
8
- * @returns The transformed route.
9
- */
10
- export declare function transformRoute<T, U>(route: T, getChildren: (route: T) => IterableIterator<T> | null | undefined, transformer: (route: T, children: readonly U[]) => U): U;
11
- //# sourceMappingURL=utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/runtime/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EACjC,KAAK,EAAE,CAAC,EACR,WAAW,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EACjE,WAAW,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,GACnD,CAAC,CAOH"}
package/runtime/utils.js DELETED
@@ -1,11 +0,0 @@
1
- function transformRoute(route, getChildren, transformer) {
2
- const children = getChildren(route);
3
- return transformer(
4
- route,
5
- children ? Array.from(children, (child) => transformRoute(child, getChildren, transformer)) : []
6
- );
7
- }
8
- export {
9
- transformRoute
10
- };
11
- //# sourceMappingURL=utils.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/runtime/utils.ts"],
4
- "sourcesContent": ["/**\n * Transforms the whole route tree into a new format.\n *\n * @param route - The route to transform.\n * @param getChildren - A function that returns the children of the route.\n * @param transformer - A function that transforms the route and its children.\n *\n * @returns The transformed route.\n */\nexport function transformRoute<T, U>(\n route: T,\n getChildren: (route: T) => IterableIterator<T> | null | undefined,\n transformer: (route: T, children: readonly U[]) => U,\n): U {\n const children = getChildren(route);\n\n return transformer(\n route,\n children ? Array.from(children, (child) => transformRoute(child, getChildren, transformer)) : [],\n );\n}\n"],
5
- "mappings": "AASO,SAAS,eACd,OACA,aACA,aACG;AACH,QAAM,WAAW,YAAY,KAAK;AAElC,SAAO;AAAA,IACL;AAAA,IACA,WAAW,MAAM,KAAK,UAAU,CAAC,UAAU,eAAe,OAAO,aAAa,WAAW,CAAC,IAAI,CAAC;AAAA,EACjG;AACF;",
6
- "names": []
7
- }