@typed/app 1.0.0-beta.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 (116) hide show
  1. package/README.md +166 -0
  2. package/dist/HttpApiVirtualModulePlugin.d.ts +26 -0
  3. package/dist/HttpApiVirtualModulePlugin.d.ts.map +1 -0
  4. package/dist/HttpApiVirtualModulePlugin.js +301 -0
  5. package/dist/RouterVirtualModulePlugin.d.ts +23 -0
  6. package/dist/RouterVirtualModulePlugin.d.ts.map +1 -0
  7. package/dist/RouterVirtualModulePlugin.js +176 -0
  8. package/dist/createTypeInfoApiSessionForApp.d.ts +29 -0
  9. package/dist/createTypeInfoApiSessionForApp.d.ts.map +1 -0
  10. package/dist/createTypeInfoApiSessionForApp.js +46 -0
  11. package/dist/httpapi/defineApiHandler.d.ts +70 -0
  12. package/dist/httpapi/defineApiHandler.d.ts.map +1 -0
  13. package/dist/httpapi/defineApiHandler.js +23 -0
  14. package/dist/index.d.ts +9 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +7 -0
  17. package/dist/internal/appConfigTypes.d.ts +11 -0
  18. package/dist/internal/appConfigTypes.d.ts.map +1 -0
  19. package/dist/internal/appConfigTypes.js +1 -0
  20. package/dist/internal/appLayerTypes.d.ts +24 -0
  21. package/dist/internal/appLayerTypes.d.ts.map +1 -0
  22. package/dist/internal/appLayerTypes.js +28 -0
  23. package/dist/internal/buildRouteDescriptors.d.ts +48 -0
  24. package/dist/internal/buildRouteDescriptors.d.ts.map +1 -0
  25. package/dist/internal/buildRouteDescriptors.js +371 -0
  26. package/dist/internal/emitHttpApiSource.d.ts +18 -0
  27. package/dist/internal/emitHttpApiSource.d.ts.map +1 -0
  28. package/dist/internal/emitHttpApiSource.js +404 -0
  29. package/dist/internal/emitRouterHelpers.d.ts +17 -0
  30. package/dist/internal/emitRouterHelpers.d.ts.map +1 -0
  31. package/dist/internal/emitRouterHelpers.js +74 -0
  32. package/dist/internal/emitRouterSource.d.ts +8 -0
  33. package/dist/internal/emitRouterSource.d.ts.map +1 -0
  34. package/dist/internal/emitRouterSource.js +139 -0
  35. package/dist/internal/extractHttpApiLiterals.d.ts +17 -0
  36. package/dist/internal/extractHttpApiLiterals.d.ts.map +1 -0
  37. package/dist/internal/extractHttpApiLiterals.js +45 -0
  38. package/dist/internal/httpapiDescriptorTree.d.ts +75 -0
  39. package/dist/internal/httpapiDescriptorTree.d.ts.map +1 -0
  40. package/dist/internal/httpapiDescriptorTree.js +182 -0
  41. package/dist/internal/httpapiEndpointContract.d.ts +32 -0
  42. package/dist/internal/httpapiEndpointContract.d.ts.map +1 -0
  43. package/dist/internal/httpapiEndpointContract.js +79 -0
  44. package/dist/internal/httpapiFileRoles.d.ts +67 -0
  45. package/dist/internal/httpapiFileRoles.d.ts.map +1 -0
  46. package/dist/internal/httpapiFileRoles.js +145 -0
  47. package/dist/internal/httpapiId.d.ts +30 -0
  48. package/dist/internal/httpapiId.d.ts.map +1 -0
  49. package/dist/internal/httpapiId.js +57 -0
  50. package/dist/internal/httpapiOpenApiConfig.d.ts +87 -0
  51. package/dist/internal/httpapiOpenApiConfig.d.ts.map +1 -0
  52. package/dist/internal/httpapiOpenApiConfig.js +144 -0
  53. package/dist/internal/httpapiSort.d.ts +16 -0
  54. package/dist/internal/httpapiSort.d.ts.map +1 -0
  55. package/dist/internal/httpapiSort.js +29 -0
  56. package/dist/internal/path.d.ts +16 -0
  57. package/dist/internal/path.d.ts.map +1 -0
  58. package/dist/internal/path.js +38 -0
  59. package/dist/internal/resolveConfig.d.ts +8 -0
  60. package/dist/internal/resolveConfig.d.ts.map +1 -0
  61. package/dist/internal/resolveConfig.js +13 -0
  62. package/dist/internal/routeIdentifiers.d.ts +18 -0
  63. package/dist/internal/routeIdentifiers.d.ts.map +1 -0
  64. package/dist/internal/routeIdentifiers.js +90 -0
  65. package/dist/internal/routeTypeNode.d.ts +45 -0
  66. package/dist/internal/routeTypeNode.d.ts.map +1 -0
  67. package/dist/internal/routeTypeNode.js +93 -0
  68. package/dist/internal/routerDescriptorTree.d.ts +110 -0
  69. package/dist/internal/routerDescriptorTree.d.ts.map +1 -0
  70. package/dist/internal/routerDescriptorTree.js +230 -0
  71. package/dist/internal/typeTargetBootstrap.d.ts +2 -0
  72. package/dist/internal/typeTargetBootstrap.d.ts.map +1 -0
  73. package/dist/internal/typeTargetBootstrap.js +23 -0
  74. package/dist/internal/typeTargetBootstrapHttpApi.d.ts +2 -0
  75. package/dist/internal/typeTargetBootstrapHttpApi.d.ts.map +1 -0
  76. package/dist/internal/typeTargetBootstrapHttpApi.js +21 -0
  77. package/dist/internal/typeTargetSpecs.d.ts +15 -0
  78. package/dist/internal/typeTargetSpecs.d.ts.map +1 -0
  79. package/dist/internal/typeTargetSpecs.js +32 -0
  80. package/dist/internal/validation.d.ts +12 -0
  81. package/dist/internal/validation.d.ts.map +1 -0
  82. package/dist/internal/validation.js +32 -0
  83. package/package.json +45 -0
  84. package/src/HttpApiVirtualModulePlugin.test.ts +1062 -0
  85. package/src/HttpApiVirtualModulePlugin.ts +376 -0
  86. package/src/RouterVirtualModulePlugin.test.ts +1254 -0
  87. package/src/RouterVirtualModulePlugin.ts +242 -0
  88. package/src/createTypeInfoApiSessionForApp.ts +57 -0
  89. package/src/defineApiHandler.test.ts +100 -0
  90. package/src/httpapi/defineApiHandler.ts +141 -0
  91. package/src/httpapiDescriptorTree.test.ts +124 -0
  92. package/src/httpapiEndpointContract.test.ts +160 -0
  93. package/src/httpapiFileRoles.test.ts +105 -0
  94. package/src/index.ts +40 -0
  95. package/src/internal/appConfigTypes.ts +12 -0
  96. package/src/internal/appLayerTypes.ts +79 -0
  97. package/src/internal/buildRouteDescriptors.ts +489 -0
  98. package/src/internal/emitHttpApiSource.ts +563 -0
  99. package/src/internal/emitRouterHelpers.ts +89 -0
  100. package/src/internal/emitRouterSource.ts +191 -0
  101. package/src/internal/extractHttpApiLiterals.ts +67 -0
  102. package/src/internal/httpapiDescriptorTree.ts +283 -0
  103. package/src/internal/httpapiEndpointContract.ts +110 -0
  104. package/src/internal/httpapiFileRoles.ts +204 -0
  105. package/src/internal/httpapiId.ts +78 -0
  106. package/src/internal/httpapiOpenApiConfig.ts +228 -0
  107. package/src/internal/httpapiSort.ts +39 -0
  108. package/src/internal/path.ts +46 -0
  109. package/src/internal/resolveConfig.ts +15 -0
  110. package/src/internal/routeIdentifiers.ts +93 -0
  111. package/src/internal/routeTypeNode.ts +120 -0
  112. package/src/internal/routerDescriptorTree.ts +366 -0
  113. package/src/internal/typeTargetBootstrap.ts +24 -0
  114. package/src/internal/typeTargetBootstrapHttpApi.ts +22 -0
  115. package/src/internal/typeTargetSpecs.ts +35 -0
  116. package/src/internal/validation.ts +46 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Emits static HttpApi assembly from endpoint modules.
