effect-start 0.19.0 → 0.20.1

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.
Files changed (144) hide show
  1. package/README.md +3 -3
  2. package/dist/Development.d.ts +3 -3
  3. package/dist/Development.js +3 -2
  4. package/dist/Effectify.d.ts +212 -0
  5. package/dist/Effectify.js +19 -0
  6. package/dist/FilePathPattern.d.ts +29 -0
  7. package/dist/FilePathPattern.js +86 -0
  8. package/dist/FileRouter.d.ts +39 -41
  9. package/dist/FileRouter.js +104 -158
  10. package/dist/FileRouterCodegen.d.ts +7 -8
  11. package/dist/FileRouterCodegen.js +97 -66
  12. package/dist/PlatformError.d.ts +46 -0
  13. package/dist/PlatformError.js +43 -0
  14. package/dist/PlatformRuntime.d.ts +23 -0
  15. package/dist/PlatformRuntime.js +42 -0
  16. package/dist/RouteBody.d.ts +1 -1
  17. package/dist/Start.d.ts +34 -3
  18. package/dist/Start.js +31 -6
  19. package/dist/bun/BunPlatformHttpServer.d.ts +10 -0
  20. package/dist/bun/BunPlatformHttpServer.js +53 -0
  21. package/dist/bun/BunRoute.d.ts +3 -5
  22. package/dist/bun/BunRoute.js +9 -17
  23. package/dist/bun/BunRuntime.d.ts +2 -1
  24. package/dist/bun/BunRuntime.js +10 -5
  25. package/dist/bun/BunServer.d.ts +33 -0
  26. package/dist/bun/BunServer.js +133 -0
  27. package/dist/bun/BunServerRequest.d.ts +60 -0
  28. package/dist/bun/BunServerRequest.js +252 -0
  29. package/dist/bun/index.d.ts +1 -1
  30. package/dist/bun/index.js +1 -1
  31. package/dist/datastar/actions/fetch.d.ts +30 -0
  32. package/dist/datastar/actions/fetch.js +411 -0
  33. package/dist/datastar/actions/peek.d.ts +1 -0
  34. package/dist/datastar/actions/peek.js +14 -0
  35. package/dist/datastar/actions/setAll.d.ts +1 -0
  36. package/dist/datastar/actions/setAll.js +13 -0
  37. package/dist/datastar/actions/toggleAll.d.ts +1 -0
  38. package/dist/datastar/actions/toggleAll.js +13 -0
  39. package/dist/datastar/attributes/attr.d.ts +1 -0
  40. package/dist/datastar/attributes/attr.js +49 -0
  41. package/dist/datastar/attributes/bind.d.ts +1 -0
  42. package/dist/datastar/attributes/bind.js +183 -0
  43. package/dist/datastar/attributes/class.d.ts +1 -0
  44. package/dist/datastar/attributes/class.js +50 -0
  45. package/dist/datastar/attributes/computed.d.ts +1 -0
  46. package/dist/datastar/attributes/computed.js +27 -0
  47. package/dist/datastar/attributes/effect.d.ts +1 -0
  48. package/dist/datastar/attributes/effect.js +10 -0
  49. package/dist/datastar/attributes/indicator.d.ts +1 -0
  50. package/dist/datastar/attributes/indicator.js +32 -0
  51. package/dist/datastar/attributes/init.d.ts +1 -0
  52. package/dist/datastar/attributes/init.js +27 -0
  53. package/dist/datastar/attributes/jsonSignals.d.ts +1 -0
  54. package/dist/datastar/attributes/jsonSignals.js +31 -0
  55. package/dist/datastar/attributes/on.d.ts +1 -0
  56. package/dist/datastar/attributes/on.js +59 -0
  57. package/dist/datastar/attributes/onIntersect.d.ts +1 -0
  58. package/dist/datastar/attributes/onIntersect.js +54 -0
  59. package/dist/datastar/attributes/onInterval.d.ts +1 -0
  60. package/dist/datastar/attributes/onInterval.js +31 -0
  61. package/dist/datastar/attributes/onSignalPatch.d.ts +1 -0
  62. package/dist/datastar/attributes/onSignalPatch.js +44 -0
  63. package/dist/datastar/attributes/ref.d.ts +1 -0
  64. package/dist/datastar/attributes/ref.js +11 -0
  65. package/dist/datastar/attributes/show.d.ts +1 -0
  66. package/dist/datastar/attributes/show.js +32 -0
  67. package/dist/datastar/attributes/signals.d.ts +1 -0
  68. package/dist/datastar/attributes/signals.js +18 -0
  69. package/dist/datastar/attributes/style.d.ts +1 -0
  70. package/dist/datastar/attributes/style.js +56 -0
  71. package/dist/datastar/attributes/text.d.ts +1 -0
  72. package/dist/datastar/attributes/text.js +27 -0
  73. package/dist/datastar/engine.d.ts +156 -0
  74. package/dist/datastar/engine.js +971 -0
  75. package/dist/datastar/index.d.ts +24 -0
  76. package/dist/datastar/index.js +24 -0
  77. package/dist/datastar/load.d.ts +24 -0
  78. package/dist/datastar/load.js +24 -0
  79. package/dist/datastar/utils.d.ts +51 -0
  80. package/dist/datastar/utils.js +205 -0
  81. package/dist/datastar/watchers/patchElements.d.ts +1 -0
  82. package/dist/datastar/watchers/patchElements.js +420 -0
  83. package/dist/datastar/watchers/patchSignals.d.ts +1 -0
  84. package/dist/datastar/watchers/patchSignals.js +15 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +1 -1
  87. package/dist/node/NodeFileSystem.d.ts +7 -0
  88. package/dist/node/NodeFileSystem.js +420 -0
  89. package/dist/node/NodeUtils.d.ts +2 -0
  90. package/dist/node/NodeUtils.js +20 -0
  91. package/dist/x/tailwind/plugin.js +1 -1
  92. package/package.json +11 -7
  93. package/src/Development.ts +26 -25
  94. package/src/{node/Effectify.ts → Effectify.ts} +10 -3
  95. package/src/FilePathPattern.ts +115 -0
  96. package/src/FileRouter.ts +178 -255
  97. package/src/FileRouterCodegen.ts +135 -92
  98. package/src/{node/PlatformError.ts → PlatformError.ts} +34 -19
  99. package/src/PlatformRuntime.ts +97 -0
  100. package/src/RouteBody.ts +1 -1
  101. package/src/RouteHttp.ts +3 -1
  102. package/src/Start.ts +61 -14
  103. package/src/bun/BunPlatformHttpServer.ts +88 -0
  104. package/src/bun/BunRoute.ts +12 -22
  105. package/src/bun/BunRuntime.ts +21 -5
  106. package/src/bun/BunServer.ts +228 -0
  107. package/src/bun/index.ts +1 -1
  108. package/src/datastar/README.md +18 -0
  109. package/src/datastar/actions/fetch.ts +609 -0
  110. package/src/datastar/actions/peek.ts +17 -0
  111. package/src/datastar/actions/setAll.ts +20 -0
  112. package/src/datastar/actions/toggleAll.ts +20 -0
  113. package/src/datastar/attributes/attr.ts +50 -0
  114. package/src/datastar/attributes/bind.ts +220 -0
  115. package/src/datastar/attributes/class.ts +57 -0
  116. package/src/datastar/attributes/computed.ts +33 -0
  117. package/src/datastar/attributes/effect.ts +11 -0
  118. package/src/datastar/attributes/indicator.ts +39 -0
  119. package/src/datastar/attributes/init.ts +35 -0
  120. package/src/datastar/attributes/jsonSignals.ts +38 -0
  121. package/src/datastar/attributes/on.ts +71 -0
  122. package/src/datastar/attributes/onIntersect.ts +65 -0
  123. package/src/datastar/attributes/onInterval.ts +39 -0
  124. package/src/datastar/attributes/onSignalPatch.ts +63 -0
  125. package/src/datastar/attributes/ref.ts +12 -0
  126. package/src/datastar/attributes/show.ts +33 -0
  127. package/src/datastar/attributes/signals.ts +22 -0
  128. package/src/datastar/attributes/style.ts +63 -0
  129. package/src/datastar/attributes/text.ts +30 -0
  130. package/src/datastar/engine.ts +1341 -0
  131. package/src/datastar/index.ts +25 -0
  132. package/src/datastar/utils.ts +286 -0
  133. package/src/datastar/watchers/patchElements.ts +554 -0
  134. package/src/datastar/watchers/patchSignals.ts +15 -0
  135. package/src/index.ts +1 -1
  136. package/src/node/{FileSystem.ts → NodeFileSystem.ts} +2 -2
  137. package/src/node/{Utils.ts → NodeUtils.ts} +2 -0
  138. package/src/x/tailwind/plugin.ts +1 -1
  139. package/src/FileRouterCodegen.todo.ts +0 -1133
  140. package/src/FileRouterPattern.ts +0 -59
  141. package/src/RouterPattern.ts +0 -416
  142. package/src/StartApp.ts +0 -47
  143. package/src/bun/BunHttpServer.ts +0 -303
  144. /package/src/bun/{BunHttpServer_web.ts → BunServerRequest.ts} +0 -0
@@ -1,203 +1,149 @@
1
1
  import * as FileSystem from "@effect/platform/FileSystem";
2
- import * as Array from "effect/Array";
3
- import * as Context from "effect/Context";
2
+ import * as Data from "effect/Data";
4
3
  import * as Effect from "effect/Effect";
4
+ import * as Either from "effect/Either";
5
5
  import * as Function from "effect/Function";
6
6
  import * as Layer from "effect/Layer";
7
- import * as Record from "effect/Record";
8
7
  import * as Stream from "effect/Stream";
9
8
  import * as NPath from "node:path";
10
9
  import * as NUrl from "node:url";
11
10
  import * as Development from "./Development.js";
11
+ import * as FilePathPattern from "./FilePathPattern.js";
12
12
  import * as FileRouterCodegen from "./FileRouterCodegen.js";
13
- import * as FileRouterPattern from "./FileRouterPattern.js";
14
- export class FileRouter extends Context.Tag("effect-start/FileRouter")() {
13
+ import * as NodeUtils from "./node/NodeUtils.js";
14
+ import * as Route from "./Route.js";
15
+ import * as RouteTree from "./RouteTree.js";
16
+ export class FileRouterError extends Data.TaggedError("FileRouterError") {
15
17
  }
16
18
  const ROUTE_PATH_REGEX = /^\/?(.*\/?)(?:route|layer)\.(jsx?|tsx?)$/;
17
- export const parse = FileRouterPattern.parse;
18
- export const formatSegment = FileRouterPattern.formatSegment;
19
- export const format = FileRouterPattern.format;
20
19
  export function parseRoute(path) {
21
- const segs = parse(path);
20
+ const segs = FilePathPattern.segments(path);
22
21
  const lastSeg = segs.at(-1);
23
22
  const handleMatch = lastSeg?._tag === "LiteralSegment"
24
23
  && lastSeg.value.match(/^(route|layer)\.(tsx?|jsx?)$/);
25
24
  const handle = handleMatch ? handleMatch[1] : null;
26
25
  if (!handle) {
27
- throw new Error(`Invalid route path "${path}": must end with a valid handle (route or layer)`);
26
+ return null;
28
27
  }
29
- // rest segments must be the last segment before the handle
30
- const pathSegments = segs.slice(0, -1); // All segments except the handle
31
- const restIndex = pathSegments.findIndex(seg => seg._tag === "RestSegment");
32
- if (restIndex !== -1) {
33
- // If there's a rest, it must be the last path segment
34
- if (restIndex !== pathSegments.length - 1) {
35
- throw new Error(`Invalid route path "${path}": rest segment ([...rest] or [[...rest]]) must be the last path segment before the handle`);
36
- }
37
- // all segments before the rest must be literal, param, or group
38
- for (let i = 0; i < restIndex; i++) {
39
- const seg = pathSegments[i];
40
- if (seg._tag !== "LiteralSegment"
41
- && seg._tag !== "ParamSegment"
42
- && seg._tag !== "GroupSegment") {
43
- throw new Error(`Invalid route path "${path}": segments before rest must be literal, param, or group segments`);
44
- }
45
- }
28
+ const pathSegments = segs.slice(0, -1);
29
+ const validated = FilePathPattern.validate(FilePathPattern.format(pathSegments));
30
+ if (Either.isLeft(validated)) {
31
+ return null;
46
32
  }
47
- else {
48
- // No rest: all path segments are literal, param, or group
49
- for (const seg of pathSegments) {
50
- if (seg._tag !== "LiteralSegment"
51
- && seg._tag !== "ParamSegment"
52
- && seg._tag !== "GroupSegment") {
53
- throw new Error(`Invalid route path "${path}": path segments must be literal, param, or group segments`);
54
- }
55
- }
33
+ const restIndex = pathSegments.findIndex((seg) => seg._tag === "RestSegment");
34
+ if (restIndex !== -1 && restIndex !== pathSegments.length - 1) {
35
+ return null;
56
36
  }
57
- const routePathSegments = pathSegments.filter(seg => seg._tag !== "GroupSegment");
58
- const routePath = FileRouterPattern.format(routePathSegments);
37
+ const routePathSegments = pathSegments.filter((seg) => seg._tag !== "GroupSegment");
38
+ const routePath = FilePathPattern.format(routePathSegments);
59
39
  return {
60
40
  handle,
61
- modulePath: path,
41
+ modulePath: `/${path}`,
62
42
  routePath,
63
43
  segments: pathSegments,
64
44
  };
65
45
  }
66
- /**
67
- * Generates a manifest file that references all routes.
68
- */
69
- export function layer(options) {
70
- let manifestPath = options.path;
71
- if (manifestPath.startsWith("file://")) {
72
- manifestPath = NUrl.fileURLToPath(manifestPath);
46
+ function importModule(load) {
47
+ return Effect.tryPromise({
48
+ try: () => load(),
49
+ catch: (cause) => new FileRouterError({ reason: "Import", cause }),
50
+ });
51
+ }
52
+ export function layer(loadOrOptions) {
53
+ const options = typeof loadOrOptions === "function"
54
+ ? {
55
+ load: loadOrOptions,
56
+ path: NPath.join(NodeUtils.getEntrypoint(), "routes"),
57
+ }
58
+ : loadOrOptions;
59
+ let treePath = options.path;
60
+ if (treePath.startsWith("file://")) {
61
+ treePath = NUrl.fileURLToPath(treePath);
73
62
  }
74
- if (NPath.extname(manifestPath) === "") {
75
- manifestPath = NPath.join(manifestPath, "index.ts");
63
+ if (NPath.extname(treePath) === "") {
64
+ treePath = NPath.join(treePath, "server.gen.ts");
76
65
  }
77
- const routesPath = NPath.dirname(manifestPath);
78
- const manifestFilename = NPath.basename(manifestPath);
79
- const resolvedManifestPath = NPath.resolve(routesPath, manifestFilename);
80
- return Layer.provide(Layer.effect(FileRouter, Effect.promise(() => options.load())), Layer.scopedDiscard(Effect.gen(function* () {
81
- yield* FileRouterCodegen.update(routesPath, manifestFilename);
82
- const stream = Function.pipe(Development.watchSource({
83
- path: routesPath,
84
- filter: (e) => !e.path.includes("node_modules"),
85
- }), Stream.onError((e) => Effect.logError(e)));
86
- yield* Function.pipe(stream,
87
- // filter out edits to gen file
88
- Stream.filter(e => e.path !== resolvedManifestPath), Stream.runForEach(() => FileRouterCodegen.update(routesPath, manifestFilename)), Effect.fork);
89
- })));
66
+ const routesPath = NPath.dirname(treePath);
67
+ const treeFilename = NPath.basename(treePath);
68
+ const relativeRoutesPath = NPath.relative(process.cwd(), routesPath);
69
+ return Layer.scoped(Route.Routes, Effect.gen(function* () {
70
+ // Generate routes file before loading
71
+ yield* FileRouterCodegen.update(routesPath, treeFilename);
72
+ // Load and build route tree
73
+ const m = yield* importModule(options.load);
74
+ const routeTree = yield* fromFileRoutes(m.default);
75
+ // Watch for changes (only when Development service is available)
76
+ yield* Function.pipe(Development.stream(), Stream.filter(e => e._tag !== "Reload" && e.path.startsWith(relativeRoutesPath)), Stream.runForEach(() => FileRouterCodegen.update(routesPath, treeFilename)), Effect.fork);
77
+ return routeTree;
78
+ }));
90
79
  }
91
- export function fromManifest(manifest) {
80
+ export function fromFileRoutes(fileRoutes) {
92
81
  return Effect.gen(function* () {
93
82
  const mounts = {};
94
- yield* Effect.forEach(manifest.routes, (lazyRoute) => Effect.gen(function* () {
95
- const routeModule = yield* Effect.promise(() => lazyRoute.load());
96
- const layerModules = lazyRoute.layers
97
- ? yield* Effect.forEach(lazyRoute.layers, (loadLayer) => Effect.promise(() => loadLayer()))
98
- : [];
99
- // Start with the route from the route module
100
- let mergedRouteSet = routeModule.default;
101
- // Concatenate each layer's routes into the routeSet
102
- for (const m of layerModules) {
103
- const layerRouteSet = m.default;
104
- if (RouteSet.isRouteSet(layerRouteSet)) {
105
- mergedRouteSet = Route.merge(layerRouteSet, mergedRouteSet);
83
+ for (const [path, loaders] of Object.entries(fileRoutes)) {
84
+ const modules = yield* Effect.forEach(loaders, (loader) => Effect.promise(() => loader()));
85
+ const allRoutes = [];
86
+ for (const m of modules) {
87
+ if (Route.isRouteSet(m.default)) {
88
+ for (const route of m.default) {
89
+ ;
90
+ allRoutes.push(route);
91
+ }
106
92
  }
107
93
  }
108
- mounts[lazyRoute.path] = mergedRouteSet;
109
- }));
110
- return Router.make(mounts, RouteSet.make());
94
+ mounts[path] = allRoutes;
95
+ }
96
+ return RouteTree.make(mounts);
111
97
  });
112
98
  }
113
99
  export function walkRoutesDirectory(dir) {
114
100
  return Effect.gen(function* () {
115
101
  const fs = yield* FileSystem.FileSystem;
116
102
  const files = yield* fs.readDirectory(dir, { recursive: true });
117
- return getRouteHandlesFromPaths(files);
103
+ return yield* getFileRoutes(files);
118
104
  });
119
105
  }
120
- /**
121
- * Given a list of paths, return a list of route handles.
122
- */
123
- export function getRouteHandlesFromPaths(paths) {
124
- const handles = paths
125
- .map(f => f.match(ROUTE_PATH_REGEX))
126
- .filter(Boolean)
127
- .map(v => {
128
- const path = v[0];
129
- try {
130
- return parseRoute(path);
106
+ export function getFileRoutes(paths) {
107
+ return Effect.gen(function* () {
108
+ const routes = paths
109
+ .map(f => f.match(ROUTE_PATH_REGEX))
110
+ .filter(Boolean)
111
+ .map(v => {
112
+ const path = v[0];
113
+ try {
114
+ return parseRoute(path);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ })
120
+ .filter((route) => route !== null)
121
+ .toSorted((a, b) => {
122
+ const aDepth = a.segments.length;
123
+ const bDepth = b.segments.length;
124
+ const aHasRest = a.segments.some((seg) => seg._tag === "RestSegment");
125
+ const bHasRest = b.segments.some((seg) => seg._tag === "RestSegment");
126
+ return (
127
+ // rest is a dominant factor (routes with rest come last)
128
+ (+aHasRest - +bHasRest) * 1000
129
+ // depth is reversed for rest
130
+ + (aDepth - bDepth) * (1 - 2 * +aHasRest)
131
+ // lexicographic comparison as tiebreaker
132
+ + a.modulePath.localeCompare(b.modulePath) * 0.001);
133
+ });
134
+ // Detect conflicting routes at the same path
135
+ const routesByPath = new Map();
136
+ for (const route of routes) {
137
+ const existing = routesByPath.get(route.routePath) || [];
138
+ existing.push(route);
139
+ routesByPath.set(route.routePath, existing);
131
140
  }
132
- catch {
133
- return null;
141
+ for (const [path, pathRoutes] of routesByPath) {
142
+ const routeHandles = pathRoutes.filter(h => h.handle === "route");
143
+ if (routeHandles.length > 1) {
144
+ yield* new FileRouterError({ reason: "Conflict", path });
145
+ }
134
146
  }
135
- })
136
- .filter((route) => route !== null)
137
- .toSorted((a, b) => {
138
- const aDepth = a.segments.length;
139
- const bDepth = b.segments.length;
140
- const aHasRest = a.segments.some(seg => seg._tag === "RestSegment");
141
- const bHasRest = b.segments.some(seg => seg._tag === "RestSegment");
142
- return (
143
- // rest is a dominant factor (routes with rest come last)
144
- (+aHasRest - +bHasRest) * 1000
145
- // depth is reversed for rest
146
- + (aDepth - bDepth) * (1 - 2 * +aHasRest)
147
- // lexicographic comparison as tiebreaker
148
- + a.modulePath.localeCompare(b.modulePath) * 0.001);
147
+ return routes;
149
148
  });
150
- // Detect conflicting routes at the same path
151
- const routesByPath = new Map();
152
- for (const handle of handles) {
153
- const existing = routesByPath.get(handle.routePath) || [];
154
- existing.push(handle);
155
- routesByPath.set(handle.routePath, existing);
156
- }
157
- for (const [path, pathHandles] of routesByPath) {
158
- const routeHandles = pathHandles.filter(h => h.handle === "route");
159
- if (routeHandles.length > 1) {
160
- const modulePaths = routeHandles.map(h => h.modulePath).join(", ");
161
- throw new Error(`Conflicting routes detected at path ${path}: ${modulePaths}`);
162
- }
163
- }
164
- return handles;
165
- }
166
- export function treeFromRouteHandles(handles) {
167
- const handlesByPath = Array.groupBy(handles, handle => handle.routePath);
168
- const paths = Record.keys(handlesByPath);
169
- const root = {
170
- path: "/",
171
- handles: handlesByPath["/"] || [],
172
- };
173
- const nodeMap = new Map([["/", root]]);
174
- for (const absolutePath of paths) {
175
- if (absolutePath === "/")
176
- continue;
177
- // Find parent path
178
- const segments = absolutePath.split("/").filter(Boolean);
179
- const parentPath = segments.length === 1
180
- ? "/"
181
- : "/" + segments.slice(0, -1).join("/");
182
- const parent = nodeMap.get(parentPath);
183
- if (!parent) {
184
- continue; // Skip orphaned paths
185
- }
186
- // Create node with relative path
187
- const relativePath = parent.path === "/"
188
- ? absolutePath
189
- : absolutePath.slice(parentPath.length);
190
- const node = {
191
- path: relativePath,
192
- handles: handlesByPath[absolutePath],
193
- };
194
- // Add to parent
195
- if (!parent.children) {
196
- parent.children = [];
197
- }
198
- parent.children.push(node);
199
- // Store for future children
200
- nodeMap.set(absolutePath, node);
201
- }
202
- return root;
203
149
  }
@@ -1,19 +1,18 @@
1
- import type { PlatformError } from "@effect/platform/Error";
2
1
  import * as FileSystem from "@effect/platform/FileSystem";
3
2
  import * as Effect from "effect/Effect";
4
3
  import * as Schema from "effect/Schema";
4
+ import * as FilePathPattern from "./FilePathPattern.ts";
5
5
  import * as FileRouter from "./FileRouter.ts";
6
- import * as FileRouterPattern from "./FileRouterPattern.ts";
7
6
  export declare function validateRouteModule(module: unknown): module is FileRouter.RouteModule;
8
- export declare function generatePathParamsSchema(segments: ReadonlyArray<FileRouterPattern.Segment>): Schema.Struct<any> | null;
7
+ export declare function generatePathParamsSchema(segments: ReadonlyArray<FilePathPattern.Segment>): Schema.Struct<any> | null;
9
8
  /**
10
9
  * Validates all route modules in the given route handles.
11
10
  */
12
- export declare function validateRouteModules(routesPath: string, handles: FileRouter.OrderedRouteHandles): Effect.Effect<void, PlatformError, FileSystem.FileSystem>;
13
- export declare function generateCode(handles: FileRouter.OrderedRouteHandles): string;
11
+ export declare function validateRouteModules(path: string, routes: FileRouter.OrderedFileRoutes): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem>;
12
+ export declare function generateCode(fileRoutes: FileRouter.OrderedFileRoutes): string | null;
14
13
  /**
15
- * Updates the manifest file only if the generated content differs from the existing file.
14
+ * Updates the tree file only if the generated content differs from the existing file.
16
15
  * This prevents infinite loops when watching for file changes.
17
16
  */
18
- export declare function update(routesPath: string, manifestPath?: string): Effect.Effect<void, PlatformError, FileSystem.FileSystem>;
19
- export declare function dump(routesPath: string, manifestPath?: string): Effect.Effect<void, PlatformError, FileSystem.FileSystem>;
17
+ export declare function update(routesPath: string, treePath?: string): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem>;
18
+ export declare function dump(routesPath: string, treePath?: string): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem>;
@@ -8,9 +8,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import * as FileSystem from "@effect/platform/FileSystem";
10
10
  import * as Effect from "effect/Effect";
11
- import * as Function from "effect/Function";
11
+ import * as Either from "effect/Either";
12
12
  import * as Schema from "effect/Schema";
13
13
  import * as NPath from "node:path";
14
+ import * as FilePathPattern from "./FilePathPattern.js";
14
15
  import * as FileRouter from "./FileRouter.js";
15
16
  import * as SchemaExtra from "./SchemaExtra.js";
16
17
  export function validateRouteModule(module) {
@@ -26,11 +27,11 @@ export function validateRouteModule(module) {
26
27
  export function generatePathParamsSchema(segments) {
27
28
  const fields = {};
28
29
  for (const segment of segments) {
29
- if (segment._tag === "ParamSegment"
30
- || segment._tag === "RestSegment") {
31
- fields[segment.name] = segment.optional
32
- ? Function.pipe(Schema.String, Schema.optional)
33
- : Schema.String;
30
+ if (segment._tag === "ParamSegment") {
31
+ fields[segment.name] = Schema.String;
32
+ }
33
+ else if (segment._tag === "RestSegment") {
34
+ fields[segment.name] = Schema.optional(Schema.String);
34
35
  }
35
36
  }
36
37
  if (Object.keys(fields).length === 0) {
@@ -41,18 +42,25 @@ export function generatePathParamsSchema(segments) {
41
42
  /**
42
43
  * Validates all route modules in the given route handles.
43
44
  */
44
- export function validateRouteModules(routesPath, handles) {
45
+ export function validateRouteModules(path, routes) {
45
46
  return Effect.gen(function* () {
46
47
  const fs = yield* FileSystem.FileSystem;
47
- const routeHandles = handles.filter(h => h.handle === "route");
48
+ const routeHandles = routes.filter(h => h.handle === "route");
48
49
  for (const handle of routeHandles) {
49
- const routeModulePath = NPath.resolve(routesPath, handle.modulePath);
50
+ const routeModulePath = NPath.resolve(path, handle.modulePath);
50
51
  const expectedSchema = generatePathParamsSchema(handle.segments);
51
- const fileExists = yield* fs.exists(routeModulePath);
52
+ const fileExists = yield* fs.exists(routeModulePath).pipe(Effect.catchAll(() => Effect.succeed(false)));
52
53
  if (!fileExists) {
53
54
  continue;
54
55
  }
55
- const module = yield* Effect.promise(() => import(__rewriteRelativeImportExtension(routeModulePath)));
56
+ const module = yield* Effect.tryPromise({
57
+ try: () => import(__rewriteRelativeImportExtension(routeModulePath)),
58
+ catch: (cause) => new FileRouter.FileRouterError({
59
+ reason: "Import",
60
+ cause,
61
+ path: routeModulePath,
62
+ }),
63
+ });
56
64
  if (!validateRouteModule(module)) {
57
65
  yield* Effect.logWarning(`Route module ${routeModulePath} should export default Route`);
58
66
  continue;
@@ -69,36 +77,31 @@ export function validateRouteModules(routesPath, handles) {
69
77
  }
70
78
  });
71
79
  }
72
- export function generateCode(handles) {
73
- const routerModuleId = "effect-start";
80
+ export function generateCode(fileRoutes) {
74
81
  // Group routes by path to find layers
75
82
  const routesByPath = new Map();
76
- for (const handle of handles) {
77
- const existing = routesByPath.get(handle.routePath) || { layers: [] };
78
- if (handle.handle === "route") {
79
- existing.route = handle;
83
+ for (const fileRoute of fileRoutes) {
84
+ const existing = routesByPath.get(fileRoute.routePath) || { layers: [] };
85
+ if (fileRoute.handle === "route") {
86
+ existing.route = fileRoute;
80
87
  }
81
- else if (handle.handle === "layer") {
82
- existing.layers.push(handle);
88
+ else if (fileRoute.handle === "layer") {
89
+ existing.layers.push(fileRoute);
83
90
  }
84
- routesByPath.set(handle.routePath, existing);
91
+ routesByPath.set(fileRoute.routePath, existing);
85
92
  }
86
- // Generate route definitions
87
- const routes = [];
88
93
  // Helper to check if layer's path is an ancestor of route's path
89
94
  const layerMatchesRoute = (layer, route) => {
90
- // Get the directory of the layer (strip the filename like layer.tsx)
91
- const layerDir = layer.modulePath.replace(/\/?(layer)\.(tsx?|jsx?)$/, "");
92
- // Layer at root (empty layerDir) applies to all routes
93
- if (layerDir === "")
95
+ const layerDir = layer.modulePath.replace(/\/(layer)\.(tsx?|jsx?)$/, "");
96
+ if (layerDir === "/")
94
97
  return true;
95
- // Route's modulePath must start with the layer's directory
96
98
  return route.modulePath.startsWith(layerDir + "/");
97
99
  };
98
- // Find layers for each route by walking up the path hierarchy
100
+ // Build entries for each route path
101
+ const entries = [];
99
102
  for (const [path, { route }] of routesByPath) {
100
103
  if (!route)
101
- continue; // Skip paths that only have layers
104
+ continue;
102
105
  // Collect all parent layers that match the route's groups
103
106
  const allLayers = [];
104
107
  let currentPath = path;
@@ -110,67 +113,95 @@ export function generateCode(handles) {
110
113
  }
111
114
  if (currentPath === "/")
112
115
  break;
113
- // Move to parent path
114
116
  const parentPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
115
117
  currentPath = parentPath || "/";
116
118
  }
117
- // Generate layers array
118
- const layersCode = allLayers.length > 0
119
- ? `\n layers: [\n ${allLayers.map(layer => `() => import("./${layer.modulePath}")`).join(",\n ")},\n ],`
120
- : "";
121
- const routeCode = ` {
122
- path: "${path}",
123
- load: () => import("./${route.modulePath}"),${layersCode}
124
- },`;
125
- routes.push(routeCode);
119
+ // Convert file-style path to colon-style PathPattern
120
+ const pathPatternResult = FilePathPattern.toPathPattern(path);
121
+ if (Either.isLeft(pathPatternResult)) {
122
+ continue;
123
+ }
124
+ const pathPattern = pathPatternResult.right;
125
+ // Order: route first, then layers from innermost to outermost
126
+ const loaders = [
127
+ `() => import(".${route.modulePath}")`,
128
+ ...allLayers.reverse().map(layer => `() => import(".${layer.modulePath}")`),
129
+ ];
130
+ entries.push({ path: pathPattern, loaders });
131
+ }
132
+ // No routes found - don't create file
133
+ if (entries.length === 0) {
134
+ return null;
126
135
  }
127
- const header = `/**
136
+ const routeEntries = entries
137
+ .map(({ path, loaders }) => {
138
+ const loadersCode = loaders.join(",\n ");
139
+ return ` "${path}": [\n ${loadersCode},\n ]`;
140
+ })
141
+ .join(",\n");
142
+ return `/**
128
143
  * Auto-generated by effect-start.
129
- */`;
130
- const routesArray = routes.length > 0
131
- ? `[\n${routes.join("\n")}\n]`
132
- : "[]";
133
- return `${header}
144
+ */
134
145
 
135
- export const routes = ${routesArray} as const
146
+ export default {
147
+ ${routeEntries},
148
+ } satisfies import("effect-start/FileRouter").FileRoutes
136
149
  `;
137
150
  }
138
151
  /**
139
- * Updates the manifest file only if the generated content differs from the existing file.
152
+ * Updates the tree file only if the generated content differs from the existing file.
140
153
  * This prevents infinite loops when watching for file changes.
141
154
  */
142
- export function update(routesPath, manifestPath = "manifest.ts") {
155
+ export function update(routesPath, treePath = "server.gen.ts") {
143
156
  return Effect.gen(function* () {
144
- manifestPath = NPath.resolve(routesPath, manifestPath);
157
+ treePath = NPath.resolve(routesPath, treePath);
145
158
  const fs = yield* FileSystem.FileSystem;
146
- const files = yield* fs.readDirectory(routesPath, { recursive: true });
147
- const handles = FileRouter.getRouteHandlesFromPaths(files);
159
+ const files = yield* fs.readDirectory(routesPath, { recursive: true }).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: routesPath })));
160
+ const fileRoutes = yield* FileRouter.getFileRoutes(files);
148
161
  // Validate route modules
149
- yield* validateRouteModules(routesPath, handles);
150
- const newCode = generateCode(handles);
151
- // Check if file exists and content differs
162
+ yield* validateRouteModules(routesPath, fileRoutes);
163
+ const newCode = generateCode(fileRoutes);
164
+ // Check if file exists (ok to fail - means file doesn't exist)
152
165
  const existingCode = yield* fs
153
- .readFileString(manifestPath)
166
+ .readFileString(treePath)
154
167
  .pipe(Effect.catchAll(() => Effect.succeed(null)));
168
+ // No routes found
169
+ if (newCode === null) {
170
+ // If gen file exists, write empty export
171
+ if (existingCode !== null) {
172
+ const emptyCode = "export default {}\n";
173
+ if (existingCode !== emptyCode) {
174
+ yield* Effect.logDebug(`Clearing file routes tree: ${treePath}`);
175
+ yield* fs.writeFileString(treePath, emptyCode).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })));
176
+ }
177
+ }
178
+ return;
179
+ }
180
+ // Write if content differs
155
181
  if (existingCode !== newCode) {
156
- yield* Effect.logDebug(`Updating file routes manifest: ${manifestPath}`);
157
- yield* fs.writeFileString(manifestPath, newCode);
182
+ yield* Effect.logDebug(`Updating file routes tree: ${treePath}`);
183
+ yield* fs.writeFileString(treePath, newCode).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })));
158
184
  }
159
185
  else {
160
- yield* Effect.logDebug(`File routes manifest unchanged: ${manifestPath}`);
186
+ yield* Effect.logDebug(`File routes tree unchanged: ${treePath}`);
161
187
  }
162
188
  });
163
189
  }
164
- export function dump(routesPath, manifestPath = "manifest.ts") {
190
+ export function dump(routesPath, treePath = "server.gen.ts") {
165
191
  return Effect.gen(function* () {
166
- manifestPath = NPath.resolve(routesPath, manifestPath);
192
+ treePath = NPath.resolve(routesPath, treePath);
167
193
  const fs = yield* FileSystem.FileSystem;
168
- const files = yield* fs.readDirectory(routesPath, { recursive: true });
169
- const handles = FileRouter.getRouteHandlesFromPaths(files);
194
+ const files = yield* fs.readDirectory(routesPath, { recursive: true }).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: routesPath })));
195
+ const fileRoutes = yield* FileRouter.getFileRoutes(files);
170
196
  // Validate route modules
171
- yield* validateRouteModules(routesPath, handles);
172
- const code = generateCode(handles);
173
- yield* Effect.logDebug(`Generating file routes manifest: ${manifestPath}`);
174
- yield* fs.writeFileString(manifestPath, code);
197
+ yield* validateRouteModules(routesPath, fileRoutes);
198
+ const code = generateCode(fileRoutes);
199
+ // No routes found - don't create file
200
+ if (code === null) {
201
+ yield* Effect.logDebug(`No routes found, skipping: ${treePath}`);
202
+ return;
203
+ }
204
+ yield* Effect.logDebug(`Generating file routes tree: ${treePath}`);
205
+ yield* fs.writeFileString(treePath, code).pipe(Effect.mapError((cause) => new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })));
175
206
  });
176
207
  }
@@ -0,0 +1,46 @@
1
+ import type * as Cause from "effect/Cause";
2
+ import * as Schema from "effect/Schema";
3
+ import type * as Types from "effect/Types";
4
+ import { TypeId as TypeId_ } from "@effect/platform/Error";
5
+ export declare const TypeId: typeof TypeId_;
6
+ export type TypeId = typeof TypeId;
7
+ export declare const isPlatformError: (u: unknown) => u is PlatformError;
8
+ export declare const TypeIdError: <const TypeId extends symbol, const Tag extends string>(typeId: TypeId, tag: Tag) => new <A extends Record<string, any>>(args: Types.Simplify<A>) => Cause.YieldableError & Record<TypeId, TypeId> & {
9
+ readonly _tag: Tag;
10
+ } & Readonly<A>;
11
+ export declare const Module: Schema.Literal<["Clipboard", "Command", "FileSystem", "KeyValueStore", "Path", "Stream", "Terminal"]>;
12
+ declare const BadArgument_base: Schema.TaggedErrorClass<BadArgument, "BadArgument", {
13
+ readonly _tag: Schema.tag<"BadArgument">;
14
+ } & {
15
+ module: Schema.Literal<["Clipboard", "Command", "FileSystem", "KeyValueStore", "Path", "Stream", "Terminal"]>;
16
+ method: typeof Schema.String;
17
+ description: Schema.optional<typeof Schema.String>;
18
+ cause: Schema.optional<typeof Schema.Defect>;
19
+ }>;
20
+ export declare class BadArgument extends BadArgument_base {
21
+ readonly [TypeId]: typeof TypeId;
22
+ get message(): string;
23
+ }
24
+ export declare const SystemErrorReason: Schema.Literal<["AlreadyExists", "BadResource", "Busy", "InvalidData", "NotFound", "PermissionDenied", "TimedOut", "UnexpectedEof", "Unknown", "WouldBlock", "WriteZero"]>;
25
+ export type SystemErrorReason = typeof SystemErrorReason.Type;
26
+ declare const SystemError_base: Schema.TaggedErrorClass<SystemError, "SystemError", {
27
+ readonly _tag: Schema.tag<"SystemError">;
28
+ } & {
29
+ reason: Schema.Literal<["AlreadyExists", "BadResource", "Busy", "InvalidData", "NotFound", "PermissionDenied", "TimedOut", "UnexpectedEof", "Unknown", "WouldBlock", "WriteZero"]>;
30
+ module: Schema.Literal<["Clipboard", "Command", "FileSystem", "KeyValueStore", "Path", "Stream", "Terminal"]>;
31
+ method: typeof Schema.String;
32
+ description: Schema.optional<typeof Schema.String>;
33
+ syscall: Schema.optional<typeof Schema.String>;
34
+ pathOrDescriptor: Schema.optional<Schema.Union<[typeof Schema.String, typeof Schema.Number]>>;
35
+ cause: Schema.optional<typeof Schema.Defect>;
36
+ }>;
37
+ export declare class SystemError extends SystemError_base {
38
+ readonly [TypeId]: typeof TypeId;
39
+ get message(): string;
40
+ }
41
+ export type PlatformError = BadArgument | SystemError;
42
+ export declare const PlatformError: Schema.Union<[
43
+ typeof BadArgument,
44
+ typeof SystemError
45
+ ]>;
46
+ export {};