3
+ * Endpoint exports: route (path + pathSchema + querySchema), method, handler;
4
+ * optional headers, body, error, success. Handler receives { path, query, headers, body }
5
+ * with type-safe decoding. Use HttpApiSchema.status(code) on error/success schemas
6
+ * to annotate response status codes.
7
+ *
8
+ * TypeInfo-first: only emits references to exports that are in optionalExportsByPath.
9
+ * The compiler must know what is available from TypeInfo—if it's not there, it is not emitted.
10
+ */
11
+ import { basename, dirname, join, relative } from "node:path";
12
+ import { compareHttpApiPathOrder } from "./httpapiFileRoles.js";
13
+ import { stripScriptExtension, toPosixPath } from "./path.js";
14
+ import { makeUniqueVarNames, pathToIdentifier } from "./routeIdentifiers.js";
15
+ const ROOT_GROUP_KEY = "__root__";
16
+ const DIRECTORY_CONVENTION_KINDS = [
17
+ "_dependencies.ts",
18
+ "_middlewares.ts",
19
+ "_prefix.ts",
20
+ "_openapi.ts",
21
+ ];
22
+ function createMutableDirectoryCompanionPaths() {
23
+ return {
24
+ "_dependencies.ts": [],
25
+ "_middlewares.ts": [],
26
+ "_prefix.ts": [],
27
+ "_openapi.ts": [],
28
+ };
29
+ }
30
+ function freezeDirectoryCompanionPaths(paths) {
31
+ return {
32
+ "_dependencies.ts": [...paths["_dependencies.ts"]],
33
+ "_middlewares.ts": [...paths["_middlewares.ts"]],
34
+ "_prefix.ts": [...paths["_prefix.ts"]],
35
+ "_openapi.ts": [...paths["_openapi.ts"]],
36
+ };
37
+ }
38
+ function createDirectoryConventionIndexEntry() {
39
+ return {
40
+ apiRootPaths: [],
41
+ groupOverridePaths: [],
42
+ companionPaths: createMutableDirectoryCompanionPaths(),
43
+ };
44
+ }
45
+ function normalizeDirPath(dirPath) {
46
+ return dirPath === "." ? "" : dirPath;
47
+ }
48
+ function dirnamePosix(path) {
49
+ const index = path.lastIndexOf("/");
50
+ return index < 0 ? "" : path.slice(0, index);
51
+ }
52
+ function ancestorDirs(dirPath) {
53
+ const normalized = normalizeDirPath(dirPath);
54
+ if (normalized === "")
55
+ return [""];
56
+ const segments = normalized.split("/").filter(Boolean);
57
+ const out = [""];
58
+ let current = "";
59
+ for (const segment of segments) {
60
+ current = current ? `${current}/${segment}` : segment;
61
+ out.push(current);
62
+ }
63
+ return out;
64
+ }
65
+ function pushUnique(values, value) {
66
+ if (!values.includes(value))
67
+ values.push(value);
68
+ }
69
+ function pushUniqueMany(values, incoming) {
70
+ for (const value of incoming) {
71
+ pushUnique(values, value);
72
+ }
73
+ }
74
+ function sortDirectoryCompanionPaths(paths) {
75
+ for (const kind of DIRECTORY_CONVENTION_KINDS) {
76
+ paths[kind].sort(compareHttpApiPathOrder);
77
+ }
78
+ }
79
+ function upsertIndexEntry(index, dirPath) {
80
+ const normalized = normalizeDirPath(dirPath);
81
+ const existing = index.get(normalized);
82
+ if (existing)
83
+ return existing;
84
+ const created = createDirectoryConventionIndexEntry();
85
+ index.set(normalized, created);
86
+ return created;
87
+ }
88
+ function addConventionToIndex(entry, convention) {
89
+ if (convention.kind === "api_root") {
90
+ pushUnique(entry.apiRootPaths, convention.path);
91
+ return;
92
+ }
93
+ if (convention.kind === "group_override") {
94
+ pushUnique(entry.groupOverridePaths, convention.path);
95
+ return;
96
+ }
97
+ pushUnique(entry.companionPaths[convention.kind], convention.path);
98
+ }
99
+ function indexDirectoryConventions(tree) {
100
+ const index = new Map();
101
+ const rootEntry = upsertIndexEntry(index, "");
102
+ for (const convention of tree.conventions) {
103
+ addConventionToIndex(rootEntry, convention);
104
+ }
105
+ const visit = (nodes) => {
106
+ for (const node of nodes) {
107
+ if (node.type === "endpoint")
108
+ continue;
109
+ const entry = upsertIndexEntry(index, node.dirPath);
110
+ for (const convention of node.conventions) {
111
+ addConventionToIndex(entry, convention);
112
+ }
113
+ visit(node.children);
114
+ }
115
+ };
116
+ visit(tree.children);
117
+ for (const entry of index.values()) {
118
+ entry.apiRootPaths.sort(compareHttpApiPathOrder);
119
+ entry.groupOverridePaths.sort(compareHttpApiPathOrder);
120
+ sortDirectoryCompanionPaths(entry.companionPaths);
121
+ }
122
+ return index;
123
+ }
124
+ function collectEndpointNodesWithGroupKey(nodes, currentGroupKey) {
125
+ const out = [];
126
+ for (const node of nodes) {
127
+ if (node.type === "endpoint") {
128
+ out.push({ node, groupKey: currentGroupKey });
129
+ continue;
130
+ }
131
+ if (node.type === "group") {
132
+ out.push(...collectEndpointNodesWithGroupKey(node.children, node.dirPath));
133
+ continue;
134
+ }
135
+ out.push(...collectEndpointNodesWithGroupKey(node.children, currentGroupKey));
136
+ }
137
+ return out;
138
+ }
139
+ function collectGroupNodes(nodes) {
140
+ const out = [];
141
+ for (const node of nodes) {
142
+ if (node.type === "group") {
143
+ out.push(node);
144
+ out.push(...collectGroupNodes(node.children));
145
+ continue;
146
+ }
147
+ if (node.type === "pathless_directory") {
148
+ out.push(...collectGroupNodes(node.children));
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+ function mapEndpointCompanionPaths(node) {
154
+ const mapped = {};
155
+ for (const companion of node.companions) {
156
+ if (!mapped[companion.kind]) {
157
+ mapped[companion.kind] = companion.path;
158
+ }
159
+ }
160
+ return {
161
+ ".name": mapped[".name"],
162
+ ".dependencies": mapped[".dependencies"],
163
+ ".middlewares": mapped[".middlewares"],
164
+ ".prefix": mapped[".prefix"],
165
+ ".openapi": mapped[".openapi"],
166
+ };
167
+ }
168
+ function createDirectoryCompanionPathsForDir(dirPath, index) {
169
+ const merged = createMutableDirectoryCompanionPaths();
170
+ for (const ancestor of ancestorDirs(dirPath)) {
171
+ const entry = index.get(ancestor);
172
+ if (!entry)
173
+ continue;
174
+ for (const kind of DIRECTORY_CONVENTION_KINDS) {
175
+ pushUniqueMany(merged[kind], entry.companionPaths[kind]);
176
+ }
177
+ }
178
+ sortDirectoryCompanionPaths(merged);
179
+ return freezeDirectoryCompanionPaths(merged);
180
+ }
181
+ function buildEndpointRenderSpecs(tree, index) {
182
+ const endpointEntries = collectEndpointNodesWithGroupKey(tree.children, ROOT_GROUP_KEY);
183
+ const specs = [];
184
+ for (const entry of endpointEntries) {
185
+ const endpointDir = dirnamePosix(entry.node.path);
186
+ specs.push({
187
+ path: entry.node.path,
188
+ stem: entry.node.stem,
189
+ groupKey: entry.groupKey,
190
+ modulePath: entry.node.path,
191
+ companions: mapEndpointCompanionPaths(entry.node),
192
+ directoryCompanions: createDirectoryCompanionPathsForDir(endpointDir, index),
193
+ });
194
+ }
195
+ return specs.sort((a, b) => compareHttpApiPathOrder(a.path, b.path));
196
+ }
197
+ function compareGroupKeys(a, b) {
198
+ const left = a === ROOT_GROUP_KEY ? "" : a;
199
+ const right = b === ROOT_GROUP_KEY ? "" : b;
200
+ return compareHttpApiPathOrder(left, right);
201
+ }
202
+ function buildGroupRenderSpecs(tree, index, endpoints) {
203
+ const byDir = new Map();
204
+ for (const groupNode of collectGroupNodes(tree.children)) {
205
+ byDir.set(groupNode.dirPath, groupNode);
206
+ }
207
+ const groupKeys = new Set(byDir.keys());
208
+ if (endpoints.some((endpoint) => endpoint.groupKey === ROOT_GROUP_KEY)) {
209
+ groupKeys.add(ROOT_GROUP_KEY);
210
+ }
211
+ const specs = [];
212
+ const sortedKeys = [...groupKeys].sort(compareGroupKeys);
213
+ for (const groupKey of sortedKeys) {
214
+ const dirPath = groupKey === ROOT_GROUP_KEY ? "" : groupKey;
215
+ const node = byDir.get(groupKey);
216
+ const defaultName = node?.groupName ?? "root";
217
+ const entry = index.get(dirPath);
218
+ specs.push({
219
+ key: groupKey,
220
+ dirPath,
221
+ defaultName,
222
+ overridePath: entry?.groupOverridePaths[0],
223
+ directoryCompanions: createDirectoryCompanionPathsForDir(dirPath, index),
224
+ });
225
+ }
226
+ return specs;
227
+ }
228
+ function buildApiRenderSpec(targetDirectory, index) {
229
+ const rootEntry = index.get("");
230
+ return {
231
+ defaultIdentifier: basename(targetDirectory) || "api",
232
+ apiRootPath: rootEntry?.apiRootPaths[0],
233
+ directoryCompanions: freezeDirectoryCompanionPaths(rootEntry ? rootEntry.companionPaths : createMutableDirectoryCompanionPaths()),
234
+ };
235
+ }
236
+ function toImportSpecifier(importerDir, targetDir, relativeFilePath) {
237
+ const absPath = join(targetDir, relativeFilePath);
238
+ const rel = toPosixPath(relative(importerDir, absPath));
239
+ const specifier = rel.startsWith(".") ? rel : `./${rel}`;
240
+ return stripScriptExtension(specifier) + ".js";
241
+ }
242
+ const METHOD_FACTORIES = {
243
+ GET: "get",
244
+ POST: "post",
245
+ PUT: "put",
246
+ PATCH: "patch",
247
+ DELETE: "delete",
248
+ HEAD: "head",
249
+ OPTIONS: "options",
250
+ };
251
+ const OPTIONAL_ENDPOINT_EXPORTS = ["headers", "body", "success", "error"];
252
+ /** body maps to payload in HttpApiEndpoint options */
253
+ const EXPORT_TO_OPTION = {
254
+ headers: "headers",
255
+ body: "payload",
256
+ success: "success",
257
+ error: "error",
258
+ };
259
+ export function emitHttpApiSource(input) {
260
+ const directoryConventions = indexDirectoryConventions(input.tree);
261
+ const endpointSpecs = buildEndpointRenderSpecs(input.tree, directoryConventions);
262
+ const groupSpecs = buildGroupRenderSpecs(input.tree, directoryConventions, endpointSpecs);
263
+ const apiSpec = buildApiRenderSpec(input.targetDirectory, directoryConventions);
264
+ const endpointPaths = endpointSpecs.map((e) => e.modulePath);
265
+ const importerDir = dirname(toPosixPath(input.importer));
266
+ const proposedNames = endpointPaths.map((path) => ({
267
+ path,
268
+ proposedName: pathToIdentifier(path),
269
+ }));
270
+ const varNameByPath = makeUniqueVarNames(proposedNames);
271
+ const importLines = [
272
+ `import { emptyRecordString, emptyRecordStringArray, composeWithLayers, resolveConfig, type AppConfig, type ComputeLayers, type LayerOrGroup, type RunConfig } from "@typed/app";`,
273
+ `import * as Effect from "effect/Effect";`,
274
+ `import * as Layer from "effect/Layer";`,
275
+ `import * as HttpApi from "effect/unstable/httpapi/HttpApi";`,
276
+ `import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";`,
277
+ `import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";`,
278
+ `import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";`,
279
+ `import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";`,
280
+ `import * as HttpApiScalar from "effect/unstable/httpapi/HttpApiScalar";`,
281
+ `import * as HttpApiSwagger from "effect/unstable/httpapi/HttpApiSwagger";`,
282
+ `import * as HttpServer from "effect/unstable/http/HttpServer";`,
283
+ `import * as HttpRouter from "effect/unstable/http/HttpRouter";`,
284
+ `import * as OpenApiModule from "effect/unstable/httpapi/OpenApi";`,
285
+ `import http from "node:http";`,
286
+ `import { NodeHttpServer } from "@effect/platform-node";`,
287
+ ];
288
+ for (const path of endpointPaths) {
289
+ const importSpecifier = toImportSpecifier(importerDir, input.targetDirectory, path);
290
+ importLines.push(`import * as ${varNameByPath.get(path)} from ${JSON.stringify(importSpecifier)};`);
291
+ }
292
+ const apiId = apiSpec.defaultIdentifier;
293
+ const groupExprs = [];
294
+ for (const groupSpec of groupSpecs) {
295
+ const endpointsInGroup = endpointSpecs.filter((e) => e.groupKey === groupSpec.key);
296
+ if (endpointsInGroup.length === 0)
297
+ continue;
298
+ const endpointExprs = [];
299
+ for (const ep of endpointsInGroup) {
300
+ const varName = varNameByPath.get(ep.modulePath);
301
+ const literals = input.extractedLiteralsByPath.get(ep.path);
302
+ const method = (literals?.method ?? "GET").toUpperCase();
303
+ const name = literals?.name ?? ep.stem;
304
+ const factory = METHOD_FACTORIES[method] ?? "get";
305
+ const m = varName;
306
+ const optionalPresent = input.optionalExportsByPath.get(ep.path) ?? new Set();
307
+ const optsParts = [
308
+ `params: ${m}.route.pathSchema`,
309
+ `query: ${m}.route.querySchema`,
310
+ ];
311
+ for (const exp of OPTIONAL_ENDPOINT_EXPORTS) {
312
+ if (optionalPresent.has(exp)) {
313
+ const optName = EXPORT_TO_OPTION[exp];
314
+ optsParts.push(`${optName}: ${m}.${exp}`);
315
+ }
316
+ }
317
+ const opts = optsParts.join(", ");
318
+ endpointExprs.push(`HttpApiEndpoint.${factory}(${JSON.stringify(name)}, ${m}.route.path, { ${opts} })`);
319
+ }
320
+ const groupName = groupSpec.defaultName;
321
+ const groupChain = endpointExprs.map((expr) => `.add(${expr})`).join("");
322
+ groupExprs.push(`HttpApiGroup.make(${JSON.stringify(groupName)})${groupChain}`);
323
+ }
324
+ const apiChain = groupExprs.map((g) => `.add(${g})`).join("");
325
+ const apiExpr = `HttpApi.make(${JSON.stringify(apiId)})${apiChain}`;
326
+ const groupLayerBlocks = [];
327
+ for (const groupSpec of groupSpecs) {
328
+ const endpointsInGroup = endpointSpecs.filter((e) => e.groupKey === groupSpec.key);
329
+ if (endpointsInGroup.length === 0)
330
+ continue;
331
+ const groupName = groupSpec.defaultName;
332
+ const handlerIsRaw = input.handlerIsRawByPath;
333
+ const handleCalls = endpointsInGroup
334
+ .map((e) => {
335
+ const varName = varNameByPath.get(e.modulePath);
336
+ const literals = input.extractedLiteralsByPath.get(e.path);
337
+ const name = literals?.name ?? e.stem;
338
+ const isRaw = handlerIsRaw?.get(e.path) === true;
339
+ if (isRaw) {
340
+ return `.handleRaw(${JSON.stringify(name)}, (ctx) => ${varName}.handler(ctx))`;
341
+ }
342
+ const optPresent = input.optionalExportsByPath.get(e.path) ?? new Set();
343
+ const headersArg = optPresent.has("headers") ? "ctx.headers" : "emptyRecordString";
344
+ const bodyArg = optPresent.has("body") ? "ctx.payload" : "undefined";
345
+ return `.handle(${JSON.stringify(name)}, (ctx) => ${varName}.handler({ path: ctx.params ?? emptyRecordString, query: ctx.query ?? emptyRecordStringArray, headers: ${headersArg}, body: ${bodyArg} }))`;
346
+ })
347
+ .join("\n ");
348
+ groupLayerBlocks.push(`HttpApiBuilder.group(Api, ${JSON.stringify(groupName)}, (handlers) => handlers${handleCalls})`);
349
+ }
350
+ const baseApiLayer = `HttpApiBuilder.layer(Api)`;
351
+ const mergedApiLayer = groupLayerBlocks.length === 0
352
+ ? baseApiLayer
353
+ : groupLayerBlocks.reduce((acc, groupBlock) => `${acc}.pipe(Layer.provideMerge(${groupBlock}))`, baseApiLayer);
354
+ const middlewaresPath = apiSpec.directoryCompanions["_middlewares.ts"][0];
355
+ const hasMiddlewares = Boolean(middlewaresPath);
356
+ if (hasMiddlewares) {
357
+ const middlewareSpecifier = toImportSpecifier(importerDir, input.targetDirectory, middlewaresPath);
358
+ importLines.push(`import * as ApiMiddlewares from ${JSON.stringify(middlewareSpecifier)};`);
359
+ }
360
+ const serveOptions = hasMiddlewares
361
+ ? `{ disableListenLog, middleware: ApiMiddlewares.middleware ?? ApiMiddlewares.default }`
362
+ : `{ disableListenLog }`;
363
+ return `${importLines.join("\n")}
364
+
365
+ export const Api = ${apiExpr};
366
+ export const ApiLayer = ${mergedApiLayer};
367
+ export const OpenApi = OpenApiModule.fromApi(Api);
368
+ export const Swagger = HttpApiSwagger.layer(Api);
369
+ export const Scalar = HttpApiScalar.layer(Api);
370
+ export const Client = HttpApiClient.make(Api);
371
+
372
+ export const App = <const Layers extends readonly LayerOrGroup[] = []>(
373
+ config?: AppConfig,
374
+ ...layersToMergeIntoRouter: Layers
375
+ ): Layer.Layer<
376
+ Layer.Success<ComputeLayers<Layers, typeof ApiLayer>>,
377
+ Layer.Error<ComputeLayers<Layers, typeof ApiLayer>>,
378
+ Exclude<Layer.Services<ComputeLayers<Layers, typeof ApiLayer>>, HttpRouter.HttpRouter> | HttpServer.HttpServer
379
+ > => {
380
+ const disableListenLog = config?.disableListenLog ?? false;
381
+ const appLayer = composeWithLayers(ApiLayer, layersToMergeIntoRouter) as ComputeLayers<
382
+ Layers,
383
+ typeof ApiLayer
384
+ >;
385
+ return HttpRouter.serve(appLayer, ${serveOptions})
386
+ };
387
+
388
+ export const serve = <const Layers extends readonly LayerOrGroup[] = []>(
389
+ config?: RunConfig,
390
+ ...layersToMergeIntoRouter: Layers
391
+ ) =>
392
+ Layer.unwrap(
393
+ Effect.gen(function* () {
394
+ const host = yield* resolveConfig(config?.host, "0.0.0.0");
395
+ const port = yield* resolveConfig(config?.port, 3000);
396
+ const disableListenLog = yield* resolveConfig(config?.disableListenLog, false);
397
+ const appConfig: AppConfig = { disableListenLog };
398
+ const appLayer = App(appConfig, ...layersToMergeIntoRouter);
399
+ const serverLayer = NodeHttpServer.layer(http.createServer, { host, port });
400
+ return appLayer.pipe(Layer.provide(serverLayer));
401
+ }),
402
+ );
403
+ `;
404
+ }
@@ -0,0 +1,17 @@
1
+ import type { CatchForm, DepsExportKind, RuntimeKind } from "./routeTypeNode.js";
2
+ /**
3
+ * Emit the handler expression that converts to a function returning Fx.
4
+ * Router passes RefSubject<Params> (an Fx) to function handlers.
5
+ * Plain sync handlers (value in, value out) always use Fx.map(params, handler).
6
+ */
7
+ export declare function handlerExprFor(runtimeKind: RuntimeKind, isFn: boolean, expectsRefSubject: boolean, varName: string, exportName: string): string;
8
+ /** Lift a value or function result to Fx based on return kind (plain, effect, stream, fx). */
9
+ export declare function liftToFx(expr: string, kind: RuntimeKind): string;
10
+ /**
11
+ * Emit the catch expression that converts to (causeRef) => Fx form.
12
+ * Supports: value fallbacks, (Cause) => ..., (E) => ..., and native (causeRef) => Fx.
13
+ */
14
+ export declare function catchExprFor(catchForm: CatchForm, varName: string, exportName: string): string;
15
+ /** Targeted lift for .provide() based on dependency export kind (layer, servicemap, array). */
16
+ export declare function depsExprFor(kind: DepsExportKind, varName: string): string;
17
+ //# sourceMappingURL=emitRouterHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitRouterHelpers.d.ts","sourceRoot":"","sources":["../../src/internal/emitRouterHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjF;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE,OAAO,EACb,iBAAiB,EAAE,OAAO,EAC1B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAoBR;AAED,8FAA8F;AAC9F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAahE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAoB9F;AAED,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAUzE"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Emit the handler expression that converts to a function returning Fx.
3
+ * Router passes RefSubject<Params> (an Fx) to function handlers.
4
+ * Plain sync handlers (value in, value out) always use Fx.map(params, handler).
5
+ */
6
+ export function handlerExprFor(runtimeKind, isFn, expectsRefSubject, varName, exportName) {
7
+ const ref = `${varName}.${exportName}`;
8
+ if (runtimeKind === "plain") {
9
+ return isFn ? `(params) => Fx.map(params, ${ref})` : `constant(Fx.succeed(${ref}))`;
10
+ }
11
+ if (isFn && expectsRefSubject) {
12
+ return `(params) => ${ref}(params)`;
13
+ }
14
+ switch (runtimeKind) {
15
+ case "effect":
16
+ return isFn ? `(params) => Fx.mapEffect(params, ${ref})` : `constant(Fx.fromEffect(${ref}))`;
17
+ case "stream":
18
+ return isFn
19
+ ? `(params) => Fx.switchMap(params, (p) => Fx.fromStream(${ref}(p)))`
20
+ : `constant(Fx.fromStream(${ref}))`;
21
+ case "fx":
22
+ return isFn ? ref : `constant(${ref})`;
23
+ case "unknown":
24
+ throw new Error("RVM-KIND-001: runtime kind unknown (should have been caught in buildRouteDescriptors)");
25
+ }
26
+ }
27
+ /** Lift a value or function result to Fx based on return kind (plain, effect, stream, fx). */
28
+ export function liftToFx(expr, kind) {
29
+ switch (kind) {
30
+ case "plain":
31
+ return `Fx.succeed(${expr})`;
32
+ case "effect":
33
+ return `Fx.fromEffect(${expr})`;
34
+ case "stream":
35
+ return `Fx.fromStream(${expr})`;
36
+ case "fx":
37
+ return expr;
38
+ case "unknown":
39
+ throw new Error("RVM-KIND-001: runtime kind unknown (should have been caught in buildRouteDescriptors)");
40
+ }
41
+ }
42
+ /**
43
+ * Emit the catch expression that converts to (causeRef) => Fx form.
44
+ * Supports: value fallbacks, (Cause) => ..., (E) => ..., and native (causeRef) => Fx.
45
+ */
46
+ export function catchExprFor(catchForm, varName, exportName) {
47
+ const ref = `${varName}.${exportName}`;
48
+ const { form, returnKind } = catchForm;
49
+ if (form === "native") {
50
+ return ref;
51
+ }
52
+ if (form === "value") {
53
+ const lifted = liftToFx(ref, returnKind);
54
+ return `(_causeRef) => ${lifted}`;
55
+ }
56
+ if (form === "fn-cause") {
57
+ const lifted = liftToFx(`${ref}(cause)`, returnKind);
58
+ return `(causeRef) => Fx.flatMap(causeRef, (cause) => ${lifted})`;
59
+ }
60
+ // form === "fn-error": (e) => A | Effect | Stream | Fx — use Cause.findFail + Result.match
61
+ return `(causeRef) => Fx.flatMap(causeRef, (cause) => Result.match(Cause.findFail(cause), { onFailure: (c) => Fx.fromEffect(Effect.failCause(c)), onSuccess: ({ error: e }) => ${liftToFx(`${ref}(e)`, returnKind)} }))`;
62
+ }
63
+ /** Targeted lift for .provide() based on dependency export kind (layer, servicemap, array). */
64
+ export function depsExprFor(kind, varName) {
65
+ const ref = `${varName}.default`;
66
+ switch (kind) {
67
+ case "layer":
68
+ return ref;
69
+ case "servicemap":
70
+ return `Layer.succeedServices(${ref})`;
71
+ case "array":
72
+ return `Router.normalizeDependencyInput(${ref})`;
73
+ }
74
+ }
@@ -0,0 +1,8 @@
1
+ import type { CatchFormByPath, CatchExportByPath, DepsFormByPath, GuardExportByPath, RouteDescriptor } from "./buildRouteDescriptors.js";
2
+ /**
3
+ * Emit Router.merge(...) of directory matchers. Each route compiles to .match(route, { handler, ...opts })
4
+ * with opts only from in-file or sibling. Directory companions (_layout, _dependencies, _catch) apply to
5
+ * all routes in that directory and are added once per directory via .layout(), .provide(), .catchCause().
6
+ */
7
+ export declare function emitRouterMatchSource(descriptors: readonly RouteDescriptor[], targetDirectory: string, importer: string, guardExportByPath: GuardExportByPath, catchExportByPath: CatchExportByPath, catchFormByPath: CatchFormByPath, depsFormByPath: DepsFormByPath): string;
8
+ //# sourceMappingURL=emitRouterSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitRouterSource.d.ts","sourceRoot":"","sources":["../../src/internal/emitRouterSource.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AAqEpC;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,SAAS,eAAe,EAAE,EACvC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,EAChB,iBAAiB,EAAE,iBAAiB,EACpC,iBAAiB,EAAE,iBAAiB,EACpC,eAAe,EAAE,eAAe,EAChC,cAAc,EAAE,cAAc,GAC7B,MAAM,CAgGR"}
@@ -0,0 +1,139 @@
1
+ import { basename, dirname, join, relative } from "node:path";
2
+ import { buildRouterDescriptorTree, renderRouterDescriptorTree, } from "./routerDescriptorTree.js";
3
+ import { siblingCompanionPath } from "./buildRouteDescriptors.js";
4
+ import { catchExprFor, depsExprFor, handlerExprFor } from "./emitRouterHelpers.js";
5
+ import { makeUniqueVarNames, pathToIdentifier, routeModuleIdentifier } from "./routeIdentifiers.js";
6
+ import { stripScriptExtension, toPosixPath } from "./path.js";
7
+ /** Path relative to baseDir → import specifier relative to importerDir (script ext → .js for ESM). */
8
+ function toImportSpecifier(importerDir, targetDir, relativeFilePath) {
9
+ const absPath = join(targetDir, relativeFilePath);
10
+ const rel = toPosixPath(relative(importerDir, absPath));
11
+ const specifier = rel.startsWith(".") ? rel : `./${rel}`;
12
+ return stripScriptExtension(specifier) + ".js";
13
+ }
14
+ /** Canonical root directory: Node's dirname returns "." for root-level files; we use "" consistently. */
15
+ function normalizeDir(dir) {
16
+ return dir === "." ? "" : dir;
17
+ }
18
+ /** True iff the companion path is directory-level (e.g. api/_layout.ts), not sibling (e.g. route.layout.ts). */
19
+ function isDirectoryCompanion(p) {
20
+ return basename(p).startsWith("_");
21
+ }
22
+ /** Directory path -> companion paths for that directory (only _layout, _dependencies, _catch; guard is per-route). */
23
+ function directoryCompanionPaths(descriptors) {
24
+ const map = new Map();
25
+ for (const d of descriptors) {
26
+ for (const kind of ["layout", "dependencies", "catch"]) {
27
+ for (const p of d.composedConcerns[kind]) {
28
+ if (!isDirectoryCompanion(p))
29
+ continue;
30
+ const dir = normalizeDir(dirname(p));
31
+ let entry = map.get(dir);
32
+ if (!entry) {
33
+ entry = {};
34
+ map.set(dir, entry);
35
+ }
36
+ if (!entry[kind])
37
+ entry[kind] = p;
38
+ }
39
+ }
40
+ }
41
+ return map;
42
+ }
43
+ /** Collect unique paths in leaf→ancestor order (closest to route first; first occurrence wins). */
44
+ function collectOrderedCompanionPaths(descriptors, kind) {
45
+ const seen = new Set();
46
+ const out = [];
47
+ for (const d of descriptors) {
48
+ const paths = d.composedConcerns[kind];
49
+ for (const p of paths) {
50
+ if (!seen.has(p)) {
51
+ seen.add(p);
52
+ out.push(p);
53
+ }
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Emit Router.merge(...) of directory matchers. Each route compiles to .match(route, { handler, ...opts })
60
+ * with opts only from in-file or sibling. Directory companions (_layout, _dependencies, _catch) apply to
61
+ * all routes in that directory and are added once per directory via .layout(), .provide(), .catchCause().
62
+ */
63
+ export function emitRouterMatchSource(descriptors, targetDirectory, importer, guardExportByPath, catchExportByPath, catchFormByPath, depsFormByPath) {
64
+ const importerDir = dirname(toPosixPath(importer));
65
+ const needsFnErrorImports = Object.values(catchFormByPath).some((f) => f.form === "fn-error");
66
+ const needsLayerImport = Object.values(depsFormByPath).includes("servicemap");
67
+ const depPaths = collectOrderedCompanionPaths(descriptors, "dependencies");
68
+ const layoutPaths = collectOrderedCompanionPaths(descriptors, "layout");
69
+ const guardPaths = collectOrderedCompanionPaths(descriptors, "guard");
70
+ const catchPaths = collectOrderedCompanionPaths(descriptors, "catch");
71
+ const nameEntries = [
72
+ ...descriptors.map((d) => ({
73
+ path: d.filePath,
74
+ proposedName: routeModuleIdentifier(d.filePath),
75
+ })),
76
+ ...depPaths.map((p) => ({ path: p, proposedName: pathToIdentifier(p) })),
77
+ ...layoutPaths.map((p) => ({ path: p, proposedName: pathToIdentifier(p) })),
78
+ ...guardPaths.map((p) => ({ path: p, proposedName: pathToIdentifier(p) })),
79
+ ...catchPaths.map((p) => ({ path: p, proposedName: pathToIdentifier(p) })),
80
+ ];
81
+ const varNameByPath = makeUniqueVarNames(nameEntries);
82
+ const importLines = [
83
+ `import * as Router from "@typed/router";`,
84
+ `import * as Fx from "@typed/fx/Fx";`,
85
+ `import { constant } from "effect/Function";`,
86
+ ...(needsFnErrorImports
87
+ ? [
88
+ `import * as Effect from "effect/Effect";`,
89
+ `import * as Cause from "effect/Cause";`,
90
+ `import * as Result from "effect/Result";`,
91
+ ]
92
+ : []),
93
+ ...(needsLayerImport ? [`import * as Layer from "effect/Layer";`] : []),
94
+ ];
95
+ for (const d of descriptors) {
96
+ const spec = toImportSpecifier(importerDir, targetDirectory, d.filePath);
97
+ const varName = varNameByPath.get(d.filePath);
98
+ importLines.push(`import * as ${varName} from ${JSON.stringify(spec)};`);
99
+ }
100
+ for (const p of depPaths) {
101
+ importLines.push(`import * as ${varNameByPath.get(p)} from ${JSON.stringify(toImportSpecifier(importerDir, targetDirectory, p))};`);
102
+ }
103
+ for (const p of layoutPaths) {
104
+ importLines.push(`import * as ${varNameByPath.get(p)} from ${JSON.stringify(toImportSpecifier(importerDir, targetDirectory, p))};`);
105
+ }
106
+ for (const p of guardPaths) {
107
+ importLines.push(`import * as ${varNameByPath.get(p)} from ${JSON.stringify(toImportSpecifier(importerDir, targetDirectory, p))};`);
108
+ }
109
+ for (const p of catchPaths) {
110
+ importLines.push(`import * as ${varNameByPath.get(p)} from ${JSON.stringify(toImportSpecifier(importerDir, targetDirectory, p))};`);
111
+ }
112
+ const dirToCompanions = directoryCompanionPaths(descriptors);
113
+ const descriptorTree = buildRouterDescriptorTree({
114
+ descriptors,
115
+ dirToCompanions,
116
+ guardExportByPath,
117
+ catchExportByPath,
118
+ catchFormByPath,
119
+ normalizeDir,
120
+ isDirectoryCompanion,
121
+ siblingCompanionPath,
122
+ });
123
+ const handlerExprForMatch = (match, varName) => handlerExprFor(match.runtimeKind, match.entrypointIsFunction, match.entrypointExpectsRefSubject, varName, match.entrypointExport);
124
+ const rootSource = renderRouterDescriptorTree(descriptorTree, {
125
+ varNameByPath,
126
+ guardExportByPath,
127
+ catchExportByPath,
128
+ catchFormByPath,
129
+ depsFormByPath,
130
+ handlerExprFor: handlerExprForMatch,
131
+ catchExprFor,
132
+ depsExprFor,
133
+ });
134
+ return `${importLines.join("\n")}
135
+
136
+ const router = ${rootSource};
137
+ export default router;
138
+ `;
139
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Extract path and method from endpoint TypeInfo snapshot.
3
+ * Uses TypeInfoApi export types instead of parsing source with TypeScript.
4
+ * Handles literal, object, and reference (e.g. Route<P,S>) shapes; unknown kinds return null.
5
+ */
6
+ import type { TypeInfoFileSnapshot } from "@typed/virtual-modules";
7
+ export interface ExtractedEndpointLiterals {
8
+ readonly path: string;
9
+ readonly method: string;
10
+ readonly name: string;
11
+ }
12
+ /**
13
+ * Extracts route path and method from TypeInfo snapshot exports.
14
+ * Reads from route and method exports; path from route type (literal or object.path).
15
+ */
16
+ export declare function extractEndpointLiterals(snapshot: TypeInfoFileSnapshot, stem: string): ExtractedEndpointLiterals;
17
+ //# sourceMappingURL=extractHttpApiLiterals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractHttpApiLiterals.d.ts","sourceRoot":"","sources":["../../src/internal/extractHttpApiLiterals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAIV,oBAAoB,EAErB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAyBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,oBAAoB,EAC9B,IAAI,EAAE,MAAM,GACX,yBAAyB,CAiB3B"}